diff --git a/.changelog/34595.txt b/.changelog/34595.txt new file mode 100644 index 00000000000..ac4d431c47a --- /dev/null +++ b/.changelog/34595.txt @@ -0,0 +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 diff --git a/internal/json/smithy.go b/internal/json/smithy.go new file mode 100644 index 00000000000..62a2b4e20f3 --- /dev/null +++ b/internal/json/smithy.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package json + +import ( + "encoding/json" + + 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{} + + err := document.UnmarshalSmithyDocument(&v) + if err != nil { + return "", err + } + + bytes, err := json.Marshal(v) + if err != nil { + return "", err + } + + return string(bytes), nil +} 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/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/generate.go b/internal/service/controltower/generate.go index 5bed2ff12f6..e748557f89b 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 -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 new file mode 100644 index 00000000000..1491a3dbf6f --- /dev/null +++ b/internal/service/controltower/landing_zone.go @@ -0,0 +1,330 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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-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" + "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" + "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{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(120 * time.Minute), + Update: schema.DefaultTimeout(120 * time.Minute), + Delete: schema.DefaultTimeout(120 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + 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, + }, + "manifest_json": { + 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, + Required: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceLandingZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + manifest, err := json.SmithyDocumentFromString(d.Get("manifest_json").(string), document.NewLazyDocument) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input := &controltower.CreateLandingZoneInput{ + Manifest: manifest, + Tags: getTagsIn(ctx), + Version: aws.String(d.Get("version").(string)), + } + + output, err := conn.CreateLandingZone(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating ControlTower Landing Zone: %s", err) + } + + id, err := landingZoneIDFromARN(aws.ToString(output.Arn)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.SetId(id) + + if _, err := waitLandingZoneOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "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) + + landingZone, err := findLandingZoneByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + 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 ControlTower Landing Zone (%s): %s", d.Id(), err) + } + + 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 { + 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 +} + +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") { + 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: manifest, + Version: aws.String(d.Get("version").(string)), + } + + 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)...) +} + +func resourceLandingZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ControlTowerClient(ctx) + + log.Printf("[DEBUG] Deleting ControlTower Landing Zone: %s", d.Id()) + output, err := conn.DeleteLandingZone(ctx, &controltower.DeleteLandingZoneInput{ + LandingZoneIdentifier: aws.String(d.Id()), + }) + + if err != nil { + 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 ControlTower Landing Zone (%s) delete: %s", d.Id(), err) + } + + return diags +} + +func landingZoneIDFromARN(arnString string) (string, error) { + arn, err := arn.Parse(arnString) + if err != nil { + return "", err + } + + // arn:${Partition}:controltower:${Region}:${Account}:landingzone/${LandingZoneId} + return strings.TrimPrefix(arn.Resource, "landingzone/"), nil +} + +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 errs.IsA[*types.ResourceNotFoundException](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 := findLandingZoneOperationByID(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: enum.Slice(types.LandingZoneOperationStatusInProgress), + Target: enum.Slice(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 +} + +func flattenLandingZoneDriftStatusSummary(apiObject *types.LandingZoneDriftStatusSummary) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "status": apiObject.Status, + } + + return tfMap +} diff --git a/internal/service/controltower/landing_zone_test.go b/internal/service/controltower/landing_zone_test.go new file mode 100644 index 00000000000..71981e49811 --- /dev/null +++ b/internal/service/controltower/landing_zone_test.go @@ -0,0 +1,230 @@ +// 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 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) + testAccPreCheckNoLandingZone(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), + CheckDestroy: testAccCheckLandingZoneDestroy(ctx), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccLandingZoneConfig_basic, + 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"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +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) + testAccPreCheckNoLandingZone(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ControlTowerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLandingZoneDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLandingZoneConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLandingZoneExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceLandingZone(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +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) + testAccPreCheckNoLandingZone(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 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] + 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) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_controltower_landing_zone" { + continue + } + + _, err := tfcontroltower.FindLandingZoneByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("ControlTower Landing Zone %s still exists", rs.Primary.ID) + } + + return nil + } +} + +const landingZoneVersion = "3.3" + +var testAccLandingZoneConfig_basic = fmt.Sprintf(` +resource "aws_controltower_landing_zone" "test" { + manifest_json = file("${path.module}/test-fixtures/LandingZoneManifest.json") + + version = %[2]q +} +`, acctest.Region(), landingZoneVersion) + +func testAccLandingZoneConfig_tags1(tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_controltower_landing_zone" "test" { + manifest_json = jfile("${path.module}/test-fixtures/LandingZoneManifest.json") + + version = %[2]q + + tags = { + %[3]q = %[4]q + } +} +`, acctest.Region(), landingZoneVersion, tagKey1, tagValue1) +} + +func testAccLandingZoneConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_controltower_landing_zone" "test" { + manifest_json = file("${path.module}/test-fixtures/LandingZoneManifest.json") + + version = %[2]q + + tags = { + %[3]q = %[4]q + %[5]q = %[6]q + } +} +`, acctest.Region(), landingZoneVersion, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/controltower/service_package_gen.go b/internal/service/controltower/service_package_gen.go index f9c58694ff9..1bec5dc8b08 100644 --- a/internal/service/controltower/service_package_gen.go +++ b/internal/service/controltower/service_package_gen.go @@ -39,6 +39,14 @@ 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", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, } } diff --git a/internal/service/controltower/tags_gen.go b/internal/service/controltower/tags_gen.go new file mode 100644 index 00000000000..6c9f2996afb --- /dev/null +++ b/internal/service/controltower/tags_gen.go @@ -0,0 +1,128 @@ +// 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/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, optFns ...func(*controltower.Options)) (tftags.KeyValueTags, error) { + input := &controltower.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(ctx, input, optFns...) + + 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 = option.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 = option.Some(KeyValueTags(ctx, 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, optFns ...func(*controltower.Options)) 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, optFns...) + + 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, optFns...) + + 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) +} diff --git a/internal/service/controltower/test-fixtures/LandingZoneManifest.json b/internal/service/controltower/test-fixtures/LandingZoneManifest.json new file mode 100644 index 00000000000..d9a449ee2ee --- /dev/null +++ b/internal/service/controltower/test-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/website/docs/r/controltower_landing_zone.html.markdown b/website/docs/r/controltower_landing_zone.html.markdown new file mode 100644 index 00000000000..e81c7ee4ea7 --- /dev/null +++ b/website/docs/r/controltower_landing_zone.html.markdown @@ -0,0 +1,65 @@ +--- +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_json = file("${path.module}/LandingZoneManifest.json") + version = "3.2" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `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. + +## 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. +* `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). + +## 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: + +```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 +```