Skip to content

Commit

Permalink
kubernetes: Require importing additional node pools manually. (#976)
Browse files Browse the repository at this point in the history
* kubernetes: Require importing additional node pools manually.

* Fix TestAccDigitalOceanKubernetesCluster_Basic

* Fix TestAccDigitalOceanKubernetesCluster_KubernetesProviderInteroperability

* Fix type in node pool import docs.
  • Loading branch information
andrewsomething committed Apr 19, 2023
1 parent 2504b4a commit 92057f6
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 105 deletions.
120 changes: 72 additions & 48 deletions digitalocean/kubernetes/import_kubernetes_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package kubernetes_test
import (
"context"
"fmt"
"reflect"
"sort"
"regexp"
"testing"

"github.com/digitalocean/godo"
Expand All @@ -15,6 +14,15 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

var (
clusterStateIgnore = []string{
"kube_config", // because kube_config was completely different for imported state
"node_pool.0.node_count", // because import test failed before DO had started the node in pool
"updated_at", // because removing default tag updates the resource outside of Terraform
"registry_integration", // registry_integration state can not be known via the API
}
)

func TestAccDigitalOceanKubernetesCluster_ImportBasic(t *testing.T) {
clusterName := acceptance.RandomTestName()

Expand All @@ -29,16 +37,69 @@ func TestAccDigitalOceanKubernetesCluster_ImportBasic(t *testing.T) {
// the need to add the tag gets triggered.
Check: testAccDigitalOceanKubernetesRemoveDefaultNodePoolTag(clusterName),
},
{
ResourceName: "digitalocean_kubernetes_cluster.foobar",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: clusterStateIgnore,
},
},
})
}

func TestAccDigitalOceanKubernetesCluster_ImportErrorNonDefaultNodePool(t *testing.T) {
testName1 := acceptance.RandomTestName()
testName2 := acceptance.RandomTestName()

config := fmt.Sprintf(testAccDigitalOceanKubernetesCusterWithMultipleNodePools, testClusterVersionLatest, testName1, testName2)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanKubernetesClusterDestroy,
Steps: []resource.TestStep{
{
Config: config,
// Remove the default node pool tag before importing in order to
// trigger the multiple node pool import error.
Check: testAccDigitalOceanKubernetesRemoveDefaultNodePoolTag(testName1),
},
{
ResourceName: "digitalocean_kubernetes_cluster.foobar",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"kube_config", // because kube_config was completely different for imported state
"node_pool.0.node_count", // because import test failed before DO had started the node in pool
"updated_at", // because removing default tag updates the resource outside of Terraform
"registry_integration", // registry_integration state can not be known via the API
},
ImportStateVerify: false,
ExpectError: regexp.MustCompile(kubernetes.MultipleNodePoolImportError.Error()),
},
},
})
}

func TestAccDigitalOceanKubernetesCluster_ImportNonDefaultNodePool(t *testing.T) {
testName1 := acceptance.RandomTestName()
testName2 := acceptance.RandomTestName()

config := fmt.Sprintf(testAccDigitalOceanKubernetesCusterWithMultipleNodePools, testClusterVersionLatest, testName1, testName2)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanKubernetesClusterDestroy,
Steps: []resource.TestStep{
{
Config: config,
},
{
ResourceName: "digitalocean_kubernetes_cluster.foobar",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: clusterStateIgnore,
},
// Import the non-default node pool as a separate digitalocean_kubernetes_node_pool resource.
{
ResourceName: "digitalocean_kubernetes_node_pool.barfoo",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: clusterStateIgnore,
},
},
})
Expand Down Expand Up @@ -92,11 +153,7 @@ func testAccDigitalOceanKubernetesRemoveDefaultNodePoolTag(clusterName string) r
}
}

func TestAccDigitalOceanKubernetesCluster_ImportNonDefaultNodePool(t *testing.T) {
testName1 := acceptance.RandomTestName()
testName2 := acceptance.RandomTestName()

config := fmt.Sprintf(`%s
const testAccDigitalOceanKubernetesCusterWithMultipleNodePools = `%s
resource "digitalocean_kubernetes_cluster" "foobar" {
name = "%s"
Expand All @@ -116,37 +173,4 @@ resource "digitalocean_kubernetes_node_pool" "barfoo" {
size = "s-1vcpu-2gb"
node_count = 1
}
`, testClusterVersionLatest, testName1, testName2)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acceptance.TestAccPreCheck(t) },
ProviderFactories: acceptance.TestAccProviderFactories,
CheckDestroy: testAccCheckDigitalOceanKubernetesClusterDestroy,
Steps: []resource.TestStep{
{
Config: config,
},
{
ResourceName: "digitalocean_kubernetes_cluster.foobar",
ImportState: true,
ImportStateCheck: func(s []*terraform.InstanceState) error {
if len(s) != 2 {
return fmt.Errorf("expected 2 states: %#v", s)
}

actualNames := []string{s[0].Attributes["name"], s[1].Attributes["name"]}
expectedNames := []string{testName1, testName2}
sort.Strings(actualNames)
sort.Strings(expectedNames)

if !reflect.DeepEqual(actualNames, expectedNames) {
return fmt.Errorf("expected name attributes for cluster and node pools to match: expected=%#v, actual=%#v, s=%#v",
expectedNames, actualNames, s)
}

return nil
},
},
},
})
}
`
58 changes: 15 additions & 43 deletions digitalocean/kubernetes/resource_kubernetes_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ import (
yaml "gopkg.in/yaml.v2"
)

var (
MultipleNodePoolImportError = fmt.Errorf("Cluster contains multiple node pools. Manually add the `%s` tag to the pool that should be used as the default. Additional pools must be imported separately as 'digitalocean_kubernetes_node_pool' resources.", DigitaloceanKubernetesDefaultNodePoolTag)
)

func ResourceDigitalOceanKubernetesCluster() *schema.Resource {
return &schema.Resource{
CreateContext: resourceDigitalOceanKubernetesClusterCreate,
ReadContext: resourceDigitalOceanKubernetesClusterRead,
UpdateContext: resourceDigitalOceanKubernetesClusterUpdate,
DeleteContext: resourceDigitalOceanKubernetesClusterDelete,
Importer: &schema.ResourceImporter{
State: resourceDigitalOceanKubernetesClusterImportState,
StateContext: resourceDigitalOceanKubernetesClusterImportState,
},
SchemaVersion: 3,

Expand Down Expand Up @@ -502,20 +506,18 @@ func resourceDigitalOceanKubernetesClusterDelete(ctx context.Context, d *schema.
// Import a Kubernetes cluster and its node pools into the Terraform state.
//
// Note: This resource cannot make use of the pass-through importer because special handling is
// required to ensure the default node pool has the `terraform:default-node-pool` tag and to
// import any non-default node pools associated with the cluster.
func resourceDigitalOceanKubernetesClusterImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// required to ensure the default node pool has the `terraform:default-node-pool` tag.
func resourceDigitalOceanKubernetesClusterImportState(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*config.CombinedConfig).GodoClient()

cluster, _, err := client.Kubernetes.Get(context.Background(), d.Id())
cluster, _, err := client.Kubernetes.Get(ctx, d.Id())
if err != nil {
return nil, err
}

// Check how many node pools have the required tag. The goal is ensure that one and only one node pool
// has the tag (i.e., the default node pool).
countOfNodePoolsWithTag := 0
refreshCluster := false
for _, nodePool := range cluster.NodePools {
for _, tag := range nodePool.Tags {
if tag == DigitaloceanKubernetesDefaultNodePoolTag {
Expand All @@ -541,53 +543,23 @@ func resourceDigitalOceanKubernetesClusterImportState(d *schema.ResourceData, me
log.Printf("[INFO] Adding %s tag to node pool %s in cluster %s", DigitaloceanKubernetesDefaultNodePoolTag,
nodePool.ID, cluster.ID)

_, _, err := client.Kubernetes.UpdateNodePool(context.Background(), cluster.ID, nodePool.ID, nodePoolUpdateRequest)
_, _, err := client.Kubernetes.UpdateNodePool(ctx, cluster.ID, nodePool.ID, nodePoolUpdateRequest)
if err != nil {
return nil, err
}

refreshCluster = true
} else {
return nil, fmt.Errorf("Cannot infer default node pool since there are multiple node pools; please manually add the `%s` tag to the default node pool", DigitaloceanKubernetesDefaultNodePoolTag)
}
}

// Refresh the cluster and node pools metadata if we added the default tag.
if refreshCluster {
cluster, _, err = client.Kubernetes.Get(context.Background(), d.Id())
if err != nil {
return nil, err
return nil, MultipleNodePoolImportError
}
}

// Generate a list of ResourceData for the cluster and node pools.
resourceDatas := make([]*schema.ResourceData, 1)
resourceDatas[0] = d // the cluster
for _, nodePool := range cluster.NodePools {
// Add every node pool except the default node pool to the list of importable resources.

importNodePool := true
for _, tag := range nodePool.Tags {
if tag == DigitaloceanKubernetesDefaultNodePoolTag {
importNodePool = false
}
}

if importNodePool {
resource := ResourceDigitalOceanKubernetesNodePool()

// Note: Must set type and ID.
// See https://www.terraform.io/docs/extend/resources/import.html#multiple-resource-import
resourceData := resource.Data(nil)
resourceData.SetType("digitalocean_kubernetes_node_pool")
resourceData.SetId(nodePool.ID)
resourceData.Set("cluster_id", cluster.ID)

resourceDatas = append(resourceDatas, resourceData)
}
if len(cluster.NodePools) > 1 {
log.Printf("[WARN] Cluster contains multiple node pools. Importing pool tagged with '%s' as default node pool. Additional pools must be imported separately as 'digitalocean_kubernetes_node_pool' resources.",
DigitaloceanKubernetesDefaultNodePoolTag,
)
}

return resourceDatas, nil
return []*schema.ResourceData{d}, nil
}

func enableRegistryIntegration(client *godo.Client, clusterUUID string) error {
Expand Down
12 changes: 4 additions & 8 deletions digitalocean/kubernetes/resource_kubernetes_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,10 @@ func TestAccDigitalOceanKubernetesCluster_Basic(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckDigitalOceanKubernetesClusterExists("digitalocean_kubernetes_cluster.foobar", &k8s),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "name", rName),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "region", "lon1"),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "region", "nyc1"),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "surge_upgrade", "true"),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "ha", "true"),
resource.TestCheckResourceAttr("digitalocean_kubernetes_cluster.foobar", "ha", "false"),
resource.TestCheckResourceAttrPair("digitalocean_kubernetes_cluster.foobar", "version", "data.digitalocean_kubernetes_versions.test", "latest_version"),
resource.TestCheckResourceAttrSet("digitalocean_kubernetes_cluster.foobar", "ipv4_address"),
resource.TestCheckResourceAttrSet("digitalocean_kubernetes_cluster.foobar", "cluster_subnet"),
resource.TestCheckResourceAttrSet("digitalocean_kubernetes_cluster.foobar", "service_subnet"),
resource.TestCheckResourceAttrSet("digitalocean_kubernetes_cluster.foobar", "endpoint"),
Expand Down Expand Up @@ -899,13 +898,10 @@ provider "kubernetes" {
token = digitalocean_kubernetes_cluster.foobar.kube_config[0].token
}
resource "kubernetes_service_account" "tiller" {
resource "kubernetes_namespace" "example" {
metadata {
name = "tiller"
namespace = "kube-system"
name = "example-namespace"
}
automount_service_account_token = true
}
`, testClusterVersion, rName)
}
Expand Down
11 changes: 9 additions & 2 deletions docs/resources/kubernetes_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,19 @@ In addition to the arguments listed above, the following additional attributes a

Before importing a Kubernetes cluster, the cluster's default node pool must be tagged with
the `terraform:default-node-pool` tag. The provider will automatically add this tag if
the cluster has a single node pool. Clusters with more than one node pool, however, will require
the cluster only has a single node pool. Clusters with more than one node pool, however, will require
that you manually add the `terraform:default-node-pool` tag to the node pool that you intend to be
the default node pool.

Then the Kubernetes cluster and all of its node pools can be imported using the cluster's `id`, e.g.
Then the Kubernetes cluster and its default node pool can be imported using the cluster's `id`, e.g.

```
terraform import digitalocean_kubernetes_cluster.mycluster 1b8b2100-0e9f-4e8f-ad78-9eb578c2a0af
```

Additional node pools must be imported separately as `digitalocean_kubernetes_cluster`
resources, e.g.

```
terraform import digitalocean_kubernetes_node_pool.mynodepool 9d76f410-9284-4436-9633-4066852442c8
```
6 changes: 2 additions & 4 deletions docs/resources/kubernetes_node_pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,8 @@ In addition to the arguments listed above, the following additional attributes a

## Import

If you are importing an existing Kubernetes cluster, just import the cluster. Importing a cluster also imports
all of its associated node pools.

If you still need to import a single node pool, then import it by using its `id`, e.g.
If you are importing an existing Kubernetes cluster with a single node pool, just
import the cluster. Additional node pools can be imported by using their `id`, e.g.

```
terraform import digitalocean_kubernetes_node_pool.mynodepool 9d76f410-9284-4436-9633-4066852442c8
Expand Down

0 comments on commit 92057f6

Please sign in to comment.