diff --git a/.changelog/38357.txt b/.changelog/38357.txt new file mode 100644 index 00000000000..e4e6044900d --- /dev/null +++ b/.changelog/38357.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +resource/aws_eks_addon: Add `pod_identity_association` argument +``` +```release-note:bug +resource/aws_eks_addon: Handle `ResourceNotFound` exceptions during resource destruction +``` diff --git a/internal/service/eks/addon.go b/internal/service/eks/addon.go index 9a62d088523..2bcbe38c163 100644 --- a/internal/service/eks/addon.go +++ b/internal/service/eks/addon.go @@ -90,6 +90,23 @@ func resourceAddon() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "pod_identity_association": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrRoleARN: { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "service_account": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, "preserve": { Type: schema.TypeBool, Optional: true, @@ -148,6 +165,10 @@ func resourceAddonCreate(ctx context.Context, d *schema.ResourceData, meta inter input.ConfigurationValues = aws.String(v.(string)) } + if v, ok := d.GetOk("pod_identity_association"); ok && v.(*schema.Set).Len() > 0 { + input.PodIdentityAssociations = expandAddonPodIdentityAssociations(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("resolve_conflicts"); ok { input.ResolveConflicts = types.ResolveConflicts(v.(string)) } else if v, ok := d.GetOk("resolve_conflicts_on_create"); ok { @@ -226,6 +247,13 @@ func resourceAddonRead(ctx context.Context, d *schema.ResourceData, meta interfa d.Set("configuration_values", addon.ConfigurationValues) d.Set(names.AttrCreatedAt, aws.ToTime(addon.CreatedAt).Format(time.RFC3339)) d.Set("modified_at", aws.ToTime(addon.ModifiedAt).Format(time.RFC3339)) + flatPIAs, err := flattenAddonPodIdentityAssociations(ctx, addon.PodIdentityAssociations, clusterName, meta) + if err != nil { + return sdkdiag.AppendErrorf(diags, "flattening pod_identity_association: %s", err) + } + if err := d.Set("pod_identity_association", flatPIAs); err != nil { + return sdkdiag.AppendErrorf(diags, "setting pod_identity_association: %s", err) + } d.Set("service_account_role_arn", addon.ServiceAccountRoleArn) setTagsOut(ctx, addon.Tags) @@ -257,6 +285,12 @@ func resourceAddonUpdate(ctx context.Context, d *schema.ResourceData, meta inter input.ConfigurationValues = aws.String(d.Get("configuration_values").(string)) } + if d.HasChange("pod_identity_association") { + if v, ok := d.GetOk("pod_identity_association"); ok && len(v.([]interface{})) > 0 { + input.PodIdentityAssociations = expandAddonPodIdentityAssociations(v.([]interface{})) + } + } + var conflictResolutionAttr string var conflictResolution types.ResolveConflicts @@ -319,6 +353,10 @@ func resourceAddonDelete(ctx context.Context, d *schema.ResourceData, meta inter log.Printf("[DEBUG] Deleting EKS Add-On: %s", d.Id()) _, err = conn.DeleteAddon(ctx, input) + if errs.IsA[*types.ResourceNotFoundException](err) { + return diags + } + if err != nil { return sdkdiag.AppendErrorf(diags, "deleting EKS Add-On (%s): %s", d.Id(), err) } @@ -330,6 +368,67 @@ func resourceAddonDelete(ctx context.Context, d *schema.ResourceData, meta inter return diags } +func expandAddonPodIdentityAssociations(tfList []interface{}) []types.AddonPodIdentityAssociations { + if len(tfList) == 0 { + return nil + } + + var addonPodIdentityAssociations []types.AddonPodIdentityAssociations + for _, raw := range tfList { + tfMap, ok := raw.(map[string]interface{}) + if !ok { + continue + } + + pia := types.AddonPodIdentityAssociations{} + if roleArn, ok := tfMap[names.AttrRoleARN].(string); ok { + pia.RoleArn = aws.String(roleArn) + } + if service_account, ok := tfMap["service_account"].(string); ok { + pia.ServiceAccount = aws.String(service_account) + } + + addonPodIdentityAssociations = append(addonPodIdentityAssociations, pia) + } + + return addonPodIdentityAssociations +} + +func flattenAddonPodIdentityAssociations(ctx context.Context, associations []string, clusterName string, meta interface{}) ([]interface{}, error) { + if len(associations) == 0 { + return nil, nil + } + + conn := meta.(*conns.AWSClient).EKSClient(ctx) + var results []interface{} + + for _, associationArn := range associations { + // CreateAddon returns the associationARN. The associationId is extracted from the end of the ARN, + // which is used in the DescribePodIdentityAssociation call to get the RoleARN and ServiceAccount + // + // Ex. "arn:aws:eks:::podidentityassociation//a-1v95i5dqqiylbo3ud" + parts := strings.Split(associationArn, "/") + if len(parts) != 3 { + return nil, fmt.Errorf(`unable to extract association ID from ARN "%s"`, associationArn) + } + + associationId := parts[2] + pia, err := findPodIdentityAssociationByTwoPartKey(ctx, conn, associationId, clusterName) + if err != nil { + return nil, err + } + + tfMap := map[string]interface{}{ + names.AttrRoleARN: pia.RoleArn, + "service_account": pia.ServiceAccount, + } + + results = append(results, tfMap) + } + + return results, nil +} + func findAddonByTwoPartKey(ctx context.Context, conn *eks.Client, clusterName, addonName string) (*types.Addon, error) { input := &eks.DescribeAddonInput{ AddonName: aws.String(addonName), diff --git a/internal/service/eks/addon_data_source.go b/internal/service/eks/addon_data_source.go index ad49554ddbc..6b1f37cd998 100644 --- a/internal/service/eks/addon_data_source.go +++ b/internal/service/eks/addon_data_source.go @@ -52,6 +52,22 @@ func dataSourceAddon() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "pod_identity_association": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrRoleARN: { + Type: schema.TypeString, + Computed: true, + }, + "service_account": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "service_account_role_arn": { Type: schema.TypeString, Computed: true, @@ -83,6 +99,13 @@ func dataSourceAddonRead(ctx context.Context, d *schema.ResourceData, meta inter d.Set("configuration_values", addon.ConfigurationValues) d.Set(names.AttrCreatedAt, aws.ToTime(addon.CreatedAt).Format(time.RFC3339)) d.Set("modified_at", aws.ToTime(addon.ModifiedAt).Format(time.RFC3339)) + flatPIAs, err := flattenAddonPodIdentityAssociations(ctx, addon.PodIdentityAssociations, clusterName, meta) + if err != nil { + return sdkdiag.AppendErrorf(diags, "flattening pod_identity_association: %s", err) + } + if err := d.Set("pod_identity_association", flatPIAs); err != nil { + return sdkdiag.AppendErrorf(diags, "setting pod_identity_association: %s", err) + } d.Set("service_account_role_arn", addon.ServiceAccountRoleArn) if err := d.Set(names.AttrTags, KeyValueTags(ctx, addon.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { diff --git a/internal/service/eks/addon_data_source_test.go b/internal/service/eks/addon_data_source_test.go index d288bb46ebd..ade79863f9a 100644 --- a/internal/service/eks/addon_data_source_test.go +++ b/internal/service/eks/addon_data_source_test.go @@ -35,6 +35,7 @@ func TestAccEKSAddonDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "configuration_values", dataSourceResourceName, "configuration_values"), resource.TestCheckResourceAttrPair(resourceName, names.AttrCreatedAt, dataSourceResourceName, names.AttrCreatedAt), resource.TestCheckResourceAttrPair(resourceName, "modified_at", dataSourceResourceName, "modified_at"), + resource.TestCheckResourceAttr(resourceName, "pod_identity_associations.#", "0"), resource.TestCheckResourceAttrPair(resourceName, "service_account_role_arn", dataSourceResourceName, "service_account_role_arn"), resource.TestCheckResourceAttrPair(resourceName, acctest.CtTagsPercent, dataSourceResourceName, acctest.CtTagsPercent), ), diff --git a/internal/service/eks/addon_test.go b/internal/service/eks/addon_test.go index a79be5a0b06..71fec2a46d2 100644 --- a/internal/service/eks/addon_test.go +++ b/internal/service/eks/addon_test.go @@ -44,6 +44,7 @@ func TestAccEKSAddon_basic(t *testing.T) { acctest.MatchResourceAttrRegionalARN(addonResourceName, names.AttrARN, "eks", regexache.MustCompile(fmt.Sprintf("addon/%s/%s/.+$", rName, addonName))), resource.TestCheckResourceAttrPair(addonResourceName, names.AttrClusterName, clusterResourceName, names.AttrName), resource.TestCheckResourceAttr(addonResourceName, "configuration_values", ""), + resource.TestCheckResourceAttr(addonResourceName, "pod_identity_association.#", "0"), resource.TestCheckNoResourceAttr(addonResourceName, "preserve"), resource.TestCheckResourceAttr(addonResourceName, acctest.CtTagsPercent, "0"), ), @@ -114,8 +115,8 @@ func TestAccEKSAddon_addonVersion(t *testing.T) { rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_eks_addon.test" addonName := "vpc-cni" - addonVersion1 := "v1.14.1-eksbuild.1" - addonVersion2 := "v1.15.3-eksbuild.1" + addonVersion1 := "v1.17.1-eksbuild.1" + addonVersion2 := "v1.18.5-eksbuild.1" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t); testAccPreCheckAddon(ctx, t) }, @@ -273,7 +274,7 @@ func TestAccEKSAddon_serviceAccountRoleARN(t *testing.T) { var addon types.Addon rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_eks_addon.test" - serviceRoleResourceName := "aws_iam_role.test-service-role" + serviceRoleResourceName := "aws_iam_role.test_service_role" addonName := "vpc-cni" resource.ParallelTest(t, resource.TestCase{ @@ -308,7 +309,7 @@ func TestAccEKSAddon_configurationValues(t *testing.T) { emptyConfigurationValues := "{}" invalidConfigurationValues := "{\"env\": {\"INVALID_FIELD\":\"2\"}}" addonName := "vpc-cni" - addonVersion := "v1.15.3-eksbuild.1" + addonVersion := "v1.17.1-eksbuild.1" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t); testAccPreCheckAddon(ctx, t) }, @@ -351,6 +352,39 @@ func TestAccEKSAddon_configurationValues(t *testing.T) { }) } +func TestAccEKSAddon_podIdentityAssociation(t *testing.T) { + ctx := acctest.Context(t) + var addon types.Addon + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_eks_addon.test" + podIdentityRoleResourceName := "aws_iam_role.test_pod_identity" + addonName := "vpc-cni" + serviceAccount := "aws-node" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t); testAccPreCheckAddon(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EKSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAddonDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAddonConfig_podIdentityAssociation(rName, addonName, serviceAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckAddonExists(ctx, resourceName, &addon), + resource.TestCheckResourceAttr(resourceName, "pod_identity_association.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "pod_identity_association.0.role_arn", podIdentityRoleResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "pod_identity_association.0.service_account", serviceAccount), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccEKSAddon_tags(t *testing.T) { ctx := acctest.Context(t) var addon1, addon2, addon3 types.Addon @@ -575,6 +609,44 @@ resource "aws_eks_addon" "test" { `, rName, addonName, resolveConflicts)) } +func testAccAddonConfig_podIdentityAssociation(rName, addonName, serviceAccount string) string { + return acctest.ConfigCompose(testAccAddonConfig_base(rName), fmt.Sprintf(` +resource "aws_iam_role" "test_pod_identity" { + name = "test-pod-identity" + assume_role_policy = <