From 34257a4a607abbc79c36a9a701c0a57e478b8ffa Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:07:04 +0200 Subject: [PATCH 1/6] chore: r/profile cleanup --- .../service/rolesanywhere/exports_test.go | 13 ++++ internal/service/rolesanywhere/find.go | 67 ------------------- internal/service/rolesanywhere/flex.go | 16 ----- internal/service/rolesanywhere/profile.go | 48 ++++++++++--- .../rolesanywhere/service_package_gen.go | 4 +- 5 files changed, 52 insertions(+), 96 deletions(-) create mode 100644 internal/service/rolesanywhere/exports_test.go delete mode 100644 internal/service/rolesanywhere/find.go delete mode 100644 internal/service/rolesanywhere/flex.go diff --git a/internal/service/rolesanywhere/exports_test.go b/internal/service/rolesanywhere/exports_test.go new file mode 100644 index 00000000000..aef41ac745b --- /dev/null +++ b/internal/service/rolesanywhere/exports_test.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package rolesanywhere + +// Exports for use in tests only. +var ( + ResourceProfile = resourceProfile + ResourceTrustAnchor = resourceTrustAnchor + + FindProfileByID = findProfileByID + FindTrustAnchorByID = findTrustAnchorByID +) diff --git a/internal/service/rolesanywhere/find.go b/internal/service/rolesanywhere/find.go deleted file mode 100644 index 6d26f5c5078..00000000000 --- a/internal/service/rolesanywhere/find.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package rolesanywhere - -import ( - "context" - "errors" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" - "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -func FindProfileByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*types.ProfileDetail, error) { - in := &rolesanywhere.GetProfileInput{ - ProfileId: aws.String(id), - } - - out, err := conn.GetProfile(ctx, in) - - var resourceNotFoundException *types.ResourceNotFoundException - if errors.As(err, &resourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: in, - } - } - - if err != nil { - return nil, err - } - - if out == nil || out.Profile == nil { - return nil, tfresource.NewEmptyResultError(in) - } - - return out.Profile, nil -} - -func FindTrustAnchorByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*types.TrustAnchorDetail, error) { - in := &rolesanywhere.GetTrustAnchorInput{ - TrustAnchorId: aws.String(id), - } - - out, err := conn.GetTrustAnchor(ctx, in) - - var resourceNotFoundException *types.ResourceNotFoundException - if errors.As(err, &resourceNotFoundException) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: in, - } - } - - if err != nil { - return nil, err - } - - if out == nil || out.TrustAnchor == nil { - return nil, tfresource.NewEmptyResultError(in) - } - - return out.TrustAnchor, nil -} diff --git a/internal/service/rolesanywhere/flex.go b/internal/service/rolesanywhere/flex.go deleted file mode 100644 index 72500c467bd..00000000000 --- a/internal/service/rolesanywhere/flex.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package rolesanywhere - -func expandStringList(tfList []interface{}) []string { - var result []string - - for _, rawVal := range tfList { - if v, ok := rawVal.(string); ok && v != "" { - result = append(result, v) - } - } - - return result -} diff --git a/internal/service/rolesanywhere/profile.go b/internal/service/rolesanywhere/profile.go index c60d2a0eeba..f99726ca284 100644 --- a/internal/service/rolesanywhere/profile.go +++ b/internal/service/rolesanywhere/profile.go @@ -5,16 +5,18 @@ package rolesanywhere import ( "context" - "errors" "log" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" - "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" + awstypes "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -23,7 +25,7 @@ import ( // @SDKResource("aws_rolesanywhere_profile", name="Profile") // @Tags(identifierAttribute="arn") -func ResourceProfile() *schema.Resource { +func resourceProfile() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceProfileCreate, ReadWithoutTimeout: resourceProfileRead, @@ -64,7 +66,7 @@ func ResourceProfile() *schema.Resource { }, "role_arns": { Type: schema.TypeSet, - Required: true, + Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, }, @@ -87,7 +89,7 @@ func resourceProfileCreate(ctx context.Context, d *schema.ResourceData, meta int name := d.Get(names.AttrName).(string) input := &rolesanywhere.CreateProfileInput{ Name: aws.String(name), - RoleArns: expandStringList(d.Get("role_arns").(*schema.Set).List()), + RoleArns: flex.ExpandStringValueSet(d.Get("role_arns").(*schema.Set)), Tags: getTagsIn(ctx), } @@ -100,7 +102,7 @@ func resourceProfileCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.GetOk("managed_policy_arns"); ok { - input.ManagedPolicyArns = expandStringList(v.(*schema.Set).List()) + input.ManagedPolicyArns = flex.ExpandStringValueSet(v.(*schema.Set)) } if v, ok := d.GetOk("require_instance_properties"); ok { @@ -127,7 +129,7 @@ func resourceProfileRead(ctx context.Context, d *schema.ResourceData, meta inter var diags diag.Diagnostics conn := meta.(*conns.AWSClient).RolesAnywhereClient(ctx) - profile, err := FindProfileByID(ctx, conn, d.Id()) + profile, err := findProfileByID(ctx, conn, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] RolesAnywhere Profile (%s) not found, removing from state", d.Id()) @@ -165,7 +167,7 @@ func resourceProfileUpdate(ctx context.Context, d *schema.ResourceData, meta int } if d.HasChange("managed_policy_arns") { - input.ManagedPolicyArns = expandStringList(d.Get("managed_policy_arns").(*schema.Set).List()) + input.ManagedPolicyArns = flex.ExpandStringValueSet(d.Get("managed_policy_arns").(*schema.Set)) } if d.HasChange(names.AttrName) { @@ -173,7 +175,7 @@ func resourceProfileUpdate(ctx context.Context, d *schema.ResourceData, meta int } if d.HasChange("role_arns") { - input.RoleArns = expandStringList(d.Get("role_arns").(*schema.Set).List()) + input.RoleArns = flex.ExpandStringValueSet(d.Get("role_arns").(*schema.Set)) } if d.HasChange("session_policy") { @@ -214,8 +216,7 @@ func resourceProfileDelete(ctx context.Context, d *schema.ResourceData, meta int ProfileId: aws.String(d.Id()), }) - var resourceNotFoundException *types.ResourceNotFoundException - if errors.As(err, &resourceNotFoundException) { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { return diags } @@ -226,6 +227,31 @@ func resourceProfileDelete(ctx context.Context, d *schema.ResourceData, meta int return diags } +func findProfileByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*awstypes.ProfileDetail, error) { + in := &rolesanywhere.GetProfileInput{ + ProfileId: aws.String(id), + } + + out, err := conn.GetProfile(ctx, in) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil || out.Profile == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.Profile, nil +} + func disableProfile(ctx context.Context, profileId string, meta interface{}) error { conn := meta.(*conns.AWSClient).RolesAnywhereClient(ctx) diff --git a/internal/service/rolesanywhere/service_package_gen.go b/internal/service/rolesanywhere/service_package_gen.go index 6ed74afb30d..05183b5675c 100644 --- a/internal/service/rolesanywhere/service_package_gen.go +++ b/internal/service/rolesanywhere/service_package_gen.go @@ -29,7 +29,7 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePackageSDKResource { return []*types.ServicePackageSDKResource{ { - Factory: ResourceProfile, + Factory: resourceProfile, TypeName: "aws_rolesanywhere_profile", Name: "Profile", Tags: &types.ServicePackageResourceTags{ @@ -37,7 +37,7 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka }, }, { - Factory: ResourceTrustAnchor, + Factory: resourceTrustAnchor, TypeName: "aws_rolesanywhere_trust_anchor", Name: "Trust Anchor", Tags: &types.ServicePackageResourceTags{ From 17956526335ecb4a2214f0599b22f3e25b7bea4f Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:08:16 +0200 Subject: [PATCH 2/6] fix: make empty roleArns optional and set empty list to API for https://github.com/hashicorp/terraform-provider-aws/issues/39102 --- .../service/rolesanywhere/profile_test.go | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/internal/service/rolesanywhere/profile_test.go b/internal/service/rolesanywhere/profile_test.go index 10150e1797c..a001f36f62d 100644 --- a/internal/service/rolesanywhere/profile_test.go +++ b/internal/service/rolesanywhere/profile_test.go @@ -49,6 +49,45 @@ func TestAccRolesAnywhereProfile_basic(t *testing.T) { }) } +func TestAccRolesAnywhereProfile_noRoleArns(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + roleName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rolesanywhere_profile.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RolesAnywhereServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProfileDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccProfileConfig_noRoleArns(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "duration_seconds", "3600"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProfileConfig_basic(rName, roleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProfileExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "role_arns.#", acctest.Ct1), + acctest.CheckResourceAttrGlobalARN(resourceName, "role_arns.0", "iam", fmt.Sprintf("role/%s", roleName)), + resource.TestCheckResourceAttr(resourceName, "duration_seconds", "3600"), + ), + }, + }, + }) +} + func TestAccRolesAnywhereProfile_tags(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -245,6 +284,14 @@ resource "aws_rolesanywhere_profile" "test" { `, rName)) } +func testAccProfileConfig_noRoleArns(rName string) string { + return fmt.Sprintf(` +resource "aws_rolesanywhere_profile" "test" { + name = %[1]q +} +`, rName) +} + func testAccProfileConfig_tags1(rName, roleName, tag, value string) string { return acctest.ConfigCompose( testAccProfileConfig_base(roleName), From 98332218e129f25c75312a3518cfbfcf03138d81 Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:08:36 +0200 Subject: [PATCH 3/6] feat: add `notification_settings` to r/trust_anchor and cleanup --- .../service/rolesanywhere/trust_anchor.go | 260 +++++++++++++++--- .../rolesanywhere/trust_anchor_test.go | 74 ++++- 2 files changed, 297 insertions(+), 37 deletions(-) diff --git a/internal/service/rolesanywhere/trust_anchor.go b/internal/service/rolesanywhere/trust_anchor.go index c83f52f3e6c..2d325ad9c67 100644 --- a/internal/service/rolesanywhere/trust_anchor.go +++ b/internal/service/rolesanywhere/trust_anchor.go @@ -5,16 +5,20 @@ package rolesanywhere import ( "context" - "errors" + "fmt" "log" + "reflect" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" - "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" + awstypes "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" @@ -22,9 +26,11 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) +const configuredByDefault string = "rolesanywhere.amazonaws.com" + // @SDKResource("aws_rolesanywhere_trust_anchor", name="Trust Anchor") // @Tags(identifierAttribute="arn") -func ResourceTrustAnchor() *schema.Resource { +func resourceTrustAnchor() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceTrustAnchorCreate, ReadWithoutTimeout: resourceTrustAnchorRead, @@ -49,6 +55,47 @@ func ResourceTrustAnchor() *schema.Resource { Type: schema.TypeString, Required: true, }, + "notification_settings": { + Type: schema.TypeSet, + Computed: true, + ForceNew: true, + Optional: true, + MaxItems: 50, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "channel": { + Type: schema.TypeString, + Computed: true, + ForceNew: true, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.NotificationChannel](), + }, + "configured_by": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrEnabled: { + Type: schema.TypeBool, + Computed: true, + ForceNew: true, + Optional: true, + }, + "event": { + Type: schema.TypeString, + Computed: true, + ForceNew: true, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.NotificationEvent](), + }, + "threshold": { + Type: schema.TypeInt, + Computed: true, + ForceNew: true, + Optional: true, + }, + }, + }, + }, names.AttrSource: { Type: schema.TypeList, Required: true, @@ -76,9 +123,9 @@ func ResourceTrustAnchor() *schema.Resource { }, }, names.AttrSourceType: { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(trustAnchorTypeValues(types.TrustAnchorType("").Values()...), false), + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: enum.Validate[awstypes.TrustAnchorType](), }, }, }, @@ -87,10 +134,51 @@ func ResourceTrustAnchor() *schema.Resource { names.AttrTagsAll: tftags.TagsSchemaComputed(), }, - CustomizeDiff: verify.SetTagsDiff, + CustomizeDiff: customdiff.Sequence( + verify.SetTagsDiff, + customizeDiffNotificationSettings, + ), } } +// The AWS API returns two default entries within notificationSettings. These entries are overwritten based on the notifications settings event when a user defines a notification setting with the same event type. +// Since notification settings cannot be updated, we need to force a resource recreation when they change, while at the same time allowing computed values. +// Because both computed and user-defined arguments need to be supported, a custom diff function is required to handle this. +// The function checks the diff, and if the difference is due to computed value change, the diff is suppressed based on the configuredBy attribute. +func customizeDiffNotificationSettings(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + oldSet, newSet := diff.GetChange("notification_settings") + + oldSetTyped, okOld := oldSet.(*schema.Set) + newSetTyped, okNew := newSet.(*schema.Set) + + if !okOld || !okNew { + return fmt.Errorf("unexpected type for notification_settings: oldSet: %T, newSet: %T", oldSet, newSet) + } + + oldList := oldSetTyped.List() + newList := newSetTyped.List() + + for _, obj1 := range oldList { + found := false + for _, obj2 := range newList { + if reflect.DeepEqual(obj1, obj2) { + found = true + break + } + } + if !found { + if object, okNew := obj1.(map[string]interface{}); okNew && object["configured_by"] == configuredByDefault { + if err := diff.Clear("notification_settings"); err != nil { + return err + } + break + } + } + } + + return nil +} + func resourceTrustAnchorCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).RolesAnywhereClient(ctx) @@ -103,6 +191,10 @@ func resourceTrustAnchorCreate(ctx context.Context, d *schema.ResourceData, meta Tags: getTagsIn(ctx), } + if v, ok := d.GetOk("notification_settings"); ok && v.(*schema.Set).Len() > 0 { + input.NotificationSettings = expandNotificationSettings(v.(*schema.Set).List()) + } + log.Printf("[DEBUG] Creating RolesAnywhere Trust Anchor (%s): %#v", d.Id(), input) output, err := conn.CreateTrustAnchor(ctx, input) @@ -119,7 +211,7 @@ func resourceTrustAnchorRead(ctx context.Context, d *schema.ResourceData, meta i var diags diag.Diagnostics conn := meta.(*conns.AWSClient).RolesAnywhereClient(ctx) - trustAnchor, err := FindTrustAnchorByID(ctx, conn, d.Id()) + trustAnchor, err := findTrustAnchorByID(ctx, conn, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] RolesAnywhere Trust Anchor (%s) not found, removing from state", d.Id()) @@ -135,6 +227,10 @@ func resourceTrustAnchorRead(ctx context.Context, d *schema.ResourceData, meta i d.Set(names.AttrEnabled, trustAnchor.Enabled) d.Set(names.AttrName, trustAnchor.Name) + if err := d.Set("notification_settings", flattenNotificationSettings(trustAnchor.NotificationSettings)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting source: %s", err) + } + if err := d.Set(names.AttrSource, flattenSource(trustAnchor.Source)); err != nil { return sdkdiag.AppendErrorf(diags, "setting source: %s", err) } @@ -186,8 +282,7 @@ func resourceTrustAnchorDelete(ctx context.Context, d *schema.ResourceData, meta TrustAnchorId: aws.String(d.Id()), }) - var resourceNotFoundException *types.ResourceNotFoundException - if errors.As(err, &resourceNotFoundException) { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { return diags } @@ -198,7 +293,32 @@ func resourceTrustAnchorDelete(ctx context.Context, d *schema.ResourceData, meta return diags } -func flattenSource(apiObject *types.Source) []interface{} { +func findTrustAnchorByID(ctx context.Context, conn *rolesanywhere.Client, id string) (*awstypes.TrustAnchorDetail, error) { + in := &rolesanywhere.GetTrustAnchorInput{ + TrustAnchorId: aws.String(id), + } + + out, err := conn.GetTrustAnchor(ctx, in) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil || out.TrustAnchor == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.TrustAnchor, nil +} + +func flattenSource(apiObject *awstypes.Source) []interface{} { if apiObject == nil { return nil } @@ -211,7 +331,7 @@ func flattenSource(apiObject *types.Source) []interface{} { return []interface{}{m} } -func flattenSourceData(apiObject types.SourceData) []interface{} { +func flattenSourceData(apiObject awstypes.SourceData) []interface{} { if apiObject == nil { return []interface{}{} } @@ -219,11 +339,11 @@ func flattenSourceData(apiObject types.SourceData) []interface{} { m := map[string]interface{}{} switch v := apiObject.(type) { - case *types.SourceDataMemberAcmPcaArn: + case *awstypes.SourceDataMemberAcmPcaArn: m["acm_pca_arn"] = v.Value - case *types.SourceDataMemberX509CertificateData: + case *awstypes.SourceDataMemberX509CertificateData: m["x509_certificate_data"] = v.Value - case *types.UnknownUnionMember: + case *awstypes.UnknownUnionMember: log.Println("unknown tag:", v.Tag) default: log.Println("union is nil or unknown type") @@ -232,7 +352,46 @@ func flattenSourceData(apiObject types.SourceData) []interface{} { return []interface{}{m} } -func expandSource(tfList []interface{}) *types.Source { +func flattenNotificationSettings(apiObjects []awstypes.NotificationSettingDetail) []map[string]interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []map[string]interface{} + + for _, apiObject := range apiObjects { + tfList = append(tfList, flattenNotificationSetting(&apiObject)) + } + + return tfList +} + +func flattenNotificationSetting(apiObject *awstypes.NotificationSettingDetail) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "channel": apiObject.Channel, + "event": apiObject.Event, + } + + if v := apiObject.ConfiguredBy; v != nil { + tfMap["configured_by"] = aws.ToString(v) + } + + if v := apiObject.Enabled; v != nil { + tfMap[names.AttrEnabled] = aws.ToBool(v) + } + + if v := apiObject.Threshold; v != nil { + tfMap["threshold"] = aws.ToInt32(v) + } + + return tfMap +} + +func expandSource(tfList []interface{}) *awstypes.Source { if len(tfList) == 0 || tfList[0] == nil { return nil } @@ -242,16 +401,16 @@ func expandSource(tfList []interface{}) *types.Source { return nil } - result := &types.Source{} + result := &awstypes.Source{} if v, ok := tfMap[names.AttrSourceType].(string); ok && v != "" { - result.SourceType = types.TrustAnchorType(v) + result.SourceType = awstypes.TrustAnchorType(v) } if v, ok := tfMap["source_data"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - if result.SourceType == types.TrustAnchorTypeAwsAcmPca { + if result.SourceType == awstypes.TrustAnchorTypeAwsAcmPca { result.SourceData = expandSourceDataACMPCA(v[0].(map[string]interface{})) - } else if result.SourceType == types.TrustAnchorTypeCertificateBundle { + } else if result.SourceType == awstypes.TrustAnchorTypeCertificateBundle { result.SourceData = expandSourceDataCertificateBundle(v[0].(map[string]interface{})) } } @@ -259,8 +418,49 @@ func expandSource(tfList []interface{}) *types.Source { return result } -func expandSourceDataACMPCA(tfMap map[string]interface{}) *types.SourceDataMemberAcmPcaArn { - result := &types.SourceDataMemberAcmPcaArn{} +func expandNotificationSettings(tfList []interface{}) []awstypes.NotificationSetting { + if len(tfList) == 0 { + return nil + } + + apiObjects := make([]awstypes.NotificationSetting, 0) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + if !ok { + continue + } + + apiObjects = append(apiObjects, expandNotificationSetting(tfMap)) + } + + return apiObjects +} + +func expandNotificationSetting(tfMap map[string]interface{}) awstypes.NotificationSetting { + apiObject := awstypes.NotificationSetting{} + + if v, ok := tfMap["channel"].(string); ok { + apiObject.Channel = awstypes.NotificationChannel(v) + } + + if v, ok := tfMap[names.AttrEnabled].(bool); ok { + apiObject.Enabled = aws.Bool(v) + } + + if v, ok := tfMap["event"].(string); ok { + apiObject.Event = awstypes.NotificationEvent(v) + } + + if v, ok := tfMap["threshold"].(int); ok { + apiObject.Threshold = aws.Int32(int32(v)) + } + + return apiObject +} + +func expandSourceDataACMPCA(tfMap map[string]interface{}) *awstypes.SourceDataMemberAcmPcaArn { + result := &awstypes.SourceDataMemberAcmPcaArn{} if v, ok := tfMap["acm_pca_arn"].(string); ok && v != "" { result.Value = v @@ -269,8 +469,8 @@ func expandSourceDataACMPCA(tfMap map[string]interface{}) *types.SourceDataMembe return result } -func expandSourceDataCertificateBundle(tfMap map[string]interface{}) *types.SourceDataMemberX509CertificateData { - result := &types.SourceDataMemberX509CertificateData{} +func expandSourceDataCertificateBundle(tfMap map[string]interface{}) *awstypes.SourceDataMemberX509CertificateData { + result := &awstypes.SourceDataMemberX509CertificateData{} if v, ok := tfMap["x509_certificate_data"].(string); ok && v != "" { result.Value = v @@ -300,13 +500,3 @@ func enableTrustAnchor(ctx context.Context, trustAnchorId string, meta interface _, err := conn.EnableTrustAnchor(ctx, input) return err } - -func trustAnchorTypeValues(input ...types.TrustAnchorType) []string { - var output []string - - for _, v := range input { - output = append(output, string(v)) - } - - return output -} diff --git a/internal/service/rolesanywhere/trust_anchor_test.go b/internal/service/rolesanywhere/trust_anchor_test.go index 7364ac8baec..91dc9a52131 100644 --- a/internal/service/rolesanywhere/trust_anchor_test.go +++ b/internal/service/rolesanywhere/trust_anchor_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" + awstypes "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -37,10 +38,11 @@ func TestAccRolesAnywhereTrustAnchor_basic(t *testing.T) { testAccCheckTrustAnchorExists(ctx, resourceName), resource.TestCheckResourceAttrSet(resourceName, names.AttrEnabled), resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "notification_settings.#", acctest.Ct2), resource.TestCheckResourceAttr(resourceName, "source.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "source.0.source_data.#", acctest.Ct1), resource.TestCheckResourceAttrPair(resourceName, "source.0.source_data.0.acm_pca_arn", "aws_acmpca_certificate_authority.test", names.AttrARN), - resource.TestCheckResourceAttr(resourceName, "source.0.source_type", "AWS_ACM_PCA"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_type", string(awstypes.TrustAnchorTypeAwsAcmPca)), ), }, { @@ -52,6 +54,50 @@ func TestAccRolesAnywhereTrustAnchor_basic(t *testing.T) { }) } +func TestAccRolesAnywhereTrustAnchor_notificationSettings(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + caCommonName := acctest.RandomDomainName() + resourceName := "aws_rolesanywhere_trust_anchor.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RolesAnywhereServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTrustAnchorDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTrustAnchorConfig_notificationSettings(rName, caCommonName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTrustAnchorExists(ctx, resourceName), + resource.TestCheckResourceAttrSet(resourceName, names.AttrEnabled), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "notification_settings.#", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "notification_settings.0.channel", string(awstypes.NotificationChannelAll)), + resource.TestCheckResourceAttrSet(resourceName, "notification_settings.0.configured_by"), + resource.TestCheckResourceAttr(resourceName, "notification_settings.0.enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "notification_settings.0.event", string(awstypes.NotificationEventCaCertificateExpiry)), + resource.TestCheckResourceAttr(resourceName, "notification_settings.0.threshold", "75"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTrustAnchorConfig_basic(rName, caCommonName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTrustAnchorExists(ctx, resourceName), + resource.TestCheckResourceAttrSet(resourceName, names.AttrEnabled), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "notification_settings.#", acctest.Ct2), + ), + }, + }, + }) +} + func TestAccRolesAnywhereTrustAnchor_tags(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -141,7 +187,7 @@ func TestAccRolesAnywhereTrustAnchor_certificateBundle(t *testing.T) { resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), resource.TestCheckResourceAttr(resourceName, "source.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "source.0.source_data.#", acctest.Ct1), - resource.TestCheckResourceAttr(resourceName, "source.0.source_type", "CERTIFICATE_BUNDLE"), + resource.TestCheckResourceAttr(resourceName, "source.0.source_type", string(awstypes.TrustAnchorTypeCertificateBundle)), ), }, { @@ -294,6 +340,30 @@ resource "aws_rolesanywhere_trust_anchor" "test" { `, rName)) } +func testAccTrustAnchorConfig_notificationSettings(rName, caCommonName string) string { + return acctest.ConfigCompose( + testAccTrustAnchorConfig_acmBase(caCommonName), + fmt.Sprintf(` +resource "aws_rolesanywhere_trust_anchor" "test" { + name = %[1]q + source { + source_data { + acm_pca_arn = aws_acmpca_certificate_authority.test.arn + } + source_type = "AWS_ACM_PCA" + } + + notification_settings { + enabled = true + event = "CA_CERTIFICATE_EXPIRY" + threshold = 75 + } + + depends_on = [aws_acmpca_certificate_authority_certificate.test] +} +`, rName)) +} + func testAccTrustAnchorConfig_tags1(rName, caCommonName, tag, value string) string { return acctest.ConfigCompose( testAccTrustAnchorConfig_acmBase(caCommonName), From b0ac5a9aa32782d8ffd7b1fcce765e36ff9a434e Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:29:38 +0200 Subject: [PATCH 4/6] chore: add changelog --- .changelog/39108.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/39108.txt diff --git a/.changelog/39108.txt b/.changelog/39108.txt new file mode 100644 index 00000000000..7cd35aa78c8 --- /dev/null +++ b/.changelog/39108.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_rolesanywhere_profile: Fix `role_arns` to make it optional and sent an empty list if unset +``` + +```release-note:enhancement +resource/aws_rolesanywhere_trust_anchor: Add `notification_settings` argument +``` \ No newline at end of file From d9462eaec71e6cc5755ee09787df36441d206176 Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:32:55 +0200 Subject: [PATCH 5/6] chore: change test names --- internal/service/rolesanywhere/profile_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/rolesanywhere/profile_test.go b/internal/service/rolesanywhere/profile_test.go index a001f36f62d..8dec5345fb5 100644 --- a/internal/service/rolesanywhere/profile_test.go +++ b/internal/service/rolesanywhere/profile_test.go @@ -49,7 +49,7 @@ func TestAccRolesAnywhereProfile_basic(t *testing.T) { }) } -func TestAccRolesAnywhereProfile_noRoleArns(t *testing.T) { +func TestAccRolesAnywhereProfile_noRoleARNs(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) roleName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -62,7 +62,7 @@ func TestAccRolesAnywhereProfile_noRoleArns(t *testing.T) { CheckDestroy: testAccCheckProfileDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccProfileConfig_noRoleArns(rName), + Config: testAccProfileConfig_noRoleARNs(rName), Check: resource.ComposeTestCheckFunc( testAccCheckProfileExists(ctx, resourceName), resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), @@ -284,7 +284,7 @@ resource "aws_rolesanywhere_profile" "test" { `, rName)) } -func testAccProfileConfig_noRoleArns(rName string) string { +func testAccProfileConfig_noRoleARNs(rName string) string { return fmt.Sprintf(` resource "aws_rolesanywhere_profile" "test" { name = %[1]q From d3b2702b0fd495a5acafcabd46781974120d01ed Mon Sep 17 00:00:00 2001 From: Daniel Rieske Date: Sun, 1 Sep 2024 12:35:43 +0200 Subject: [PATCH 6/6] chore: change `customizeDiffNotificationSettings` comment --- internal/service/rolesanywhere/trust_anchor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/rolesanywhere/trust_anchor.go b/internal/service/rolesanywhere/trust_anchor.go index 2d325ad9c67..84b5f4d6ad2 100644 --- a/internal/service/rolesanywhere/trust_anchor.go +++ b/internal/service/rolesanywhere/trust_anchor.go @@ -141,7 +141,7 @@ func resourceTrustAnchor() *schema.Resource { } } -// The AWS API returns two default entries within notificationSettings. These entries are overwritten based on the notifications settings event when a user defines a notification setting with the same event type. +// The AWS API returns two default entries for notification_settings. These entries are overwritten based on if a user defines a notification setting with the same event type. // Since notification settings cannot be updated, we need to force a resource recreation when they change, while at the same time allowing computed values. // Because both computed and user-defined arguments need to be supported, a custom diff function is required to handle this. // The function checks the diff, and if the difference is due to computed value change, the diff is suppressed based on the configuredBy attribute.