From 0b7a1cd1665e21d037c8304de273377778bb5172 Mon Sep 17 00:00:00 2001 From: david7482 Date: Mon, 24 May 2021 03:50:03 +0800 Subject: [PATCH 1/3] resource/aws_eks_node_group: added support for taints --- aws/resource_aws_eks_node_group.go | 162 +++++++++++++++++++- aws/resource_aws_eks_node_group_test.go | 128 ++++++++++++++++ website/docs/r/eks_node_group.html.markdown | 7 + 3 files changed, 296 insertions(+), 1 deletion(-) diff --git a/aws/resource_aws_eks_node_group.go b/aws/resource_aws_eks_node_group.go index 7f4f9b7c62fa..80f8a5353b94 100644 --- a/aws/resource_aws_eks_node_group.go +++ b/aws/resource_aws_eks_node_group.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "reflect" "strings" "time" @@ -219,6 +220,30 @@ func resourceAwsEksNodeGroup() *schema.Resource { }, "tags": tagsSchema(), "tags_all": tagsSchemaComputed(), + "taints": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 50, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 63), + }, + "value": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 63), + }, + "effect": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(eks.TaintEffect_Values(), false), + }, + }, + }, + }, "version": { Type: schema.TypeString, Optional: true, @@ -283,6 +308,10 @@ func resourceAwsEksNodeGroupCreate(d *schema.ResourceData, meta interface{}) err input.Tags = tags.IgnoreAws().EksTags() } + if v, ok := d.GetOk("taints"); ok && v.(*schema.Set).Len() > 0 { + input.Taints = expandEksTaints(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("version"); ok { input.Version = aws.String(v.(string)) } @@ -396,6 +425,10 @@ func resourceAwsEksNodeGroupRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting tags_all: %w", err) } + if err := d.Set("taints", flattenEksTaints(nodeGroup.Taints)); err != nil { + return fmt.Errorf("error setting taints: %w", err) + } + d.Set("version", nodeGroup.Version) return nil @@ -410,7 +443,7 @@ func resourceAwsEksNodeGroupUpdate(d *schema.ResourceData, meta interface{}) err return err } - if d.HasChanges("labels", "scaling_config") { + if d.HasChanges("labels", "scaling_config", "taints") { oldLabelsRaw, newLabelsRaw := d.GetChange("labels") input := &eks.UpdateNodegroupConfigInput{ @@ -424,6 +457,9 @@ func resourceAwsEksNodeGroupUpdate(d *schema.ResourceData, meta interface{}) err input.ScalingConfig = expandEksNodegroupScalingConfig(v) } + oldTaintsRaw, newTaintsRaw := d.GetChange("taints") + input.Taints = expandEksUpdateTaintsPayload(oldTaintsRaw.(*schema.Set).List(), newTaintsRaw.(*schema.Set).List()) + output, err := conn.UpdateNodegroupConfig(input) if err != nil { @@ -585,6 +621,108 @@ func expandEksNodegroupScalingConfig(l []interface{}) *eks.NodegroupScalingConfi return config } +func expandEksTaints(l []interface{}) []*eks.Taint { + if len(l) == 0 { + return nil + } + + var taints []*eks.Taint + + for _, raw := range l { + t, ok := raw.(map[string]interface{}) + + if !ok { + continue + } + + taint := &eks.Taint{} + + if k, ok := t["key"].(string); ok { + taint.Key = aws.String(k) + } + + if v, ok := t["value"].(string); ok { + taint.Value = aws.String(v) + } + + if e, ok := t["effect"].(string); ok { + taint.Effect = aws.String(e) + } + + taints = append(taints, taint) + } + + return taints +} + +func expandEksUpdateTaintsPayload(oldTaintsRaw, newTaintsRaw []interface{}) *eks.UpdateTaintsPayload { + oldTaints := expandEksTaints(oldTaintsRaw) + newTaints := expandEksTaints(newTaintsRaw) + + var removedTaints []*eks.Taint + for _, ot := range oldTaints { + if ot == nil { + continue + } + + removed := true + for _, nt := range newTaints { + if nt == nil { + continue + } + + // if both taint.key and taint.effect are the same, we don't need to remove it. + if aws.StringValue(nt.Key) == aws.StringValue(ot.Key) && + aws.StringValue(nt.Effect) == aws.StringValue(ot.Effect) { + removed = false + break + } + } + + if removed { + removedTaints = append(removedTaints, ot) + } + } + + var updatedTaints []*eks.Taint + for _, nt := range newTaints { + if nt == nil { + continue + } + + updated := true + for _, ot := range oldTaints { + if nt == nil { + continue + } + + if reflect.DeepEqual(nt, ot) { + updated = false + break + } + } + if updated { + updatedTaints = append(updatedTaints, nt) + } + } + + if len(removedTaints) == 0 && len(updatedTaints) == 0 { + return nil + } + + updateTaintsPayload := &eks.UpdateTaintsPayload{} + + if len(removedTaints) > 0 { + updateTaintsPayload.RemoveTaints = removedTaints + } + + if len(updatedTaints) > 0 { + updateTaintsPayload.AddOrUpdateTaints = updatedTaints + } + + return updateTaintsPayload +} + func expandEksRemoteAccessConfig(l []interface{}) *eks.RemoteAccessConfig { if len(l) == 0 || l[0] == nil { return nil @@ -710,6 +848,28 @@ func flattenEksRemoteAccessConfig(config *eks.RemoteAccessConfig) []map[string]i return []map[string]interface{}{m} } +func flattenEksTaints(taints []*eks.Taint) []interface{} { + if len(taints) == 0 { + return nil + } + + var results []interface{} + + for _, taint := range taints { + if taint == nil { + continue + } + + t := make(map[string]interface{}) + t["key"] = aws.StringValue(taint.Key) + t["value"] = aws.StringValue(taint.Value) + t["effect"] = aws.StringValue(taint.Effect) + + results = append(results, t) + } + return results +} + func refreshEksNodeGroupStatus(conn *eks.EKS, clusterName string, nodeGroupName string) resource.StateRefreshFunc { return func() (interface{}, string, error) { input := &eks.DescribeNodegroupInput{ diff --git a/aws/resource_aws_eks_node_group_test.go b/aws/resource_aws_eks_node_group_test.go index b381cba95981..8dc80d6e5a38 100644 --- a/aws/resource_aws_eks_node_group_test.go +++ b/aws/resource_aws_eks_node_group_test.go @@ -126,6 +126,7 @@ func TestAccAWSEksNodeGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "status", eks.NodegroupStatusActive), resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "taints.#", "0"), resource.TestCheckResourceAttrPair(resourceName, "version", eksClusterResourceName, "version"), ), }, @@ -818,6 +819,69 @@ func TestAccAWSEksNodeGroup_Tags(t *testing.T) { }) } +func TestAccAWSEksNodeGroup_Taints(t *testing.T) { + var nodeGroup1 eks.Nodegroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_eks_node_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSEks(t) }, + ErrorCheck: testAccErrorCheck(t, eks.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEksNodeGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEksNodeGroupConfigTaints1(rName, "key1", "value1", "NO_SCHEDULE"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), + resource.TestCheckResourceAttr(resourceName, "taints.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + "key": "key1", + "value": "value1", + "effect": "NO_SCHEDULE", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSEksNodeGroupConfigTaints2(rName, + "key1", "value1updated", "NO_EXECUTE", + "key2", "value2", "NO_SCHEDULE"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), + resource.TestCheckResourceAttr(resourceName, "taints.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + "key": "key1", + "value": "value1updated", + "effect": "NO_EXECUTE", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + "key": "key2", + "value": "value2", + "effect": "NO_SCHEDULE", + }), + ), + }, + { + Config: testAccAWSEksNodeGroupConfigTaints1(rName, "key2", "value2", "NO_SCHEDULE"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), + resource.TestCheckResourceAttr(resourceName, "taints.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + "key": "key2", + "value": "value2", + "effect": "NO_SCHEDULE", + }), + ), + }, + }, + }) +} + func TestAccAWSEksNodeGroup_Version(t *testing.T) { var nodeGroup1, nodeGroup2 eks.Nodegroup rName := acctest.RandomWithPrefix("tf-acc-test") @@ -1900,6 +1964,70 @@ resource "aws_eks_node_group" "test" { `, rName, tagKey1, tagValue1, tagKey2, tagValue2)) } +func testAccAWSEksNodeGroupConfigTaints1(rName, taintKey1, taintValue1, taintEffect1 string) string { + return composeConfig(testAccAWSEksNodeGroupConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_node_group" "test" { + cluster_name = aws_eks_cluster.test.name + node_group_name = %[1]q + node_role_arn = aws_iam_role.node.arn + subnet_ids = aws_subnet.test[*].id + + taints { + key = %[2]q + value = %[3]q + effect = %[4]q + } + + scaling_config { + desired_size = 1 + max_size = 1 + min_size = 1 + } + + depends_on = [ + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + ] +} +`, rName, taintKey1, taintValue1, taintEffect1)) +} + +func testAccAWSEksNodeGroupConfigTaints2(rName, taintKey1, taintValue1, taintEffect1, taintKey2, taintValue2, taintEffect2 string) string { + return composeConfig(testAccAWSEksNodeGroupConfigBase(rName), fmt.Sprintf(` +resource "aws_eks_node_group" "test" { + cluster_name = aws_eks_cluster.test.name + node_group_name = %[1]q + node_role_arn = aws_iam_role.node.arn + subnet_ids = aws_subnet.test[*].id + + taints { + key = %[2]q + value = %[3]q + effect = %[4]q + } + + taints { + key = %[5]q + value = %[6]q + effect = %[7]q + } + + scaling_config { + desired_size = 1 + max_size = 1 + min_size = 1 + } + + depends_on = [ + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + ] +} +`, rName, taintKey1, taintValue1, taintEffect1, taintKey2, taintValue2, taintEffect2)) +} + func testAccAWSEksNodeGroupConfigVersion(rName, version string) string { return composeConfig(testAccAWSEksNodeGroupConfigBaseVersion(rName, version), fmt.Sprintf(` resource "aws_eks_node_group" "test" { diff --git a/website/docs/r/eks_node_group.html.markdown b/website/docs/r/eks_node_group.html.markdown index e021880b5c8d..5e7c676ceaf6 100644 --- a/website/docs/r/eks_node_group.html.markdown +++ b/website/docs/r/eks_node_group.html.markdown @@ -134,6 +134,7 @@ The following arguments are optional: * `release_version` – (Optional) AMI version of the EKS Node Group. Defaults to latest version for Kubernetes version. * `remote_access` - (Optional) Configuration block with remote access settings. Detailed below. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `taints` - (Optional) The Kubernetes taints to be applied to the nodes in the node group. Maximum of 50 taints per node group. Detailed below. * `version` – (Optional) Kubernetes version. Defaults to EKS Cluster Kubernetes version. Terraform will only perform drift detection if a configuration value is provided. ### launch_template Configuration Block @@ -155,6 +156,12 @@ The following arguments are optional: * `max_size` - (Required) Maximum number of worker nodes. * `min_size` - (Required) Minimum number of worker nodes. +### taints Configuration Block + +* `key` - (Required) The key of the taint. Maximum length of 63. +* `value` - (Optional) The value of the taint. Maximum length of 63. +* `effect` - (Required) The effect of the taint. Valid values: `NO_SCHEDULE`, `NO_EXECUTE`, `PREFER_NO_SCHEDULE`. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From d89aa814b8b8b8c7649831240f86087c52b680dc Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 25 May 2021 11:36:46 -0400 Subject: [PATCH 2/3] Add CHANGELOG entry. --- .changelog/19482.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/19482.txt diff --git a/.changelog/19482.txt b/.changelog/19482.txt new file mode 100644 index 000000000000..f43aebc58847 --- /dev/null +++ b/.changelog/19482.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_eks_node_group: Add `taint` argument +``` \ No newline at end of file From a64adbce64bd2852df610e4569cc8be0ed29b2fc Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 25 May 2021 13:02:19 -0400 Subject: [PATCH 3/3] r/aws_eks_node_group: Rename 'taints' to 'taint'. --- aws/resource_aws_eks_node_group.go | 12 +++++------ aws/resource_aws_eks_node_group_test.go | 22 ++++++++++----------- website/docs/r/eks_node_group.html.markdown | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/aws/resource_aws_eks_node_group.go b/aws/resource_aws_eks_node_group.go index 80f8a5353b94..17001047f7a3 100644 --- a/aws/resource_aws_eks_node_group.go +++ b/aws/resource_aws_eks_node_group.go @@ -220,7 +220,7 @@ func resourceAwsEksNodeGroup() *schema.Resource { }, "tags": tagsSchema(), "tags_all": tagsSchemaComputed(), - "taints": { + "taint": { Type: schema.TypeSet, Optional: true, MaxItems: 50, @@ -308,7 +308,7 @@ func resourceAwsEksNodeGroupCreate(d *schema.ResourceData, meta interface{}) err input.Tags = tags.IgnoreAws().EksTags() } - if v, ok := d.GetOk("taints"); ok && v.(*schema.Set).Len() > 0 { + if v, ok := d.GetOk("taint"); ok && v.(*schema.Set).Len() > 0 { input.Taints = expandEksTaints(v.(*schema.Set).List()) } @@ -425,8 +425,8 @@ func resourceAwsEksNodeGroupRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting tags_all: %w", err) } - if err := d.Set("taints", flattenEksTaints(nodeGroup.Taints)); err != nil { - return fmt.Errorf("error setting taints: %w", err) + if err := d.Set("taint", flattenEksTaints(nodeGroup.Taints)); err != nil { + return fmt.Errorf("error setting taint: %w", err) } d.Set("version", nodeGroup.Version) @@ -443,7 +443,7 @@ func resourceAwsEksNodeGroupUpdate(d *schema.ResourceData, meta interface{}) err return err } - if d.HasChanges("labels", "scaling_config", "taints") { + if d.HasChanges("labels", "scaling_config", "taint") { oldLabelsRaw, newLabelsRaw := d.GetChange("labels") input := &eks.UpdateNodegroupConfigInput{ @@ -457,7 +457,7 @@ func resourceAwsEksNodeGroupUpdate(d *schema.ResourceData, meta interface{}) err input.ScalingConfig = expandEksNodegroupScalingConfig(v) } - oldTaintsRaw, newTaintsRaw := d.GetChange("taints") + oldTaintsRaw, newTaintsRaw := d.GetChange("taint") input.Taints = expandEksUpdateTaintsPayload(oldTaintsRaw.(*schema.Set).List(), newTaintsRaw.(*schema.Set).List()) output, err := conn.UpdateNodegroupConfig(input) diff --git a/aws/resource_aws_eks_node_group_test.go b/aws/resource_aws_eks_node_group_test.go index 8dc80d6e5a38..55a1ad905c5f 100644 --- a/aws/resource_aws_eks_node_group_test.go +++ b/aws/resource_aws_eks_node_group_test.go @@ -126,7 +126,7 @@ func TestAccAWSEksNodeGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "status", eks.NodegroupStatusActive), resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), - resource.TestCheckResourceAttr(resourceName, "taints.#", "0"), + resource.TestCheckResourceAttr(resourceName, "taint.#", "0"), resource.TestCheckResourceAttrPair(resourceName, "version", eksClusterResourceName, "version"), ), }, @@ -834,8 +834,8 @@ func TestAccAWSEksNodeGroup_Taints(t *testing.T) { Config: testAccAWSEksNodeGroupConfigTaints1(rName, "key1", "value1", "NO_SCHEDULE"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), - resource.TestCheckResourceAttr(resourceName, "taints.#", "1"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "taint.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taint.*", map[string]string{ "key": "key1", "value": "value1", "effect": "NO_SCHEDULE", @@ -853,13 +853,13 @@ func TestAccAWSEksNodeGroup_Taints(t *testing.T) { "key2", "value2", "NO_SCHEDULE"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), - resource.TestCheckResourceAttr(resourceName, "taints.#", "2"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "taint.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taint.*", map[string]string{ "key": "key1", "value": "value1updated", "effect": "NO_EXECUTE", }), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taint.*", map[string]string{ "key": "key2", "value": "value2", "effect": "NO_SCHEDULE", @@ -870,8 +870,8 @@ func TestAccAWSEksNodeGroup_Taints(t *testing.T) { Config: testAccAWSEksNodeGroupConfigTaints1(rName, "key2", "value2", "NO_SCHEDULE"), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEksNodeGroupExists(resourceName, &nodeGroup1), - resource.TestCheckResourceAttr(resourceName, "taints.#", "1"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taints.*", map[string]string{ + resource.TestCheckResourceAttr(resourceName, "taint.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "taint.*", map[string]string{ "key": "key2", "value": "value2", "effect": "NO_SCHEDULE", @@ -1972,7 +1972,7 @@ resource "aws_eks_node_group" "test" { node_role_arn = aws_iam_role.node.arn subnet_ids = aws_subnet.test[*].id - taints { + taint { key = %[2]q value = %[3]q effect = %[4]q @@ -2001,13 +2001,13 @@ resource "aws_eks_node_group" "test" { node_role_arn = aws_iam_role.node.arn subnet_ids = aws_subnet.test[*].id - taints { + taint { key = %[2]q value = %[3]q effect = %[4]q } - taints { + taint { key = %[5]q value = %[6]q effect = %[7]q diff --git a/website/docs/r/eks_node_group.html.markdown b/website/docs/r/eks_node_group.html.markdown index 5e7c676ceaf6..f7d4f6490d4d 100644 --- a/website/docs/r/eks_node_group.html.markdown +++ b/website/docs/r/eks_node_group.html.markdown @@ -134,7 +134,7 @@ The following arguments are optional: * `release_version` – (Optional) AMI version of the EKS Node Group. Defaults to latest version for Kubernetes version. * `remote_access` - (Optional) Configuration block with remote access settings. Detailed below. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. -* `taints` - (Optional) The Kubernetes taints to be applied to the nodes in the node group. Maximum of 50 taints per node group. Detailed below. +* `taint` - (Optional) The Kubernetes taints to be applied to the nodes in the node group. Maximum of 50 taints per node group. Detailed below. * `version` – (Optional) Kubernetes version. Defaults to EKS Cluster Kubernetes version. Terraform will only perform drift detection if a configuration value is provided. ### launch_template Configuration Block @@ -156,7 +156,7 @@ The following arguments are optional: * `max_size` - (Required) Maximum number of worker nodes. * `min_size` - (Required) Minimum number of worker nodes. -### taints Configuration Block +### taint Configuration Block * `key` - (Required) The key of the taint. Maximum length of 63. * `value` - (Optional) The value of the taint. Maximum length of 63.