From 208ac08527f6ad10e45434c777b8ef2c61bcb86d Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 28 Nov 2023 16:24:29 +0100 Subject: [PATCH 01/42] feat: implement `aws_controltower_landing_zone` --- internal/service/controltower/landing_zone.go | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 internal/service/controltower/landing_zone.go diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go new file mode 100644 index 00000000000..31263f01cc5 --- /dev/null +++ b/internal/service/controltower/landing_zone.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package controltower + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_controltower_landing_zone", name="Landing Zone") +func resourceLandingZone() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceLandingZoneCreate, + ReadWithoutTimeout: resourceLandingZoneRead, + DeleteWithoutTimeout: resourceLandingZoneDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(120 * time.Minute), + Delete: schema.DefaultTimeout(120 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "control_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + "target_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + }, + } +} + +func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + return resourceLandingZoneRead(ctx, d, meta) +} + +func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + return nil +} + +func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + return nil +} From 0aa03053e95943ef93766357a318a8b5f9efca4a Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 28 Nov 2023 23:47:22 +0100 Subject: [PATCH 02/42] feat: implement `aws_controltower_landing_zone` --- internal/service/controltower/generate.go | 1 + internal/service/controltower/landing_zone.go | 176 ++++++++++++++++-- .../controltower/service_package_gen.go | 5 + internal/service/controltower/tags_gen.go | 137 ++++++++++++++ 4 files changed, 307 insertions(+), 12 deletions(-) create mode 100644 internal/service/controltower/tags_gen.go diff --git a/internal/service/controltower/generate.go b/internal/service/controltower/generate.go index 5bed2ff12f6..c196b2488bd 100644 --- a/internal/service/controltower/generate.go +++ b/internal/service/controltower/generate.go @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 //go:generate go run ../../generate/servicepackage/main.go +//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ServiceTagsMap -KVTValues -SkipTypesImp -ListTags -CreateTags -UpdateTags // ONLY generate directives and package declaration! Do not add anything else to this file. package controltower diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 31263f01cc5..23eb348ab4b 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -5,12 +5,24 @@ package controltower import ( "context" + "errors" + "log" + "strings" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/aws/aws-sdk-go-v2/service/controltower" + "github.com/aws/aws-sdk-go-v2/service/controltower/document" + "github.com/aws/aws-sdk-go-v2/service/controltower/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/verify" + "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" + "github.com/hashicorp/terraform-provider-aws/names" ) // @SDKResource("aws_controltower_landing_zone", name="Landing Zone") @@ -30,36 +42,176 @@ func resourceLandingZone() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "control_identifier": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: verify.ValidARN, + "arn": { + Type: schema.TypeString, + Computed: true, }, - "target_identifier": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: verify.ValidARN, + "manifest": { + Type: schema.TypeString, + Required: true, + ForceNew: true, }, + "version": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), }, } } func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - return resourceLandingZoneRead(ctx, d, meta) + manifest := d.Get("manifest").(string) + version := d.Get("version").(string) + input := &controltower.CreateLandingZoneInput{ + Manifest: document.NewLazyDocument(manifest), + Version: aws.String(version), + } + + output, err := conn.CreateLandingZone(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating Control Tower Landing Zone: %s", err) + } + + identifier, err := getLandingZoneIdentifierFromARN(*output.Arn) + if err != nil { + return sdkdiag.AppendErrorf(diags, "getting identifier from ARN: %s", err) + } + + d.SetId(*aws.String(identifier)) + + if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for ControlTower Landing Zone (%s) create: %s", d.Id(), err) + } + + return append(diags, resourceLandingZoneRead(ctx, d, meta)...) } func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + input := &controltower.GetLandingZoneInput{ + LandingZoneIdentifier: aws.String(d.Id()), + } + + output, err := conn.GetLandingZone(ctx, input) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Control Tower Landing Zone (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading Control Tower Landing Zone: %s", err) + } + + d.Set("manifest", output.LandingZone.Manifest) + d.Set("version", output.LandingZone.Version) + d.Set("arn", output.LandingZone.Arn) + return nil } func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + input := &controltower.DeleteLandingZoneInput{ + LandingZoneIdentifier: aws.String(d.Id()), + } + + output, err := conn.DeleteLandingZone(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting Control Tower Landing Zone: %s", err) + } + + if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutDelete)); err != nil { + return diag.Errorf("waiting for Control Tower Landing Zone (%s) delete: %s", d.Id(), err) + } + return nil } + +// gets the landing zone identifier from the ARN +func getLandingZoneIdentifierFromARN(arnString string) (string, error) { + arn, err := arn.Parse(arnString) + if err != nil { + return "", err + } + resourceParts := strings.Split(arn.Resource, "/") + return resourceParts[len(resourceParts)-1], nil +} + +func findLandingZoneOperationDetailsByID(ctx context.Context, conn *controltower.Client, id string) (*types.LandingZoneOperationDetail, error) { + input := &controltower.GetLandingZoneOperationInput{ + OperationIdentifier: aws.String(id), + } + + output, err := conn.GetLandingZoneOperation(ctx, input) + + if tfresource.NotFound(err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.OperationDetails == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.OperationDetails, nil +} + +func statusLandingZoneOperation(ctx context.Context, conn *controltower.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findLandingZoneOperationDetailsByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.Client, id string, timeout time.Duration) (*types.LandingZoneOperationDetail, error) { //nolint:unparam + stateConf := &retry.StateChangeConf{ + Pending: []string{string(types.LandingZoneOperationStatusInProgress)}, + Target: []string{string(types.LandingZoneOperationStatusSucceeded)}, + Refresh: statusLandingZoneOperation(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*types.LandingZoneOperationDetail); ok { + if status := output.Status; status == types.LandingZoneOperationStatusFailed { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} diff --git a/internal/service/controltower/service_package_gen.go b/internal/service/controltower/service_package_gen.go index f9c58694ff9..137e6403673 100644 --- a/internal/service/controltower/service_package_gen.go +++ b/internal/service/controltower/service_package_gen.go @@ -39,6 +39,11 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka TypeName: "aws_controltower_control", Name: "Control", }, + { + Factory: resourceLandingZone, + TypeName: "aws_controltower_landing_zone", + Name: "Landing Zone", + }, } } diff --git a/internal/service/controltower/tags_gen.go b/internal/service/controltower/tags_gen.go new file mode 100644 index 00000000000..4182d4cdd0c --- /dev/null +++ b/internal/service/controltower/tags_gen.go @@ -0,0 +1,137 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package controltower + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/controltower" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/logging" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// listTags lists controltower service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func listTags(ctx context.Context, conn *controltower.Client, identifier string) (tftags.KeyValueTags, error) { + input := &controltower.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(ctx, input) + + if err != nil { + return tftags.New(ctx, nil), err + } + + return KeyValueTags(ctx, output.Tags), nil +} + +// ListTags lists controltower service tags and set them in Context. +// It is called from outside this package. +func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier string) error { + tags, err := listTags(ctx, meta.(*conns.AWSClient).ControlTowerClient(ctx), identifier) + + if err != nil { + return err + } + + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = types.Some(tags) + } + + return nil +} + +// map[string]string handling + +// Tags returns controltower service tags. +func Tags(tags tftags.KeyValueTags) map[string]string { + return tags.Map() +} + +// KeyValueTags creates tftags.KeyValueTags from controltower service tags. +func KeyValueTags(ctx context.Context, tags map[string]string) tftags.KeyValueTags { + return tftags.New(ctx, tags) +} + +// getTagsIn returns controltower service tags from Context. +// nil is returned if there are no input tags. +func getTagsIn(ctx context.Context) map[string]string { + if inContext, ok := tftags.FromContext(ctx); ok { + if tags := Tags(inContext.TagsIn.UnwrapOrDefault()); len(tags) > 0 { + return tags + } + } + + return nil +} + +// setTagsOut sets controltower service tags in Context. +func setTagsOut(ctx context.Context, tags map[string]string) { + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = types.Some(KeyValueTags(ctx, tags)) + } +} + +// createTags creates controltower service tags for new resources. +func createTags(ctx context.Context, conn *controltower.Client, identifier string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + + return updateTags(ctx, conn, identifier, nil, tags) +} + +// updateTags updates controltower service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func updateTags(ctx context.Context, conn *controltower.Client, identifier string, oldTagsMap, newTagsMap any) error { + oldTags := tftags.New(ctx, oldTagsMap) + newTags := tftags.New(ctx, newTagsMap) + + ctx = tflog.SetField(ctx, logging.KeyResourceId, identifier) + + removedTags := oldTags.Removed(newTags) + removedTags = removedTags.IgnoreSystem(names.ControlTower) + if len(removedTags) > 0 { + input := &controltower.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: removedTags.Keys(), + } + + _, err := conn.UntagResource(ctx, input) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + updatedTags := oldTags.Updated(newTags) + updatedTags = updatedTags.IgnoreSystem(names.ControlTower) + if len(updatedTags) > 0 { + input := &controltower.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags), + } + + _, err := conn.TagResource(ctx, input) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// UpdateTags updates controltower service tags. +// It is called from outside this package. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + return updateTags(ctx, meta.(*conns.AWSClient).ControlTowerClient(ctx), identifier, oldTags, newTags) +} From 942d4603f511edf13cda1db9b11e46759b57684a Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Tue, 28 Nov 2023 23:48:56 +0100 Subject: [PATCH 03/42] feat: add tags --- internal/service/controltower/landing_zone.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 23eb348ab4b..3426cdba560 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -72,6 +72,7 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta input := &controltower.CreateLandingZoneInput{ Manifest: document.NewLazyDocument(manifest), Version: aws.String(version), + Tags: getTagsIn(ctx), } output, err := conn.CreateLandingZone(ctx, input) From 18ba23df95393cb3b806d71bc79cbd5e7a8e0adb Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Fri, 8 Dec 2023 14:50:20 +0100 Subject: [PATCH 04/42] chore: add landing zone test --- .../fixtures/LandingZoneManifest.json | 30 +++++ .../service/controltower/landing_zone_test.go | 121 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 internal/service/controltower/fixtures/LandingZoneManifest.json create mode 100644 internal/service/controltower/landing_zone_test.go diff --git a/internal/service/controltower/fixtures/LandingZoneManifest.json b/internal/service/controltower/fixtures/LandingZoneManifest.json new file mode 100644 index 00000000000..d9a449ee2ee --- /dev/null +++ b/internal/service/controltower/fixtures/LandingZoneManifest.json @@ -0,0 +1,30 @@ +{ + "governedRegions": ["us-west-2", "us-west-1"], + "organizationStructure": { + "security": { + "name": "CORE" + }, + "sandbox": { + "name": "Sandbox" + } + }, + "centralizedLogging": { + "accountId": "222222222222", + "configurations": { + "loggingBucket": { + "retentionDays": 60 + }, + "accessLoggingBucket": { + "retentionDays": 60 + }, + "kmsKeyArn": "arn:aws:kms:us-west-1:123456789123:key/e84XXXXX-6bXX-49XX-9eXX-ecfXXXXXXXXX" + }, + "enabled": true + }, + "securityRoles": { + "accountId": "333333333333" + }, + "accessManagement": { + "enabled": true + } +} diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go new file mode 100644 index 00000000000..2c65fea7019 --- /dev/null +++ b/internal/service/controltower/landing_zone_test.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package controltower_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/controltower" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfcontroltower "github.com/hashicorp/terraform-provider-aws/internal/service/controltower" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccLandingZoneTowerLandingZone_serial(t *testing.T) { + t.Parallel() + + testCases := map[string]map[string]func(t *testing.T){ + "LandingZone": { + "basic": testAccLandingZone_basic, + "disappears": testAccLandingZone_disappears, + }, + } + + acctest.RunSerialTests2Levels(t, testCases, 0) +} + +func testAccLandingZone_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_controltower_landing_zone.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), + CheckDestroy: testAccCheckLandingZoneDestroy(ctx), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccLandingZoneConfig_basic("v1.0.0"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "version", "v1.0.0"), + ), + }, + }, + }) +} + +func testAccLandingZone_disappears(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_controltower_landing_zone.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLandingZoneDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLandingZoneConfig_basic("v1.0.0"), + Check: resource.ComposeTestCheckFunc( + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceControl(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ControlTowerClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_controltower_landing_zone" { + continue + } + + input := &controltower.GetLandingZoneInput{ + LandingZoneIdentifier: &rs.Primary.ID, + } + + _, err := conn.GetLandingZone(ctx, input) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Control Tower Landing Zone %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccLandingZoneConfig_basic(version string) string { + return fmt.Sprintf(` +resource "aws_controltower_landing_zone" "test" { + manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + version = "%[1]s" +} +`, version) +} From ea3817761d3a8f2d082be90fab60e7760520abfc Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Fri, 8 Dec 2023 16:47:55 +0100 Subject: [PATCH 05/42] chore: add changelog --- .changelog/34595.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/34595.txt diff --git a/.changelog/34595.txt b/.changelog/34595.txt new file mode 100644 index 00000000000..6476b00d734 --- /dev/null +++ b/.changelog/34595.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_controltower_landing_zone +``` From e58c446840ead5e9cb0ba05580304213a133323a Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Fri, 8 Dec 2023 17:12:26 +0100 Subject: [PATCH 06/42] chore: set to a working version --- internal/service/controltower/landing_zone_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 2c65fea7019..8f9308f0346 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -46,10 +46,10 @@ func testAccLandingZone_basic(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccLandingZoneConfig_basic("v1.0.0"), + Config: testAccLandingZoneConfig_basic("1.0"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "arn"), - resource.TestCheckResourceAttr(resourceName, "version", "v1.0.0"), + resource.TestCheckResourceAttr(resourceName, "version", "1.0"), ), }, }, @@ -71,7 +71,7 @@ func testAccLandingZone_disappears(t *testing.T) { CheckDestroy: testAccCheckLandingZoneDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLandingZoneConfig_basic("v1.0.0"), + Config: testAccLandingZoneConfig_basic("1.0"), Check: resource.ComposeTestCheckFunc( acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceControl(), resourceName), ), From 567a6eab00fc7e56e6335c2130f73ec4e06cad19 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Fri, 8 Dec 2023 17:12:42 +0100 Subject: [PATCH 07/42] chore: create controltower_landing_zone.html.markdown resource doc --- .../r/controltower_landing_zone.html.markdown | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 website/docs/r/controltower_landing_zone.html.markdown diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown new file mode 100644 index 00000000000..62b5fa3a758 --- /dev/null +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "Control Tower" +layout: "aws" +page_title: "AWS: aws_controltower_landing_zone" +description: |- + Creates a new landing zone using Control Tower. +--- + +# Resource: aws_controltower_landing_zone + +Creates a new landing zone using Control Tower. For more information on usage, please see the +[AWS Control Tower Landing Zone User Guide](https://docs.aws.amazon.com/controltower/latest/userguide/how-control-tower-works.html). + +## Example Usage + +```terraform +resource "aws_controltower_landing_zone" "example" { + manifest = jsondecode(file("${path.module}/LandingZoneManifest.json")) + version = "1.0" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `manifest` - (Required) The manifest JSON file is a text file that describes your AWS resources. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). +* `version` - (Required) The landing zone version, for example, 1.0. +* `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - The identifier of the landing zone. +* `arn` - The ARN of the landing zone. +* `tags_all` - A map of tags assigned to the landing zone, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import a Control Tower Landing Zone using the `id`. For example: + +```terraform +import { + to = aws_controltower_landing_zone.example + id = "1A2B3C4D5E6F7G8H" +} +``` + +Using `terraform import`, import a Control Tower Landing Zone using the `id`. For example: + +```console +% terraform import aws_controltower_landing_zone.example 1A2B3C4D5E6F7G8H +``` From dc13b886d58e82fd9db808184d7cb301e6e18e52 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 16:40:09 -0500 Subject: [PATCH 08/42] controltower: Regenerate tagging code. --- internal/service/controltower/generate.go | 2 +- internal/service/controltower/tags_gen.go | 25 ++++++++--------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/internal/service/controltower/generate.go b/internal/service/controltower/generate.go index c196b2488bd..e748557f89b 100644 --- a/internal/service/controltower/generate.go +++ b/internal/service/controltower/generate.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 //go:generate go run ../../generate/servicepackage/main.go -//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ServiceTagsMap -KVTValues -SkipTypesImp -ListTags -CreateTags -UpdateTags +//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ServiceTagsMap -KVTValues -SkipTypesImp -ListTags -UpdateTags // ONLY generate directives and package declaration! Do not add anything else to this file. package controltower diff --git a/internal/service/controltower/tags_gen.go b/internal/service/controltower/tags_gen.go index 4182d4cdd0c..6c9f2996afb 100644 --- a/internal/service/controltower/tags_gen.go +++ b/internal/service/controltower/tags_gen.go @@ -11,19 +11,19 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/logging" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" - "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/internal/types/option" "github.com/hashicorp/terraform-provider-aws/names" ) // listTags lists controltower service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func listTags(ctx context.Context, conn *controltower.Client, identifier string) (tftags.KeyValueTags, error) { +func listTags(ctx context.Context, conn *controltower.Client, identifier string, optFns ...func(*controltower.Options)) (tftags.KeyValueTags, error) { input := &controltower.ListTagsForResourceInput{ ResourceArn: aws.String(identifier), } - output, err := conn.ListTagsForResource(ctx, input) + output, err := conn.ListTagsForResource(ctx, input, optFns...) if err != nil { return tftags.New(ctx, nil), err @@ -42,7 +42,7 @@ func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier stri } if inContext, ok := tftags.FromContext(ctx); ok { - inContext.TagsOut = types.Some(tags) + inContext.TagsOut = option.Some(tags) } return nil @@ -75,23 +75,14 @@ func getTagsIn(ctx context.Context) map[string]string { // setTagsOut sets controltower service tags in Context. func setTagsOut(ctx context.Context, tags map[string]string) { if inContext, ok := tftags.FromContext(ctx); ok { - inContext.TagsOut = types.Some(KeyValueTags(ctx, tags)) + inContext.TagsOut = option.Some(KeyValueTags(ctx, tags)) } } -// createTags creates controltower service tags for new resources. -func createTags(ctx context.Context, conn *controltower.Client, identifier string, tags map[string]string) error { - if len(tags) == 0 { - return nil - } - - return updateTags(ctx, conn, identifier, nil, tags) -} - // updateTags updates controltower service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func updateTags(ctx context.Context, conn *controltower.Client, identifier string, oldTagsMap, newTagsMap any) error { +func updateTags(ctx context.Context, conn *controltower.Client, identifier string, oldTagsMap, newTagsMap any, optFns ...func(*controltower.Options)) error { oldTags := tftags.New(ctx, oldTagsMap) newTags := tftags.New(ctx, newTagsMap) @@ -105,7 +96,7 @@ func updateTags(ctx context.Context, conn *controltower.Client, identifier strin TagKeys: removedTags.Keys(), } - _, err := conn.UntagResource(ctx, input) + _, err := conn.UntagResource(ctx, input, optFns...) if err != nil { return fmt.Errorf("untagging resource (%s): %w", identifier, err) @@ -120,7 +111,7 @@ func updateTags(ctx context.Context, conn *controltower.Client, identifier strin Tags: Tags(updatedTags), } - _, err := conn.TagResource(ctx, input) + _, err := conn.TagResource(ctx, input, optFns...) if err != nil { return fmt.Errorf("tagging resource (%s): %w", identifier, err) From 006abd79c6044b890261e4955a6dcb3a604beab4 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 16:40:48 -0500 Subject: [PATCH 09/42] Fix semgrep 'ci.semgrep.aws.pointer-conversion-ResourceData-SetId'. --- internal/service/controltower/landing_zone.go | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 3426cdba560..efe107f23e5 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -64,15 +64,12 @@ func resourceLandingZone() *schema.Resource { func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - manifest := d.Get("manifest").(string) - version := d.Get("version").(string) input := &controltower.CreateLandingZoneInput{ - Manifest: document.NewLazyDocument(manifest), - Version: aws.String(version), + Manifest: document.NewLazyDocument(d.Get("manifest").(string)), Tags: getTagsIn(ctx), + Version: aws.String(d.Get("version").(string)), } output, err := conn.CreateLandingZone(ctx, input) @@ -81,15 +78,15 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta return sdkdiag.AppendErrorf(diags, "creating Control Tower Landing Zone: %s", err) } - identifier, err := getLandingZoneIdentifierFromARN(*output.Arn) + id, err := landingZoneIDFromARN(aws.ToString(output.Arn)) if err != nil { - return sdkdiag.AppendErrorf(diags, "getting identifier from ARN: %s", err) + return sdkdiag.AppendFromErr(diags, err) } - d.SetId(*aws.String(identifier)) + d.SetId(id) if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil { - return diag.Errorf("waiting for ControlTower Landing Zone (%s) create: %s", d.Id(), err) + return diag.Errorf("waiting for Control Tower Landing Zone (%s) create: %s", d.Id(), err) } return append(diags, resourceLandingZoneRead(ctx, d, meta)...) @@ -145,14 +142,14 @@ func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta return nil } -// gets the landing zone identifier from the ARN -func getLandingZoneIdentifierFromARN(arnString string) (string, error) { +func landingZoneIDFromARN(arnString string) (string, error) { arn, err := arn.Parse(arnString) if err != nil { return "", err } - resourceParts := strings.Split(arn.Resource, "/") - return resourceParts[len(resourceParts)-1], nil + + // arn:${Partition}:controltower:${Region}:${Account}:landingzone/${LandingZoneId} + return strings.TrimPrefix(arn.Resource, "landingzone/"), nil } func findLandingZoneOperationDetailsByID(ctx context.Context, conn *controltower.Client, id string) (*types.LandingZoneOperationDetail, error) { From 32e64b9cf71565fd816bcc387f2ec4c872da26bd Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 16:42:06 -0500 Subject: [PATCH 10/42] Fix semgrep 'ci.semgrep.pluginsdk.avoid-diag_Errorf'. --- internal/service/controltower/landing_zone.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index efe107f23e5..7db29386089 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -86,7 +86,7 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(id) if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil { - return diag.Errorf("waiting for Control Tower Landing Zone (%s) create: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for Control Tower Landing Zone (%s) create: %s", d.Id(), err) } return append(diags, resourceLandingZoneRead(ctx, d, meta)...) @@ -136,7 +136,7 @@ func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta } if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutDelete)); err != nil { - return diag.Errorf("waiting for Control Tower Landing Zone (%s) delete: %s", d.Id(), err) + sdkdiag.AppendErrorf(diags, "waiting for Control Tower Landing Zone (%s) delete: %s", d.Id(), err) } return nil From 85e1183d20df74deded24c041e7bd74a16fa9238 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 16:43:42 -0500 Subject: [PATCH 11/42] Fix semgrep 'ci.typed-enum-conversion'. --- internal/service/controltower/landing_zone.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 7db29386089..5c849331ba0 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -19,6 +19,7 @@ import ( "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/enum" "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" @@ -195,8 +196,8 @@ func statusLandingZoneOperation(ctx context.Context, conn *controltower.Client, func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.Client, id string, timeout time.Duration) (*types.LandingZoneOperationDetail, error) { //nolint:unparam stateConf := &retry.StateChangeConf{ - Pending: []string{string(types.LandingZoneOperationStatusInProgress)}, - Target: []string{string(types.LandingZoneOperationStatusSucceeded)}, + Pending: enum.Slice(types.LandingZoneOperationStatusInProgress), + Target: enum.Slice(types.LandingZoneOperationStatusSucceeded), Refresh: statusLandingZoneOperation(ctx, conn, id), Timeout: timeout, } From 4fb825df367361a9948b5a344fa2814e56047f60 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 16:56:06 -0500 Subject: [PATCH 12/42] Add 'FindLandingZoneByID'. --- internal/service/controltower/exports_test.go | 4 +- internal/service/controltower/landing_zone.go | 65 ++++++++++++------- .../service/controltower/landing_zone_test.go | 30 ++++++--- 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/internal/service/controltower/exports_test.go b/internal/service/controltower/exports_test.go index f5a047b800c..57ee78e0f1a 100644 --- a/internal/service/controltower/exports_test.go +++ b/internal/service/controltower/exports_test.go @@ -5,7 +5,9 @@ package controltower // Exports for use in tests only. var ( - ResourceControl = resourceControl + ResourceControl = resourceControl + ResourceLandingZone = resourceLandingZone FindEnabledControlByTwoPartKey = findEnabledControlByTwoPartKey + FindLandingZoneByID = findLandingZoneByID ) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 5c849331ba0..7beba729fd5 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "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" @@ -76,7 +77,7 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta output, err := conn.CreateLandingZone(ctx, input) if err != nil { - return sdkdiag.AppendErrorf(diags, "creating Control Tower Landing Zone: %s", err) + return sdkdiag.AppendErrorf(diags, "creating ControlTower Landing Zone: %s", err) } id, err := landingZoneIDFromARN(aws.ToString(output.Arn)) @@ -87,7 +88,7 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(id) if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for Control Tower Landing Zone (%s) create: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for ControlTower Landing Zone (%s) create: %s", d.Id(), err) } return append(diags, resourceLandingZoneRead(ctx, d, meta)...) @@ -95,49 +96,42 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - input := &controltower.GetLandingZoneInput{ - LandingZoneIdentifier: aws.String(d.Id()), - } - - output, err := conn.GetLandingZone(ctx, input) + landingZone, err := findLandingZoneByID(ctx, conn, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { - log.Printf("[WARN] Control Tower Landing Zone (%s) not found, removing from state", d.Id()) + log.Printf("[WARN] ControlTower Landing Zone (%s) not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Control Tower Landing Zone: %s", err) + return sdkdiag.AppendErrorf(diags, "reading ControlTower Landing Zone (%s): %s", d.Id(), err) } - d.Set("manifest", output.LandingZone.Manifest) - d.Set("version", output.LandingZone.Version) - d.Set("arn", output.LandingZone.Arn) + d.Set("arn", landingZone.Arn) + d.Set("manifest", landingZone.Manifest) + d.Set("version", landingZone.Version) return nil } func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - input := &controltower.DeleteLandingZoneInput{ + log.Printf("[DEBUG] Deleting ControlTower Landing Zone: %s", d.Id()) + output, err := conn.DeleteLandingZone(ctx, &controltower.DeleteLandingZoneInput{ LandingZoneIdentifier: aws.String(d.Id()), - } - - output, err := conn.DeleteLandingZone(ctx, input) + }) if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting Control Tower Landing Zone: %s", err) + return sdkdiag.AppendErrorf(diags, "deleting ControlTower Landing Zone: %s", err) } if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutDelete)); err != nil { - sdkdiag.AppendErrorf(diags, "waiting for Control Tower Landing Zone (%s) delete: %s", d.Id(), err) + sdkdiag.AppendErrorf(diags, "waiting for ControlTower Landing Zone (%s) delete: %s", d.Id(), err) } return nil @@ -153,14 +147,39 @@ func landingZoneIDFromARN(arnString string) (string, error) { return strings.TrimPrefix(arn.Resource, "landingzone/"), nil } -func findLandingZoneOperationDetailsByID(ctx context.Context, conn *controltower.Client, id string) (*types.LandingZoneOperationDetail, error) { +func findLandingZoneByID(ctx context.Context, conn *controltower.Client, id string) (*types.LandingZoneDetail, error) { + input := &controltower.GetLandingZoneInput{ + LandingZoneIdentifier: aws.String(id), + } + + output, err := conn.GetLandingZone(ctx, input) + + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.LandingZone == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.LandingZone, nil +} + +func findLandingZoneOperationByID(ctx context.Context, conn *controltower.Client, id string) (*types.LandingZoneOperationDetail, error) { input := &controltower.GetLandingZoneOperationInput{ OperationIdentifier: aws.String(id), } output, err := conn.GetLandingZoneOperation(ctx, input) - if tfresource.NotFound(err) { + if errs.IsA[*types.ResourceNotFoundException](err) { return nil, &retry.NotFoundError{ LastError: err, LastRequest: input, @@ -180,7 +199,7 @@ func findLandingZoneOperationDetailsByID(ctx context.Context, conn *controltower func statusLandingZoneOperation(ctx context.Context, conn *controltower.Client, id string) retry.StateRefreshFunc { return func() (interface{}, string, error) { - output, err := findLandingZoneOperationDetailsByID(ctx, conn, id) + output, err := findLandingZoneOperationByID(ctx, conn, id) if tfresource.NotFound(err) { return nil, "", nil diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 8f9308f0346..3fce0388d5b 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -8,7 +8,6 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go-v2/service/controltower" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -47,7 +46,8 @@ func testAccLandingZone_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccLandingZoneConfig_basic("1.0"), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLandingZoneExists(ctx, resourceName), resource.TestCheckResourceAttrSet(resourceName, "arn"), resource.TestCheckResourceAttr(resourceName, "version", "1.0"), ), @@ -73,7 +73,8 @@ func testAccLandingZone_disappears(t *testing.T) { { Config: testAccLandingZoneConfig_basic("1.0"), Check: resource.ComposeTestCheckFunc( - acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceControl(), resourceName), + testAccCheckLandingZoneExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceLandingZone(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -81,6 +82,21 @@ func testAccLandingZone_disappears(t *testing.T) { }) } +func testAccCheckLandingZoneExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ControlTowerClient(ctx) + + _, err := tfcontroltower.FindLandingZoneByID(ctx, conn, rs.Primary.ID) + + return err + } +} + func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).ControlTowerClient(ctx) @@ -90,11 +106,7 @@ func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc continue } - input := &controltower.GetLandingZoneInput{ - LandingZoneIdentifier: &rs.Primary.ID, - } - - _, err := conn.GetLandingZone(ctx, input) + _, err := tfcontroltower.FindLandingZoneByID(ctx, conn, rs.Primary.ID) if tfresource.NotFound(err) { continue @@ -104,7 +116,7 @@ func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc return err } - return fmt.Errorf("Control Tower Landing Zone %s still exists", rs.Primary.ID) + return fmt.Errorf("ControlTower Landing Zone %s still exists", rs.Primary.ID) } return nil From d99e4d2321925c3de8d81992d9861ed0ad1c5d90 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 17:00:38 -0500 Subject: [PATCH 13/42] Fix terrafmt error. --- website/docs/r/controltower_landing_zone.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 62b5fa3a758..5d882cc36c8 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -16,7 +16,7 @@ Creates a new landing zone using Control Tower. For more information on usage, p ```terraform resource "aws_controltower_landing_zone" "example" { manifest = jsondecode(file("${path.module}/LandingZoneManifest.json")) - version = "1.0" + version = "1.0" } ``` From 0dfb6e84590e203ae4afdcb7eaa718b7779da756 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 17:02:43 -0500 Subject: [PATCH 14/42] Fix 'ci.controltower-in-test-name'. --- internal/service/controltower/landing_zone_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 3fce0388d5b..1ca2465b5dc 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -func TestAccLandingZoneTowerLandingZone_serial(t *testing.T) { +func TestAccControlTower_serial(t *testing.T) { t.Parallel() testCases := map[string]map[string]func(t *testing.T){ From 08fac466ce5d0392b553d18e9d5cf5dd9727091e Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 17:09:58 -0500 Subject: [PATCH 15/42] r/aws_controltower_landing_zone: Add tagging support. --- .../service/controltower/controltower_test.go | 24 +++++ internal/service/controltower/landing_zone.go | 13 +++ .../service/controltower/landing_zone_test.go | 101 ++++++++++++++---- .../controltower/service_package_gen.go | 3 + 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 internal/service/controltower/controltower_test.go diff --git a/internal/service/controltower/controltower_test.go b/internal/service/controltower/controltower_test.go new file mode 100644 index 00000000000..71af0b6f009 --- /dev/null +++ b/internal/service/controltower/controltower_test.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package controltower_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccControlTower_serial(t *testing.T) { + t.Parallel() + + testCases := map[string]map[string]func(t *testing.T){ + "LandingZone": { + "basic": testAccLandingZone_basic, + "disappears": testAccLandingZone_disappears, + "tags": testAccLandingZone_tags, + }, + } + + acctest.RunSerialTests2Levels(t, testCases, 0) +} diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 7beba729fd5..219f69d0031 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -24,14 +24,17 @@ import ( "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" + "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) // @SDKResource("aws_controltower_landing_zone", name="Landing Zone") +// @Tags(identifierAttribute="arn") func resourceLandingZone() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceLandingZoneCreate, ReadWithoutTimeout: resourceLandingZoneRead, + UpdateWithoutTimeout: resourceLandingZoneUpdate, DeleteWithoutTimeout: resourceLandingZoneDelete, Importer: &schema.ResourceImporter{ @@ -61,6 +64,8 @@ func resourceLandingZone() *schema.Resource { names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), }, + + CustomizeDiff: verify.SetTagsDiff, } } @@ -117,6 +122,14 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i return nil } +func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + // Tags only. + + return append(diags, resourceLandingZoneRead(ctx, d, meta)...) +} + func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 1ca2465b5dc..d00f1c97608 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -17,19 +17,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -func TestAccControlTower_serial(t *testing.T) { - t.Parallel() - - testCases := map[string]map[string]func(t *testing.T){ - "LandingZone": { - "basic": testAccLandingZone_basic, - "disappears": testAccLandingZone_disappears, - }, - } - - acctest.RunSerialTests2Levels(t, testCases, 0) -} - func testAccLandingZone_basic(t *testing.T) { ctx := acctest.Context(t) resourceName := "aws_controltower_landing_zone.test" @@ -45,13 +32,18 @@ func testAccLandingZone_basic(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccLandingZoneConfig_basic("1.0"), + Config: testAccLandingZoneConfig_basic, Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLandingZoneExists(ctx, resourceName), resource.TestCheckResourceAttrSet(resourceName, "arn"), resource.TestCheckResourceAttr(resourceName, "version", "1.0"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -71,7 +63,7 @@ func testAccLandingZone_disappears(t *testing.T) { CheckDestroy: testAccCheckLandingZoneDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLandingZoneConfig_basic("1.0"), + Config: testAccLandingZoneConfig_basic, Check: resource.ComposeTestCheckFunc( testAccCheckLandingZoneExists(ctx, resourceName), acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceLandingZone(), resourceName), @@ -82,6 +74,54 @@ func testAccLandingZone_disappears(t *testing.T) { }) } +func testAccLandingZone_tags(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_controltower_landing_zone.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), + CheckDestroy: testAccCheckLandingZoneDestroy(ctx), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccLandingZoneConfig_tags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckLandingZoneExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccLandingZoneConfig_tags2("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckLandingZoneExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccLandingZoneConfig_tags1("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckLandingZoneExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + func testAccCheckLandingZoneExists(ctx context.Context, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -123,11 +163,36 @@ func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc } } -func testAccLandingZoneConfig_basic(version string) string { +const testAccLandingZoneConfig_basic = ` +resource "aws_controltower_landing_zone" "test" { + manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + version = "1.0" +} +` + +func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = "%[1]s" + version = "1.0" + + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_controltower_landing_zone" "test" { + manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + version = "1.0" + + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } } -`, version) +`, tagKey1, tagValue1, tagKey2, tagValue2) } diff --git a/internal/service/controltower/service_package_gen.go b/internal/service/controltower/service_package_gen.go index 137e6403673..1bec5dc8b08 100644 --- a/internal/service/controltower/service_package_gen.go +++ b/internal/service/controltower/service_package_gen.go @@ -43,6 +43,9 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: resourceLandingZone, TypeName: "aws_controltower_landing_zone", Name: "Landing Zone", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, }, } } From e23bb3cfedee9eb039bdbab383f9362cb064ea01 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 17:27:45 -0500 Subject: [PATCH 16/42] r/aws_controltower_landing_zone: Tweak acceptance test configurations. --- .../service/controltower/landing_zone_test.go | 24 ++++++++++--------- .../LandingZoneManifest.json | 0 2 files changed, 13 insertions(+), 11 deletions(-) rename internal/service/controltower/{fixtures => test-fixtures}/LandingZoneManifest.json (100%) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index d00f1c97608..0a9fe302556 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -163,36 +163,38 @@ func testAccCheckLandingZoneDestroy(ctx context.Context) resource.TestCheckFunc } } -const testAccLandingZoneConfig_basic = ` +const landingZoneVersion = "3.3" + +var testAccLandingZoneConfig_basic = fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = "1.0" + manifest = file("test-fixtures/LandingZoneManifest.json") + version = %[1]q } -` +`, landingZoneVersion) func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = "1.0" + version = %[1]q tags = { - %[1]q = %[2]q + %[2]q = %[3]q } } -`, tagKey1, tagValue1) +`, landingZoneVersion, tagKey1, tagValue1) } func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = "1.0" + version = %[1]q tags = { - %[1]q = %[2]q - %[3]q = %[4]q + %[2]q = %[3]q + %[4]q = %[5]q } } -`, tagKey1, tagValue1, tagKey2, tagValue2) +`, landingZoneVersion, tagKey1, tagValue1, tagKey2, tagValue2) } diff --git a/internal/service/controltower/fixtures/LandingZoneManifest.json b/internal/service/controltower/test-fixtures/LandingZoneManifest.json similarity index 100% rename from internal/service/controltower/fixtures/LandingZoneManifest.json rename to internal/service/controltower/test-fixtures/LandingZoneManifest.json From acbf939ea4d171e558cf04ed5a6c61b4c93a3cb4 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 6 Jan 2024 17:31:37 -0500 Subject: [PATCH 17/42] Fix semgrep 'ci.semgrep.pluginsdk.return-diags-not-nil'. --- internal/service/controltower/landing_zone.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 219f69d0031..a9e64aecab0 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -119,7 +119,7 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i d.Set("manifest", landingZone.Manifest) d.Set("version", landingZone.Version) - return nil + return diags } func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -147,7 +147,7 @@ func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta sdkdiag.AppendErrorf(diags, "waiting for ControlTower Landing Zone (%s) delete: %s", d.Id(), err) } - return nil + return diags } func landingZoneIDFromARN(arnString string) (string, error) { From 7963bdcc3bc0675e07380dd0c58c880bd6371dd3 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 08:49:10 -0500 Subject: [PATCH 18/42] r/aws_controltower_landing_zone: Add Go structure representing JSON manifest. --- internal/service/controltower/landing_zone.go | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index a9e64aecab0..f09f6a99f98 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -246,3 +246,36 @@ func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.C return nil, err } + +// https://mholt.github.io/json-to-go/ +// https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html +type landingZoneManifest struct { + GovernedRegions []string `json:"governedRegions,omitempty"` + OrganizationStructure struct { + Security struct { + Name string `json:"name,omitempty"` + } `json:"security,omitempty"` + Sandbox struct { + Name string `json:"name,omitempty"` + } `json:"sandbox,omitempty"` + } `json:"organizationStructure,omitempty"` + CentralizedLogging struct { + AccountID string `json:"accountId,omitempty"` + Configurations struct { + LoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` + } `json:"loggingBucket,omitempty"` + AccessLoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` + } `json:"accessLoggingBucket,omitempty"` + KmsKeyARN string `json:"kmsKeyArn,omitempty"` + } `json:"configurations,omitempty"` + Enabled bool `json:"enabled,omitempty"` + } `json:"centralizedLogging,omitempty"` + SecurityRoles struct { + AccountID string `json:"accountId,omitempty"` + } `json:"securityRoles,omitempty"` + AccessManagement struct { + Enabled bool `json:"enabled,omitempty"` + } `json:"accessManagement,omitempty"` +} From e7cd81b12d960cff12a1d71bbfd9785f218b8799 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 11:17:39 -0500 Subject: [PATCH 19/42] Add 'manifest' nested block. --- internal/service/controltower/landing_zone.go | 168 +++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index f09f6a99f98..28a16759610 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -22,6 +22,7 @@ import ( "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" + "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" @@ -52,9 +53,156 @@ func resourceLandingZone() *schema.Resource { Computed: true, }, "manifest": { - Type: schema.TypeString, + Type: schema.TypeList, + MaxItems: 1, Required: true, - ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access_management": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + }, + }, + "centralized_logging": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "configurations": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access_logging_bucket": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "retention_days": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + "kms_key_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "logging_bucket": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "retention_days": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + }, + }, + "governed_regions": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "organization_structure": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sandbox": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "security": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "security_roles": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, }, "version": { Type: schema.TypeString, @@ -74,7 +222,7 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) input := &controltower.CreateLandingZoneInput{ - Manifest: document.NewLazyDocument(d.Get("manifest").(string)), + Manifest: document.NewLazyDocument(expandLandingZoneManifest(d.Get("manifest").([]interface{})[0].(map[string]interface{}))), Tags: getTagsIn(ctx), Version: aws.String(d.Get("version").(string)), } @@ -279,3 +427,17 @@ type landingZoneManifest struct { Enabled bool `json:"enabled,omitempty"` } `json:"accessManagement,omitempty"` } + +func expandLandingZoneManifest(tfMap map[string]interface{}) *landingZoneManifest { + if tfMap == nil { + return nil + } + + apiObject := &landingZoneManifest{} + + if v, ok := tfMap["governed_regions"].(*schema.Set); ok && v.Len() > 0 { + apiObject.GovernedRegions = flex.ExpandStringValueSet(v) + } + + return apiObject +} From 9fd9764ab51a6fc547a5dc99d9d5705da4d713ce Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 11:43:55 -0500 Subject: [PATCH 20/42] Add types for nested structures. --- internal/service/controltower/landing_zone.go | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 28a16759610..ecb6f103d5a 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -398,34 +398,52 @@ func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.C // https://mholt.github.io/json-to-go/ // https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html type landingZoneManifest struct { - GovernedRegions []string `json:"governedRegions,omitempty"` - OrganizationStructure struct { - Security struct { - Name string `json:"name,omitempty"` - } `json:"security,omitempty"` - Sandbox struct { - Name string `json:"name,omitempty"` - } `json:"sandbox,omitempty"` - } `json:"organizationStructure,omitempty"` - CentralizedLogging struct { - AccountID string `json:"accountId,omitempty"` - Configurations struct { - LoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` - } `json:"loggingBucket,omitempty"` - AccessLoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` - } `json:"accessLoggingBucket,omitempty"` - KmsKeyARN string `json:"kmsKeyArn,omitempty"` - } `json:"configurations,omitempty"` - Enabled bool `json:"enabled,omitempty"` - } `json:"centralizedLogging,omitempty"` - SecurityRoles struct { - AccountID string `json:"accountId,omitempty"` - } `json:"securityRoles,omitempty"` - AccessManagement struct { - Enabled bool `json:"enabled,omitempty"` - } `json:"accessManagement,omitempty"` + AccessManagement landingZoneManifestAccessManagement `json:"accessManagement,omitempty"` + CentralizedLogging landingZoneManifestCentralizedLogging `json:"centralizedLogging,omitempty"` + GovernedRegions []string `json:"governedRegions,omitempty"` + OrganizationStructure landingZoneManifestOrganizationStructure `json:"organizationStructure,omitempty"` + SecurityRoles landingZoneManifestSecurityRoles `json:"securityRoles,omitempty"` +} + +type landingZoneManifestAccessManagement struct { + Enabled bool `json:"enabled,omitempty"` +} + +type landingZoneManifestCentralizedLogging struct { + AccountID string `json:"accountId,omitempty"` + Configurations landingZoneManifestCentralizedLoggingConfigurations `json:"configurations,omitempty"` + Enabled bool `json:"enabled,omitempty"` +} + +type landingZoneManifestCentralizedLoggingConfigurations struct { + AccessLoggingBucket landingZoneManifestCentralizedLoggingConfigurationsAccessLoggingBucket `json:"accessLoggingBucket,omitempty"` + KmsKeyARN string `json:"kmsKeyArn,omitempty"` + LoggingBucket landingZoneManifestCentralizedLoggingConfigurationsLoggingBucket `json:"loggingBucket,omitempty"` +} + +type landingZoneManifestCentralizedLoggingConfigurationsAccessLoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` +} + +type landingZoneManifestCentralizedLoggingConfigurationsLoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` +} + +type landingZoneManifestOrganizationStructure struct { + Sandbox landingZoneManifestOrganizationStructureSandbox `json:"sandbox,omitempty"` + Security landingZoneManifestOrganizationStructureSecurity `json:"security,omitempty"` +} + +type landingZoneManifestOrganizationStructureSandbox struct { + Name string `json:"name,omitempty"` +} + +type landingZoneManifestOrganizationStructureSecurity struct { + Name string `json:"name,omitempty"` +} + +type landingZoneManifestSecurityRoles struct { + AccountID string `json:"accountId,omitempty"` } func expandLandingZoneManifest(tfMap map[string]interface{}) *landingZoneManifest { From e2a819a040165f942226c2b8cff8c80148e9f769 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 11:44:05 -0500 Subject: [PATCH 21/42] Revert "Add types for nested structures." This reverts commit 9fd9764ab51a6fc547a5dc99d9d5705da4d713ce. --- internal/service/controltower/landing_zone.go | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index ecb6f103d5a..28a16759610 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -398,52 +398,34 @@ func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.C // https://mholt.github.io/json-to-go/ // https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html type landingZoneManifest struct { - AccessManagement landingZoneManifestAccessManagement `json:"accessManagement,omitempty"` - CentralizedLogging landingZoneManifestCentralizedLogging `json:"centralizedLogging,omitempty"` - GovernedRegions []string `json:"governedRegions,omitempty"` - OrganizationStructure landingZoneManifestOrganizationStructure `json:"organizationStructure,omitempty"` - SecurityRoles landingZoneManifestSecurityRoles `json:"securityRoles,omitempty"` -} - -type landingZoneManifestAccessManagement struct { - Enabled bool `json:"enabled,omitempty"` -} - -type landingZoneManifestCentralizedLogging struct { - AccountID string `json:"accountId,omitempty"` - Configurations landingZoneManifestCentralizedLoggingConfigurations `json:"configurations,omitempty"` - Enabled bool `json:"enabled,omitempty"` -} - -type landingZoneManifestCentralizedLoggingConfigurations struct { - AccessLoggingBucket landingZoneManifestCentralizedLoggingConfigurationsAccessLoggingBucket `json:"accessLoggingBucket,omitempty"` - KmsKeyARN string `json:"kmsKeyArn,omitempty"` - LoggingBucket landingZoneManifestCentralizedLoggingConfigurationsLoggingBucket `json:"loggingBucket,omitempty"` -} - -type landingZoneManifestCentralizedLoggingConfigurationsAccessLoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` -} - -type landingZoneManifestCentralizedLoggingConfigurationsLoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` -} - -type landingZoneManifestOrganizationStructure struct { - Sandbox landingZoneManifestOrganizationStructureSandbox `json:"sandbox,omitempty"` - Security landingZoneManifestOrganizationStructureSecurity `json:"security,omitempty"` -} - -type landingZoneManifestOrganizationStructureSandbox struct { - Name string `json:"name,omitempty"` -} - -type landingZoneManifestOrganizationStructureSecurity struct { - Name string `json:"name,omitempty"` -} - -type landingZoneManifestSecurityRoles struct { - AccountID string `json:"accountId,omitempty"` + GovernedRegions []string `json:"governedRegions,omitempty"` + OrganizationStructure struct { + Security struct { + Name string `json:"name,omitempty"` + } `json:"security,omitempty"` + Sandbox struct { + Name string `json:"name,omitempty"` + } `json:"sandbox,omitempty"` + } `json:"organizationStructure,omitempty"` + CentralizedLogging struct { + AccountID string `json:"accountId,omitempty"` + Configurations struct { + LoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` + } `json:"loggingBucket,omitempty"` + AccessLoggingBucket struct { + RetentionDays int `json:"retentionDays,omitempty"` + } `json:"accessLoggingBucket,omitempty"` + KmsKeyARN string `json:"kmsKeyArn,omitempty"` + } `json:"configurations,omitempty"` + Enabled bool `json:"enabled,omitempty"` + } `json:"centralizedLogging,omitempty"` + SecurityRoles struct { + AccountID string `json:"accountId,omitempty"` + } `json:"securityRoles,omitempty"` + AccessManagement struct { + Enabled bool `json:"enabled,omitempty"` + } `json:"accessManagement,omitempty"` } func expandLandingZoneManifest(tfMap map[string]interface{}) *landingZoneManifest { From 1069ac5a65e036d2d85463c0d50b8fcdaff9adff Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 12:19:36 -0500 Subject: [PATCH 22/42] Add 'flattenLandingZoneManifest'. --- internal/service/controltower/landing_zone.go | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 28a16759610..c05df063152 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -25,6 +25,7 @@ import ( "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" + itypes "github.com/hashicorp/terraform-provider-aws/internal/types" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -435,9 +436,146 @@ func expandLandingZoneManifest(tfMap map[string]interface{}) *landingZoneManifes apiObject := &landingZoneManifest{} + if v, ok := tfMap["access_management"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["enabled"].(bool); ok { + apiObject.AccessManagement.Enabled = v + } + } + + if v, ok := tfMap["centralized_logging"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["account_id"].(string); ok { + apiObject.CentralizedLogging.AccountID = v + } + + if v, ok := tfMap["configurations"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["access_logging_bucket"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["retention_days"].(int); ok { + apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays = v + } + } + + if v, ok := tfMap["kms_key_arn"].(string); ok { + apiObject.CentralizedLogging.Configurations.KmsKeyARN = v + } + + if v, ok := tfMap["logging_bucket"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["retention_days"].(int); ok { + apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays = v + } + } + } + + if v, ok := tfMap["enabled"].(bool); ok { + apiObject.CentralizedLogging.Enabled = v + } + } + if v, ok := tfMap["governed_regions"].(*schema.Set); ok && v.Len() > 0 { apiObject.GovernedRegions = flex.ExpandStringValueSet(v) } + if v, ok := tfMap["organization_structure"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["sandbox"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["name"].(string); ok { + apiObject.OrganizationStructure.Sandbox.Name = v + } + } + + if v, ok := tfMap["security"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["name"].(string); ok { + apiObject.OrganizationStructure.Security.Name = v + } + } + } + + if v, ok := tfMap["security_roles"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + tfMap := v[0].(map[string]interface{}) + + if v, ok := tfMap["account_id"].(string); ok { + apiObject.SecurityRoles.AccountID = v + } + } + return apiObject } + +func flattenLandingZoneManifest(apiObject *landingZoneManifest) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "governed_regions": apiObject.GovernedRegions, + } + + if !itypes.IsZero(&apiObject.AccessManagement) { + tfMap["access_management"] = []interface{}{map[string]interface{}{ + "enabled": apiObject.AccessManagement.Enabled, + }} + } + + if !itypes.IsZero(&apiObject.CentralizedLogging) { + tfMap["centralized_logging"] = []interface{}{map[string]interface{}{ + "account_id": apiObject.CentralizedLogging.AccountID, + "enabled": apiObject.CentralizedLogging.Enabled, + }} + + if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations) { + tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"] = []interface{}{map[string]interface{}{ + "kms_key_arn": apiObject.CentralizedLogging.Configurations.KmsKeyARN, + }} + + if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations.AccessLoggingBucket) { + tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"].([]interface{})[0].(map[string]interface{})["access_logging_bucket"] = []interface{}{map[string]interface{}{ + "retention_days": apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays, + }} + } + + if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations.LoggingBucket) { + tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"].([]interface{})[0].(map[string]interface{})["logging_bucket"] = []interface{}{map[string]interface{}{ + "retention_days": apiObject.CentralizedLogging.Configurations.LoggingBucket.RetentionDays, + }} + } + } + } + + if !itypes.IsZero(&apiObject.OrganizationStructure) { + tfMap["organization_structure"] = []interface{}{map[string]interface{}{}} + + if !itypes.IsZero(&apiObject.OrganizationStructure.Sandbox) { + tfMap["organization_structure"].([]interface{})[0].(map[string]interface{})["sandbox"] = []interface{}{map[string]interface{}{ + "name": apiObject.OrganizationStructure.Sandbox.Name, + }} + } + + if !itypes.IsZero(&apiObject.OrganizationStructure.Security) { + tfMap["organization_structure"].([]interface{})[0].(map[string]interface{})["security"] = []interface{}{map[string]interface{}{ + "name": apiObject.OrganizationStructure.Security.Name, + }} + } + } + + if !itypes.IsZero(&apiObject.SecurityRoles) { + tfMap["security_roles"] = []interface{}{map[string]interface{}{ + "account_id": apiObject.SecurityRoles.AccountID, + }} + } + + return tfMap +} From 1ad4d59c08c09a6d65b85e248ff37ec83965e5a8 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 13:41:41 -0500 Subject: [PATCH 23/42] r/aws_controltower_landing_zone: Set 'manifest' in state. --- internal/service/controltower/landing_zone.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index c05df063152..5875306670f 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -265,7 +265,18 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i } d.Set("arn", landingZone.Arn) - d.Set("manifest", landingZone.Manifest) + if landingZone.Manifest != nil { + var v landingZoneManifest + + if err := landingZone.Manifest.UnmarshalSmithyDocument(&v); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + if err := d.Set("manifest", []interface{}{flattenLandingZoneManifest(&v)}); err != nil { + return sdkdiag.AppendErrorf(diags, "setting manifest: %s", err) + } + } else { + d.Set("manifest", nil) + } d.Set("version", landingZone.Version) return diags From e0b96a8753deda62c358a7fac23ffa86bf0108b1 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 13:49:31 -0500 Subject: [PATCH 24/42] r/aws_controltower_landing_zone: Add 'latest_available_version' attribute. --- internal/service/controltower/landing_zone.go | 5 +++++ internal/service/controltower/landing_zone_test.go | 1 + website/docs/r/controltower_landing_zone.html.markdown | 1 + 3 files changed, 7 insertions(+) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 5875306670f..efdad89de89 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -53,6 +53,10 @@ func resourceLandingZone() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "latest_available_version": { + Type: schema.TypeString, + Computed: true, + }, "manifest": { Type: schema.TypeList, MaxItems: 1, @@ -265,6 +269,7 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i } d.Set("arn", landingZone.Arn) + d.Set("latest_available_version", landingZone.LatestAvailableVersion) if landingZone.Manifest != nil { var v landingZoneManifest diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 0a9fe302556..14eea6035e7 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -36,6 +36,7 @@ func testAccLandingZone_basic(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLandingZoneExists(ctx, resourceName), resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttrSet(resourceName, "latest_available_version"), resource.TestCheckResourceAttr(resourceName, "version", "1.0"), ), }, diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 5d882cc36c8..78be7c8a044 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -34,6 +34,7 @@ This resource exports the following attributes in addition to the arguments abov * `id` - The identifier of the landing zone. * `arn` - The ARN of the landing zone. +* `latest_available_version` - The latest available version of the landing zone. * `tags_all` - A map of tags assigned to the landing zone, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). ## Import From abe363c2d1aa69c6e383b4b329485ae9758e6766 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 13:57:58 -0500 Subject: [PATCH 25/42] r/aws_controltower_landing_zone: Add 'drift_status' attribute. --- internal/service/controltower/landing_zone.go | 31 +++++++++++++++++++ .../service/controltower/landing_zone_test.go | 1 + .../r/controltower_landing_zone.html.markdown | 2 ++ 3 files changed, 34 insertions(+) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index efdad89de89..75383428092 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -53,6 +53,18 @@ func resourceLandingZone() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "drift_status": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "latest_available_version": { Type: schema.TypeString, Computed: true, @@ -269,6 +281,13 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i } d.Set("arn", landingZone.Arn) + if landingZone.DriftStatus != nil { + if err := d.Set("drift_status", []interface{}{flattenLandingZoneDriftStatusSummary(landingZone.DriftStatus)}); err != nil { + return sdkdiag.AppendErrorf(diags, "setting drift_status: %s", err) + } + } else { + d.Set("drift_status", nil) + } d.Set("latest_available_version", landingZone.LatestAvailableVersion) if landingZone.Manifest != nil { var v landingZoneManifest @@ -412,6 +431,18 @@ func waitLandingZoneOperationSucceeded(ctx context.Context, conn *controltower.C return nil, err } +func flattenLandingZoneDriftStatusSummary(apiObject *types.LandingZoneDriftStatusSummary) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "status": apiObject.Status, + } + + return tfMap +} + // https://mholt.github.io/json-to-go/ // https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html type landingZoneManifest struct { diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 14eea6035e7..2a9f2826a10 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -36,6 +36,7 @@ func testAccLandingZone_basic(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLandingZoneExists(ctx, resourceName), resource.TestCheckResourceAttrSet(resourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "drift_status.#", "1"), resource.TestCheckResourceAttrSet(resourceName, "latest_available_version"), resource.TestCheckResourceAttr(resourceName, "version", "1.0"), ), diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 78be7c8a044..edfe0465102 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -34,6 +34,8 @@ This resource exports the following attributes in addition to the arguments abov * `id` - The identifier of the landing zone. * `arn` - The ARN of the landing zone. +* `drift_status` - The drift status summary of the landing zone. + * `status` - The drift status of the landing zone. * `latest_available_version` - The latest available version of the landing zone. * `tags_all` - A map of tags assigned to the landing zone, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). From 683a44fa87a1ee7254fab93bfb39c3fc98c8e207 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 14:53:38 -0500 Subject: [PATCH 26/42] Document 'manifest' structure. --- internal/service/controltower/landing_zone.go | 4 +- .../r/controltower_landing_zone.html.markdown | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 75383428092..13e54cb37bd 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -443,8 +443,8 @@ func flattenLandingZoneDriftStatusSummary(apiObject *types.LandingZoneDriftStatu return tfMap } -// https://mholt.github.io/json-to-go/ -// https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html +// Manifest example JSON https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html, using https://mholt.github.io/json-to-go/. +// Some description of the fields https://docs.aws.amazon.com/controltower/latest/userguide/lz-apis-cfn-launch.html. type landingZoneManifest struct { GovernedRegions []string `json:"governedRegions,omitempty"` OrganizationStructure struct { diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index edfe0465102..14d2e40f7e4 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -15,8 +15,31 @@ Creates a new landing zone using Control Tower. For more information on usage, p ```terraform resource "aws_controltower_landing_zone" "example" { - manifest = jsondecode(file("${path.module}/LandingZoneManifest.json")) - version = "1.0" + manifest { + governed_regions = ["us-west-2"] + + centralized_logging { + configurations { + access_logging_bucket { + retention_days = 3650 + } + logging_bucket { + retention_days = 365 + } + } + } + + organization_structure { + sandbox { + name = "Sandbox" + } + security { + name = "Security" + } + } + } + + version = "3.2" } ``` @@ -24,9 +47,28 @@ resource "aws_controltower_landing_zone" "example" { This resource supports the following arguments: -* `manifest` - (Required) The manifest JSON file is a text file that describes your AWS resources. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). -* `version` - (Required) The landing zone version, for example, 1.0. +* `manifest` - (Required) Landing zone configuration. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). + * `access_management` - (Optional) Access management configuration. + * `enabled` - (Optional) Whether AWS Control Tower sets up AWS account access with AWS Identity and Access Management (IAM), or whether to self-manage AWS account access. + * `centralized_logging` - (Optional) Log configuration for Amazon S3. + * `account_id` - (Optional) The AWS account ID for centralized logging. + * `configurations` - (Optional) Configurations. + * `access_logging_bucket` - (Optional) Amazon S3 bucket retention for access logging. + * `retention_days` - (Optional) Retention period for access logging bucket. + * `kms_key_arn` - (Optional) KMS key ARN used by CloudTrail and Config service to encrypt data in logging bucket. + * `logging_bucket` - (Optional) Amazon S3 bucket retention for logging. + * `retention_days` - (Optional) Retention period for centralized logging bucket. + * `enabled` - (Optional) Whether or not logging is enabled. + * `governed_regions` - (Required) AWS Regions to govern. + * `organization_structure` - (Optional) Organization structure. + * `sandbox` - (Optional) Sandbox Organizational Unit configuration. + * `name` - (Optional) The sandbox Organizational Unit name. + * `security` - (Optional) Security Organizational Unit configuration. + * `name` - (Optional) The security Organizational Unit name. + * `security_roles` - (Optional) Organization structure. + * `account_id` - (Optional) The AWS account ID for security roles. * `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `version` - (Required) The landing zone version. ## Attribute Reference From 7a2af8fff40f03b6b8ce83423ce86c29e1d480d8 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 15:01:41 -0500 Subject: [PATCH 27/42] r/aws_controltower_landing_zone: Allow 'manifest' and 'version' to be updated. --- internal/service/controltower/landing_zone.go | 21 +++++++++++++++++-- .../r/controltower_landing_zone.html.markdown | 8 +++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 13e54cb37bd..793a830c2d0 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -45,6 +45,7 @@ func resourceLandingZone() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(120 * time.Minute), + Update: schema.DefaultTimeout(120 * time.Minute), Delete: schema.DefaultTimeout(120 * time.Minute), }, @@ -224,7 +225,6 @@ func resourceLandingZone() *schema.Resource { "version": { Type: schema.TypeString, Required: true, - ForceNew: true, }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), @@ -308,8 +308,25 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + if d.HasChangesExcept("tags", "tags_all") { + input := &controltower.UpdateLandingZoneInput{ + LandingZoneIdentifier: aws.String(d.Id()), + Manifest: document.NewLazyDocument(expandLandingZoneManifest(d.Get("manifest").([]interface{})[0].(map[string]interface{}))), + Version: aws.String(d.Get("version").(string)), + } - // Tags only. + output, err := conn.UpdateLandingZone(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating ControlTower Landing Zone (%s): %s", d.Id(), err) + } + + if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutUpdate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ControlTower Landing Zone (%s) update: %s", d.Id(), err) + } + } return append(diags, resourceLandingZoneRead(ctx, d, meta)...) } diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 14d2e40f7e4..6ed453efdda 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -81,6 +81,14 @@ This resource exports the following attributes in addition to the arguments abov * `latest_available_version` - The latest available version of the landing zone. * `tags_all` - A map of tags assigned to the landing zone, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `120m`) +- `update` - (Default `120m`) +- `delete` - (Default `120m`) + ## Import In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import a Control Tower Landing Zone using the `id`. For example: From 5753b41c442fbc795bbd16fc6801a5793b4869fe Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 16:08:31 -0500 Subject: [PATCH 28/42] r/aws_controltower_landing_zone: Tidy up acceptance tests. --- .../service/controltower/landing_zone_test.go | 33 ++++++++++++------- .../test-fixtures/LandingZoneManifest.json | 30 ----------------- 2 files changed, 21 insertions(+), 42 deletions(-) delete mode 100644 internal/service/controltower/test-fixtures/LandingZoneManifest.json diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 2a9f2826a10..bce052ab132 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -169,34 +169,43 @@ const landingZoneVersion = "3.3" var testAccLandingZoneConfig_basic = fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest = file("test-fixtures/LandingZoneManifest.json") - version = %[1]q + manifest { + governed_regions = [%[1]q] + } + + version = %[2]q } -`, landingZoneVersion) +`, acctest.Region(), landingZoneVersion) func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = %[1]q + manifest { + governed_regions = [%[1]q] + } + + version = %[2]q tags = { - %[2]q = %[3]q + %[3]q = %[4]q } } -`, landingZoneVersion, tagKey1, tagValue1) +`, acctest.Region(), landingZoneVersion, tagKey1, tagValue1) } func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) - version = %[1]q + manifest { + governed_regions = [%[1]q] + } + + version = %[2]q tags = { - %[2]q = %[3]q - %[4]q = %[5]q + %[3]q = %[4]q + %[5]q = %[6]q } } -`, landingZoneVersion, tagKey1, tagValue1, tagKey2, tagValue2) +`, acctest.Region(), landingZoneVersion, tagKey1, tagValue1, tagKey2, tagValue2) } diff --git a/internal/service/controltower/test-fixtures/LandingZoneManifest.json b/internal/service/controltower/test-fixtures/LandingZoneManifest.json deleted file mode 100644 index d9a449ee2ee..00000000000 --- a/internal/service/controltower/test-fixtures/LandingZoneManifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "governedRegions": ["us-west-2", "us-west-1"], - "organizationStructure": { - "security": { - "name": "CORE" - }, - "sandbox": { - "name": "Sandbox" - } - }, - "centralizedLogging": { - "accountId": "222222222222", - "configurations": { - "loggingBucket": { - "retentionDays": 60 - }, - "accessLoggingBucket": { - "retentionDays": 60 - }, - "kmsKeyArn": "arn:aws:kms:us-west-1:123456789123:key/e84XXXXX-6bXX-49XX-9eXX-ecfXXXXXXXXX" - }, - "enabled": true - }, - "securityRoles": { - "accountId": "333333333333" - }, - "accessManagement": { - "enabled": true - } -} From 7404dd2469485085134bfa55b024b05ba3d402cd Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 16:18:04 -0500 Subject: [PATCH 29/42] Fix markdownlint 'MD007/ul-indent Unordered list indentation'. --- .../r/controltower_landing_zone.html.markdown | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 6ed453efdda..d0195fcd333 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -48,25 +48,25 @@ resource "aws_controltower_landing_zone" "example" { This resource supports the following arguments: * `manifest` - (Required) Landing zone configuration. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). - * `access_management` - (Optional) Access management configuration. - * `enabled` - (Optional) Whether AWS Control Tower sets up AWS account access with AWS Identity and Access Management (IAM), or whether to self-manage AWS account access. - * `centralized_logging` - (Optional) Log configuration for Amazon S3. - * `account_id` - (Optional) The AWS account ID for centralized logging. - * `configurations` - (Optional) Configurations. - * `access_logging_bucket` - (Optional) Amazon S3 bucket retention for access logging. - * `retention_days` - (Optional) Retention period for access logging bucket. - * `kms_key_arn` - (Optional) KMS key ARN used by CloudTrail and Config service to encrypt data in logging bucket. - * `logging_bucket` - (Optional) Amazon S3 bucket retention for logging. - * `retention_days` - (Optional) Retention period for centralized logging bucket. - * `enabled` - (Optional) Whether or not logging is enabled. - * `governed_regions` - (Required) AWS Regions to govern. - * `organization_structure` - (Optional) Organization structure. - * `sandbox` - (Optional) Sandbox Organizational Unit configuration. - * `name` - (Optional) The sandbox Organizational Unit name. - * `security` - (Optional) Security Organizational Unit configuration. - * `name` - (Optional) The security Organizational Unit name. - * `security_roles` - (Optional) Organization structure. - * `account_id` - (Optional) The AWS account ID for security roles. + * `access_management` - (Optional) Access management configuration. + * `enabled` - (Optional) Whether AWS Control Tower sets up AWS account access with AWS Identity and Access Management (IAM), or whether to self-manage AWS account access. + * `centralized_logging` - (Optional) Log configuration for Amazon S3. + * `account_id` - (Optional) The AWS account ID for centralized logging. + * `configurations` - (Optional) Configurations. + * `access_logging_bucket` - (Optional) Amazon S3 bucket retention for access logging. + * `retention_days` - (Optional) Retention period for access logging bucket. + * `kms_key_arn` - (Optional) KMS key ARN used by CloudTrail and Config service to encrypt data in logging bucket. + * `logging_bucket` - (Optional) Amazon S3 bucket retention for logging. + * `retention_days` - (Optional) Retention period for centralized logging bucket. + * `enabled` - (Optional) Whether or not logging is enabled. + * `governed_regions` - (Required) AWS Regions to govern. + * `organization_structure` - (Optional) Organization structure. + * `sandbox` - (Optional) Sandbox Organizational Unit configuration. + * `name` - (Optional) The sandbox Organizational Unit name. + * `security` - (Optional) Security Organizational Unit configuration. + * `name` - (Optional) The security Organizational Unit name. + * `security_roles` - (Optional) Organization structure. + * `account_id` - (Optional) The AWS account ID for security roles. * `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `version` - (Required) The landing zone version. From f2c02cd481f3404d58741d251cb608f652bfdf9c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 16:19:20 -0500 Subject: [PATCH 30/42] Fix terrafmt errors. --- internal/service/controltower/landing_zone_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index bce052ab132..49db4560a2b 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -173,7 +173,7 @@ resource "aws_controltower_landing_zone" "test" { governed_regions = [%[1]q] } - version = %[2]q + version = %[2]q } `, acctest.Region(), landingZoneVersion) @@ -184,7 +184,7 @@ resource "aws_controltower_landing_zone" "test" { governed_regions = [%[1]q] } - version = %[2]q + version = %[2]q tags = { %[3]q = %[4]q @@ -200,7 +200,7 @@ resource "aws_controltower_landing_zone" "test" { governed_regions = [%[1]q] } - version = %[2]q + version = %[2]q tags = { %[3]q = %[4]q From 14ae8a911f4f6693733c66fe54a38fa1eed1bb9b Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 19 Jan 2024 16:21:17 -0500 Subject: [PATCH 31/42] Fix markdownlint 'MD007/ul-indent Unordered list indentation'. --- website/docs/r/controltower_landing_zone.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index d0195fcd333..619340bfd13 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -77,7 +77,7 @@ This resource exports the following attributes in addition to the arguments abov * `id` - The identifier of the landing zone. * `arn` - The ARN of the landing zone. * `drift_status` - The drift status summary of the landing zone. - * `status` - The drift status of the landing zone. + * `status` - The drift status of the landing zone. * `latest_available_version` - The latest available version of the landing zone. * `tags_all` - A map of tags assigned to the landing zone, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). From 94970b8246091faa284e117de5445e950aa76616 Mon Sep 17 00:00:00 2001 From: Bruno Schaatsbergen Date: Thu, 1 Feb 2024 21:05:28 +0100 Subject: [PATCH 32/42] chore: rewrite back to use a manifest_json --- .../fixtures/LandingZoneManifest.json | 30 ++ internal/service/controltower/landing_zone.go | 355 +----------------- .../service/controltower/landing_zone_test.go | 12 +- .../r/controltower_landing_zone.html.markdown | 50 +-- 4 files changed, 45 insertions(+), 402 deletions(-) create mode 100644 internal/service/controltower/fixtures/LandingZoneManifest.json diff --git a/internal/service/controltower/fixtures/LandingZoneManifest.json b/internal/service/controltower/fixtures/LandingZoneManifest.json new file mode 100644 index 00000000000..d9a449ee2ee --- /dev/null +++ b/internal/service/controltower/fixtures/LandingZoneManifest.json @@ -0,0 +1,30 @@ +{ + "governedRegions": ["us-west-2", "us-west-1"], + "organizationStructure": { + "security": { + "name": "CORE" + }, + "sandbox": { + "name": "Sandbox" + } + }, + "centralizedLogging": { + "accountId": "222222222222", + "configurations": { + "loggingBucket": { + "retentionDays": 60 + }, + "accessLoggingBucket": { + "retentionDays": 60 + }, + "kmsKeyArn": "arn:aws:kms:us-west-1:123456789123:key/e84XXXXX-6bXX-49XX-9eXX-ecfXXXXXXXXX" + }, + "enabled": true + }, + "securityRoles": { + "accountId": "333333333333" + }, + "accessManagement": { + "enabled": true + } +} diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 793a830c2d0..3dd44688f69 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -22,10 +22,8 @@ import ( "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" - "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" - itypes "github.com/hashicorp/terraform-provider-aws/internal/types" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -70,157 +68,9 @@ func resourceLandingZone() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "manifest": { - Type: schema.TypeList, - MaxItems: 1, + "manifest_json": { + Type: schema.TypeString, Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "access_management": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, - Optional: true, - Computed: true, - }, - }, - }, - }, - "centralized_logging": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "account_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "configurations": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "access_logging_bucket": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "retention_days": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - }, - }, - }, - }, - "kms_key_arn": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "logging_bucket": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "retention_days": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "enabled": { - Type: schema.TypeBool, - Optional: true, - Computed: true, - }, - }, - }, - }, - "governed_regions": { - Type: schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "organization_structure": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "sandbox": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - "security": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "security_roles": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "account_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, }, "version": { Type: schema.TypeString, @@ -238,8 +88,9 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + manifestJson := d.Get("manifest_json").(string) input := &controltower.CreateLandingZoneInput{ - Manifest: document.NewLazyDocument(expandLandingZoneManifest(d.Get("manifest").([]interface{})[0].(map[string]interface{}))), + Manifest: document.NewLazyDocument(manifestJson), Tags: getTagsIn(ctx), Version: aws.String(d.Get("version").(string)), } @@ -290,16 +141,9 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i } d.Set("latest_available_version", landingZone.LatestAvailableVersion) if landingZone.Manifest != nil { - var v landingZoneManifest - - if err := landingZone.Manifest.UnmarshalSmithyDocument(&v); err != nil { - return sdkdiag.AppendFromErr(diags, err) - } - if err := d.Set("manifest", []interface{}{flattenLandingZoneManifest(&v)}); err != nil { - return sdkdiag.AppendErrorf(diags, "setting manifest: %s", err) - } + d.Set("manifest_json", landingZone.Manifest) } else { - d.Set("manifest", nil) + d.Set("manifest_json", nil) } d.Set("version", landingZone.Version) @@ -310,10 +154,11 @@ func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + manifestJson := d.Get("manifest_json").(string) if d.HasChangesExcept("tags", "tags_all") { input := &controltower.UpdateLandingZoneInput{ LandingZoneIdentifier: aws.String(d.Id()), - Manifest: document.NewLazyDocument(expandLandingZoneManifest(d.Get("manifest").([]interface{})[0].(map[string]interface{}))), + Manifest: document.NewLazyDocument(manifestJson), Version: aws.String(d.Get("version").(string)), } @@ -459,187 +304,3 @@ func flattenLandingZoneDriftStatusSummary(apiObject *types.LandingZoneDriftStatu return tfMap } - -// Manifest example JSON https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch.html, using https://mholt.github.io/json-to-go/. -// Some description of the fields https://docs.aws.amazon.com/controltower/latest/userguide/lz-apis-cfn-launch.html. -type landingZoneManifest struct { - GovernedRegions []string `json:"governedRegions,omitempty"` - OrganizationStructure struct { - Security struct { - Name string `json:"name,omitempty"` - } `json:"security,omitempty"` - Sandbox struct { - Name string `json:"name,omitempty"` - } `json:"sandbox,omitempty"` - } `json:"organizationStructure,omitempty"` - CentralizedLogging struct { - AccountID string `json:"accountId,omitempty"` - Configurations struct { - LoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` - } `json:"loggingBucket,omitempty"` - AccessLoggingBucket struct { - RetentionDays int `json:"retentionDays,omitempty"` - } `json:"accessLoggingBucket,omitempty"` - KmsKeyARN string `json:"kmsKeyArn,omitempty"` - } `json:"configurations,omitempty"` - Enabled bool `json:"enabled,omitempty"` - } `json:"centralizedLogging,omitempty"` - SecurityRoles struct { - AccountID string `json:"accountId,omitempty"` - } `json:"securityRoles,omitempty"` - AccessManagement struct { - Enabled bool `json:"enabled,omitempty"` - } `json:"accessManagement,omitempty"` -} - -func expandLandingZoneManifest(tfMap map[string]interface{}) *landingZoneManifest { - if tfMap == nil { - return nil - } - - apiObject := &landingZoneManifest{} - - if v, ok := tfMap["access_management"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["enabled"].(bool); ok { - apiObject.AccessManagement.Enabled = v - } - } - - if v, ok := tfMap["centralized_logging"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["account_id"].(string); ok { - apiObject.CentralizedLogging.AccountID = v - } - - if v, ok := tfMap["configurations"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["access_logging_bucket"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["retention_days"].(int); ok { - apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays = v - } - } - - if v, ok := tfMap["kms_key_arn"].(string); ok { - apiObject.CentralizedLogging.Configurations.KmsKeyARN = v - } - - if v, ok := tfMap["logging_bucket"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["retention_days"].(int); ok { - apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays = v - } - } - } - - if v, ok := tfMap["enabled"].(bool); ok { - apiObject.CentralizedLogging.Enabled = v - } - } - - if v, ok := tfMap["governed_regions"].(*schema.Set); ok && v.Len() > 0 { - apiObject.GovernedRegions = flex.ExpandStringValueSet(v) - } - - if v, ok := tfMap["organization_structure"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["sandbox"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["name"].(string); ok { - apiObject.OrganizationStructure.Sandbox.Name = v - } - } - - if v, ok := tfMap["security"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["name"].(string); ok { - apiObject.OrganizationStructure.Security.Name = v - } - } - } - - if v, ok := tfMap["security_roles"].([]interface{}); ok && len(v) > 0 && v[0] != nil { - tfMap := v[0].(map[string]interface{}) - - if v, ok := tfMap["account_id"].(string); ok { - apiObject.SecurityRoles.AccountID = v - } - } - - return apiObject -} - -func flattenLandingZoneManifest(apiObject *landingZoneManifest) map[string]interface{} { - if apiObject == nil { - return nil - } - - tfMap := map[string]interface{}{ - "governed_regions": apiObject.GovernedRegions, - } - - if !itypes.IsZero(&apiObject.AccessManagement) { - tfMap["access_management"] = []interface{}{map[string]interface{}{ - "enabled": apiObject.AccessManagement.Enabled, - }} - } - - if !itypes.IsZero(&apiObject.CentralizedLogging) { - tfMap["centralized_logging"] = []interface{}{map[string]interface{}{ - "account_id": apiObject.CentralizedLogging.AccountID, - "enabled": apiObject.CentralizedLogging.Enabled, - }} - - if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations) { - tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"] = []interface{}{map[string]interface{}{ - "kms_key_arn": apiObject.CentralizedLogging.Configurations.KmsKeyARN, - }} - - if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations.AccessLoggingBucket) { - tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"].([]interface{})[0].(map[string]interface{})["access_logging_bucket"] = []interface{}{map[string]interface{}{ - "retention_days": apiObject.CentralizedLogging.Configurations.AccessLoggingBucket.RetentionDays, - }} - } - - if !itypes.IsZero(&apiObject.CentralizedLogging.Configurations.LoggingBucket) { - tfMap["centralized_logging"].([]interface{})[0].(map[string]interface{})["configurations"].([]interface{})[0].(map[string]interface{})["logging_bucket"] = []interface{}{map[string]interface{}{ - "retention_days": apiObject.CentralizedLogging.Configurations.LoggingBucket.RetentionDays, - }} - } - } - } - - if !itypes.IsZero(&apiObject.OrganizationStructure) { - tfMap["organization_structure"] = []interface{}{map[string]interface{}{}} - - if !itypes.IsZero(&apiObject.OrganizationStructure.Sandbox) { - tfMap["organization_structure"].([]interface{})[0].(map[string]interface{})["sandbox"] = []interface{}{map[string]interface{}{ - "name": apiObject.OrganizationStructure.Sandbox.Name, - }} - } - - if !itypes.IsZero(&apiObject.OrganizationStructure.Security) { - tfMap["organization_structure"].([]interface{})[0].(map[string]interface{})["security"] = []interface{}{map[string]interface{}{ - "name": apiObject.OrganizationStructure.Security.Name, - }} - } - } - - if !itypes.IsZero(&apiObject.SecurityRoles) { - tfMap["security_roles"] = []interface{}{map[string]interface{}{ - "account_id": apiObject.SecurityRoles.AccountID, - }} - } - - return tfMap -} diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 49db4560a2b..39037393015 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -169,9 +169,7 @@ const landingZoneVersion = "3.3" var testAccLandingZoneConfig_basic = fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest { - governed_regions = [%[1]q] - } + manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) version = %[2]q } @@ -180,9 +178,7 @@ resource "aws_controltower_landing_zone" "test" { func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest { - governed_regions = [%[1]q] - } + manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) version = %[2]q @@ -196,9 +192,7 @@ resource "aws_controltower_landing_zone" "test" { func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest { - governed_regions = [%[1]q] - } + manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) version = %[2]q diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 619340bfd13..9e477a9dc9f 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -15,31 +15,8 @@ Creates a new landing zone using Control Tower. For more information on usage, p ```terraform resource "aws_controltower_landing_zone" "example" { - manifest { - governed_regions = ["us-west-2"] - - centralized_logging { - configurations { - access_logging_bucket { - retention_days = 3650 - } - logging_bucket { - retention_days = 365 - } - } - } - - organization_structure { - sandbox { - name = "Sandbox" - } - security { - name = "Security" - } - } - } - - version = "3.2" + manifest_json = jsondecode(file("${path.module}/LandingZoneManifest.json")) + version = "1.0" } ``` @@ -47,28 +24,9 @@ resource "aws_controltower_landing_zone" "example" { This resource supports the following arguments: -* `manifest` - (Required) Landing zone configuration. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). - * `access_management` - (Optional) Access management configuration. - * `enabled` - (Optional) Whether AWS Control Tower sets up AWS account access with AWS Identity and Access Management (IAM), or whether to self-manage AWS account access. - * `centralized_logging` - (Optional) Log configuration for Amazon S3. - * `account_id` - (Optional) The AWS account ID for centralized logging. - * `configurations` - (Optional) Configurations. - * `access_logging_bucket` - (Optional) Amazon S3 bucket retention for access logging. - * `retention_days` - (Optional) Retention period for access logging bucket. - * `kms_key_arn` - (Optional) KMS key ARN used by CloudTrail and Config service to encrypt data in logging bucket. - * `logging_bucket` - (Optional) Amazon S3 bucket retention for logging. - * `retention_days` - (Optional) Retention period for centralized logging bucket. - * `enabled` - (Optional) Whether or not logging is enabled. - * `governed_regions` - (Required) AWS Regions to govern. - * `organization_structure` - (Optional) Organization structure. - * `sandbox` - (Optional) Sandbox Organizational Unit configuration. - * `name` - (Optional) The sandbox Organizational Unit name. - * `security` - (Optional) Security Organizational Unit configuration. - * `name` - (Optional) The security Organizational Unit name. - * `security_roles` - (Optional) Organization structure. - * `account_id` - (Optional) The AWS account ID for security roles. -* `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `manifest` - (Required) The manifest JSON file is a text file that describes your AWS resources. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). * `version` - (Required) The landing zone version. +* `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attribute Reference From 2ff0a42432263fd46a9b33d685452d7a2eb46105 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 07:54:18 -0500 Subject: [PATCH 33/42] Fix semgrep 'ci.helper-schema-ResourceData-Set-extraneous-nil-check'. --- internal/service/controltower/landing_zone.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 3dd44688f69..493212ff712 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -140,11 +140,7 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i d.Set("drift_status", nil) } d.Set("latest_available_version", landingZone.LatestAvailableVersion) - if landingZone.Manifest != nil { - d.Set("manifest_json", landingZone.Manifest) - } else { - d.Set("manifest_json", nil) - } + d.Set("manifest_json", landingZone.Manifest) d.Set("version", landingZone.Version) return diags From 4d4e35eea164eac18e4fb690e1fe913fe2b056cb Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 07:55:40 -0500 Subject: [PATCH 34/42] Fix terrafmt error. --- website/docs/r/controltower_landing_zone.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 9e477a9dc9f..3f1dbc0c058 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -16,7 +16,7 @@ Creates a new landing zone using Control Tower. For more information on usage, p ```terraform resource "aws_controltower_landing_zone" "example" { manifest_json = jsondecode(file("${path.module}/LandingZoneManifest.json")) - version = "1.0" + version = "1.0" } ``` @@ -24,7 +24,7 @@ resource "aws_controltower_landing_zone" "example" { This resource supports the following arguments: -* `manifest` - (Required) The manifest JSON file is a text file that describes your AWS resources. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). +* `manifest_json` - (Required) The manifest JSON file is a text file that describes your AWS resources. For examples, review [Launch your landing zone](https://docs.aws.amazon.com/controltower/latest/userguide/lz-api-launch). * `version` - (Required) The landing zone version. * `tags` - (Optional) Tags to apply to the landing zone. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. From 9f037e4d2dc6326bbc06d8f3a5733693521bba65 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 08:01:52 -0500 Subject: [PATCH 35/42] r/aws_controltower_landing_zone: Normalize JSON. --- internal/service/controltower/landing_zone.go | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index 493212ff712..c6d25448213 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -18,6 +18,8 @@ import ( "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-plugin-sdk/v2/helper/structure" + "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" @@ -69,8 +71,15 @@ func resourceLandingZone() *schema.Resource { Computed: true, }, "manifest_json": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs, + DiffSuppressOnRefresh: true, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, }, "version": { Type: schema.TypeString, @@ -88,9 +97,13 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - manifestJson := d.Get("manifest_json").(string) + manifestJSON, err := structure.NormalizeJsonString(d.Get("manifest_json").(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + input := &controltower.CreateLandingZoneInput{ - Manifest: document.NewLazyDocument(manifestJson), + Manifest: document.NewLazyDocument(manifestJSON), Tags: getTagsIn(ctx), Version: aws.String(d.Get("version").(string)), } @@ -150,11 +163,15 @@ func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - manifestJson := d.Get("manifest_json").(string) if d.HasChangesExcept("tags", "tags_all") { + manifestJSON, err := structure.NormalizeJsonString(d.Get("manifest_json").(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + input := &controltower.UpdateLandingZoneInput{ LandingZoneIdentifier: aws.String(d.Id()), - Manifest: document.NewLazyDocument(manifestJson), + Manifest: document.NewLazyDocument(manifestJSON), Version: aws.String(d.Get("version").(string)), } From 8f22fa0944a1dec759c4a9030bcce06a44b5e441 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 08:42:53 -0500 Subject: [PATCH 36/42] Add 'json.SmithyDocumentToString'. --- internal/json/smithy.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 internal/json/smithy.go diff --git a/internal/json/smithy.go b/internal/json/smithy.go new file mode 100644 index 00000000000..93d19433915 --- /dev/null +++ b/internal/json/smithy.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package json + +import ( + "encoding/json" + + smithydocument "github.com/aws/smithy-go/document" +) + +// SmithyDocumentToString converts a [Smithy document](https://smithy.io/2.0/spec/simple-types.html#document) to a JSON string. +func SmithyDocumentToString(document smithydocument.Unmarshaler) (string, error) { + var v map[string]interface{} + + err := document.UnmarshalSmithyDocument(&v) + if err != nil { + return "", err + } + + bytes, err := json.Marshal(v) + if err != nil { + return "", err + } + + return string(bytes), nil +} From 5763799b2cf3a3957d4494b5e605d15d5422152f Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 08:44:49 -0500 Subject: [PATCH 37/42] r/aws_controltower_landing_zone: No need to call 'jsondecode' in tests. --- internal/service/controltower/landing_zone_test.go | 6 +++--- .../{fixtures => test-fixtures}/LandingZoneManifest.json | 0 website/docs/r/controltower_landing_zone.html.markdown | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename internal/service/controltower/{fixtures => test-fixtures}/LandingZoneManifest.json (100%) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index 39037393015..ff0dcd84489 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -169,7 +169,7 @@ const landingZoneVersion = "3.3" var testAccLandingZoneConfig_basic = fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + manifest_json = file("${path.module}/test-fixtures/LandingZoneManifest.json") version = %[2]q } @@ -178,7 +178,7 @@ resource "aws_controltower_landing_zone" "test" { func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + manifest_json = jfile("${path.module}/test-fixtures/LandingZoneManifest.json") version = %[2]q @@ -192,7 +192,7 @@ resource "aws_controltower_landing_zone" "test" { func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_controltower_landing_zone" "test" { - manifest_json = jsondecode(file("${path.module}/fixtures/LandingZoneManifest.json")) + manifest_json = file("${path.module}/test-fixtures/LandingZoneManifest.json") version = %[2]q diff --git a/internal/service/controltower/fixtures/LandingZoneManifest.json b/internal/service/controltower/test-fixtures/LandingZoneManifest.json similarity index 100% rename from internal/service/controltower/fixtures/LandingZoneManifest.json rename to internal/service/controltower/test-fixtures/LandingZoneManifest.json diff --git a/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown index 3f1dbc0c058..e81c7ee4ea7 100644 --- a/website/docs/r/controltower_landing_zone.html.markdown +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -15,8 +15,8 @@ Creates a new landing zone using Control Tower. For more information on usage, p ```terraform resource "aws_controltower_landing_zone" "example" { - manifest_json = jsondecode(file("${path.module}/LandingZoneManifest.json")) - version = "1.0" + manifest_json = file("${path.module}/LandingZoneManifest.json") + version = "3.2" } ``` From 3e971eb605bcaa69a3483e8624a40c56d220d4b7 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 08:45:24 -0500 Subject: [PATCH 38/42] r/aws_controltower_landing_zone: Call 'json.SmithyDocumentToString' in Read. --- internal/service/controltower/landing_zone.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index c6d25448213..a27242aef46 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -24,6 +24,7 @@ import ( "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" + "github.com/hashicorp/terraform-provider-aws/internal/json" 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" @@ -153,7 +154,17 @@ func resourceLandingZoneRead(ctx context.Context, d *schema.ResourceData, meta i d.Set("drift_status", nil) } d.Set("latest_available_version", landingZone.LatestAvailableVersion) - d.Set("manifest_json", landingZone.Manifest) + if landingZone.Manifest != nil { + v, err := json.SmithyDocumentToString(landingZone.Manifest) + + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.Set("manifest_json", v) + } else { + d.Set("manifest_json", nil) + } d.Set("version", landingZone.Version) return diags From f1bee74445a42a16696303134ced06e2cddbe5c0 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 10:39:56 -0500 Subject: [PATCH 39/42] Add 'json.SmithyDocumentFromString'. --- internal/json/smithy.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/json/smithy.go b/internal/json/smithy.go index 93d19433915..62a2b4e20f3 100644 --- a/internal/json/smithy.go +++ b/internal/json/smithy.go @@ -9,6 +9,18 @@ import ( smithydocument "github.com/aws/smithy-go/document" ) +func SmithyDocumentFromString[T smithydocument.Marshaler](s string, f func(any) T) (T, error) { + var v map[string]interface{} + + err := json.Unmarshal([]byte(s), &v) + if err != nil { + var zero T + return zero, err + } + + return f(v), nil +} + // SmithyDocumentToString converts a [Smithy document](https://smithy.io/2.0/spec/simple-types.html#document) to a JSON string. func SmithyDocumentToString(document smithydocument.Unmarshaler) (string, error) { var v map[string]interface{} From 73676e4e156c8cbbaab0798af43629a9ff5dd8cf Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 10:40:36 -0500 Subject: [PATCH 40/42] r/aws_controltower_landing_zone: Call 'json.SmithyDocumentFromString' in Create and Update. --- internal/service/controltower/landing_zone.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/controltower/landing_zone.go b/internal/service/controltower/landing_zone.go index a27242aef46..1491a3dbf6f 100644 --- a/internal/service/controltower/landing_zone.go +++ b/internal/service/controltower/landing_zone.go @@ -98,13 +98,13 @@ func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) - manifestJSON, err := structure.NormalizeJsonString(d.Get("manifest_json").(string)) + manifest, err := json.SmithyDocumentFromString(d.Get("manifest_json").(string), document.NewLazyDocument) if err != nil { return sdkdiag.AppendFromErr(diags, err) } input := &controltower.CreateLandingZoneInput{ - Manifest: document.NewLazyDocument(manifestJSON), + Manifest: manifest, Tags: getTagsIn(ctx), Version: aws.String(d.Get("version").(string)), } @@ -175,14 +175,14 @@ func resourceLandingZoneUpdate(ctx context.Context, d *schema.ResourceData, meta conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) if d.HasChangesExcept("tags", "tags_all") { - manifestJSON, err := structure.NormalizeJsonString(d.Get("manifest_json").(string)) + manifest, err := json.SmithyDocumentFromString(d.Get("manifest_json").(string), document.NewLazyDocument) if err != nil { return sdkdiag.AppendFromErr(diags, err) } input := &controltower.UpdateLandingZoneInput{ LandingZoneIdentifier: aws.String(d.Id()), - Manifest: document.NewLazyDocument(manifestJSON), + Manifest: manifest, Version: aws.String(d.Get("version").(string)), } From 3a2f612374958a42d1f4ff9b2ca85198d094bf4c Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 10:53:13 -0500 Subject: [PATCH 41/42] r/aws_controltower_landing_zone: Skip acceptance tests if landing zone already exists. --- .../service/controltower/landing_zone_test.go | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go index ff0dcd84489..71981e49811 100644 --- a/internal/service/controltower/landing_zone_test.go +++ b/internal/service/controltower/landing_zone_test.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go-v2/service/controltower" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -26,6 +27,7 @@ func testAccLandingZone_basic(t *testing.T) { acctest.PreCheck(ctx, t) acctest.PreCheckOrganizationManagementAccount(ctx, t) testAccPreCheck(ctx, t) + testAccPreCheckNoLandingZone(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), CheckDestroy: testAccCheckLandingZoneDestroy(ctx), @@ -59,6 +61,7 @@ func testAccLandingZone_disappears(t *testing.T) { acctest.PreCheck(ctx, t) acctest.PreCheckOrganizationManagementAccount(ctx, t) testAccPreCheck(ctx, t) + testAccPreCheckNoLandingZone(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -85,6 +88,7 @@ func testAccLandingZone_tags(t *testing.T) { acctest.PreCheck(ctx, t) acctest.PreCheckOrganizationManagementAccount(ctx, t) testAccPreCheck(ctx, t) + testAccPreCheckNoLandingZone(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), CheckDestroy: testAccCheckLandingZoneDestroy(ctx), @@ -124,6 +128,27 @@ func testAccLandingZone_tags(t *testing.T) { }) } +func testAccPreCheckNoLandingZone(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).ControlTowerClient(ctx) + + input := &controltower.ListLandingZonesInput{} + var n int + pages := controltower.NewListLandingZonesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } + + n += len(page.LandingZones) + } + + if n > 0 { + t.Skip("skipping since Landing Zone already exists") + } +} + func testAccCheckLandingZoneExists(ctx context.Context, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] From 139597a0d3fa1c0db97aca2c6c49a67656b187e5 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sun, 4 Feb 2024 10:54:56 -0500 Subject: [PATCH 42/42] Additional CHANGELOG entry. --- .changelog/34595.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.changelog/34595.txt b/.changelog/34595.txt index 6476b00d734..ac4d431c47a 100644 --- a/.changelog/34595.txt +++ b/.changelog/34595.txt @@ -1,3 +1,7 @@ ```release-note:new-resource aws_controltower_landing_zone ``` + +```release-note:note +resource/aws_controltower_landing_zone: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing +``` \ No newline at end of file