2018-11-28 17:53:36 +00:00
package digitalocean
import (
"context"
"fmt"
"net/http"
2019-10-30 22:39:39 +00:00
"strings"
2018-11-28 17:53:36 +00:00
"time"
"github.com/digitalocean/godo"
2020-10-16 19:50:20 +00:00
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
2018-11-28 17:53:36 +00:00
)
2018-11-29 07:12:52 +00:00
// to distinguish between a node pool resource and the default pool from the cluster
// we automatically add this tag to the default pool
const digitaloceanKubernetesDefaultNodePoolTag = "terraform:default-node-pool"
2018-11-29 06:40:40 +00:00
2018-11-29 16:51:30 +00:00
func resourceDigitalOceanKubernetesNodePool ( ) * schema . Resource {
2018-11-28 17:53:36 +00:00
return & schema . Resource {
2020-02-11 18:40:42 +00:00
Create : resourceDigitalOceanKubernetesNodePoolCreate ,
Read : resourceDigitalOceanKubernetesNodePoolRead ,
Update : resourceDigitalOceanKubernetesNodePoolUpdate ,
Delete : resourceDigitalOceanKubernetesNodePoolDelete ,
Importer : & schema . ResourceImporter {
State : resourceDigitalOceanKubernetesNodePoolImportState ,
} ,
2018-11-28 17:53:36 +00:00
SchemaVersion : 1 ,
Schema : nodePoolResourceSchema ( ) ,
}
}
func nodePoolResourceSchema ( ) map [ string ] * schema . Schema {
s := nodePoolSchema ( )
// add the cluster id
s [ "cluster_id" ] = & schema . Schema {
Type : schema . TypeString ,
Required : true ,
ValidateFunc : validation . NoZeroValues ,
ForceNew : true ,
}
2018-12-06 10:39:59 +00:00
// remove the id when this is used in a specific resource
// not as a child
2018-11-29 16:51:30 +00:00
delete ( s , "id" )
2018-11-28 17:53:36 +00:00
return s
}
func nodePoolSchema ( ) map [ string ] * schema . Schema {
return map [ string ] * schema . Schema {
"id" : {
Type : schema . TypeString ,
Computed : true ,
} ,
"name" : {
2018-11-29 06:40:40 +00:00
Type : schema . TypeString ,
Required : true ,
ValidateFunc : validation . NoZeroValues ,
2018-11-28 17:53:36 +00:00
} ,
"size" : {
Type : schema . TypeString ,
Required : true ,
2018-12-06 10:39:59 +00:00
ForceNew : true ,
2018-11-28 17:53:36 +00:00
ValidateFunc : validation . NoZeroValues ,
} ,
2019-10-30 22:39:39 +00:00
"actual_node_count" : {
Type : schema . TypeInt ,
Computed : true ,
} ,
2018-11-29 16:51:30 +00:00
"node_count" : {
2018-11-28 17:53:36 +00:00
Type : schema . TypeInt ,
2019-10-30 22:39:39 +00:00
Optional : true ,
2018-11-28 17:53:36 +00:00
ValidateFunc : validation . IntAtLeast ( 1 ) ,
2019-10-30 22:39:39 +00:00
DiffSuppressFunc : func ( key , old , new string , d * schema . ResourceData ) bool {
nodeCountKey := "node_count"
actualNodeCountKey := "actual_node_count"
// Since this schema is shared between the node pool resource
// and as the node pool sub-element of the cluster resource,
// we need to check for both variants of the incoming key.
keyParts := strings . Split ( key , "." )
if keyParts [ 0 ] == "node_pool" {
npKeyParts := keyParts [ : len ( keyParts ) - 1 ]
nodeCountKeyParts := append ( npKeyParts , "node_count" )
nodeCountKey = strings . Join ( nodeCountKeyParts , "." )
actualNodeCountKeyParts := append ( npKeyParts , "actual_node_count" )
actualNodeCountKey = strings . Join ( actualNodeCountKeyParts , "." )
}
// If node_count equals actual_node_count already, then
// suppress the diff.
if d . Get ( nodeCountKey ) . ( int ) == d . Get ( actualNodeCountKey ) . ( int ) {
return true
}
// Otherwise suppress the diff only if old equals new.
return old == new
} ,
} ,
"auto_scale" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
} ,
"min_nodes" : {
Type : schema . TypeInt ,
Optional : true ,
} ,
"max_nodes" : {
Type : schema . TypeInt ,
Optional : true ,
2018-11-28 17:53:36 +00:00
} ,
"tags" : tagsSchema ( ) ,
2020-02-18 22:26:36 +00:00
"labels" : {
Type : schema . TypeMap ,
Optional : true ,
Elem : & schema . Schema {
Type : schema . TypeString ,
} ,
} ,
2018-11-28 17:53:36 +00:00
"nodes" : nodeSchema ( ) ,
}
}
func nodeSchema ( ) * schema . Schema {
return & schema . Schema {
Type : schema . TypeList ,
Computed : true ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"id" : {
Type : schema . TypeString ,
Computed : true ,
} ,
"name" : {
Type : schema . TypeString ,
Computed : true ,
} ,
"status" : {
Type : schema . TypeString ,
Computed : true ,
} ,
2020-02-05 22:02:52 +00:00
"droplet_id" : {
Type : schema . TypeString ,
Computed : true ,
} ,
2018-11-28 17:53:36 +00:00
"created_at" : {
Type : schema . TypeString ,
Computed : true ,
} ,
"updated_at" : {
Type : schema . TypeString ,
Computed : true ,
} ,
} ,
} ,
}
}
func resourceDigitalOceanKubernetesNodePoolCreate ( d * schema . ResourceData , meta interface { } ) error {
2019-01-08 22:56:02 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2018-11-28 17:53:36 +00:00
rawPool := map [ string ] interface { } {
2018-11-29 16:51:30 +00:00
"name" : d . Get ( "name" ) ,
"size" : d . Get ( "size" ) ,
"tags" : d . Get ( "tags" ) ,
2020-02-18 22:26:36 +00:00
"labels" : d . Get ( "labels" ) ,
2019-10-30 22:39:39 +00:00
"node_count" : d . Get ( "node_count" ) ,
"auto_scale" : d . Get ( "auto_scale" ) ,
"min_nodes" : d . Get ( "min_nodes" ) ,
"max_nodes" : d . Get ( "max_nodes" ) ,
2018-11-28 17:53:36 +00:00
}
2018-11-29 16:51:30 +00:00
pool , err := digitaloceanKubernetesNodePoolCreate ( client , rawPool , d . Get ( "cluster_id" ) . ( string ) )
2018-11-28 17:53:36 +00:00
if err != nil {
return fmt . Errorf ( "Error creating Kubernetes node pool: %s" , err )
}
d . SetId ( pool . ID )
return resourceDigitalOceanKubernetesNodePoolRead ( d , meta )
}
func resourceDigitalOceanKubernetesNodePoolRead ( d * schema . ResourceData , meta interface { } ) error {
2019-01-08 22:56:02 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2018-11-28 17:53:36 +00:00
pool , resp , err := client . Kubernetes . GetNodePool ( context . Background ( ) , d . Get ( "cluster_id" ) . ( string ) , d . Id ( ) )
if err != nil {
2019-07-23 16:11:05 +00:00
if resp != nil && resp . StatusCode == 404 {
2018-11-28 17:53:36 +00:00
d . SetId ( "" )
return nil
}
return fmt . Errorf ( "Error retrieving Kubernetes node pool: %s" , err )
}
d . Set ( "name" , pool . Name )
d . Set ( "size" , pool . Size )
2018-11-29 16:51:30 +00:00
d . Set ( "node_count" , pool . Count )
2019-10-30 22:39:39 +00:00
d . Set ( "actual_node_count" , pool . Count )
2019-04-22 15:59:54 +00:00
d . Set ( "tags" , flattenTags ( filterTags ( pool . Tags ) ) )
2020-02-18 22:26:36 +00:00
d . Set ( "labels" , flattenLabels ( pool . Labels ) )
2019-10-30 22:39:39 +00:00
d . Set ( "auto_scale" , pool . AutoScale )
d . Set ( "min_nodes" , pool . MinNodes )
d . Set ( "max_nodes" , pool . MaxNodes )
d . Set ( "nodes" , flattenNodes ( pool . Nodes ) )
// Assign a node_count only if it's been set explicitly, since it's
// optional and we don't want to update with a 0 if it's not set.
if _ , ok := d . GetOk ( "node_count" ) ; ok {
d . Set ( "node_count" , pool . Count )
2018-11-28 17:53:36 +00:00
}
return nil
}
func resourceDigitalOceanKubernetesNodePoolUpdate ( d * schema . ResourceData , meta interface { } ) error {
2019-01-08 22:56:02 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2018-11-28 17:53:36 +00:00
rawPool := map [ string ] interface { } {
2019-10-30 22:39:39 +00:00
"name" : d . Get ( "name" ) ,
"tags" : d . Get ( "tags" ) ,
2018-11-28 17:53:36 +00:00
}
2019-10-30 22:39:39 +00:00
if _ , ok := d . GetOk ( "node_count" ) ; ok {
rawPool [ "node_count" ] = d . Get ( "node_count" )
}
2020-02-18 22:26:36 +00:00
rawPool [ "labels" ] = d . Get ( "labels" )
2019-10-30 22:39:39 +00:00
rawPool [ "auto_scale" ] = d . Get ( "auto_scale" )
rawPool [ "min_nodes" ] = d . Get ( "min_nodes" )
rawPool [ "max_nodes" ] = d . Get ( "max_nodes" )
2018-11-29 06:40:40 +00:00
_ , err := digitaloceanKubernetesNodePoolUpdate ( client , rawPool , d . Get ( "cluster_id" ) . ( string ) , d . Id ( ) )
2018-11-28 17:53:36 +00:00
if err != nil {
return fmt . Errorf ( "Error updating node pool: %s" , err )
}
return resourceDigitalOceanKubernetesNodePoolRead ( d , meta )
}
func resourceDigitalOceanKubernetesNodePoolDelete ( d * schema . ResourceData , meta interface { } ) error {
2019-01-08 22:56:02 +00:00
client := meta . ( * CombinedConfig ) . godoClient ( )
2018-11-28 17:53:36 +00:00
return digitaloceanKubernetesNodePoolDelete ( client , d . Get ( "cluster_id" ) . ( string ) , d . Id ( ) )
}
2020-02-11 18:40:42 +00:00
func resourceDigitalOceanKubernetesNodePoolImportState ( d * schema . ResourceData , meta interface { } ) ( [ ] * schema . ResourceData , error ) {
if _ , ok := d . GetOk ( "cluster_id" ) ; ok {
// Short-circuit: The resource already has a cluster ID, no need to search for it.
return [ ] * schema . ResourceData { d } , nil
}
client := meta . ( * CombinedConfig ) . godoClient ( )
nodePoolId := d . Id ( )
// Scan all of the Kubernetes clusters to recover the node pool's cluster ID.
var clusterId string
var nodePool * godo . KubernetesNodePool
listOptions := godo . ListOptions { }
for {
clusters , response , err := client . Kubernetes . List ( context . Background ( ) , & listOptions )
if err != nil {
return nil , fmt . Errorf ( "Unable to list Kubernetes clusters: %v" , err )
}
for _ , cluster := range clusters {
for _ , np := range cluster . NodePools {
if np . ID == nodePoolId {
if clusterId != "" {
// This should never happen but good practice to assert that it does not occur.
return nil , fmt . Errorf ( "Illegal state: node pool ID %s is associated with multiple clusters" , nodePoolId )
}
clusterId = cluster . ID
nodePool = np
}
}
}
if response . Links == nil || response . Links . IsLastPage ( ) {
break
}
page , err := response . Links . CurrentPage ( )
if err != nil {
return nil , err
}
listOptions . Page = page + 1
}
if clusterId == "" {
return nil , fmt . Errorf ( "Did not find the cluster owning the node pool %s" , nodePoolId )
}
// Ensure that the node pool does not have the default tag set.
for _ , tag := range nodePool . Tags {
if tag == digitaloceanKubernetesDefaultNodePoolTag {
return nil , fmt . Errorf ( "Node pool %s has the default node pool tag set; import the owning digitalocean_kubernetes_cluster resource instead (cluster ID=%s)" ,
nodePoolId , clusterId )
}
}
// Set the cluster_id attribute with the cluster's ID.
d . Set ( "cluster_id" , clusterId )
return [ ] * schema . ResourceData { d } , nil
}
2018-11-29 06:40:40 +00:00
func digitaloceanKubernetesNodePoolCreate ( client * godo . Client , pool map [ string ] interface { } , clusterID string , customTags ... string ) ( * godo . KubernetesNodePool , error ) {
// append any custom tags
tags := expandTags ( pool [ "tags" ] . ( * schema . Set ) . List ( ) )
tags = append ( tags , customTags ... )
2019-10-30 22:39:39 +00:00
req := & godo . KubernetesNodePoolCreateRequest {
Name : pool [ "name" ] . ( string ) ,
Size : pool [ "size" ] . ( string ) ,
Count : pool [ "node_count" ] . ( int ) ,
Tags : tags ,
2020-02-18 22:26:36 +00:00
Labels : expandLabels ( pool [ "labels" ] . ( map [ string ] interface { } ) ) ,
2019-10-30 22:39:39 +00:00
AutoScale : pool [ "auto_scale" ] . ( bool ) ,
MinNodes : pool [ "min_nodes" ] . ( int ) ,
MaxNodes : pool [ "max_nodes" ] . ( int ) ,
}
p , _ , err := client . Kubernetes . CreateNodePool ( context . Background ( ) , clusterID , req )
2018-11-28 17:53:36 +00:00
if err != nil {
return nil , fmt . Errorf ( "Unable to create new default node pool %s" , err )
}
err = waitForKubernetesNodePoolCreate ( client , clusterID , p . ID )
if err != nil {
return nil , err
}
return p , nil
}
2018-11-29 06:40:40 +00:00
func digitaloceanKubernetesNodePoolUpdate ( client * godo . Client , pool map [ string ] interface { } , clusterID , poolID string , customTags ... string ) ( * godo . KubernetesNodePool , error ) {
tags := expandTags ( pool [ "tags" ] . ( * schema . Set ) . List ( ) )
tags = append ( tags , customTags ... )
2019-10-30 22:39:39 +00:00
req := & godo . KubernetesNodePoolUpdateRequest {
Name : pool [ "name" ] . ( string ) ,
Tags : tags ,
}
if pool [ "node_count" ] != nil {
req . Count = intPtr ( pool [ "node_count" ] . ( int ) )
}
if pool [ "auto_scale" ] == nil {
pool [ "auto_scale" ] = false
}
req . AutoScale = boolPtr ( pool [ "auto_scale" ] . ( bool ) )
if pool [ "min_nodes" ] != nil {
req . MinNodes = intPtr ( pool [ "min_nodes" ] . ( int ) )
}
if pool [ "max_nodes" ] != nil {
req . MaxNodes = intPtr ( pool [ "max_nodes" ] . ( int ) )
}
2020-02-18 22:26:36 +00:00
if pool [ "labels" ] != nil {
req . Labels = expandLabels ( pool [ "labels" ] . ( map [ string ] interface { } ) )
}
2019-10-30 22:39:39 +00:00
p , resp , err := client . Kubernetes . UpdateNodePool ( context . Background ( ) , clusterID , poolID , req )
2018-11-28 17:53:36 +00:00
if err != nil {
2019-07-23 16:11:05 +00:00
if resp != nil && resp . StatusCode == 404 {
2018-11-29 06:40:40 +00:00
return nil , nil
}
return nil , fmt . Errorf ( "Unable to update nodepool: %s" , err )
2018-11-28 17:53:36 +00:00
}
2018-11-29 07:40:05 +00:00
err = waitForKubernetesNodePoolCreate ( client , clusterID , p . ID )
if err != nil {
return nil , err
}
2018-11-28 17:53:36 +00:00
return p , nil
}
func digitaloceanKubernetesNodePoolDelete ( client * godo . Client , clusterID , poolID string ) error {
// delete the old pool
_ , err := client . Kubernetes . DeleteNodePool ( context . Background ( ) , clusterID , poolID )
if err != nil {
return fmt . Errorf ( "Unable to delete node pool %s" , err )
}
err = waitForKubernetesNodePoolDelete ( client , clusterID , poolID )
if err != nil {
return err
}
return nil
}
func waitForKubernetesNodePoolCreate ( client * godo . Client , id string , poolID string ) error {
2018-11-29 06:40:40 +00:00
tickerInterval := 10 //10s
timeout := 1800 //1800s, 30min
2018-11-28 17:53:36 +00:00
n := 0
2018-11-29 06:40:40 +00:00
ticker := time . NewTicker ( time . Duration ( tickerInterval ) * time . Second )
2018-11-28 17:53:36 +00:00
for range ticker . C {
pool , _ , err := client . Kubernetes . GetNodePool ( context . Background ( ) , id , poolID )
if err != nil {
ticker . Stop ( )
return fmt . Errorf ( "Error trying to read nodepool state: %s" , err )
}
2019-10-07 16:07:49 +00:00
allRunning := len ( pool . Nodes ) == pool . Count
2018-11-28 17:53:36 +00:00
for _ , n := range pool . Nodes {
if n . Status . State != "running" {
allRunning = false
}
}
if allRunning {
ticker . Stop ( )
return nil
}
2018-11-29 06:40:40 +00:00
if n * tickerInterval > timeout {
2018-11-28 17:53:36 +00:00
ticker . Stop ( )
break
}
n ++
}
return fmt . Errorf ( "Timeout waiting to create nodepool" )
}
func waitForKubernetesNodePoolDelete ( client * godo . Client , id string , poolID string ) error {
2018-11-29 16:51:30 +00:00
tickerInterval := 10 //10s
timeout := 1800 //1800s, 30min
2018-11-28 17:53:36 +00:00
n := 0
2018-11-29 16:51:30 +00:00
ticker := time . NewTicker ( time . Duration ( tickerInterval ) * time . Second )
2018-11-28 17:53:36 +00:00
for range ticker . C {
_ , resp , err := client . Kubernetes . GetNodePool ( context . Background ( ) , id , poolID )
if err != nil {
ticker . Stop ( )
2018-11-29 06:40:40 +00:00
if resp . StatusCode == http . StatusNotFound {
return nil
}
return fmt . Errorf ( "Error trying to read nodepool state: %s" , err )
2018-11-28 17:53:36 +00:00
}
2018-11-29 16:51:30 +00:00
if n * tickerInterval > timeout {
2018-11-28 17:53:36 +00:00
ticker . Stop ( )
break
}
n ++
}
return fmt . Errorf ( "Timeout waiting to delete nodepool" )
}
2020-02-18 22:26:36 +00:00
func expandLabels ( labels map [ string ] interface { } ) map [ string ] string {
expandedLabels := make ( map [ string ] string )
if labels != nil {
for key , value := range labels {
expandedLabels [ key ] = value . ( string )
}
}
return expandedLabels
}
func flattenLabels ( labels map [ string ] string ) map [ string ] interface { } {
flattenedLabels := make ( map [ string ] interface { } )
if labels != nil {
for key , value := range labels {
flattenedLabels [ key ] = value
}
}
return flattenedLabels
}
2018-11-28 17:53:36 +00:00
func expandNodePools ( nodePools [ ] interface { } ) [ ] * godo . KubernetesNodePool {
expandedNodePools := make ( [ ] * godo . KubernetesNodePool , 0 , len ( nodePools ) )
for _ , rawPool := range nodePools {
pool := rawPool . ( map [ string ] interface { } )
cr := & godo . KubernetesNodePool {
2019-10-30 22:39:39 +00:00
ID : pool [ "id" ] . ( string ) ,
Name : pool [ "name" ] . ( string ) ,
Size : pool [ "size" ] . ( string ) ,
Count : pool [ "node_count" ] . ( int ) ,
AutoScale : pool [ "auto_scale" ] . ( bool ) ,
MinNodes : pool [ "min_nodes" ] . ( int ) ,
MaxNodes : pool [ "max_nodes" ] . ( int ) ,
Tags : expandTags ( pool [ "tags" ] . ( * schema . Set ) . List ( ) ) ,
2020-02-18 22:26:36 +00:00
Labels : expandLabels ( pool [ "labels" ] . ( map [ string ] interface { } ) ) ,
2019-10-30 22:39:39 +00:00
Nodes : expandNodes ( pool [ "nodes" ] . ( [ ] interface { } ) ) ,
2018-11-28 17:53:36 +00:00
}
expandedNodePools = append ( expandedNodePools , cr )
}
return expandedNodePools
}
func expandNodes ( nodes [ ] interface { } ) [ ] * godo . KubernetesNode {
expandedNodes := make ( [ ] * godo . KubernetesNode , 0 , len ( nodes ) )
for _ , rawNode := range nodes {
node := rawNode . ( map [ string ] interface { } )
n := & godo . KubernetesNode {
ID : node [ "id" ] . ( string ) ,
Name : node [ "name" ] . ( string ) ,
}
expandedNodes = append ( expandedNodes , n )
}
return expandedNodes
}
func flattenNodes ( nodes [ ] * godo . KubernetesNode ) [ ] interface { } {
2019-10-30 22:39:39 +00:00
flattenedNodes := make ( [ ] interface { } , 0 )
2018-11-28 17:53:36 +00:00
if nodes == nil {
2019-10-30 22:39:39 +00:00
return flattenedNodes
2018-11-28 17:53:36 +00:00
}
for _ , node := range nodes {
rawNode := map [ string ] interface { } {
"id" : node . ID ,
"name" : node . Name ,
"status" : node . Status . State ,
2020-02-05 22:02:52 +00:00
"droplet_id" : node . DropletID ,
2018-11-28 17:53:36 +00:00
"created_at" : node . CreatedAt . UTC ( ) . String ( ) ,
"updated_at" : node . UpdatedAt . UTC ( ) . String ( ) ,
}
flattenedNodes = append ( flattenedNodes , rawNode )
}
return flattenedNodes
}