From 07b3af46b9fb788e2e6194cf21e579895f37e554 Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 8 Sep 2025 11:47:09 +0000 Subject: [PATCH 01/15] done implementation and tests --- .../distribution_tenant_data_source.go | 95 +++++++++++++++++++ .../distribution_tenant_data_source_test.go | 48 ++++++++++ .../service/cloudfront/service_package_gen.go | 9 ++ 3 files changed, 152 insertions(+) create mode 100644 internal/service/cloudfront/distribution_tenant_data_source.go create mode 100644 internal/service/cloudfront/distribution_tenant_data_source_test.go diff --git a/internal/service/cloudfront/distribution_tenant_data_source.go b/internal/service/cloudfront/distribution_tenant_data_source.go new file mode 100644 index 000000000000..a327bd05c63b --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_data_source.go @@ -0,0 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "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/errs/sdkdiag" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKDataSource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") +// @Tags(identifierAttribute="arn") +func dataSourceDistributionTenant() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceDistributionTenantRead, + + Schema: map[string]*schema.Schema{ + names.AttrARN: { + Type: schema.TypeString, + Computed: true, + }, + "connection_group_id": { + Type: schema.TypeString, + Computed: true, + }, + "distribution_id": { + Type: schema.TypeString, + Computed: true, + }, + "domains": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + names.AttrEnabled: { + Type: schema.TypeBool, + Computed: true, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrID: { + Type: schema.TypeString, + Required: true, + }, + "last_modified_time": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrName: { + Type: schema.TypeString, + Computed: true, + }, + names.AttrStatus: { + Type: schema.TypeString, + Computed: true, + }, + names.AttrTags: tftags.TagsSchemaComputed(), + }, + } +} + +func dataSourceDistributionTenantRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + + id := d.Get(names.AttrID).(string) + output, err := findDistributionTenantByID(ctx, conn, id) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant (%s): %s", id, err) + } + + d.SetId(aws.ToString(output.DistributionTenant.Id)) + tenant := output.DistributionTenant + d.Set(names.AttrARN, tenant.Arn) + d.Set("connection_group_id", tenant.ConnectionGroupId) + d.Set("distribution_id", tenant.DistributionId) + d.Set("domains", flattenDomains(tenant.Domains)) + d.Set(names.AttrEnabled, tenant.Enabled) + d.Set("etag", output.ETag) + d.Set(names.AttrName, tenant.Name) + d.Set(names.AttrStatus, tenant.Status) + d.Set("last_modified_time", tenant.LastModifiedTime.String()) + + return diags +} diff --git a/internal/service/cloudfront/distribution_tenant_data_source_test.go b/internal/service/cloudfront/distribution_tenant_data_source_test.go new file mode 100644 index 000000000000..b1f1ee479387 --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_data_source_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccCloudFrontDistributionTenantDataSource_basic(t *testing.T) { + ctx := acctest.Context(t) + dataSourceName := "data.aws_cloudfront_distribution_tenant.test" + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantDataSourceConfig_basic(t), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "distribution_id", resourceName, "distribution_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "domains.#", resourceName, "domains.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrEnabled, resourceName, names.AttrEnabled), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "last_modified_time", resourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + ), + }, + }, + }) +} + +func testAccDistributionTenantDataSourceConfig_basic(t *testing.T) string { + return acctest.ConfigCompose(testAccDistributionTenantConfig_basic(t), ` +data "aws_cloudfront_distribution_tenant" "test" { + id = aws_cloudfront_distribution_tenant.test.id +} +`) +} diff --git a/internal/service/cloudfront/service_package_gen.go b/internal/service/cloudfront/service_package_gen.go index 1db1f6705adc..79a77ecbe595 100644 --- a/internal/service/cloudfront/service_package_gen.go +++ b/internal/service/cloudfront/service_package_gen.go @@ -86,6 +86,15 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*inttypes.Service }), Region: unique.Make(inttypes.ResourceRegionDisabled()), }, + { + Factory: dataSourceDistributionTenant, + TypeName: "aws_cloudfront_distribution_tenant", + Name: "Distribution Tenant", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, { Factory: dataSourceFunction, TypeName: "aws_cloudfront_function", From d0db6c99081138a4d2aa848722d3d692c3accfda Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 8 Sep 2025 11:51:01 +0000 Subject: [PATCH 02/15] changelog --- .changelog/44183.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/44183.txt diff --git a/.changelog/44183.txt b/.changelog/44183.txt new file mode 100644 index 000000000000..62eba15aa10d --- /dev/null +++ b/.changelog/44183.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_cloudfront_distribution_tenant +``` \ No newline at end of file From b39a7a8c84e7c83b7b30362b3b9475a12d229563 Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 15 Sep 2025 10:26:21 +0000 Subject: [PATCH 03/15] added tenant lookup by domain --- .../distribution_tenant_data_source.go | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant_data_source.go b/internal/service/cloudfront/distribution_tenant_data_source.go index a327bd05c63b..b2957a7e86f6 100644 --- a/internal/service/cloudfront/distribution_tenant_data_source.go +++ b/internal/service/cloudfront/distribution_tenant_data_source.go @@ -7,11 +7,16 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -34,6 +39,11 @@ func dataSourceDistributionTenant() *schema.Resource { Type: schema.TypeString, Computed: true, }, + names.AttrDomain: { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"domain", names.AttrID}, + }, "domains": { Type: schema.TypeSet, Computed: true, @@ -48,8 +58,10 @@ func dataSourceDistributionTenant() *schema.Resource { Computed: true, }, names.AttrID: { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"domain", names.AttrID}, }, "last_modified_time": { Type: schema.TypeString, @@ -72,24 +84,63 @@ func dataSourceDistributionTenantRead(ctx context.Context, d *schema.ResourceDat var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) - id := d.Get(names.AttrID).(string) - output, err := findDistributionTenantByID(ctx, conn, id) + var identifier string + var tenant *awstypes.DistributionTenant + var etag *string - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant (%s): %s", id, err) + if id, ok := d.GetOk(names.AttrID); ok { + identifier = id.(string) + output, err := findDistributionTenantByID(ctx, conn, identifier) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant by ID (%s): %s", identifier, err) + } + tenant = output.DistributionTenant + etag = output.ETag + } else { + identifier = d.Get(names.AttrDomain).(string) + output, err := findDistributionTenantByDomain(ctx, conn, identifier) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant by Domain (%s): %s", identifier, err) + } + tenant = output.DistributionTenant + etag = output.ETag } - d.SetId(aws.ToString(output.DistributionTenant.Id)) - tenant := output.DistributionTenant + d.SetId(aws.ToString(tenant.Id)) d.Set(names.AttrARN, tenant.Arn) d.Set("connection_group_id", tenant.ConnectionGroupId) d.Set("distribution_id", tenant.DistributionId) d.Set("domains", flattenDomains(tenant.Domains)) d.Set(names.AttrEnabled, tenant.Enabled) - d.Set("etag", output.ETag) + d.Set("etag", etag) d.Set(names.AttrName, tenant.Name) d.Set(names.AttrStatus, tenant.Status) d.Set("last_modified_time", tenant.LastModifiedTime.String()) return diags } + +func findDistributionTenantByDomain(ctx context.Context, conn *cloudfront.Client, domain string) (*cloudfront.GetDistributionTenantByDomainOutput, error) { + input := cloudfront.GetDistributionTenantByDomainInput{ + Domain: aws.String(domain), + } + + output, err := conn.GetDistributionTenantByDomain(ctx, &input) + + if errs.IsA[*awstypes.EntityNotFound](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.DistributionTenant == nil || output.DistributionTenant.Domains == nil || output.DistributionTenant.DistributionId == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} From 19b99a256502929ebd93c8ae7e8c6c9bb1bee4f5 Mon Sep 17 00:00:00 2001 From: codyzhao2770 <122773685+codyzhao2770@users.noreply.github.com> Date: Wed, 24 Sep 2025 03:09:52 -0700 Subject: [PATCH 04/15] docs --- ...oudfront_distribution_tenant.html.markdown | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 website/docs/d/cloudfront_distribution_tenant.html.markdown diff --git a/website/docs/d/cloudfront_distribution_tenant.html.markdown b/website/docs/d/cloudfront_distribution_tenant.html.markdown new file mode 100644 index 000000000000..ef9ff462d455 --- /dev/null +++ b/website/docs/d/cloudfront_distribution_tenant.html.markdown @@ -0,0 +1,49 @@ +--- +subcategory: "CloudFront" +layout: "aws" +page_title: "AWS: aws_cloudfront_distribution_tenant" +description: |- + Provides a CloudFront distribution tenant data source. +--- + +# Data Source: aws_cloudfront_distribution_tenant + +Use this data source to retrieve information about a CloudFront distribution tenant. + +## Example Usage + +```terraform +data "aws_cloudfront_distribution_tenant" "test" { + id = "EDFDVBD632BHDS5" +} +``` + +## Argument Reference + +Exactly one of the following arguments must be specified for the data source: + +* `id` (optional) - Identifier for the distribution tenant. For example: `EDFDVBD632BHDS5`. +* `domain` (optional) - An associated domain of the distribution tenant. + +## Attribute Reference + +This data source exports the following attributes in addition to the arguments above: + +* `domains` - List of domains for the distribution tenant. + +* `arn` - ARN (Amazon Resource Name) for the distribution tenant. + +* `status` - Current status of the distribution tenant. `Deployed` if the + distribution tenant's information is fully propagated throughout the Amazon + CloudFront system. + +* `last_modified_time` - Date and time the distribution tenant was last modified. + +* `distribution_id` - The ID of the CloudFront distribution the tenant is associated with. + +* `etag` - Current version of the distribution tenant's information. For example: + `E2QWRUHAPOMQZL`. + +* `enabled` - Whether the distribution tenant is enabled. + +* `connection_group_id` - The CloudFront connection group the tenant is associated with. From 60ba37a23172b55c9faf04a655b1dcb62870fc6f Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 8 Sep 2025 11:25:53 +0000 Subject: [PATCH 05/15] done implementation and tests --- internal/service/cloudfront/consts.go | 5 + .../service/cloudfront/distribution_tenant.go | 818 ++++++++++++++++ .../cloudfront/distribution_tenant_test.go | 903 ++++++++++++++++++ .../service/cloudfront/service_package_gen.go | 9 + 4 files changed, 1735 insertions(+) create mode 100644 internal/service/cloudfront/distribution_tenant.go create mode 100644 internal/service/cloudfront/distribution_tenant_test.go diff --git a/internal/service/cloudfront/consts.go b/internal/service/cloudfront/consts.go index 557fb6d52381..6d5396582bac 100644 --- a/internal/service/cloudfront/consts.go +++ b/internal/service/cloudfront/consts.go @@ -20,6 +20,11 @@ const ( distributionStatusInProgress = "InProgress" ) +const ( + distributionTenantStatusDeployed = "Deployed" + distributionTenantStatusInProgress = "InProgress" +) + const ( keyValueStoreStatusProvisioning = "PROVISIONING" keyValueStoreStatusReady = "READY" diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go new file mode 100644 index 000000000000..69ed84829c1b --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant.go @@ -0,0 +1,818 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/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/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" + ret "github.com/hashicorp/terraform-provider-aws/internal/retry" + 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_cloudfront_distribution_tenant", name="Distribution Tenant") +// @Tags(identifierAttribute="arn") +func resourceDistributionTenant() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceDistributionTenantCreate, + ReadWithoutTimeout: resourceDistributionTenantRead, + UpdateWithoutTimeout: resourceDistributionTenantUpdate, + DeleteWithoutTimeout: resourceDistributionTenantDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + // Set non API attributes to their Default settings in the schema + d.Set("wait_for_deployment", true) + return []*schema.ResourceData{d}, nil + }}, + + Schema: map[string]*schema.Schema{ + names.AttrARN: { + Type: schema.TypeString, + Computed: true, + }, + "connection_group_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "customizations": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "geo_restriction": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "locations": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "restriction_type": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.GeoRestrictionType](), + }, + }, + }, + }, + "certificate": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrARN: { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, + "web_acl": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrAction: { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.CustomizationActionType](), + }, + names.AttrARN: { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, + }, + }, + }, + "distribution_id": { + Type: schema.TypeString, + Required: true, + }, + "domains": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + names.AttrEnabled: { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + "last_modified_time": { + Type: schema.TypeString, + Computed: true, + }, + "managed_certificate_request": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate_transparency_logging_preference": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.CertificateTransparencyLoggingPreference](), + }, + "primary_domain_name": { + Type: schema.TypeString, + Optional: true, + }, + "validation_token_host": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.ValidationTokenHost](), + }, + }, + }, + }, + names.AttrName: { + Type: schema.TypeString, + Required: true, + }, + names.AttrParameters: { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrName: { + Type: schema.TypeString, + Required: true, + }, + names.AttrValue: { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + names.AttrStatus: { + Type: schema.TypeString, + Computed: true, + }, + "wait_for_deployment": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + } +} + +func resourceDistributionTenantCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + + input := cloudfront.CreateDistributionTenantInput{ + DistributionId: aws.String(d.Get("distribution_id").(string)), + Domains: expandDomains(d.Get("domains").(*schema.Set)), + Enabled: aws.Bool(d.Get(names.AttrEnabled).(bool)), + Name: aws.String(d.Get(names.AttrName).(string)), + } + + if v, ok := d.GetOk("connection_group_id"); ok { + input.ConnectionGroupId = aws.String(v.(string)) + } + if v, ok := d.GetOk("customizations"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { + input.Customizations = expandCustomizations(v.([]any)[0].(map[string]any)) + } + if v, ok := d.GetOk("managed_certificate_request"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { + input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) + } + if v, ok := d.GetOk(names.AttrParameters); ok { + input.Parameters = expandParameters(v.([]any)) + } + if tags := getTagsIn(ctx); len(tags) > 0 { + input.Tags = &awstypes.Tags{Items: tags} + } + + // ACM and IAM certificate eventual consistency. + // InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain. + const ( + timeout = 1 * time.Minute + ) + + outputRaw, err := tfresource.RetryWhenIsA[any, *awstypes.InvalidViewerCertificate](ctx, timeout, func(ctx context.Context) (any, error) { + return conn.CreateDistributionTenant(ctx, &input) + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating CloudFront Distribution Tenant: %s", err) + } + + d.SetId(aws.ToString(outputRaw.(*cloudfront.CreateDistributionTenantOutput).DistributionTenant.Id)) + + if d.Get("wait_for_deployment").(bool) { + if _, err := waitDistributionTenantDeployed(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for CloudFront Distribution Tenant (%s) deploy: %s", d.Id(), err) + } + } + + return append(diags, resourceDistributionTenantRead(ctx, d, meta)...) +} + +func resourceDistributionTenantRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + + output, err := findDistributionTenantByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && ret.NotFound(err) { + log.Printf("[WARN] CloudFront Distribution Tenant (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant (%s): %s", d.Id(), err) + } + + tenant := output.DistributionTenant + d.Set(names.AttrARN, tenant.Arn) + d.Set("connection_group_id", tenant.ConnectionGroupId) + if tenant.Customizations != nil { + if err := d.Set("customizations", []any{flattenCustomizations(tenant.Customizations)}); err != nil { + return sdkdiag.AppendErrorf(diags, "setting customizations: %s", err) + } + } + d.Set("distribution_id", tenant.DistributionId) + d.Set("domains", flattenDomains(tenant.Domains)) + d.Set("etag", output.ETag) + d.Set(names.AttrEnabled, tenant.Enabled) + d.Set("last_modified_time", aws.String(tenant.LastModifiedTime.String())) + d.Set(names.AttrName, tenant.Name) + if tenant.Parameters != nil { + if err := d.Set(names.AttrParameters, flattenParameters(tenant.Parameters)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) + } + } + d.Set(names.AttrStatus, tenant.Status) + + return diags +} + +func resourceDistributionTenantUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + + if d.HasChangesExcept(names.AttrTags, names.AttrTagsAll) { + input := cloudfront.UpdateDistributionTenantInput{ + Id: aws.String(d.Id()), + IfMatch: aws.String(d.Get("etag").(string)), + DistributionId: aws.String(d.Get("distribution_id").(string)), + Domains: expandDomains(d.Get("domains").(*schema.Set)), + Enabled: aws.Bool(d.Get(names.AttrEnabled).(bool)), + } + + if v, ok := d.GetOk("connection_group_id"); ok { + input.ConnectionGroupId = aws.String(v.(string)) + } + if v, ok := d.GetOk("customizations"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { + input.Customizations = expandCustomizations(v.([]any)[0].(map[string]any)) + } + if v, ok := d.GetOk("managed_certificate_request"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { + input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) + } + if v, ok := d.GetOk(names.AttrParameters); ok { + input.Parameters = expandParameters(v.([]any)) + } + + // ACM and IAM certificate eventual consistency. + // InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain. + const ( + timeout = 1 * time.Minute + ) + _, err := tfresource.RetryWhenIsA[any, *awstypes.InvalidViewerCertificate](ctx, timeout, func(ctx context.Context) (any, error) { + return conn.UpdateDistributionTenant(ctx, &input) + }) + + // Refresh our ETag if it is out of date and attempt update again. + if errs.IsA[*awstypes.PreconditionFailed](err) { + var etag string + etag, err = distributionTenantETag(ctx, conn, d.Id()) + + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input.IfMatch = aws.String(etag) + + _, err = conn.UpdateDistributionTenant(ctx, &input) + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating CloudFront Distribution Tenant (%s): %s", d.Id(), err) + } + if d.Get("wait_for_deployment").(bool) { + if _, err := waitDistributionTenantDeployed(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for CloudFront Distribution Tenant (%s) deploy: %s", d.Id(), err) + } + } + } + + return append(diags, resourceDistributionTenantRead(ctx, d, meta)...) +} + +func resourceDistributionTenantDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + + if d.Get(names.AttrARN).(string) == "" { + diags = append(diags, resourceDistributionTenantRead(ctx, d, meta)...) + } + + if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + if ret.NotFound(err) { + return diags + } + return sdkdiag.AppendFromErr(diags, err) + } + + err := deleteDistributionTenant(ctx, conn, d.Id()) + + if err == nil || ret.NotFound(err) || errs.IsA[*awstypes.EntityNotFound](err) { + return diags + } + + // Disable distribution tenant if it is not yet disabled and attempt deletion again. + // Here we update via the deployed configuration to ensure we are not submitting an out of date + // configuration from the Terraform configuration, should other changes have occurred manually. + if errs.IsA[*awstypes.ResourceNotDisabled](err) { + if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + if ret.NotFound(err) { + return diags + } + + return sdkdiag.AppendFromErr(diags, err) + } + + const ( + timeout = 3 * time.Minute + ) + _, err = tfresource.RetryWhenIsA[any, *awstypes.ResourceNotDisabled](ctx, timeout, func(ctx context.Context) (any, error) { + return nil, deleteDistributionTenant(ctx, conn, d.Id()) + }) + } + + if errs.IsA[*awstypes.PreconditionFailed](err) || errs.IsA[*awstypes.InvalidIfMatchVersion](err) { + const ( + timeout = 1 * time.Minute + ) + _, err = tfresource.RetryWhenIsOneOf2[any, *awstypes.PreconditionFailed, *awstypes.InvalidIfMatchVersion](ctx, timeout, func(ctx context.Context) (any, error) { + return nil, deleteDistributionTenant(ctx, conn, d.Id()) + }) + } + + if errs.IsA[*awstypes.ResourceNotDisabled](err) { + if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + if ret.NotFound(err) { + return diags + } + + return sdkdiag.AppendFromErr(diags, err) + } + + err = deleteDistributionTenant(ctx, conn, d.Id()) + } + + if errs.IsA[*awstypes.EntityNotFound](err) { + return diags + } + + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + return diags +} + +func waitDistributionTenantDeployed(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{distributionTenantStatusInProgress}, + Target: []string{distributionTenantStatusDeployed}, + Refresh: statusDistributionTenant(ctx, conn, id), + Timeout: 90 * time.Minute, + MinTimeout: 15 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { + return output, err + } + + return nil, err +} + +func waitDistributionTenantDeleted(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{distributionTenantStatusInProgress, distributionTenantStatusDeployed}, + Target: []string{}, + Refresh: statusDistributionTenant(ctx, conn, id), + Timeout: 90 * time.Minute, + MinTimeout: 15 * time.Second, + Delay: 15 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { + return output, err + } + + return nil, err +} + +func statusDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + output, err := findDistributionTenantByID(ctx, conn, id) + + if ret.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if output == nil { + return nil, "", nil + } + + return output, aws.ToString(output.DistributionTenant.Status), nil + } +} + +func findDistributionTenantByID(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + input := cloudfront.GetDistributionTenantInput{ + Identifier: aws.String(id), + } + + output, err := conn.GetDistributionTenant(ctx, &input) + + if errs.IsA[*awstypes.EntityNotFound](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.DistributionTenant == nil || output.DistributionTenant.Domains == nil || output.DistributionTenant.DistributionId == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func disableDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) error { + output, err := findDistributionTenantByID(ctx, conn, id) + + if err != nil { + return fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) + } + + if aws.ToString(output.DistributionTenant.Status) == distributionTenantStatusInProgress { + output, err = waitDistributionTenantDeployed(ctx, conn, id) + + if err != nil { + return fmt.Errorf("waiting for CloudFront Distribution (%s) deploy: %w", id, err) + } + } + + if !aws.ToBool(output.DistributionTenant.Enabled) { + return nil + } + + input := cloudfront.UpdateDistributionTenantInput{ + Id: aws.String(id), + IfMatch: output.ETag, + ConnectionGroupId: output.DistributionTenant.ConnectionGroupId, + Customizations: output.DistributionTenant.Customizations, + DistributionId: output.DistributionTenant.DistributionId, + Domains: convertDomainResultsToDomainItems(output.DistributionTenant.Domains), + Parameters: output.DistributionTenant.Parameters, + } + + input.Enabled = aws.Bool(false) + + _, err = conn.UpdateDistributionTenant(ctx, &input) + + if err != nil { + return fmt.Errorf("updating CloudFront Distribution Tenant (%s): %w", id, err) + } + + if _, err := waitDistributionTenantDeployed(ctx, conn, id); err != nil { + return fmt.Errorf("waiting for CloudFront Distribution Tenant (%s) deploy: %w", id, err) + } + + return nil +} + +func convertDomainResultsToDomainItems(results []awstypes.DomainResult) []awstypes.DomainItem { + items := make([]awstypes.DomainItem, len(results)) + for i, result := range results { + items[i] = awstypes.DomainItem{ + Domain: result.Domain, + } + } + return items +} + +func distributionTenantETag(ctx context.Context, conn *cloudfront.Client, id string) (string, error) { + output, err := findDistributionTenantByID(ctx, conn, id) + + if err != nil { + return "", fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) + } + + return aws.ToString(output.ETag), nil +} + +func deleteDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) error { + etag, err := distributionTenantETag(ctx, conn, id) + + if err != nil { + return err + } + + input := cloudfront.DeleteDistributionTenantInput{ + Id: aws.String(id), + IfMatch: aws.String(etag), + } + + _, err = conn.DeleteDistributionTenant(ctx, &input) + + if err != nil { + return fmt.Errorf("deleting CloudFront Distribution Tenant (%s): %w", id, err) + } + + if _, err := waitDistributionTenantDeleted(ctx, conn, id); err != nil { + return fmt.Errorf("waiting for CloudFront Distribution Tenant (%s) delete: %w", id, err) + } + + return nil +} + +func expandDomains(tfSet *schema.Set) []awstypes.DomainItem { + if tfSet.Len() == 0 { + return nil + } + + domains := make([]awstypes.DomainItem, tfSet.Len()) + for i, v := range tfSet.List() { + domains[i] = awstypes.DomainItem{ + Domain: aws.String(v.(string)), + } + } + return domains +} + +func flattenDomains(apiObjects []awstypes.DomainResult) *schema.Set { + if len(apiObjects) == 0 { + return nil + } + + tfSet := schema.NewSet(schema.HashString, []any{}) + for _, apiObject := range apiObjects { + if apiObject.Domain != nil { + tfSet.Add(aws.ToString(apiObject.Domain)) + } + } + return tfSet +} + +func expandCustomizations(tfMap map[string]any) *awstypes.Customizations { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.Customizations{} + + if v, ok := tfMap["geo_restriction"].([]any); ok && len(v) > 0 && v[0] != nil { + apiObject.GeoRestrictions = expandTenantGeoRestriction(v[0].(map[string]any)) + } + + if v, ok := tfMap["certificate"].([]any); ok && len(v) > 0 && v[0] != nil { + apiObject.Certificate = expandCertificate(v[0].(map[string]any)) + } + + if v, ok := tfMap["web_acl"].([]any); ok && len(v) > 0 && v[0] != nil { + apiObject.WebAcl = expandWebAcl(v[0].(map[string]any)) + } + + return apiObject +} + +func expandTenantGeoRestriction(tfMap map[string]any) *awstypes.GeoRestrictionCustomization { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.GeoRestrictionCustomization{} + + if v, ok := tfMap["locations"].(*schema.Set); ok && v.Len() > 0 { + apiObject.Locations = flex.ExpandStringValueSet(v) + } + + if v, ok := tfMap["restriction_type"].(string); ok && v != "" { + apiObject.RestrictionType = awstypes.GeoRestrictionType(v) + } + + return apiObject +} + +func expandCertificate(tfMap map[string]any) *awstypes.Certificate { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.Certificate{} + + if v, ok := tfMap[names.AttrARN].(string); ok && v != "" { + apiObject.Arn = aws.String(v) + } + + return apiObject +} + +func expandWebAcl(tfMap map[string]any) *awstypes.WebAclCustomization { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.WebAclCustomization{} + + if v, ok := tfMap[names.AttrAction].(string); ok && v != "" { + apiObject.Action = awstypes.CustomizationActionType(v) + } + + if v, ok := tfMap[names.AttrARN].(string); ok && v != "" { + apiObject.Arn = aws.String(v) + } + + return apiObject +} + +func flattenCustomizations(apiObject *awstypes.Customizations) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + + if apiObject.GeoRestrictions != nil { + tfMap["geo_restriction"] = []any{flattenTenantGeoRestriction(apiObject.GeoRestrictions)} + } + + if apiObject.Certificate != nil { + tfMap["certificate"] = []any{flattenCertificate(apiObject.Certificate)} + } + + if apiObject.WebAcl != nil { + tfMap["web_acl"] = []any{flattenWebAcl(apiObject.WebAcl)} + } + + return tfMap +} + +func flattenTenantGeoRestriction(apiObject *awstypes.GeoRestrictionCustomization) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + + if apiObject.Locations != nil { + tfMap["locations"] = flex.FlattenStringValueSet(apiObject.Locations) + } + + tfMap["restriction_type"] = string(apiObject.RestrictionType) + + return tfMap +} + +func flattenCertificate(apiObject *awstypes.Certificate) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + + if apiObject.Arn != nil { + tfMap[names.AttrARN] = aws.ToString(apiObject.Arn) + } + + return tfMap +} + +func flattenWebAcl(apiObject *awstypes.WebAclCustomization) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + + tfMap[names.AttrAction] = string(apiObject.Action) + + if apiObject.Arn != nil { + tfMap[names.AttrARN] = aws.ToString(apiObject.Arn) + } + + return tfMap +} + +func expandManagedCertificateRequest(tfMap map[string]any) *awstypes.ManagedCertificateRequest { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.ManagedCertificateRequest{} + + if v, ok := tfMap["certificate_transparency_logging_preference"].(string); ok && v != "" { + apiObject.CertificateTransparencyLoggingPreference = awstypes.CertificateTransparencyLoggingPreference(v) + } + + if v, ok := tfMap["primary_domain_name"].(string); ok && v != "" { + apiObject.PrimaryDomainName = aws.String(v) + } + + if v, ok := tfMap["validation_token_host"].(string); ok && v != "" { + apiObject.ValidationTokenHost = awstypes.ValidationTokenHost(v) + } + + return apiObject +} + +func expandParameters(tfList []any) []awstypes.Parameter { + if len(tfList) == 0 { + return nil + } + + parameters := make([]awstypes.Parameter, len(tfList)) + for i, v := range tfList { + tfMap := v.(map[string]any) + parameters[i] = awstypes.Parameter{ + Name: aws.String(tfMap[names.AttrName].(string)), + Value: aws.String(tfMap[names.AttrValue].(string)), + } + } + return parameters +} + +func flattenParameters(apiObjects []awstypes.Parameter) []any { + if len(apiObjects) == 0 { + return nil + } + + tfList := make([]any, len(apiObjects)) + for i, apiObject := range apiObjects { + tfList[i] = map[string]any{ + names.AttrName: aws.ToString(apiObject.Name), + names.AttrValue: aws.ToString(apiObject.Value), + } + } + return tfList +} diff --git a/internal/service/cloudfront/distribution_tenant_test.go b/internal/service/cloudfront/distribution_tenant_test.go new file mode 100644 index 000000000000..18759edcc21a --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_test.go @@ -0,0 +1,903 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront_test + +import ( + "context" + "fmt" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + "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" + ret "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfcloudfront "github.com/hashicorp/terraform-provider-aws/internal/service/cloudfront" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccCloudFrontDistributionTenant_basic(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_basic(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), + resource.TestCheckResourceAttr(resourceName, "domains.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "distribution_id"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrName), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttrSet(resourceName, "etag"), + resource.TestCheckResourceAttrSet(resourceName, "last_modified_time"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrStatus), + ), + }, + { + Config: testAccDistributionTenantConfig_basic(t), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + }, + }, + }, + }) +} +func TestAccCloudFrontDistributionTenant_disappears(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_basic(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudfront.ResourceDistributionTenant(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_customCertificate(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.geo_restriction.#", "1"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.geo_restriction.0.restriction_type", "whitelist"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.geo_restriction.0.locations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.certificate.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "customizations.0.certificate.0.arn", "data.aws_acm_certificate.test", names.AttrARN), + ), + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenant_customCertificateWithWebAcl(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_customCertificateWithWebAcl(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.web_acl.#", "1"), + resource.TestCheckResourceAttr(resourceName, "customizations.0.web_acl.0.action", "override"), + resource.TestCheckResourceAttrPair(resourceName, "customizations.0.web_acl.0.arn", "aws_wafv2_web_acl.testacl", names.AttrARN), + ), + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_parameters(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, "parameters.#", "2"), + resource.TestCheckResourceAttr(resourceName, "parameters.0.name", "region"), + resource.TestCheckResourceAttr(resourceName, "parameters.0.value", "us-east-1"), + resource.TestCheckResourceAttr(resourceName, "parameters.1.name", "tenantid"), + resource.TestCheckResourceAttr(resourceName, "parameters.1.value", "tenant-123"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + }, + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenant_managedCertificateRequest(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_managedCertificateRequest(t), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), + resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "managed_certificate_request.0.primary_domain_name"), + resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.0.validation_token_host", "cloudfront"), + resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.0.certificate_transparency_logging_preference", "disabled"), + ), + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { + ctx := acctest.Context(t) + var tenant awstypes.DistributionTenant + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantConfig_tags1(t, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + ), + }, + { + Config: testAccDistributionTenantConfig_tags2(t, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + { + Config: testAccDistributionTenantConfig_tags1(t, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + }, + }) +} + +func testAccCheckDistributionTenantDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFrontClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudfront_distribution_tenant" { + continue + } + + _, err := tfcloudfront.FindDistributionTenantById(ctx, conn, rs.Primary.ID) + + if ret.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFront Distribution Tenant (%s) still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckDistributionTenantExists(ctx context.Context, n string, v *awstypes.DistributionTenant) 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).CloudFrontClient(ctx) + + output, err := tfcloudfront.FindDistributionTenantById(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output.DistributionTenant + + return nil + } +} + +func testAccDistributionTenantConfig_basic(t *testing.T) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` + +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false +} +`, certDomain) +} + +func testAccDistributionTenantConfig_customCertificate(t *testing.T) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` + +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false + + customizations { + geo_restriction { + locations = ["US", "CA"] + restriction_type = "whitelist" + } + certificate { + arn = data.aws_acm_certificate.test.arn + } + } +} +`, certDomain) +} + +func testAccDistributionTenantConfig_customCertificateWithWebAcl(t *testing.T) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` +provider "aws" { + alias = "us-east-1" + region = "us-east-1" +} + +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_wafv2_web_acl" "testacl" { + provider = aws.us-east-1 + name = "tftest" + description = "tftest" + scope = "CLOUDFRONT" + + default_action { + allow { + custom_request_handling { + insert_header { + name = "X-WebACL-Test" + value = "test" + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "friendly-metric-name" + sampled_requests_enabled = false + } +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false + + customizations { + geo_restriction { + locations = ["US", "CA"] + restriction_type = "whitelist" + } + certificate { + arn = data.aws_acm_certificate.test.arn + } + web_acl { + action = "override" + arn = aws_wafv2_web_acl.testacl.arn + } + } +} +`, certDomain) +} + +func testAccDistributionTenantConfig_parameters(t *testing.T) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` + +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false + + parameters { + name = "tenantid" + value = "tenant-123" + } + parameters { + name = "region" + value = "us-east-1" + } +} +`, certDomain) +} + +func testAccDistributionTenantConfig_tags1(t *testing.T, tagKey1, tagValue1 string) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false + + tags = { + %[2]q = %[3]q + } +} +`, certDomain, tagKey1, tagValue1) +} + +func testAccDistributionTenantConfig_tags2(t *testing.T, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + certDomain := acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = %[1]q + region = "us-east-1" + most_recent = true +} + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[1]q] + name = "tftenant" + enabled = false + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, certDomain, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccDistributionTenantConfig_managedCertificateRequest(t *testing.T) string { + zoneDomain := acctest.ACMCertificateDomainFromEnv(t) + domainName := "tf." + zoneDomain + return fmt.Sprintf(` + +resource "aws_cloudfront_cache_policy" "tf-policy" { + name = "tfpolicy" + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } + + depends_on = [aws_route53_record.testrecord] +} + +data "aws_route53_zone" "test" { + name = %[1]q + private_zone = false +} + +resource "aws_cloudfront_connection_group" "testgroup" { + name = "testgroup" +} + +resource "aws_route53_record" "testrecord" { + zone_id = data.aws_route53_zone.test.id + type = "CNAME" + ttl = 300 + name = %[2]q + records = [aws_cloudfront_connection_group.testgroup.routing_endpoint] +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[2]q] + name = "tftenant" + enabled = false + connection_group_id = aws_cloudfront_connection_group.testgroup.id + + managed_certificate_request { + primary_domain_name = %[2]q + validation_token_host = "cloudfront" + certificate_transparency_logging_preference = "disabled" + } +} +`, zoneDomain, domainName) +} diff --git a/internal/service/cloudfront/service_package_gen.go b/internal/service/cloudfront/service_package_gen.go index 79a77ecbe595..ee2797353c6f 100644 --- a/internal/service/cloudfront/service_package_gen.go +++ b/internal/service/cloudfront/service_package_gen.go @@ -157,6 +157,15 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePa }), Region: unique.Make(inttypes.ResourceRegionDisabled()), }, + { + Factory: resourceDistributionTenant, + TypeName: "aws_cloudfront_distribution_tenant", + Name: "Distribution Tenant", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, { Factory: resourceFieldLevelEncryptionConfig, TypeName: "aws_cloudfront_field_level_encryption_config", From 351e05064098529a8d5b224eb8ea435964e22fcb Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 8 Sep 2025 11:36:26 +0000 Subject: [PATCH 06/15] changelog --- .changelog/44181.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/44181.txt diff --git a/.changelog/44181.txt b/.changelog/44181.txt new file mode 100644 index 000000000000..a89fe4cfb97e --- /dev/null +++ b/.changelog/44181.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_cloudfront_distribution_tenant +``` \ No newline at end of file From 471bd21954e77511262c9fbb83568f24c6d6639d Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 8 Sep 2025 12:04:58 +0000 Subject: [PATCH 07/15] test exports --- internal/service/cloudfront/exports_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/service/cloudfront/exports_test.go b/internal/service/cloudfront/exports_test.go index 2243d6a6f6fa..5c62bcb89b6a 100644 --- a/internal/service/cloudfront/exports_test.go +++ b/internal/service/cloudfront/exports_test.go @@ -8,6 +8,7 @@ var ( ResourceCachePolicy = resourceCachePolicy ResourceContinuousDeploymentPolicy = newContinuousDeploymentPolicyResource ResourceDistribution = resourceDistribution + ResourceDistributionTenant = resourceDistributionTenant ResourceFieldLevelEncryptionConfig = resourceFieldLevelEncryptionConfig ResourceFieldLevelEncryptionProfile = resourceFieldLevelEncryptionProfile ResourceFunction = resourceFunction @@ -23,6 +24,7 @@ var ( FindCachePolicyByID = findCachePolicyByID FindContinuousDeploymentPolicyByID = findContinuousDeploymentPolicyByID + FindDistributionTenantById = findDistributionTenantByID FindFieldLevelEncryptionConfigByID = findFieldLevelEncryptionConfigByID FindFieldLevelEncryptionProfileByID = findFieldLevelEncryptionProfileByID FindFunctionByTwoPartKey = findFunctionByTwoPartKey From 032d1e7e463c43fa3b732b10ebb9b76a127c441c Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Sun, 14 Sep 2025 11:36:55 +0000 Subject: [PATCH 08/15] fixes for lint and semgrep checks --- .../service/cloudfront/distribution_tenant.go | 14 +- .../cloudfront/distribution_tenant_test.go | 132 ++++++++++-------- 2 files changed, 77 insertions(+), 69 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go index 69ed84829c1b..1c871994c3cb 100644 --- a/internal/service/cloudfront/distribution_tenant.go +++ b/internal/service/cloudfront/distribution_tenant.go @@ -79,7 +79,7 @@ func resourceDistributionTenant() *schema.Resource { }, }, }, - "certificate": { + names.AttrCertificate: { Type: schema.TypeList, Optional: true, MaxItems: 1, @@ -635,12 +635,12 @@ func expandCustomizations(tfMap map[string]any) *awstypes.Customizations { apiObject.GeoRestrictions = expandTenantGeoRestriction(v[0].(map[string]any)) } - if v, ok := tfMap["certificate"].([]any); ok && len(v) > 0 && v[0] != nil { + if v, ok := tfMap[names.AttrCertificate].([]any); ok && len(v) > 0 && v[0] != nil { apiObject.Certificate = expandCertificate(v[0].(map[string]any)) } if v, ok := tfMap["web_acl"].([]any); ok && len(v) > 0 && v[0] != nil { - apiObject.WebAcl = expandWebAcl(v[0].(map[string]any)) + apiObject.WebAcl = expandWebACL(v[0].(map[string]any)) } return apiObject @@ -678,7 +678,7 @@ func expandCertificate(tfMap map[string]any) *awstypes.Certificate { return apiObject } -func expandWebAcl(tfMap map[string]any) *awstypes.WebAclCustomization { +func expandWebACL(tfMap map[string]any) *awstypes.WebAclCustomization { if tfMap == nil { return nil } @@ -708,11 +708,11 @@ func flattenCustomizations(apiObject *awstypes.Customizations) map[string]any { } if apiObject.Certificate != nil { - tfMap["certificate"] = []any{flattenCertificate(apiObject.Certificate)} + tfMap[names.AttrCertificate] = []any{flattenCertificate(apiObject.Certificate)} } if apiObject.WebAcl != nil { - tfMap["web_acl"] = []any{flattenWebAcl(apiObject.WebAcl)} + tfMap["web_acl"] = []any{flattenWebACL(apiObject.WebAcl)} } return tfMap @@ -748,7 +748,7 @@ func flattenCertificate(apiObject *awstypes.Certificate) map[string]any { return tfMap } -func flattenWebAcl(apiObject *awstypes.WebAclCustomization) map[string]any { +func flattenWebACL(apiObject *awstypes.WebAclCustomization) map[string]any { if apiObject == nil { return nil } diff --git a/internal/service/cloudfront/distribution_tenant_test.go b/internal/service/cloudfront/distribution_tenant_test.go index 18759edcc21a..aaca7313a973 100644 --- a/internal/service/cloudfront/distribution_tenant_test.go +++ b/internal/service/cloudfront/distribution_tenant_test.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + "github.com/YakDriver/regexache" awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -40,7 +41,7 @@ func TestAccCloudFrontDistributionTenant_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "domains.#", "1"), resource.TestCheckResourceAttrSet(resourceName, "distribution_id"), resource.TestCheckResourceAttrSet(resourceName, names.AttrName), - resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + acctest.MatchResourceAttrGlobalARN(ctx, resourceName, names.AttrARN, "cloudfront", regexache.MustCompile(`distribution-tenant/[0-9A-Z]+$`)), resource.TestCheckResourceAttrSet(resourceName, "etag"), resource.TestCheckResourceAttrSet(resourceName, "last_modified_time"), resource.TestCheckResourceAttrSet(resourceName, names.AttrStatus), @@ -114,7 +115,7 @@ func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { }) } -func TestAccCloudFrontDistributionTenant_customCertificateWithWebAcl(t *testing.T) { +func TestAccCloudFrontDistributionTenant_customCertificateWithWebACL(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant resourceName := "aws_cloudfront_distribution_tenant.test" @@ -129,7 +130,7 @@ func TestAccCloudFrontDistributionTenant_customCertificateWithWebAcl(t *testing. CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_customCertificateWithWebAcl(t), + Config: testAccDistributionTenantConfig_customCertificateWithWebACL(t), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), @@ -161,8 +162,8 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "parameters.#", "2"), - resource.TestCheckResourceAttr(resourceName, "parameters.0.name", "region"), - resource.TestCheckResourceAttr(resourceName, "parameters.0.value", "us-east-1"), + resource.TestCheckResourceAttr(resourceName, "parameters.0.name", "place"), + resource.TestCheckResourceAttr(resourceName, "parameters.0.value", "na"), resource.TestCheckResourceAttr(resourceName, "parameters.1.name", "tenantid"), resource.TestCheckResourceAttr(resourceName, "parameters.1.value", "tenant-123"), ), @@ -301,10 +302,11 @@ func testAccCheckDistributionTenantExists(ctx context.Context, n string, v *awst func testAccDistributionTenantConfig_basic(t *testing.T) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` +data "aws_region" "current" {} data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } @@ -342,7 +344,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -350,7 +352,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -367,7 +369,7 @@ resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id domains = [%[1]q] name = "tftenant" - enabled = false + enabled = false } `, certDomain) } @@ -375,10 +377,11 @@ resource "aws_cloudfront_distribution_tenant" "test" { func testAccDistributionTenantConfig_customCertificate(t *testing.T) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` +data "aws_region" "current" {} data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } @@ -416,7 +419,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -424,7 +427,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -440,13 +443,14 @@ resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id domains = [%[1]q] name = "tftenant" - enabled = false + enabled = false customizations { geo_restriction { - locations = ["US", "CA"] + locations = ["US", "CA"] restriction_type = "whitelist" } + certificate { arn = data.aws_acm_certificate.test.arn } @@ -455,25 +459,22 @@ resource "aws_cloudfront_distribution_tenant" "test" { `, certDomain) } -func testAccDistributionTenantConfig_customCertificateWithWebAcl(t *testing.T) string { +func testAccDistributionTenantConfig_customCertificateWithWebACL(t *testing.T) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` -provider "aws" { - alias = "us-east-1" - region = "us-east-1" -} +data "aws_region" "current" {} data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } resource "aws_wafv2_web_acl" "testacl" { - provider = aws.us-east-1 name = "tftest" description = "tftest" scope = "CLOUDFRONT" + region = "data.aws_region.current.region" default_action { allow { @@ -527,7 +528,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -535,7 +536,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -555,16 +556,18 @@ resource "aws_cloudfront_distribution_tenant" "test" { customizations { geo_restriction { - locations = ["US", "CA"] + locations = ["US", "CA"] restriction_type = "whitelist" } + certificate { arn = data.aws_acm_certificate.test.arn } - web_acl { - action = "override" - arn = aws_wafv2_web_acl.testacl.arn - } + + web_acl { + action = "override" + arn = aws_wafv2_web_acl.testacl.arn + } } } `, certDomain) @@ -573,10 +576,11 @@ resource "aws_cloudfront_distribution_tenant" "test" { func testAccDistributionTenantConfig_parameters(t *testing.T) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` +data "aws_region" "current" {} data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } @@ -614,7 +618,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -622,7 +626,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -639,15 +643,16 @@ resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id domains = [%[1]q] name = "tftenant" - enabled = false + enabled = false parameters { - name = "tenantid" + name = "tenantid" value = "tenant-123" } + parameters { - name = "region" - value = "us-east-1" + name = "place" + value = "na" } } `, certDomain) @@ -656,9 +661,11 @@ resource "aws_cloudfront_distribution_tenant" "test" { func testAccDistributionTenantConfig_tags1(t *testing.T, tagKey1, tagValue1 string) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` +data "aws_region" "current" {} + data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } @@ -696,7 +703,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -704,7 +711,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -721,7 +728,7 @@ resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id domains = [%[1]q] name = "tftenant" - enabled = false + enabled = false tags = { %[2]q = %[3]q @@ -733,9 +740,11 @@ resource "aws_cloudfront_distribution_tenant" "test" { func testAccDistributionTenantConfig_tags2(t *testing.T, tagKey1, tagValue1, tagKey2, tagValue2 string) string { certDomain := acctest.ACMCertificateDomainFromEnv(t) return fmt.Sprintf(` +data "aws_region" "current" {} + data "aws_acm_certificate" "test" { - domain = %[1]q - region = "us-east-1" + domain = %[1]q + region = "data.aws_region.current.region" most_recent = true } @@ -773,7 +782,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -781,7 +790,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -798,7 +807,7 @@ resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id domains = [%[1]q] name = "tftenant" - enabled = false + enabled = false tags = { %[2]q = %[3]q @@ -812,7 +821,6 @@ func testAccDistributionTenantConfig_managedCertificateRequest(t *testing.T) str zoneDomain := acctest.ACMCertificateDomainFromEnv(t) domainName := "tf." + zoneDomain return fmt.Sprintf(` - resource "aws_cloudfront_cache_policy" "tf-policy" { name = "tfpolicy" comment = "test tenant cache policy" @@ -847,7 +855,7 @@ resource "aws_cloudfront_distribution" "test" { origin_ssl_protocols = ["TLSv1.2"] } } - + default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -855,7 +863,7 @@ resource "aws_cloudfront_distribution" "test" { viewer_protocol_policy = "allow-all" cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id } - + restrictions { geo_restriction { restriction_type = "none" @@ -870,7 +878,7 @@ resource "aws_cloudfront_distribution" "test" { } data "aws_route53_zone" "test" { - name = %[1]q + name = %[1]q private_zone = false } @@ -880,22 +888,22 @@ resource "aws_cloudfront_connection_group" "testgroup" { resource "aws_route53_record" "testrecord" { zone_id = data.aws_route53_zone.test.id - type = "CNAME" - ttl = 300 - name = %[2]q + type = "CNAME" + ttl = 300 + name = %[2]q records = [aws_cloudfront_connection_group.testgroup.routing_endpoint] } resource "aws_cloudfront_distribution_tenant" "test" { - distribution_id = aws_cloudfront_distribution.test.id - domains = [%[2]q] - name = "tftenant" - enabled = false + distribution_id = aws_cloudfront_distribution.test.id + domains = [%[2]q] + name = "tftenant" + enabled = false connection_group_id = aws_cloudfront_connection_group.testgroup.id managed_certificate_request { - primary_domain_name = %[2]q - validation_token_host = "cloudfront" + primary_domain_name = %[2]q + validation_token_host = "cloudfront" certificate_transparency_logging_preference = "disabled" } } From b3aa2e1b25e6b3f5ab0b7cbf36589eed511bb3d1 Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Mon, 15 Sep 2025 02:51:39 +0000 Subject: [PATCH 09/15] changed parameters to set to fix drift issue caused by parameter ordering --- .../service/cloudfront/distribution_tenant.go | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go index 1c871994c3cb..242aa016284b 100644 --- a/internal/service/cloudfront/distribution_tenant.go +++ b/internal/service/cloudfront/distribution_tenant.go @@ -166,7 +166,7 @@ func resourceDistributionTenant() *schema.Resource { Required: true, }, names.AttrParameters: { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -218,7 +218,7 @@ func resourceDistributionTenantCreate(ctx context.Context, d *schema.ResourceDat input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) } if v, ok := d.GetOk(names.AttrParameters); ok { - input.Parameters = expandParameters(v.([]any)) + input.Parameters = expandParameters(v.(*schema.Set)) } if tags := getTagsIn(ctx); len(tags) > 0 { input.Tags = &awstypes.Tags{Items: tags} @@ -312,7 +312,7 @@ func resourceDistributionTenantUpdate(ctx context.Context, d *schema.ResourceDat input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) } if v, ok := d.GetOk(names.AttrParameters); ok { - input.Parameters = expandParameters(v.([]any)) + input.Parameters = expandParameters(v.(*schema.Set)) } // ACM and IAM certificate eventual consistency. @@ -786,13 +786,13 @@ func expandManagedCertificateRequest(tfMap map[string]any) *awstypes.ManagedCert return apiObject } -func expandParameters(tfList []any) []awstypes.Parameter { - if len(tfList) == 0 { +func expandParameters(tfSet *schema.Set) []awstypes.Parameter { + if tfSet.Len() == 0 { return nil } - parameters := make([]awstypes.Parameter, len(tfList)) - for i, v := range tfList { + parameters := make([]awstypes.Parameter, tfSet.Len()) + for i, v := range tfSet.List() { tfMap := v.(map[string]any) parameters[i] = awstypes.Parameter{ Name: aws.String(tfMap[names.AttrName].(string)), @@ -802,17 +802,27 @@ func expandParameters(tfList []any) []awstypes.Parameter { return parameters } -func flattenParameters(apiObjects []awstypes.Parameter) []any { +func flattenParameters(apiObjects []awstypes.Parameter) *schema.Set { if len(apiObjects) == 0 { return nil } - tfList := make([]any, len(apiObjects)) - for i, apiObject := range apiObjects { - tfList[i] = map[string]any{ + tfSet := schema.NewSet(schema.HashResource(&schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrName: { + Type: schema.TypeString, + }, + names.AttrValue: { + Type: schema.TypeString, + }, + }, + }), []any{}) + + for _, apiObject := range apiObjects { + tfSet.Add(map[string]any{ names.AttrName: aws.ToString(apiObject.Name), names.AttrValue: aws.ToString(apiObject.Value), - } + }) } - return tfList + return tfSet } From 8da0e75bb81cc08212c6a6a61741483f815328a0 Mon Sep 17 00:00:00 2001 From: Cody Zhao Date: Fri, 19 Sep 2025 08:36:37 +0000 Subject: [PATCH 10/15] docs --- ...oudfront_distribution_tenant.html.markdown | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 website/docs/r/cloudfront_distribution_tenant.html.markdown diff --git a/website/docs/r/cloudfront_distribution_tenant.html.markdown b/website/docs/r/cloudfront_distribution_tenant.html.markdown new file mode 100644 index 000000000000..cb756b44f25a --- /dev/null +++ b/website/docs/r/cloudfront_distribution_tenant.html.markdown @@ -0,0 +1,227 @@ +--- +subcategory: "CloudFront" +layout: "aws" +page_title: "AWS: aws_cloudfront_distribution_tenant" +description: |- + Provides a CloudFront distribution tenant resource. +--- + +# Resource: aws_cloudfront_distribution_tenant + +Creates an Amazon CloudFront distribution tenant. + +Distribution tenants allow you to create isolated configurations within a multi-tenant CloudFront distribution. Each tenant can have its own domains, customizations, and parameters while sharing the underlying distribution infrastructure. + +For information about CloudFront distribution tenants, see the [Amazon CloudFront Developer Guide](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-tenants.html). + +## Example Usage + +### Basic Distribution Tenant + +```terraform +resource "aws_cloudfront_distribution_tenant" "example" { + name = "example-tenant" + distribution_id = aws_cloudfront_distribution.multi_tenant.id + domains = ["tenant.example.com"] + enabled = true + + tags = { + Environment = "production" + } +} +``` + +### Distribution Tenant with Customizations + +```terraform +resource "aws_cloudfront_distribution_tenant" "example" { + name = "example-tenant" + distribution_id = aws_cloudfront_distribution.multi_tenant.id + domains = ["tenant.example.com"] + enabled = false + + customizations { + geo_restriction { + restriction_type = "whitelist" + locations = ["US", "CA"] + } + + certificate { + arn = aws_acm_certificate.tenant_cert.arn + } + + web_acl { + action = "override" + arn = aws_wafv2_web_acl.tenant_waf.arn + } + } + + tags = { + Environment = "production" + Tenant = "example" + } +} +``` + +### Distribution Tenant with Managed Certificate + +```terraform +resource "aws_cloudfront_distribution_tenant" "main" { + distribution_id = aws_cloudfront_distribution.main.id + domains = ["app.example.com"] + name = "main-tenant" + enabled = false + connection_group_id = aws_cloudfront_connection_group.main_group.id + + managed_certificate_request { + primary_domain_name = "app.example.com" + validation_token_host = "cloudfront" + certificate_transparency_logging_preference = "disabled" + } +} + +data "aws_route53_zone" "main" { + name = "example.com" + private_zone = false +} + +resource "aws_cloudfront_connection_group" "main_group" { + name = "main-group" +} + +resource "aws_route53_record" "domain_record" { + zone_id = data.aws_route53_zone.main.id + type = "CNAME" + ttl = 300 + name = "app.example.com" + records = [aws_cloudfront_connection_group.main_group.routing_endpoint] +} + +resource "aws_cloudfront_cache_policy" "main_policy" { + name = "main-policy" + comment = "tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "main" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "main" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "main" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.main_policy.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `name` (Required) - Name of the distribution tenant. +* `distribution_id` (Required) - ID of the multi-tenant distribution. +* `domains` (Required) - Set of domains associated with the distribution tenant. +* `enabled` (Optional) - Whether the distribution tenant is enabled to serve traffic. Defaults to `true`. +* `connection_group_id` (Optional) - ID of the connection group for the distribution tenant. If not specified, CloudFront uses the default connection group. +* `customizations` (Optional) - [Customizations](#customizations-arguments) for the distribution tenant (maximum one). +* `managed_certificate_request` (Optional) - [Managed certificate request](#managed-certificate-request-arguments) for CloudFront managed ACM certificate (maximum one). +* `parameters` (Optional) - Set of [parameter](#parameter-arguments) values for the distribution tenant. +* `wait_for_deployment` (Optional) - If enabled, the resource will wait for the distribution tenant status to change from `InProgress` to `Deployed`. Setting this to `false` will skip the process. Default: `true`. +* `tags` (Optional) - Map of tags to assign to the resource. 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. + +#### Customizations Arguments + +* `certificate` (Optional) - [Certificate](#certificate-arguments) configuration for the tenant (maximum one). +* `geo_restriction` (Optional) - [Geographic restrictions](#geo-restriction-arguments) configuration for the tenant (maximum one). +* `web_acl` (Optional) - [Web ACL](#web-acl-arguments) configuration for the tenant (maximum one). + +##### Certificate Arguments + +* `arn` (Optional) - ARN of the AWS Certificate Manager certificate to use with this distribution tenant. + +##### Geo Restriction Arguments + +* `restriction_type` (Optional) - Method to restrict distribution by country: `none`, `whitelist`, or `blacklist`. +* `locations` (Optional) - Set of ISO 3166-1-alpha-2 country codes for the restriction. Required if `restriction_type` is `whitelist` or `blacklist`. + +##### Web ACL Arguments + +* `action` (Optional) - Action to take for the web ACL. Valid values: `allow`, `block`. +* `arn` (Optional) - ARN of the AWS WAF web ACL to associate with this distribution tenant. + +#### Managed Certificate Request Arguments + +* `certificate_transparency_logging_preference` (Optional) - Certificate transparency logging preference. Valid values: `enabled`, `disabled`. +* `primary_domain_name` (Optional) - Primary domain name for the certificate. +* `validation_token_host` (Optional) - Host for validation token. Valid values: `cloudfront`, `domain`. + +#### Parameter Arguments + +* `name` (Required) - Name of the parameter. +* `value` (Required) - Value of the parameter. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - ID of the distribution tenant. +* `arn` - ARN of the distribution tenant. +* `status` - Current status of the distribution tenant. +* `last_modified_time` - Date and time when the distribution tenant was last modified. +* `etag` - Current version of the distribution tenant. +* `tags_all` - Map of tags assigned to the resource, 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 CloudFront Distribution Tenants using the `id`. For example: + +```terraform +import { + to = aws_cloudfront_distribution_tenant.example + id = "TENANT123EXAMPLE" +} +``` + +Using `terraform import`, import CloudFront Distribution Tenants using the `id`. For example: + +```console +% terraform import aws_cloudfront_distribution_tenant.example TENANT123EXAMPLE +``` From fad62c736d39b730cb70e952687d818d5c865942 Mon Sep 17 00:00:00 2001 From: Jason Kim Date: Mon, 3 Nov 2025 11:41:32 -0800 Subject: [PATCH 11/15] Rewrite distribution_tenant.go using the Framework --- .../service/cloudfront/distribution_tenant.go | 1154 +++++++++-------- .../cloudfront/distribution_tenant_test.go | 279 ++-- internal/service/cloudfront/exports_test.go | 2 +- .../service/cloudfront/service_package_gen.go | 9 - 4 files changed, 783 insertions(+), 661 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go index 242aa016284b..e21505d7cc60 100644 --- a/internal/service/cloudfront/distribution_tenant.go +++ b/internal/service/cloudfront/distribution_tenant.go @@ -6,108 +6,164 @@ package cloudfront import ( "context" "fmt" - "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudfront" awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "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/create" "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" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" ret "github.com/hashicorp/terraform-provider-aws/internal/retry" 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_cloudfront_distribution_tenant", name="Distribution Tenant") +// @FrameworkResource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") // @Tags(identifierAttribute="arn") -func resourceDistributionTenant() *schema.Resource { - return &schema.Resource{ - CreateWithoutTimeout: resourceDistributionTenantCreate, - ReadWithoutTimeout: resourceDistributionTenantRead, - UpdateWithoutTimeout: resourceDistributionTenantUpdate, - DeleteWithoutTimeout: resourceDistributionTenantDelete, - - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - // Set non API attributes to their Default settings in the schema - d.Set("wait_for_deployment", true) - return []*schema.ResourceData{d}, nil - }}, - - Schema: map[string]*schema.Schema{ - names.AttrARN: { - Type: schema.TypeString, +func newDistributionTenantResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &distributionTenantResource{} + + r.SetDefaultCreateTimeout(15 * time.Minute) + r.SetDefaultUpdateTimeout(15 * time.Minute) + r.SetDefaultDeleteTimeout(15 * time.Minute) + + return r, nil +} + +const ( + ResNameDistributionTenant = "Distribution Tenant" +) + +type distributionTenantResource struct { + framework.ResourceWithModel[distributionTenantResourceModel] + framework.WithImportByID + framework.WithTimeouts +} + +func (r *distributionTenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + "connection_group_id": schema.StringAttribute{ + Optional: true, Computed: true, }, - "connection_group_id": { - Type: schema.TypeString, + "distribution_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "domains": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + names.AttrEnabled: schema.BoolAttribute{ Optional: true, Computed: true, + Default: booldefault.StaticBool(true), + }, + "etag": schema.StringAttribute{ + Computed: true, + }, + names.AttrID: framework.IDAttribute(), + "last_modified_time": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, }, - "customizations": { - Type: schema.TypeList, + names.AttrName: schema.StringAttribute{ + Required: true, + }, + names.AttrStatus: schema.StringAttribute{ + Computed: true, + }, + "wait_for_deployment": schema.BoolAttribute{ Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "geo_restriction": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "locations": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Default: booldefault.StaticBool(true), + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + "customizations": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[customizationsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "geo_restriction": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[geoRestrictionModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "locations": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, }, - "restriction_type": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: enum.Validate[awstypes.GeoRestrictionType](), + "restriction_type": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.GeoRestrictionType](), }, }, }, }, - names.AttrCertificate: { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - names.AttrARN: { - Type: schema.TypeString, - Optional: true, - ValidateFunc: verify.ValidARN, + names.AttrCertificate: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[certificateModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.ARNType, }, }, }, }, - "web_acl": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - names.AttrAction: { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: enum.Validate[awstypes.CustomizationActionType](), + "web_acl": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[webAclModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrAction: schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.CustomizationActionType](), }, - names.AttrARN: { - Type: schema.TypeString, - Optional: true, - ValidateFunc: verify.ValidARN, + names.AttrARN: schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.ARNType, }, }, }, @@ -115,379 +171,451 @@ func resourceDistributionTenant() *schema.Resource { }, }, }, - "distribution_id": { - Type: schema.TypeString, - Required: true, - }, - "domains": { - Type: schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - names.AttrEnabled: { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - "etag": { - Type: schema.TypeString, - Computed: true, - }, - "last_modified_time": { - Type: schema.TypeString, - Computed: true, - }, - "managed_certificate_request": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "certificate_transparency_logging_preference": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: enum.Validate[awstypes.CertificateTransparencyLoggingPreference](), + "managed_certificate_request": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[managedCertificateRequestModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "certificate_transparency_logging_preference": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.CertificateTransparencyLoggingPreference](), }, - "primary_domain_name": { - Type: schema.TypeString, + "primary_domain_name": schema.StringAttribute{ Optional: true, }, - "validation_token_host": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: enum.Validate[awstypes.ValidationTokenHost](), + "validation_token_host": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.ValidationTokenHost](), }, }, }, }, - names.AttrName: { - Type: schema.TypeString, - Required: true, - }, - names.AttrParameters: { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - names.AttrName: { - Type: schema.TypeString, + names.AttrParameters: schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[parameterModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ Required: true, }, - names.AttrValue: { - Type: schema.TypeString, + names.AttrValue: schema.StringAttribute{ Required: true, }, }, }, }, - names.AttrStatus: { - Type: schema.TypeString, - Computed: true, - }, - "wait_for_deployment": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - names.AttrTags: tftags.TagsSchema(), - names.AttrTagsAll: tftags.TagsSchemaComputed(), }, } } -func resourceDistributionTenantCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) +type distributionTenantResourceModel struct { + ARN types.String `tfsdk:"arn"` + ConnectionGroupID types.String `tfsdk:"connection_group_id"` + Customizations fwtypes.ListNestedObjectValueOf[customizationsModel] `tfsdk:"customizations"` + DistributionID types.String `tfsdk:"distribution_id"` + Domains fwtypes.SetValueOf[types.String] `tfsdk:"domains"` + Enabled types.Bool `tfsdk:"enabled"` + ETag types.String `tfsdk:"etag"` + ID types.String `tfsdk:"id"` + LastModifiedTime timetypes.RFC3339 `tfsdk:"last_modified_time"` + ManagedCertificateRequest fwtypes.ListNestedObjectValueOf[managedCertificateRequestModel] `tfsdk:"managed_certificate_request"` + Name types.String `tfsdk:"name"` + Parameters fwtypes.SetNestedObjectValueOf[parameterModel] `tfsdk:"parameters"` + Status types.String `tfsdk:"status"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + WaitForDeployment types.Bool `tfsdk:"wait_for_deployment"` +} - input := cloudfront.CreateDistributionTenantInput{ - DistributionId: aws.String(d.Get("distribution_id").(string)), - Domains: expandDomains(d.Get("domains").(*schema.Set)), - Enabled: aws.Bool(d.Get(names.AttrEnabled).(bool)), - Name: aws.String(d.Get(names.AttrName).(string)), - } +type customizationsModel struct { + GeoRestriction fwtypes.ListNestedObjectValueOf[geoRestrictionModel] `tfsdk:"geo_restriction"` + Certificate fwtypes.ListNestedObjectValueOf[certificateModel] `tfsdk:"certificate"` + WebAcl fwtypes.ListNestedObjectValueOf[webAclModel] `tfsdk:"web_acl"` +} + +type geoRestrictionModel struct { + Locations fwtypes.SetValueOf[types.String] `tfsdk:"locations"` + RestrictionType fwtypes.StringEnum[awstypes.GeoRestrictionType] `tfsdk:"restriction_type"` +} + +type certificateModel struct { + ARN fwtypes.ARN `tfsdk:"arn"` +} - if v, ok := d.GetOk("connection_group_id"); ok { - input.ConnectionGroupId = aws.String(v.(string)) +type webAclModel struct { + Action fwtypes.StringEnum[awstypes.CustomizationActionType] `tfsdk:"action"` + ARN fwtypes.ARN `tfsdk:"arn"` +} + +type managedCertificateRequestModel struct { + CertificateTransparencyLoggingPreference fwtypes.StringEnum[awstypes.CertificateTransparencyLoggingPreference] `tfsdk:"certificate_transparency_logging_preference"` + PrimaryDomainName types.String `tfsdk:"primary_domain_name"` + ValidationTokenHost fwtypes.StringEnum[awstypes.ValidationTokenHost] `tfsdk:"validation_token_host"` +} + +type parameterModel struct { + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` +} + +func (r *distributionTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data distributionTenantResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - if v, ok := d.GetOk("customizations"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { - input.Customizations = expandCustomizations(v.([]any)[0].(map[string]any)) + + conn := r.Meta().CloudFrontClient(ctx) + + input := &cloudfront.CreateDistributionTenantInput{} + resp.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if resp.Diagnostics.HasError() { + return } - if v, ok := d.GetOk("managed_certificate_request"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { - input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) + + if tags := getTagsIn(ctx); len(tags) > 0 { + input.Tags = &awstypes.Tags{ + Items: tags, + } } - if v, ok := d.GetOk(names.AttrParameters); ok { - input.Parameters = expandParameters(v.(*schema.Set)) + + output, err := conn.CreateDistributionTenant(ctx, input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionCreating, ResNameDistributionTenant, data.Name.String(), err), + err.Error(), + ) + return } - if tags := getTagsIn(ctx); len(tags) > 0 { - input.Tags = &awstypes.Tags{Items: tags} + + // Use create response directly - no extra read needed + resp.Diagnostics.Append(fwflex.Flatten(ctx, output.DistributionTenant, &data)...) + if resp.Diagnostics.HasError() { + return } - // ACM and IAM certificate eventual consistency. - // InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain. - const ( - timeout = 1 * time.Minute - ) + // Set fields that fwflex.Flatten might not handle correctly + data.ID = fwflex.StringToFramework(ctx, output.DistributionTenant.Id) + data.LastModifiedTime = fwflex.TimeToFramework(ctx, output.DistributionTenant.LastModifiedTime) + data.ARN = fwflex.StringToFramework(ctx, output.DistributionTenant.Arn) + data.ETag = fwflex.StringToFramework(ctx, output.ETag) - outputRaw, err := tfresource.RetryWhenIsA[any, *awstypes.InvalidViewerCertificate](ctx, timeout, func(ctx context.Context) (any, error) { - return conn.CreateDistributionTenant(ctx, &input) - }) + if data.WaitForDeployment.ValueBool() { + if _, err := waitDistributionTenantDeployed(ctx, conn, data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionWaitingForCreation, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } - if err != nil { - return sdkdiag.AppendErrorf(diags, "creating CloudFront Distribution Tenant: %s", err) - } + // Wait for managed certificate if specified + if !data.ManagedCertificateRequest.IsNull() && !data.ManagedCertificateRequest.IsUnknown() { + var managedCertRequest *awstypes.ManagedCertificateRequest + resp.Diagnostics.Append(fwflex.Expand(ctx, data.ManagedCertificateRequest, &managedCertRequest)...) + if resp.Diagnostics.HasError() { + return + } + + if err := waitManagedCertificateReady(ctx, conn, data.ID.ValueString(), managedCertRequest); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionWaitingForCreation, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + // Refresh the distribution tenant data after managed certificate processing + refreshedOutput, err := findDistributionTenantByIdentifier(ctx, conn, data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } - d.SetId(aws.ToString(outputRaw.(*cloudfront.CreateDistributionTenantOutput).DistributionTenant.Id)) + // Update the data model with refreshed information + resp.Diagnostics.Append(fwflex.Flatten(ctx, refreshedOutput.DistributionTenant, &data)...) + if resp.Diagnostics.HasError() { + return + } - if d.Get("wait_for_deployment").(bool) { - if _, err := waitDistributionTenantDeployed(ctx, conn, d.Id()); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for CloudFront Distribution Tenant (%s) deploy: %s", d.Id(), err) + data.ETag = fwflex.StringToFramework(ctx, refreshedOutput.ETag) + data.LastModifiedTime = fwflex.TimeToFramework(ctx, refreshedOutput.DistributionTenant.LastModifiedTime) } } - return append(diags, resourceDistributionTenantRead(ctx, d, meta)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceDistributionTenantRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) +func (r *distributionTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data distributionTenantResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } - output, err := findDistributionTenantByID(ctx, conn, d.Id()) + conn := r.Meta().CloudFrontClient(ctx) - if !d.IsNewResource() && ret.NotFound(err) { - log.Printf("[WARN] CloudFront Distribution Tenant (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags + output, err := findDistributionTenantByIdentifier(ctx, conn, data.ID.ValueString()) + if ret.NotFound(err) { + resp.State.RemoveResource(ctx) + return } - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant (%s): %s", d.Id(), err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return } tenant := output.DistributionTenant - d.Set(names.AttrARN, tenant.Arn) - d.Set("connection_group_id", tenant.ConnectionGroupId) - if tenant.Customizations != nil { - if err := d.Set("customizations", []any{flattenCustomizations(tenant.Customizations)}); err != nil { - return sdkdiag.AppendErrorf(diags, "setting customizations: %s", err) - } - } - d.Set("distribution_id", tenant.DistributionId) - d.Set("domains", flattenDomains(tenant.Domains)) - d.Set("etag", output.ETag) - d.Set(names.AttrEnabled, tenant.Enabled) - d.Set("last_modified_time", aws.String(tenant.LastModifiedTime.String())) - d.Set(names.AttrName, tenant.Name) - if tenant.Parameters != nil { - if err := d.Set(names.AttrParameters, flattenParameters(tenant.Parameters)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) - } + + // Flatten the distribution tenant data into the model + resp.Diagnostics.Append(fwflex.Flatten(ctx, tenant, &data)...) + if resp.Diagnostics.HasError() { + return } - d.Set(names.AttrStatus, tenant.Status) - return diags + // Set computed fields that need special handling + data.ETag = fwflex.StringToFramework(ctx, output.ETag) + data.LastModifiedTime = fwflex.TimeToFramework(ctx, tenant.LastModifiedTime) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceDistributionTenantUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) - - if d.HasChangesExcept(names.AttrTags, names.AttrTagsAll) { - input := cloudfront.UpdateDistributionTenantInput{ - Id: aws.String(d.Id()), - IfMatch: aws.String(d.Get("etag").(string)), - DistributionId: aws.String(d.Get("distribution_id").(string)), - Domains: expandDomains(d.Get("domains").(*schema.Set)), - Enabled: aws.Bool(d.Get(names.AttrEnabled).(bool)), - } +func (r *distributionTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var old, new distributionTenantResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &old)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &new)...) + if resp.Diagnostics.HasError() { + return + } - if v, ok := d.GetOk("connection_group_id"); ok { - input.ConnectionGroupId = aws.String(v.(string)) - } - if v, ok := d.GetOk("customizations"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { - input.Customizations = expandCustomizations(v.([]any)[0].(map[string]any)) - } - if v, ok := d.GetOk("managed_certificate_request"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { - input.ManagedCertificateRequest = expandManagedCertificateRequest(v.([]any)[0].(map[string]any)) - } - if v, ok := d.GetOk(names.AttrParameters); ok { - input.Parameters = expandParameters(v.(*schema.Set)) + conn := r.Meta().CloudFrontClient(ctx) + + var output *cloudfront.UpdateDistributionTenantOutput + + // Check if configuration changed (excluding tags) + if !new.ConnectionGroupID.Equal(old.ConnectionGroupID) || + !new.Customizations.Equal(old.Customizations) || + !new.DistributionID.Equal(old.DistributionID) || + !new.Domains.Equal(old.Domains) || + !new.Enabled.Equal(old.Enabled) || + !new.ManagedCertificateRequest.Equal(old.ManagedCertificateRequest) || + !new.Parameters.Equal(old.Parameters) { + + input := &cloudfront.UpdateDistributionTenantInput{} + resp.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + if resp.Diagnostics.HasError() { + return } - // ACM and IAM certificate eventual consistency. - // InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain. - const ( - timeout = 1 * time.Minute - ) - _, err := tfresource.RetryWhenIsA[any, *awstypes.InvalidViewerCertificate](ctx, timeout, func(ctx context.Context) (any, error) { - return conn.UpdateDistributionTenant(ctx, &input) - }) + // Handle special fields manually + input.Id = fwflex.StringFromFramework(ctx, new.ID) + input.IfMatch = fwflex.StringFromFramework(ctx, old.ETag) + + output, err := conn.UpdateDistributionTenant(ctx, input) // Refresh our ETag if it is out of date and attempt update again. if errs.IsA[*awstypes.PreconditionFailed](err) { var etag string - etag, err = distributionTenantETag(ctx, conn, d.Id()) + etag, err = distributionTenantETag(ctx, conn, new.ID.ValueString()) if err != nil { - return sdkdiag.AppendFromErr(diags, err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionUpdating, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return } input.IfMatch = aws.String(etag) - - _, err = conn.UpdateDistributionTenant(ctx, &input) + output, err = conn.UpdateDistributionTenant(ctx, input) } if err != nil { - return sdkdiag.AppendErrorf(diags, "updating CloudFront Distribution Tenant (%s): %s", d.Id(), err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionUpdating, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return } - if d.Get("wait_for_deployment").(bool) { - if _, err := waitDistributionTenantDeployed(ctx, conn, d.Id()); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for CloudFront Distribution Tenant (%s) deploy: %s", d.Id(), err) + + if new.WaitForDeployment.ValueBool() { + if _, err := waitDistributionTenantDeployed(ctx, conn, new.ID.ValueString()); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionWaitingForUpdate, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + // Wait for managed certificate if specified + if !new.ManagedCertificateRequest.IsNull() && !new.ManagedCertificateRequest.IsUnknown() { + var managedCertRequest *awstypes.ManagedCertificateRequest + resp.Diagnostics.Append(fwflex.Expand(ctx, new.ManagedCertificateRequest, &managedCertRequest)...) + if resp.Diagnostics.HasError() { + return + } + + if err := waitManagedCertificateReady(ctx, conn, new.ID.ValueString(), managedCertRequest); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionWaitingForUpdate, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + // Refresh the distribution tenant data after managed certificate processing + refreshedOutput, err := findDistributionTenantByIdentifier(ctx, conn, new.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + // Update the model with refreshed information + resp.Diagnostics.Append(fwflex.Flatten(ctx, refreshedOutput.DistributionTenant, &new)...) + if resp.Diagnostics.HasError() { + return + } + + new.ETag = fwflex.StringToFramework(ctx, refreshedOutput.ETag) + new.LastModifiedTime = fwflex.TimeToFramework(ctx, refreshedOutput.DistributionTenant.LastModifiedTime) } } + + new.LastModifiedTime = fwflex.TimeToFramework(ctx, output.DistributionTenant.LastModifiedTime) + } else { + new.LastModifiedTime = old.LastModifiedTime } - return append(diags, resourceDistributionTenantRead(ctx, d, meta)...) -} + // Flatten the distribution tenant data into the model + if output != nil { + resp.Diagnostics.Append(fwflex.Flatten(ctx, output.DistributionTenant, &new)...) + if resp.Diagnostics.HasError() { + return + } -func resourceDistributionTenantDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) + new.ETag = fwflex.StringToFramework(ctx, output.ETag) + } else { + // If no update was performed (e.g., tag-only changes), we still need to refresh the distribution tenant data + // to ensure all computed fields are properly set + getOutput, err := conn.GetDistributionTenant(ctx, &cloudfront.GetDistributionTenantInput{ + Identifier: fwflex.StringFromFramework(ctx, new.ID), + }) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(fwflex.Flatten(ctx, getOutput.DistributionTenant, &new)...) + if resp.Diagnostics.HasError() { + return + } - if d.Get(names.AttrARN).(string) == "" { - diags = append(diags, resourceDistributionTenantRead(ctx, d, meta)...) + new.ETag = fwflex.StringToFramework(ctx, getOutput.ETag) } - if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + resp.Diagnostics.Append(resp.State.Set(ctx, &new)...) +} + +func (r *distributionTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data distributionTenantResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + conn := r.Meta().CloudFrontClient(ctx) + id := data.ID.ValueString() + + if err := disableDistributionTenant(ctx, conn, id); err != nil { if ret.NotFound(err) { - return diags + return } - return sdkdiag.AppendFromErr(diags, err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return } - err := deleteDistributionTenant(ctx, conn, d.Id()) + err := deleteDistributionTenant(ctx, conn, id) if err == nil || ret.NotFound(err) || errs.IsA[*awstypes.EntityNotFound](err) { - return diags + return } // Disable distribution tenant if it is not yet disabled and attempt deletion again. - // Here we update via the deployed configuration to ensure we are not submitting an out of date - // configuration from the Terraform configuration, should other changes have occurred manually. if errs.IsA[*awstypes.ResourceNotDisabled](err) { - if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + if err := disableDistributionTenant(ctx, conn, id); err != nil { if ret.NotFound(err) { - return diags + return } - - return sdkdiag.AppendFromErr(diags, err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return } - const ( - timeout = 3 * time.Minute - ) + const timeout = 30 * time.Second _, err = tfresource.RetryWhenIsA[any, *awstypes.ResourceNotDisabled](ctx, timeout, func(ctx context.Context) (any, error) { - return nil, deleteDistributionTenant(ctx, conn, d.Id()) + return nil, deleteDistributionTenant(ctx, conn, id) }) } if errs.IsA[*awstypes.PreconditionFailed](err) || errs.IsA[*awstypes.InvalidIfMatchVersion](err) { - const ( - timeout = 1 * time.Minute - ) + const timeout = 30 * time.Second _, err = tfresource.RetryWhenIsOneOf2[any, *awstypes.PreconditionFailed, *awstypes.InvalidIfMatchVersion](ctx, timeout, func(ctx context.Context) (any, error) { - return nil, deleteDistributionTenant(ctx, conn, d.Id()) + return nil, deleteDistributionTenant(ctx, conn, id) }) } if errs.IsA[*awstypes.ResourceNotDisabled](err) { - if err := disableDistributionTenant(ctx, conn, d.Id()); err != nil { + if err := disableDistributionTenant(ctx, conn, id); err != nil { if ret.NotFound(err) { - return diags + return } - - return sdkdiag.AppendFromErr(diags, err) + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return } - err = deleteDistributionTenant(ctx, conn, d.Id()) + err = deleteDistributionTenant(ctx, conn, id) } if errs.IsA[*awstypes.EntityNotFound](err) { - return diags + return } if err != nil { - return sdkdiag.AppendFromErr(diags, err) - } - - return diags -} - -func waitDistributionTenantDeployed(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{distributionTenantStatusInProgress}, - Target: []string{distributionTenantStatusDeployed}, - Refresh: statusDistributionTenant(ctx, conn, id), - Timeout: 90 * time.Minute, - MinTimeout: 15 * time.Second, - Delay: 30 * time.Second, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { - return output, err - } - - return nil, err -} - -func waitDistributionTenantDeleted(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{distributionTenantStatusInProgress, distributionTenantStatusDeployed}, - Target: []string{}, - Refresh: statusDistributionTenant(ctx, conn, id), - Timeout: 90 * time.Minute, - MinTimeout: 15 * time.Second, - Delay: 15 * time.Second, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { - return output, err - } - - return nil, err -} - -func statusDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) retry.StateRefreshFunc { - return func() (any, string, error) { - output, err := findDistributionTenantByID(ctx, conn, id) - - if ret.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - if output == nil { - return nil, "", nil - } - - return output, aws.ToString(output.DistributionTenant.Status), nil + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return } } - -func findDistributionTenantByID(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { - input := cloudfront.GetDistributionTenantInput{ +func findDistributionTenantByIdentifier(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + input := &cloudfront.GetDistributionTenantInput{ Identifier: aws.String(id), } - output, err := conn.GetDistributionTenant(ctx, &input) + output, err := conn.GetDistributionTenant(ctx, input) if errs.IsA[*awstypes.EntityNotFound](err) { return nil, &retry.NotFoundError{ @@ -508,7 +636,7 @@ func findDistributionTenantByID(ctx context.Context, conn *cloudfront.Client, id } func disableDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) error { - output, err := findDistributionTenantByID(ctx, conn, id) + output, err := findDistributionTenantByIdentifier(ctx, conn, id) if err != nil { return fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) @@ -518,7 +646,7 @@ func disableDistributionTenant(ctx context.Context, conn *cloudfront.Client, id output, err = waitDistributionTenantDeployed(ctx, conn, id) if err != nil { - return fmt.Errorf("waiting for CloudFront Distribution (%s) deploy: %w", id, err) + return fmt.Errorf("waiting for CloudFront Distribution Tenant (%s) deploy: %w", id, err) } } @@ -551,26 +679,6 @@ func disableDistributionTenant(ctx context.Context, conn *cloudfront.Client, id return nil } -func convertDomainResultsToDomainItems(results []awstypes.DomainResult) []awstypes.DomainItem { - items := make([]awstypes.DomainItem, len(results)) - for i, result := range results { - items[i] = awstypes.DomainItem{ - Domain: result.Domain, - } - } - return items -} - -func distributionTenantETag(ctx context.Context, conn *cloudfront.Client, id string) (string, error) { - output, err := findDistributionTenantByID(ctx, conn, id) - - if err != nil { - return "", fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) - } - - return aws.ToString(output.ETag), nil -} - func deleteDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) error { etag, err := distributionTenantETag(ctx, conn, id) @@ -596,233 +704,259 @@ func deleteDistributionTenant(ctx context.Context, conn *cloudfront.Client, id s return nil } -func expandDomains(tfSet *schema.Set) []awstypes.DomainItem { - if tfSet.Len() == 0 { - return nil +func waitDistributionTenantDeployed(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{distributionTenantStatusInProgress}, + Target: []string{distributionTenantStatusDeployed}, + Refresh: statusDistributionTenant(ctx, conn, id), + Timeout: 30 * time.Minute, + MinTimeout: 15 * time.Second, + Delay: 15 * time.Second, } - domains := make([]awstypes.DomainItem, tfSet.Len()) - for i, v := range tfSet.List() { - domains[i] = awstypes.DomainItem{ - Domain: aws.String(v.(string)), - } - } - return domains -} + outputRaw, err := stateConf.WaitForStateContext(ctx) -func flattenDomains(apiObjects []awstypes.DomainResult) *schema.Set { - if len(apiObjects) == 0 { - return nil + if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { + return output, err } - tfSet := schema.NewSet(schema.HashString, []any{}) - for _, apiObject := range apiObjects { - if apiObject.Domain != nil { - tfSet.Add(aws.ToString(apiObject.Domain)) - } - } - return tfSet + return nil, err } -func expandCustomizations(tfMap map[string]any) *awstypes.Customizations { - if tfMap == nil { - return nil - } - - apiObject := &awstypes.Customizations{} - - if v, ok := tfMap["geo_restriction"].([]any); ok && len(v) > 0 && v[0] != nil { - apiObject.GeoRestrictions = expandTenantGeoRestriction(v[0].(map[string]any)) +func waitDistributionTenantDeleted(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{distributionTenantStatusInProgress, distributionTenantStatusDeployed}, + Target: []string{}, + Refresh: statusDistributionTenant(ctx, conn, id), + Timeout: 30 * time.Minute, + MinTimeout: 15 * time.Second, + Delay: 15 * time.Second, } - if v, ok := tfMap[names.AttrCertificate].([]any); ok && len(v) > 0 && v[0] != nil { - apiObject.Certificate = expandCertificate(v[0].(map[string]any)) - } + outputRaw, err := stateConf.WaitForStateContext(ctx) - if v, ok := tfMap["web_acl"].([]any); ok && len(v) > 0 && v[0] != nil { - apiObject.WebAcl = expandWebACL(v[0].(map[string]any)) + if output, ok := outputRaw.(*cloudfront.GetDistributionTenantOutput); ok { + return output, err } - return apiObject + return nil, err } -func expandTenantGeoRestriction(tfMap map[string]any) *awstypes.GeoRestrictionCustomization { - if tfMap == nil { - return nil - } - - apiObject := &awstypes.GeoRestrictionCustomization{} +func statusDistributionTenant(ctx context.Context, conn *cloudfront.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + output, err := findDistributionTenantByIdentifier(ctx, conn, id) - if v, ok := tfMap["locations"].(*schema.Set); ok && v.Len() > 0 { - apiObject.Locations = flex.ExpandStringValueSet(v) - } + if ret.NotFound(err) { + return nil, "", nil + } - if v, ok := tfMap["restriction_type"].(string); ok && v != "" { - apiObject.RestrictionType = awstypes.GeoRestrictionType(v) - } + if err != nil { + return nil, "", err + } - return apiObject -} + if output == nil { + return nil, "", nil + } -func expandCertificate(tfMap map[string]any) *awstypes.Certificate { - if tfMap == nil { - return nil + return output, aws.ToString(output.DistributionTenant.Status), nil } +} - apiObject := &awstypes.Certificate{} +func distributionTenantETag(ctx context.Context, conn *cloudfront.Client, id string) (string, error) { + output, err := findDistributionTenantByIdentifier(ctx, conn, id) - if v, ok := tfMap[names.AttrARN].(string); ok && v != "" { - apiObject.Arn = aws.String(v) + if err != nil { + return "", fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) } - return apiObject + return aws.ToString(output.ETag), nil } -func expandWebACL(tfMap map[string]any) *awstypes.WebAclCustomization { - if tfMap == nil { +func waitManagedCertificateReady(ctx context.Context, conn *cloudfront.Client, id string, managedCertRequest *awstypes.ManagedCertificateRequest) error { + if managedCertRequest == nil { + // No managed certificate request, nothing to wait for return nil } - apiObject := &awstypes.WebAclCustomization{} + // Wait for distribution tenant to be deployed first + dtOutput, err := waitForDistributionTenantDeployed(ctx, conn, id) + if err != nil { + return fmt.Errorf("waiting for CloudFront Distribution Tenant (%s) deploy: %w", id, err) + } - if v, ok := tfMap[names.AttrAction].(string); ok && v != "" { - apiObject.Action = awstypes.CustomizationActionType(v) + // Step 1: Wait for DNS configuration to be valid (15 minutes max) + if err := waitForDNSConfiguration(ctx, conn, dtOutput); err != nil { + return fmt.Errorf("CloudFront Distribution Tenant (%s) DNS configuration failed: %w", id, err) } - if v, ok := tfMap[names.AttrARN].(string); ok && v != "" { - apiObject.Arn = aws.String(v) + // Step 2: Wait for managed certificate to be issued (3 hours max) + mcOutput, err := waitForManagedCertificateIssued(ctx, conn, id) + if err != nil { + return fmt.Errorf("CloudFront Distribution Tenant (%s) managed certificate issuance failed: %w", id, err) } - return apiObject + // Step 3: Update distribution tenant with the issued certificate + return updateDistributionTenantWithManagedCertificate(ctx, conn, dtOutput, mcOutput) } -func flattenCustomizations(apiObject *awstypes.Customizations) map[string]any { - if apiObject == nil { - return nil - } +func waitForDistributionTenantDeployed(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetDistributionTenantOutput, error) { + // Simple loop to wait for deployment - reuse existing logic if needed + for { + dtOutput, err := findDistributionTenantByIdentifier(ctx, conn, id) + if err != nil { + return nil, fmt.Errorf("failed reading CloudFront Distribution Tenant (%s): %w", id, err) + } - tfMap := map[string]any{} + if aws.ToString(dtOutput.DistributionTenant.Status) == distributionTenantStatusDeployed { + return dtOutput, nil + } - if apiObject.GeoRestrictions != nil { - tfMap["geo_restriction"] = []any{flattenTenantGeoRestriction(apiObject.GeoRestrictions)} + time.Sleep(30 * time.Second) } +} - if apiObject.Certificate != nil { - tfMap[names.AttrCertificate] = []any{flattenCertificate(apiObject.Certificate)} - } +func waitForDNSConfiguration(ctx context.Context, conn *cloudfront.Client, dtOutput *cloudfront.GetDistributionTenantOutput) error { + timeout := 15 * time.Minute + deadline := time.Now().Add(timeout) - if apiObject.WebAcl != nil { - tfMap["web_acl"] = []any{flattenWebACL(apiObject.WebAcl)} + for time.Now().Before(deadline) { + if err := verifyDNSConfiguration(ctx, conn, dtOutput); err == nil { + return nil // DNS is valid + } + time.Sleep(30 * time.Second) } - return tfMap + return fmt.Errorf("CloudFront Distribution Tenant (%s) timeout after 15 minutes waiting for expected DNS configuration", aws.ToString(dtOutput.DistributionTenant.Id)) } -func flattenTenantGeoRestriction(apiObject *awstypes.GeoRestrictionCustomization) map[string]any { - if apiObject == nil { - return nil - } - - tfMap := map[string]any{} - - if apiObject.Locations != nil { - tfMap["locations"] = flex.FlattenStringValueSet(apiObject.Locations) - } +func waitForManagedCertificateIssued(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetManagedCertificateDetailsOutput, error) { + timeout := 3 * time.Hour + deadline := time.Now().Add(timeout) - tfMap["restriction_type"] = string(apiObject.RestrictionType) + for time.Now().Before(deadline) { + mcInput := &cloudfront.GetManagedCertificateDetailsInput{ + Identifier: aws.String(id), + } - return tfMap -} + mcOutput, err := conn.GetManagedCertificateDetails(ctx, mcInput) + if errs.IsA[*awstypes.EntityNotFound](err) { + // No managed certificate found - domains are covered by existing certs + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed getting CloudFront Distribution Tenant (%s) managed certificate details: %w", id, err) + } -func flattenCertificate(apiObject *awstypes.Certificate) map[string]any { - if apiObject == nil { - return nil - } + // Check certificate status + switch mcOutput.ManagedCertificateDetails.CertificateStatus { + case awstypes.ManagedCertificateStatusIssued: + return mcOutput, nil - tfMap := map[string]any{} + case awstypes.ManagedCertificateStatusPendingValidation: + // Certificate still being validated, continue waiting + time.Sleep(1 * time.Minute) // Longer sleep for certificate issuance + continue - if apiObject.Arn != nil { - tfMap[names.AttrARN] = aws.ToString(apiObject.Arn) + default: + return nil, fmt.Errorf("CloudFront Distribution Tenant (%s) managed certificate failed with status: %s", id, mcOutput.ManagedCertificateDetails.CertificateStatus) + } } - return tfMap + return nil, fmt.Errorf("CloudFront Distribution Tenant (%s) timeout after 3 hours waiting for managed certificate to be issued", id) } -func flattenWebACL(apiObject *awstypes.WebAclCustomization) map[string]any { - if apiObject == nil { +func updateDistributionTenantWithManagedCertificate(ctx context.Context, conn *cloudfront.Client, dtOutput *cloudfront.GetDistributionTenantOutput, mcOutput *cloudfront.GetManagedCertificateDetailsOutput) error { + // Check if we need to update the certificate ARN + if !needToUpdateCertificateARN(dtOutput.DistributionTenant, aws.ToString(mcOutput.ManagedCertificateDetails.CertificateArn)) { + // Certificate ARN already matches, nothing to do return nil } - tfMap := map[string]any{} - - tfMap[names.AttrAction] = string(apiObject.Action) - - if apiObject.Arn != nil { - tfMap[names.AttrARN] = aws.ToString(apiObject.Arn) + // Get fresh ETag before update + freshOutput, err := findDistributionTenantByIdentifier(ctx, conn, aws.ToString(dtOutput.DistributionTenant.Id)) + if err != nil { + return fmt.Errorf("failed reading CloudFront Distribution Tenant (%s): %w", aws.ToString(dtOutput.DistributionTenant.Id), err) } - return tfMap -} - -func expandManagedCertificateRequest(tfMap map[string]any) *awstypes.ManagedCertificateRequest { - if tfMap == nil { - return nil + // Update distribution tenant with managed certificate ARN + updateInput := &cloudfront.UpdateDistributionTenantInput{ + Id: dtOutput.DistributionTenant.Id, + IfMatch: freshOutput.ETag, + Customizations: &awstypes.Customizations{ + Certificate: &awstypes.Certificate{ + Arn: mcOutput.ManagedCertificateDetails.CertificateArn, + }, + }, } - apiObject := &awstypes.ManagedCertificateRequest{} - - if v, ok := tfMap["certificate_transparency_logging_preference"].(string); ok && v != "" { - apiObject.CertificateTransparencyLoggingPreference = awstypes.CertificateTransparencyLoggingPreference(v) - } + // Copy other required fields from current distribution tenant + updateInput.ConnectionGroupId = dtOutput.DistributionTenant.ConnectionGroupId + updateInput.DistributionId = dtOutput.DistributionTenant.DistributionId + updateInput.Domains = convertDomainResultsToDomainItems(dtOutput.DistributionTenant.Domains) + updateInput.Enabled = dtOutput.DistributionTenant.Enabled + updateInput.Parameters = dtOutput.DistributionTenant.Parameters - if v, ok := tfMap["primary_domain_name"].(string); ok && v != "" { - apiObject.PrimaryDomainName = aws.String(v) + _, err = conn.UpdateDistributionTenant(ctx, updateInput) + if err != nil { + return fmt.Errorf("updating CloudFront Distribution Tenant (%s) with managed certificate: %w", aws.ToString(dtOutput.DistributionTenant.Id), err) } - if v, ok := tfMap["validation_token_host"].(string); ok && v != "" { - apiObject.ValidationTokenHost = awstypes.ValidationTokenHost(v) + // Wait for the distribution tenant update to be deployed + _, err = waitForDistributionTenantDeployed(ctx, conn, aws.ToString(dtOutput.DistributionTenant.Id)) + if err != nil { + return fmt.Errorf("failed waiting for CloudFront Distribution Tenant (%s) deploy: %w", aws.ToString(dtOutput.DistributionTenant.Id), err) } - return apiObject + return nil } -func expandParameters(tfSet *schema.Set) []awstypes.Parameter { - if tfSet.Len() == 0 { - return nil +func needToUpdateCertificateARN(dt *awstypes.DistributionTenant, certArn string) bool { + if dt.Customizations == nil || dt.Customizations.Certificate == nil { + return true } + return certArn != aws.ToString(dt.Customizations.Certificate.Arn) +} + +func verifyDNSConfiguration(ctx context.Context, conn *cloudfront.Client, dtOutput *cloudfront.GetDistributionTenantOutput) error { + for _, domain := range dtOutput.DistributionTenant.Domains { + verifyInput := &cloudfront.VerifyDnsConfigurationInput{ + Domain: domain.Domain, + Identifier: dtOutput.DistributionTenant.Id, + } - parameters := make([]awstypes.Parameter, tfSet.Len()) - for i, v := range tfSet.List() { - tfMap := v.(map[string]any) - parameters[i] = awstypes.Parameter{ - Name: aws.String(tfMap[names.AttrName].(string)), - Value: aws.String(tfMap[names.AttrValue].(string)), + verifyOutput, err := conn.VerifyDnsConfiguration(ctx, verifyInput) + if err != nil { + return fmt.Errorf("verifying CloudFront Distribution Tenant (%s) DNS configuration for domain %s: %w", aws.ToString(dtOutput.DistributionTenant.Id), aws.ToString(domain.Domain), err) + } + + for _, dnsConfig := range verifyOutput.DnsConfigurationList { + switch dnsConfig.Status { + case awstypes.DnsConfigurationStatusValid: + // DNS is properly configured, continue + continue + case awstypes.DnsConfigurationStatusInvalid, awstypes.DnsConfigurationStatusUnknown: + // DNS not ready yet, return error to continue waiting + return fmt.Errorf("CloudFront Distribution Tenant (%s) DNS configuration not ready for domain %s: %s", aws.ToString(dtOutput.DistributionTenant.Id), aws.ToString(domain.Domain), dnsConfig.Status) + default: + return fmt.Errorf("CloudFront Distribution Tenant (%s) unknown DNS configuration status for domain %s: %s", aws.ToString(dtOutput.DistributionTenant.Id), aws.ToString(domain.Domain), dnsConfig.Status) + } } } - return parameters + + return nil } -func flattenParameters(apiObjects []awstypes.Parameter) *schema.Set { - if len(apiObjects) == 0 { +func convertDomainResultsToDomainItems(domainResults []awstypes.DomainResult) []awstypes.DomainItem { + if len(domainResults) == 0 { return nil } - tfSet := schema.NewSet(schema.HashResource(&schema.Resource{ - Schema: map[string]*schema.Schema{ - names.AttrName: { - Type: schema.TypeString, - }, - names.AttrValue: { - Type: schema.TypeString, - }, - }, - }), []any{}) - - for _, apiObject := range apiObjects { - tfSet.Add(map[string]any{ - names.AttrName: aws.ToString(apiObject.Name), - names.AttrValue: aws.ToString(apiObject.Value), - }) + domainItems := make([]awstypes.DomainItem, len(domainResults)) + for i, domainResult := range domainResults { + domainItems[i] = awstypes.DomainItem{ + Domain: domainResult.Domain, + } } - return tfSet + + return domainItems } diff --git a/internal/service/cloudfront/distribution_tenant_test.go b/internal/service/cloudfront/distribution_tenant_test.go index aaca7313a973..d28e1ab4b6de 100644 --- a/internal/service/cloudfront/distribution_tenant_test.go +++ b/internal/service/cloudfront/distribution_tenant_test.go @@ -10,11 +10,12 @@ import ( "github.com/YakDriver/regexache" awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" - ret "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/retry" tfcloudfront "github.com/hashicorp/terraform-provider-aws/internal/service/cloudfront" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -22,6 +23,7 @@ import ( func TestAccCloudFrontDistributionTenant_basic(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -34,34 +36,37 @@ func TestAccCloudFrontDistributionTenant_basic(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_basic(t), + Config: testAccDistributionTenantConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), resource.TestCheckResourceAttr(resourceName, "domains.#", "1"), resource.TestCheckResourceAttrSet(resourceName, "distribution_id"), - resource.TestCheckResourceAttrSet(resourceName, names.AttrName), - acctest.MatchResourceAttrGlobalARN(ctx, resourceName, names.AttrARN, "cloudfront", regexache.MustCompile(`distribution-tenant/[0-9A-Z]+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + acctest.MatchResourceAttrGlobalARN(ctx, resourceName, names.AttrARN, "cloudfront", regexache.MustCompile(`distribution-tenant/dt_[0-9A-Za-z]+$`)), resource.TestCheckResourceAttrSet(resourceName, "etag"), resource.TestCheckResourceAttrSet(resourceName, "last_modified_time"), resource.TestCheckResourceAttrSet(resourceName, names.AttrStatus), ), }, { - Config: testAccDistributionTenantConfig_basic(t), ResourceName: resourceName, ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "wait_for_deployment", + "status", }, }, }, }) } + func TestAccCloudFrontDistributionTenant_disappears(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -74,10 +79,10 @@ func TestAccCloudFrontDistributionTenant_disappears(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_basic(t), + Config: testAccDistributionTenantConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), - acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudfront.ResourceDistributionTenant(), resourceName), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfcloudfront.ResourceDistributionTenant, resourceName), ), ExpectNonEmptyPlan: true, }, @@ -88,6 +93,7 @@ func TestAccCloudFrontDistributionTenant_disappears(t *testing.T) { func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -100,7 +106,7 @@ func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_customCertificate(t), + Config: testAccDistributionTenantConfig_customCertificate(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), @@ -118,6 +124,7 @@ func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { func TestAccCloudFrontDistributionTenant_customCertificateWithWebACL(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -130,13 +137,13 @@ func TestAccCloudFrontDistributionTenant_customCertificateWithWebACL(t *testing. CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_customCertificateWithWebACL(t), + Config: testAccDistributionTenantConfig_customCertificateWithWebACL(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), resource.TestCheckResourceAttr(resourceName, "customizations.0.web_acl.#", "1"), resource.TestCheckResourceAttr(resourceName, "customizations.0.web_acl.0.action", "override"), - resource.TestCheckResourceAttrPair(resourceName, "customizations.0.web_acl.0.arn", "aws_wafv2_web_acl.testacl", names.AttrARN), + resource.TestCheckResourceAttrPair(resourceName, "customizations.0.web_acl.0.arn", "aws_wafv2_web_acl.test", names.AttrARN), ), }, }, @@ -146,6 +153,7 @@ func TestAccCloudFrontDistributionTenant_customCertificateWithWebACL(t *testing. func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -158,7 +166,7 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_parameters(t), + Config: testAccDistributionTenantConfig_parameters(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "parameters.#", "2"), @@ -171,6 +179,7 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { { ResourceName: resourceName, ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "wait_for_deployment", @@ -183,6 +192,7 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { func TestAccCloudFrontDistributionTenant_managedCertificateRequest(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -195,7 +205,7 @@ func TestAccCloudFrontDistributionTenant_managedCertificateRequest(t *testing.T) CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_managedCertificateRequest(t), + Config: testAccDistributionTenantConfig_managedCertificateRequest(rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), @@ -212,6 +222,7 @@ func TestAccCloudFrontDistributionTenant_managedCertificateRequest(t *testing.T) func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ @@ -224,7 +235,7 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_tags1(t, acctest.CtKey1, acctest.CtValue1), + Config: testAccDistributionTenantConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), @@ -232,7 +243,7 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ), }, { - Config: testAccDistributionTenantConfig_tags2(t, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Config: testAccDistributionTenantConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), @@ -241,7 +252,7 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ), }, { - Config: testAccDistributionTenantConfig_tags1(t, acctest.CtKey2, acctest.CtValue2), + Config: testAccDistributionTenantConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), @@ -261,9 +272,9 @@ func testAccCheckDistributionTenantDestroy(ctx context.Context) resource.TestChe continue } - _, err := tfcloudfront.FindDistributionTenantById(ctx, conn, rs.Primary.ID) + _, err := tfcloudfront.FindDistributionTenantByIdentifier(ctx, conn, rs.Primary.ID) - if ret.NotFound(err) { + if retry.NotFound(err) { continue } @@ -287,7 +298,7 @@ func testAccCheckDistributionTenantExists(ctx context.Context, n string, v *awst conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFrontClient(ctx) - output, err := tfcloudfront.FindDistributionTenantById(ctx, conn, rs.Primary.ID) + output, err := tfcloudfront.FindDistributionTenantByIdentifier(ctx, conn, rs.Primary.ID) if err != nil { return err @@ -299,19 +310,17 @@ func testAccCheckDistributionTenantExists(ctx context.Context, n string, v *awst } } -func testAccDistributionTenantConfig_basic(t *testing.T) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_basic(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { domain = %[1]q - region = "data.aws_region.current.region" most_recent = true } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -350,7 +359,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -367,26 +376,23 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" + domains = ["example.com"] + name = %[1]q enabled = false } -`, certDomain) +`, rName)) } - -func testAccDistributionTenantConfig_customCertificate(t *testing.T) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_customCertificate(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q - region = "data.aws_region.current.region" + domain = "example.com" most_recent = true } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -425,7 +431,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -441,8 +447,8 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" + domains = ["example.com"] + name = %[1]q enabled = false customizations { @@ -456,25 +462,22 @@ resource "aws_cloudfront_distribution_tenant" "test" { } } } -`, certDomain) +`, rName)) } -func testAccDistributionTenantConfig_customCertificateWithWebACL(t *testing.T) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_customCertificateWithWebACL(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q - region = "data.aws_region.current.region" + domain = "example.com" most_recent = true } -resource "aws_wafv2_web_acl" "testacl" { - name = "tftest" - description = "tftest" +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = "test web acl" scope = "CLOUDFRONT" - region = "data.aws_region.current.region" default_action { allow { @@ -494,8 +497,8 @@ resource "aws_wafv2_web_acl" "testacl" { } } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -534,7 +537,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -550,9 +553,9 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" - enabled = false + domains = ["example.com"] + name = %[1]q + enabled = false customizations { geo_restriction { @@ -564,28 +567,26 @@ resource "aws_cloudfront_distribution_tenant" "test" { arn = data.aws_acm_certificate.test.arn } - web_acl { - action = "override" - arn = aws_wafv2_web_acl.testacl.arn - } + web_acl { + action = "override" + arn = aws_wafv2_web_acl.test.arn + } } } -`, certDomain) +`, rName)) } -func testAccDistributionTenantConfig_parameters(t *testing.T) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_parameters(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q - region = "data.aws_region.current.region" + domain = "example.com" most_recent = true } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -624,7 +625,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -641,36 +642,34 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" + domains = ["example.com"] + name = %[1]q enabled = false parameters { - name = "tenantid" - value = "tenant-123" + name = "tenantid" + value = "tenant-123" } parameters { - name = "place" - value = "na" + name = "place" + value = "na" } } -`, certDomain) +`, rName)) } -func testAccDistributionTenantConfig_tags1(t *testing.T, tagKey1, tagValue1 string) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q - region = "data.aws_region.current.region" + domain = "example.com" most_recent = true } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -709,7 +708,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -726,30 +725,28 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" + domains = ["example.com"] + name = %[1]q enabled = false tags = { %[2]q = %[3]q } } -`, certDomain, tagKey1, tagValue1) +`, rName, tagKey1, tagValue1)) } -func testAccDistributionTenantConfig_tags2(t *testing.T, tagKey1, tagValue1, tagKey2, tagValue2 string) string { - certDomain := acctest.ACMCertificateDomainFromEnv(t) - return fmt.Sprintf(` -data "aws_region" "current" {} - +func testAccDistributionTenantConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q - region = "data.aws_region.current.region" + domain = "example.com" most_recent = true } -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -788,7 +785,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -805,8 +802,8 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[1]q] - name = "tftenant" + domains = ["example.com"] + name = %[1]q enabled = false tags = { @@ -814,15 +811,32 @@ resource "aws_cloudfront_distribution_tenant" "test" { %[4]q = %[5]q } } -`, certDomain, tagKey1, tagValue1, tagKey2, tagValue2) +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) } -func testAccDistributionTenantConfig_managedCertificateRequest(t *testing.T) string { - zoneDomain := acctest.ACMCertificateDomainFromEnv(t) - domainName := "tf." + zoneDomain - return fmt.Sprintf(` -resource "aws_cloudfront_cache_policy" "tf-policy" { - name = "tfpolicy" +func testAccDistributionTenantConfig_managedCertificateRequest(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +data "aws_route53_zone" "test" { + name = "example.com" + private_zone = false +} + +resource "aws_cloudfront_connection_group" "test" { + name = %[1]q +} + +resource "aws_route53_record" "test" { + zone_id = data.aws_route53_zone.test.id + type = "CNAME" + ttl = 300 + name = "tf.example.com" + records = [aws_cloudfront_connection_group.test.routing_endpoint] +} + +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q comment = "test tenant cache policy" default_ttl = 50 max_ttl = 100 @@ -861,7 +875,7 @@ resource "aws_cloudfront_distribution" "test" { cached_methods = ["GET", "HEAD"] target_origin_id = "test" viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.tf-policy.id + cache_policy_id = aws_cloudfront_cache_policy.test.id } restrictions { @@ -874,38 +888,21 @@ resource "aws_cloudfront_distribution" "test" { cloudfront_default_certificate = true } - depends_on = [aws_route53_record.testrecord] -} - -data "aws_route53_zone" "test" { - name = %[1]q - private_zone = false -} - -resource "aws_cloudfront_connection_group" "testgroup" { - name = "testgroup" -} - -resource "aws_route53_record" "testrecord" { - zone_id = data.aws_route53_zone.test.id - type = "CNAME" - ttl = 300 - name = %[2]q - records = [aws_cloudfront_connection_group.testgroup.routing_endpoint] + depends_on = [aws_route53_record.test] } resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = [%[2]q] - name = "tftenant" + domains = ["tf.example.com"] + name = %[1]q enabled = false - connection_group_id = aws_cloudfront_connection_group.testgroup.id + connection_group_id = aws_cloudfront_connection_group.test.id managed_certificate_request { - primary_domain_name = %[2]q - validation_token_host = "cloudfront" - certificate_transparency_logging_preference = "disabled" + primary_domain_name = "tf.example.com" + validation_token_host = "cloudfront" + certificate_transparency_logging_preference = "disabled" } } -`, zoneDomain, domainName) +`, rName)) } diff --git a/internal/service/cloudfront/exports_test.go b/internal/service/cloudfront/exports_test.go index 5c62bcb89b6a..04cc0579c3ed 100644 --- a/internal/service/cloudfront/exports_test.go +++ b/internal/service/cloudfront/exports_test.go @@ -8,7 +8,7 @@ var ( ResourceCachePolicy = resourceCachePolicy ResourceContinuousDeploymentPolicy = newContinuousDeploymentPolicyResource ResourceDistribution = resourceDistribution - ResourceDistributionTenant = resourceDistributionTenant + ResourceDistributionTenant = newDistributionTenantResource ResourceFieldLevelEncryptionConfig = resourceFieldLevelEncryptionConfig ResourceFieldLevelEncryptionProfile = resourceFieldLevelEncryptionProfile ResourceFunction = resourceFunction diff --git a/internal/service/cloudfront/service_package_gen.go b/internal/service/cloudfront/service_package_gen.go index ee2797353c6f..79a77ecbe595 100644 --- a/internal/service/cloudfront/service_package_gen.go +++ b/internal/service/cloudfront/service_package_gen.go @@ -157,15 +157,6 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePa }), Region: unique.Make(inttypes.ResourceRegionDisabled()), }, - { - Factory: resourceDistributionTenant, - TypeName: "aws_cloudfront_distribution_tenant", - Name: "Distribution Tenant", - Tags: unique.Make(inttypes.ServicePackageResourceTags{ - IdentifierAttribute: names.AttrARN, - }), - Region: unique.Make(inttypes.ResourceRegionDisabled()), - }, { Factory: resourceFieldLevelEncryptionConfig, TypeName: "aws_cloudfront_field_level_encryption_config", From efa02c315ce18f67e25c3c3ee5fadce5d324cba0 Mon Sep 17 00:00:00 2001 From: Jason Kim Date: Tue, 4 Nov 2025 11:24:26 -0800 Subject: [PATCH 12/15] Rewrite distribution tenant data source using the Framework --- .../distribution_tenant_data_source.go | 341 ++++++++++---- .../distribution_tenant_data_source_test.go | 444 +++++++++++++++++- internal/service/cloudfront/exports_test.go | 2 +- .../service/cloudfront/service_package_gen.go | 27 +- 4 files changed, 714 insertions(+), 100 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant_data_source.go b/internal/service/cloudfront/distribution_tenant_data_source.go index b2957a7e86f6..eba55f43c042 100644 --- a/internal/service/cloudfront/distribution_tenant_data_source.go +++ b/internal/service/cloudfront/distribution_tenant_data_source.go @@ -6,139 +6,310 @@ package cloudfront import ( "context" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudfront" awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/errs" - "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) -// @SDKDataSource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") +// @FrameworkDataSource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") // @Tags(identifierAttribute="arn") -func dataSourceDistributionTenant() *schema.Resource { - return &schema.Resource{ - ReadWithoutTimeout: dataSourceDistributionTenantRead, +func newDistributionTenantDataSource(_ context.Context) (datasource.DataSourceWithConfigure, error) { + d := &distributionTenantDataSource{} + return d, nil +} + +const ( + DSNameDistributionTenant = "Distribution Tenant Data Source" +) - Schema: map[string]*schema.Schema{ - names.AttrARN: { - Type: schema.TypeString, +type distributionTenantDataSource struct { + framework.DataSourceWithModel[distributionTenantDataSourceModel] +} + +func (d *distributionTenantDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + Optional: true, Computed: true, }, - "connection_group_id": { - Type: schema.TypeString, + "connection_group_id": schema.StringAttribute{ Computed: true, }, - "distribution_id": { - Type: schema.TypeString, + "distribution_id": schema.StringAttribute{ Computed: true, }, - names.AttrDomain: { - Type: schema.TypeString, - Optional: true, - ExactlyOneOf: []string{"domain", names.AttrID}, + names.AttrDomain: schema.StringAttribute{ + Optional: true, + }, + "domains": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, }, - "domains": { - Type: schema.TypeSet, + names.AttrEnabled: schema.BoolAttribute{ Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, }, - names.AttrEnabled: { - Type: schema.TypeBool, + "etag": schema.StringAttribute{ Computed: true, }, - "etag": { - Type: schema.TypeString, + names.AttrID: schema.StringAttribute{ + Optional: true, Computed: true, }, - names.AttrID: { - Type: schema.TypeString, - Optional: true, - Computed: true, - ExactlyOneOf: []string{"domain", names.AttrID}, + "last_modified_time": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, }, - "last_modified_time": { - Type: schema.TypeString, + names.AttrName: schema.StringAttribute{ + Optional: true, Computed: true, }, - names.AttrName: { - Type: schema.TypeString, + names.AttrStatus: schema.StringAttribute{ Computed: true, }, - names.AttrStatus: { - Type: schema.TypeString, - Computed: true, + names.AttrTags: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "customizations": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[customizationsDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "geo_restriction": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[geoRestrictionDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "locations": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "restriction_type": schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.StringEnumType[awstypes.GeoRestrictionType](), + }, + }, + }, + }, + names.AttrCertificate: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[certificateDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.ARNType, + }, + }, + }, + }, + "web_acl": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[webAclDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrAction: schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.StringEnumType[awstypes.CustomizationActionType](), + }, + names.AttrARN: schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.ARNType, + }, + }, + }, + }, + }, + }, + }, + "managed_certificate_request": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[managedCertificateRequestDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "certificate_transparency_logging_preference": schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.StringEnumType[awstypes.CertificateTransparencyLoggingPreference](), + }, + "primary_domain_name": schema.StringAttribute{ + Computed: true, + }, + "validation_token_host": schema.StringAttribute{ + Computed: true, + CustomType: fwtypes.StringEnumType[awstypes.ValidationTokenHost](), + }, + }, + }, + }, + names.AttrParameters: schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[parameterDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ + Computed: true, + }, + names.AttrValue: schema.StringAttribute{ + Computed: true, + }, + }, + }, }, - names.AttrTags: tftags.TagsSchemaComputed(), }, } } -func dataSourceDistributionTenantRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).CloudFrontClient(ctx) +func (d *distributionTenantDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var data distributionTenantDataSourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := d.Meta().CloudFrontClient(ctx) + + // Define lookup strategies in order of preference + lookupStrategies := []struct { + value types.String + fn func(context.Context, *cloudfront.Client, string) (interface{}, error) + }{ + {data.ID, func(ctx context.Context, conn *cloudfront.Client, id string) (interface{}, error) { + return findDistributionTenantByIdentifier(ctx, conn, id) + }}, + {data.ARN, func(ctx context.Context, conn *cloudfront.Client, arn string) (interface{}, error) { + return findDistributionTenantByIdentifier(ctx, conn, arn) + }}, + {data.Name, func(ctx context.Context, conn *cloudfront.Client, name string) (interface{}, error) { + return findDistributionTenantByIdentifier(ctx, conn, name) + }}, + {data.Domain, func(ctx context.Context, conn *cloudfront.Client, domain string) (interface{}, error) { + return findDistributionTenantByDomain(ctx, conn, domain) + }}, + } + + var output interface{} + var err error - var identifier string + // Try each lookup strategy until we find a non-null, non-unknown value + for _, strategy := range lookupStrategies { + if !strategy.value.IsNull() && !strategy.value.IsUnknown() { + output, err = strategy.fn(ctx, conn, strategy.value.ValueString()) + break + } + } + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, DSNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + // Handle both output types var tenant *awstypes.DistributionTenant var etag *string + switch v := output.(type) { + case *cloudfront.GetDistributionTenantOutput: + tenant = v.DistributionTenant + etag = v.ETag + case *cloudfront.GetDistributionTenantByDomainOutput: + tenant = v.DistributionTenant + etag = v.ETag + } - if id, ok := d.GetOk(names.AttrID); ok { - identifier = id.(string) - output, err := findDistributionTenantByID(ctx, conn, identifier) - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant by ID (%s): %s", identifier, err) - } - tenant = output.DistributionTenant - etag = output.ETag - } else { - identifier = d.Get(names.AttrDomain).(string) - output, err := findDistributionTenantByDomain(ctx, conn, identifier) - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFront Distribution Tenant by Domain (%s): %s", identifier, err) - } - tenant = output.DistributionTenant - etag = output.ETag + // Flatten the distribution tenant data into the model + response.Diagnostics.Append(fwflex.Flatten(ctx, tenant, &data)...) + if response.Diagnostics.HasError() { + return + } + + // Set computed fields that need special handling + data.ID = fwflex.StringToFramework(ctx, tenant.Id) + data.ETag = fwflex.StringToFramework(ctx, etag) + if tenant.LastModifiedTime != nil { + data.LastModifiedTime = fwflex.TimeToFramework(ctx, tenant.LastModifiedTime) } - d.SetId(aws.ToString(tenant.Id)) - d.Set(names.AttrARN, tenant.Arn) - d.Set("connection_group_id", tenant.ConnectionGroupId) - d.Set("distribution_id", tenant.DistributionId) - d.Set("domains", flattenDomains(tenant.Domains)) - d.Set(names.AttrEnabled, tenant.Enabled) - d.Set("etag", etag) - d.Set(names.AttrName, tenant.Name) - d.Set(names.AttrStatus, tenant.Status) - d.Set("last_modified_time", tenant.LastModifiedTime.String()) - - return diags + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func findDistributionTenantByDomain(ctx context.Context, conn *cloudfront.Client, domain string) (*cloudfront.GetDistributionTenantByDomainOutput, error) { - input := cloudfront.GetDistributionTenantByDomainInput{ - Domain: aws.String(domain), - } +type distributionTenantDataSourceModel struct { + ARN types.String `tfsdk:"arn"` + ConnectionGroupID types.String `tfsdk:"connection_group_id"` + Customizations fwtypes.ListNestedObjectValueOf[customizationsDataSourceModel] `tfsdk:"customizations"` + DistributionID types.String `tfsdk:"distribution_id"` + Domain types.String `tfsdk:"domain"` + Domains fwtypes.SetValueOf[types.String] `tfsdk:"domains"` + Enabled types.Bool `tfsdk:"enabled"` + ETag types.String `tfsdk:"etag"` + ID types.String `tfsdk:"id"` + LastModifiedTime timetypes.RFC3339 `tfsdk:"last_modified_time"` + ManagedCertificateRequest fwtypes.ListNestedObjectValueOf[managedCertificateRequestDataSourceModel] `tfsdk:"managed_certificate_request"` + Name types.String `tfsdk:"name"` + Parameters fwtypes.SetNestedObjectValueOf[parameterDataSourceModel] `tfsdk:"parameters"` + Status types.String `tfsdk:"status"` + Tags tftags.Map `tfsdk:"tags"` +} - output, err := conn.GetDistributionTenantByDomain(ctx, &input) +type customizationsDataSourceModel struct { + GeoRestriction fwtypes.ListNestedObjectValueOf[geoRestrictionDataSourceModel] `tfsdk:"geo_restriction"` + Certificate fwtypes.ListNestedObjectValueOf[certificateDataSourceModel] `tfsdk:"certificate"` + WebAcl fwtypes.ListNestedObjectValueOf[webAclDataSourceModel] `tfsdk:"web_acl"` +} - if errs.IsA[*awstypes.EntityNotFound](err) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } +type geoRestrictionDataSourceModel struct { + Locations fwtypes.SetValueOf[types.String] `tfsdk:"locations"` + RestrictionType fwtypes.StringEnum[awstypes.GeoRestrictionType] `tfsdk:"restriction_type"` +} + +type certificateDataSourceModel struct { + ARN fwtypes.ARN `tfsdk:"arn"` +} + +type webAclDataSourceModel struct { + Action fwtypes.StringEnum[awstypes.CustomizationActionType] `tfsdk:"action"` + ARN fwtypes.ARN `tfsdk:"arn"` +} + +type managedCertificateRequestDataSourceModel struct { + CertificateTransparencyLoggingPreference fwtypes.StringEnum[awstypes.CertificateTransparencyLoggingPreference] `tfsdk:"certificate_transparency_logging_preference"` + PrimaryDomainName types.String `tfsdk:"primary_domain_name"` + ValidationTokenHost fwtypes.StringEnum[awstypes.ValidationTokenHost] `tfsdk:"validation_token_host"` +} + +type parameterDataSourceModel struct { + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` +} + +func (d *distributionTenantDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot(names.AttrID), + path.MatchRoot(names.AttrARN), + path.MatchRoot(names.AttrName), + path.MatchRoot(names.AttrDomain), + ), + } +} +func findDistributionTenantByDomain(ctx context.Context, conn *cloudfront.Client, domain string) (*cloudfront.GetDistributionTenantByDomainOutput, error) { + input := &cloudfront.GetDistributionTenantByDomainInput{ + Domain: &domain, } + output, err := conn.GetDistributionTenantByDomain(ctx, input) if err != nil { return nil, err } - if output == nil || output.DistributionTenant == nil || output.DistributionTenant.Domains == nil || output.DistributionTenant.DistributionId == nil { + if output == nil || output.DistributionTenant == nil { return nil, tfresource.NewEmptyResultError(input) } diff --git a/internal/service/cloudfront/distribution_tenant_data_source_test.go b/internal/service/cloudfront/distribution_tenant_data_source_test.go index b1f1ee479387..535f5ec002f7 100644 --- a/internal/service/cloudfront/distribution_tenant_data_source_test.go +++ b/internal/service/cloudfront/distribution_tenant_data_source_test.go @@ -4,8 +4,10 @@ package cloudfront_test import ( + "fmt" "testing" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/names" @@ -13,25 +15,102 @@ import ( func TestAccCloudFrontDistributionTenantDataSource_basic(t *testing.T) { ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) dataSourceName := "data.aws_cloudfront_distribution_tenant.test" resourceName := "aws_cloudfront_distribution_tenant.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantDataSourceConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "customizations.#", resourceName, "customizations.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "distribution_id", resourceName, "distribution_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "domains.#", resourceName, "domains.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrEnabled, resourceName, names.AttrEnabled), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "last_modified_time", resourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), + resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + ), + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenantDataSource_byARN(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_cloudfront_distribution_tenant.test" + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantDataSourceConfig_byARN(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "customizations.#", resourceName, "customizations.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "distribution_id", resourceName, "distribution_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "domains.#", resourceName, "domains.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrEnabled, resourceName, names.AttrEnabled), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "last_modified_time", resourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), + resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + ), + }, + }, + }) +} + +func TestAccCloudFrontDistributionTenantDataSource_byName(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_cloudfront_distribution_tenant.test" + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDistributionTenantDataSourceConfig_basic(t), + Config: testAccDistributionTenantDataSourceConfig_byName(rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "customizations.#", resourceName, "customizations.#"), resource.TestCheckResourceAttrPair(dataSourceName, "distribution_id", resourceName, "distribution_id"), resource.TestCheckResourceAttrPair(dataSourceName, "domains.#", resourceName, "domains.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrEnabled, resourceName, names.AttrEnabled), resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), resource.TestCheckResourceAttrPair(dataSourceName, "last_modified_time", resourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), + resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), ), }, @@ -39,10 +118,365 @@ func TestAccCloudFrontDistributionTenantDataSource_basic(t *testing.T) { }) } -func testAccDistributionTenantDataSourceConfig_basic(t *testing.T) string { - return acctest.ConfigCompose(testAccDistributionTenantConfig_basic(t), ` +func TestAccCloudFrontDistributionTenantDataSource_byDomain(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_cloudfront_distribution_tenant.test" + resourceName := "aws_cloudfront_distribution_tenant.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDistributionTenantDataSourceConfig_byDomain(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "customizations.#", resourceName, "customizations.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "distribution_id", resourceName, "distribution_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "domains.#", resourceName, "domains.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrEnabled, resourceName, names.AttrEnabled), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "last_modified_time", resourceName, "last_modified_time"), + resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), + resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + ), + }, + }, + }) +} + +func testAccDistributionTenantDataSourceConfig_basic(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = "example.com" + most_recent = true +} + +resource "aws_cloudfront_connection_group" "test" { + name = %[1]q +} + +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.test.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = ["example.com"] + name = %[1]q + enabled = false + connection_group_id = aws_cloudfront_connection_group.test.id +} + data "aws_cloudfront_distribution_tenant" "test" { id = aws_cloudfront_distribution_tenant.test.id } -`) +`, rName)) +} + +func testAccDistributionTenantDataSourceConfig_byARN(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = "example.com" + most_recent = true +} + +resource "aws_cloudfront_connection_group" "test" { + name = %[1]q +} + +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.test.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = ["example.com"] + name = %[1]q + enabled = false + connection_group_id = aws_cloudfront_connection_group.test.id +} + +data "aws_cloudfront_distribution_tenant" "test" { + arn = aws_cloudfront_distribution_tenant.test.arn +} +`, rName)) +} + +func testAccDistributionTenantDataSourceConfig_byName(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = "example.com" + most_recent = true +} + +resource "aws_cloudfront_connection_group" "test" { + name = %[1]q +} + +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.test.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = ["example.com"] + name = %[1]q + enabled = false + connection_group_id = aws_cloudfront_connection_group.test.id +} + +data "aws_cloudfront_distribution_tenant" "test" { + name = aws_cloudfront_distribution_tenant.test.name +} +`, rName)) +} + +func testAccDistributionTenantDataSourceConfig_byDomain(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +data "aws_acm_certificate" "test" { + domain = "example.com" + most_recent = true +} + +resource "aws_cloudfront_connection_group" "test" { + name = %[1]q +} + +resource "aws_cloudfront_cache_policy" "test" { + name = %[1]q + comment = "test tenant cache policy" + default_ttl = 50 + max_ttl = 100 + min_ttl = 1 + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_distribution" "test" { + connection_mode = "tenant-only" + enabled = true + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + cache_policy_id = aws_cloudfront_cache_policy.test.id + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = data.aws_acm_certificate.test.arn + ssl_support_method = "sni-only" + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains = ["example-%[1]s.com"] + name = %[1]q + enabled = false + connection_group_id = aws_cloudfront_connection_group.test.id +} + +data "aws_cloudfront_distribution_tenant" "test" { + domain = "example-%[1]s.com" +} +`, rName)) } diff --git a/internal/service/cloudfront/exports_test.go b/internal/service/cloudfront/exports_test.go index 04cc0579c3ed..fd0680f17a9b 100644 --- a/internal/service/cloudfront/exports_test.go +++ b/internal/service/cloudfront/exports_test.go @@ -24,7 +24,7 @@ var ( FindCachePolicyByID = findCachePolicyByID FindContinuousDeploymentPolicyByID = findContinuousDeploymentPolicyByID - FindDistributionTenantById = findDistributionTenantByID + FindDistributionTenantByIdentifier = findDistributionTenantByIdentifier FindFieldLevelEncryptionConfigByID = findFieldLevelEncryptionConfigByID FindFieldLevelEncryptionProfileByID = findFieldLevelEncryptionProfileByID FindFunctionByTwoPartKey = findFunctionByTwoPartKey diff --git a/internal/service/cloudfront/service_package_gen.go b/internal/service/cloudfront/service_package_gen.go index 79a77ecbe595..9ff9f5ef705a 100644 --- a/internal/service/cloudfront/service_package_gen.go +++ b/internal/service/cloudfront/service_package_gen.go @@ -30,6 +30,15 @@ func (p *servicePackage) Actions(ctx context.Context) []*inttypes.ServicePackage func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.ServicePackageFrameworkDataSource { return []*inttypes.ServicePackageFrameworkDataSource{ + { + Factory: newDistributionTenantDataSource, + TypeName: "aws_cloudfront_distribution_tenant", + Name: "Distribution Tenant", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, { Factory: newOriginAccessControlDataSource, TypeName: "aws_cloudfront_origin_access_control", @@ -47,6 +56,15 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser Name: "Continuous Deployment Policy", Region: unique.Make(inttypes.ResourceRegionDisabled()), }, + { + Factory: newDistributionTenantResource, + TypeName: "aws_cloudfront_distribution_tenant", + Name: "Distribution Tenant", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, { Factory: newKeyValueStoreResource, TypeName: "aws_cloudfront_key_value_store", @@ -86,15 +104,6 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*inttypes.Service }), Region: unique.Make(inttypes.ResourceRegionDisabled()), }, - { - Factory: dataSourceDistributionTenant, - TypeName: "aws_cloudfront_distribution_tenant", - Name: "Distribution Tenant", - Tags: unique.Make(inttypes.ServicePackageResourceTags{ - IdentifierAttribute: names.AttrARN, - }), - Region: unique.Make(inttypes.ResourceRegionDisabled()), - }, { Factory: dataSourceFunction, TypeName: "aws_cloudfront_function", From ed244c91833538266f0c5703b8f13070857d8cc4 Mon Sep 17 00:00:00 2001 From: Jason Kim Date: Fri, 14 Nov 2025 11:24:53 -0800 Subject: [PATCH 13/15] fix: adjust schema and do proper flattening --- .../service/cloudfront/distribution_tenant.go | 306 +++++++++++++++++- .../distribution_tenant_data_source.go | 278 +++++++++++++++- .../distribution_tenant_data_source_test.go | 92 +++--- .../cloudfront/distribution_tenant_test.go | 290 ++++++----------- 4 files changed, 711 insertions(+), 255 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go index e21505d7cc60..b369683119d3 100644 --- a/internal/service/cloudfront/distribution_tenant.go +++ b/internal/service/cloudfront/distribution_tenant.go @@ -14,7 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" @@ -22,6 +23,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs" @@ -70,13 +72,6 @@ func (r *distributionTenantResource) Schema(ctx context.Context, req resource.Sc stringplanmodifier.RequiresReplace(), }, }, - "domains": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), - }, - }, names.AttrEnabled: schema.BoolAttribute{ Optional: true, Computed: true, @@ -118,7 +113,7 @@ func (r *distributionTenantResource) Schema(ctx context.Context, req resource.Sc NestedObject: schema.NestedBlockObject{ Blocks: map[string]schema.Block{ "geo_restriction": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[geoRestrictionModel](ctx), + CustomType: fwtypes.NewListNestedObjectTypeOf[geoRestrictionCustomizationModel](ctx), Validators: []validator.List{ listvalidator.SizeAtMost(1), }, @@ -151,7 +146,7 @@ func (r *distributionTenantResource) Schema(ctx context.Context, req resource.Sc }, }, "web_acl": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[webAclModel](ctx), + CustomType: fwtypes.NewListNestedObjectTypeOf[webAclCustomizationModel](ctx), Validators: []validator.List{ listvalidator.SizeAtMost(1), }, @@ -192,6 +187,16 @@ func (r *distributionTenantResource) Schema(ctx context.Context, req resource.Sc }, }, }, + "domains": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[domainItemModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, names.AttrParameters: schema.SetNestedBlock{ CustomType: fwtypes.NewSetNestedObjectTypeOf[parameterModel](ctx), NestedObject: schema.NestedBlockObject{ @@ -214,7 +219,7 @@ type distributionTenantResourceModel struct { ConnectionGroupID types.String `tfsdk:"connection_group_id"` Customizations fwtypes.ListNestedObjectValueOf[customizationsModel] `tfsdk:"customizations"` DistributionID types.String `tfsdk:"distribution_id"` - Domains fwtypes.SetValueOf[types.String] `tfsdk:"domains"` + Domains fwtypes.SetNestedObjectValueOf[domainItemModel] `tfsdk:"domains"` Enabled types.Bool `tfsdk:"enabled"` ETag types.String `tfsdk:"etag"` ID types.String `tfsdk:"id"` @@ -230,13 +235,26 @@ type distributionTenantResourceModel struct { } type customizationsModel struct { - GeoRestriction fwtypes.ListNestedObjectValueOf[geoRestrictionModel] `tfsdk:"geo_restriction"` - Certificate fwtypes.ListNestedObjectValueOf[certificateModel] `tfsdk:"certificate"` - WebAcl fwtypes.ListNestedObjectValueOf[webAclModel] `tfsdk:"web_acl"` + GeoRestriction fwtypes.ListNestedObjectValueOf[geoRestrictionCustomizationModel] `tfsdk:"geo_restriction"` + Certificate fwtypes.ListNestedObjectValueOf[certificateModel] `tfsdk:"certificate"` + WebAcl fwtypes.ListNestedObjectValueOf[webAclCustomizationModel] `tfsdk:"web_acl"` +} + +// Implement fwflex.Flattener interface +var ( + _ fwflex.Flattener = &customizationsModel{} + _ fwflex.Flattener = &geoRestrictionCustomizationModel{} + _ fwflex.Flattener = &certificateModel{} + _ fwflex.Flattener = &webAclCustomizationModel{} + _ fwflex.Flattener = &managedCertificateRequestModel{} +) + +type domainItemModel struct { + Domain types.String `tfsdk:"domain"` } -type geoRestrictionModel struct { - Locations fwtypes.SetValueOf[types.String] `tfsdk:"locations"` +type geoRestrictionCustomizationModel struct { + Locations fwtypes.SetOfString `tfsdk:"locations"` RestrictionType fwtypes.StringEnum[awstypes.GeoRestrictionType] `tfsdk:"restriction_type"` } @@ -244,7 +262,7 @@ type certificateModel struct { ARN fwtypes.ARN `tfsdk:"arn"` } -type webAclModel struct { +type webAclCustomizationModel struct { Action fwtypes.StringEnum[awstypes.CustomizationActionType] `tfsdk:"action"` ARN fwtypes.ARN `tfsdk:"arn"` } @@ -296,6 +314,15 @@ func (r *distributionTenantResource) Create(ctx context.Context, req resource.Cr return } + // Manually flatten domains and parameters + var d diag.Diagnostics + data.Domains, d = flattenDomains(ctx, output.DistributionTenant.Domains) + resp.Diagnostics.Append(d...) + data.Parameters, d = flattenParameters(ctx, output.DistributionTenant.Parameters) + resp.Diagnostics.Append(d...) + data.Customizations, d = flattenCustomizations(ctx, output.DistributionTenant.Customizations) + resp.Diagnostics.Append(d...) + // Set fields that fwflex.Flatten might not handle correctly data.ID = fwflex.StringToFramework(ctx, output.DistributionTenant.Id) data.LastModifiedTime = fwflex.TimeToFramework(ctx, output.DistributionTenant.LastModifiedTime) @@ -343,6 +370,14 @@ func (r *distributionTenantResource) Create(ctx context.Context, req resource.Cr return } + // Manually flatten domains and parameters + data.Domains, d = flattenDomains(ctx, refreshedOutput.DistributionTenant.Domains) + resp.Diagnostics.Append(d...) + data.Parameters, d = flattenParameters(ctx, refreshedOutput.DistributionTenant.Parameters) + resp.Diagnostics.Append(d...) + data.Customizations, d = flattenCustomizations(ctx, refreshedOutput.DistributionTenant.Customizations) + resp.Diagnostics.Append(d...) + data.ETag = fwflex.StringToFramework(ctx, refreshedOutput.ETag) data.LastModifiedTime = fwflex.TimeToFramework(ctx, refreshedOutput.DistributionTenant.LastModifiedTime) } @@ -381,6 +416,15 @@ func (r *distributionTenantResource) Read(ctx context.Context, req resource.Read return } + // Manually flatten domains and parameters + var d diag.Diagnostics + data.Domains, d = flattenDomains(ctx, tenant.Domains) + resp.Diagnostics.Append(d...) + data.Parameters, d = flattenParameters(ctx, tenant.Parameters) + resp.Diagnostics.Append(d...) + data.Customizations, d = flattenCustomizations(ctx, tenant.Customizations) + resp.Diagnostics.Append(d...) + // Set computed fields that need special handling data.ETag = fwflex.StringToFramework(ctx, output.ETag) data.LastModifiedTime = fwflex.TimeToFramework(ctx, tenant.LastModifiedTime) @@ -487,6 +531,15 @@ func (r *distributionTenantResource) Update(ctx context.Context, req resource.Up return } + // Manually flatten domains and parameters + var d diag.Diagnostics + new.Domains, d = flattenDomains(ctx, refreshedOutput.DistributionTenant.Domains) + resp.Diagnostics.Append(d...) + new.Parameters, d = flattenParameters(ctx, refreshedOutput.DistributionTenant.Parameters) + resp.Diagnostics.Append(d...) + new.Customizations, d = flattenCustomizations(ctx, refreshedOutput.DistributionTenant.Customizations) + resp.Diagnostics.Append(d...) + new.ETag = fwflex.StringToFramework(ctx, refreshedOutput.ETag) new.LastModifiedTime = fwflex.TimeToFramework(ctx, refreshedOutput.DistributionTenant.LastModifiedTime) } @@ -504,6 +557,15 @@ func (r *distributionTenantResource) Update(ctx context.Context, req resource.Up return } + // Manually flatten domains and parameters + var d diag.Diagnostics + new.Domains, d = flattenDomains(ctx, output.DistributionTenant.Domains) + resp.Diagnostics.Append(d...) + new.Parameters, d = flattenParameters(ctx, output.DistributionTenant.Parameters) + resp.Diagnostics.Append(d...) + new.Customizations, d = flattenCustomizations(ctx, output.DistributionTenant.Customizations) + resp.Diagnostics.Append(d...) + new.ETag = fwflex.StringToFramework(ctx, output.ETag) } else { // If no update was performed (e.g., tag-only changes), we still need to refresh the distribution tenant data @@ -524,6 +586,15 @@ func (r *distributionTenantResource) Update(ctx context.Context, req resource.Up return } + // Manually flatten domains and parameters + var d diag.Diagnostics + new.Domains, d = flattenDomains(ctx, getOutput.DistributionTenant.Domains) + resp.Diagnostics.Append(d...) + new.Parameters, d = flattenParameters(ctx, getOutput.DistributionTenant.Parameters) + resp.Diagnostics.Append(d...) + new.Customizations, d = flattenCustomizations(ctx, getOutput.DistributionTenant.Customizations) + resp.Diagnostics.Append(d...) + new.ETag = fwflex.StringToFramework(ctx, getOutput.ETag) } @@ -960,3 +1031,204 @@ func convertDomainResultsToDomainItems(domainResults []awstypes.DomainResult) [] return domainItems } + +// Implement fwflex.Flattener interface methods +func (m *customizationsModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.Customizations); ok { + if t.GeoRestrictions != nil { + var geoModel geoRestrictionCustomizationModel + diags.Append(geoModel.Flatten(ctx, t.GeoRestrictions)...) + if diags.HasError() { + return diags + } + geoList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &geoModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.GeoRestriction = geoList + } else { + m.GeoRestriction = fwtypes.NewListNestedObjectValueOfNull[geoRestrictionCustomizationModel](ctx) + } + + if t.Certificate != nil { + var certModel certificateModel + diags.Append(certModel.Flatten(ctx, t.Certificate)...) + if diags.HasError() { + return diags + } + certList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &certModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.Certificate = certList + } else { + m.Certificate = fwtypes.NewListNestedObjectValueOfNull[certificateModel](ctx) + } + + if t.WebAcl != nil { + var webAclModel webAclCustomizationModel + diags.Append(webAclModel.Flatten(ctx, t.WebAcl)...) + if diags.HasError() { + return diags + } + webAclList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &webAclModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.WebAcl = webAclList + } else { + m.WebAcl = fwtypes.NewListNestedObjectValueOfNull[webAclCustomizationModel](ctx) + } + } + + return diags +} + +func (m *geoRestrictionCustomizationModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.GeoRestrictionCustomization); ok { + m.RestrictionType = fwtypes.StringEnumValue(t.RestrictionType) + + // Convert locations slice to SetOfString + if len(t.Locations) > 0 { + // Filter out empty strings + filteredLocations := make([]string, 0, len(t.Locations)) + for _, location := range t.Locations { + if location != "" { + filteredLocations = append(filteredLocations, location) + } + } + + if len(filteredLocations) > 0 { + // Convert strings to attr.Value slice + elements := make([]attr.Value, len(filteredLocations)) + for i, location := range filteredLocations { + elements[i] = basetypes.NewStringValue(location) + } + setVal, d := fwtypes.NewSetValueOf[basetypes.StringValue](ctx, elements) + diags.Append(d...) + m.Locations = setVal + } else { + m.Locations = fwtypes.NewSetValueOfNull[basetypes.StringValue](ctx) + } + } else { + m.Locations = fwtypes.NewSetValueOfNull[basetypes.StringValue](ctx) + } + } + + return diags +} + +func (m *certificateModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.Certificate); ok { + m.ARN = fwtypes.ARNValue(aws.ToString(t.Arn)) + } + + return diags +} + +func (m *webAclCustomizationModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.WebAclCustomization); ok { + m.Action = fwtypes.StringEnumValue(t.Action) + m.ARN = fwtypes.ARNValue(aws.ToString(t.Arn)) + } + + return diags +} + +func (m *managedCertificateRequestModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.ManagedCertificateRequest); ok { + m.CertificateTransparencyLoggingPreference = fwtypes.StringEnumValue(t.CertificateTransparencyLoggingPreference) + m.PrimaryDomainName = fwflex.StringToFramework(ctx, t.PrimaryDomainName) + m.ValidationTokenHost = fwtypes.StringEnumValue(t.ValidationTokenHost) + } + + return diags +} + +// flattenDomains converts AWS SDK DomainResult slice to framework ListNestedObjectValueOf +func flattenDomains(ctx context.Context, domains []awstypes.DomainResult) (fwtypes.SetNestedObjectValueOf[domainItemModel], diag.Diagnostics) { + var diags diag.Diagnostics + + domainModels := make([]*domainItemModel, 0, len(domains)) + for _, domainResult := range domains { + domainModels = append(domainModels, &domainItemModel{ + Domain: fwflex.StringToFramework(ctx, domainResult.Domain), + }) + } + + domainsSet, d := fwtypes.NewSetNestedObjectValueOfSlice(ctx, domainModels, nil) + diags.Append(d...) + + return domainsSet, diags +} + +// flattenParameters converts AWS SDK Parameter slice to framework ListNestedObjectValueOf +func flattenParameters(ctx context.Context, parameters []awstypes.Parameter) (fwtypes.SetNestedObjectValueOf[parameterModel], diag.Diagnostics) { + var diags diag.Diagnostics + + parameterModels := make([]*parameterModel, 0, len(parameters)) + for _, param := range parameters { + parameterModels = append(parameterModels, ¶meterModel{ + Name: fwflex.StringToFramework(ctx, param.Name), + Value: fwflex.StringToFramework(ctx, param.Value), + }) + } + + parametersSet, d := fwtypes.NewSetNestedObjectValueOfSlice(ctx, parameterModels, nil) + diags.Append(d...) + + return parametersSet, diags +} + +// flattenCustomizations converts AWS SDK Customizations to framework ListNestedObjectValueOf +func flattenCustomizations(ctx context.Context, customizations *awstypes.Customizations) (fwtypes.ListNestedObjectValueOf[customizationsModel], diag.Diagnostics) { + var diags diag.Diagnostics + + if customizations == nil { + return fwtypes.NewListNestedObjectValueOfNull[customizationsModel](ctx), diags + } + + var customModel customizationsModel + diags.Append(customModel.Flatten(ctx, customizations)...) + if diags.HasError() { + return fwtypes.NewListNestedObjectValueOfNull[customizationsModel](ctx), diags + } + + customList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &customModel) + diags.Append(d...) + + return customList, diags +} diff --git a/internal/service/cloudfront/distribution_tenant_data_source.go b/internal/service/cloudfront/distribution_tenant_data_source.go index eba55f43c042..a3774cda8664 100644 --- a/internal/service/cloudfront/distribution_tenant_data_source.go +++ b/internal/service/cloudfront/distribution_tenant_data_source.go @@ -6,14 +6,18 @@ package cloudfront import ( "context" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudfront" awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/framework" fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" @@ -54,10 +58,6 @@ func (d *distributionTenantDataSource) Schema(ctx context.Context, _ datasource. names.AttrDomain: schema.StringAttribute{ Optional: true, }, - "domains": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, - }, names.AttrEnabled: schema.BoolAttribute{ Computed: true, }, @@ -148,6 +148,16 @@ func (d *distributionTenantDataSource) Schema(ctx context.Context, _ datasource. }, }, }, + "domains": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[domainItemDataSourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, names.AttrParameters: schema.SetNestedBlock{ CustomType: fwtypes.NewSetNestedObjectTypeOf[parameterDataSourceModel](ctx), NestedObject: schema.NestedBlockObject{ @@ -174,7 +184,7 @@ func (d *distributionTenantDataSource) Read(ctx context.Context, request datasou conn := d.Meta().CloudFrontClient(ctx) - // Define lookup strategies in order of preference + // Define lookup strategies using config values lookupStrategies := []struct { value types.String fn func(context.Context, *cloudfront.Client, string) (interface{}, error) @@ -230,6 +240,15 @@ func (d *distributionTenantDataSource) Read(ctx context.Context, request datasou return } + // Manually flatten domains and parameters using helper functions from resource file + var diags diag.Diagnostics + data.Domains, diags = flattenDomainsDataSource(ctx, tenant.Domains) + response.Diagnostics.Append(diags...) + data.Parameters, diags = flattenParametersDataSource(ctx, tenant.Parameters) + response.Diagnostics.Append(diags...) + data.Customizations, diags = flattenCustomizationsDataSource(ctx, tenant.Customizations) + response.Diagnostics.Append(diags...) + // Set computed fields that need special handling data.ID = fwflex.StringToFramework(ctx, tenant.Id) data.ETag = fwflex.StringToFramework(ctx, etag) @@ -246,7 +265,7 @@ type distributionTenantDataSourceModel struct { Customizations fwtypes.ListNestedObjectValueOf[customizationsDataSourceModel] `tfsdk:"customizations"` DistributionID types.String `tfsdk:"distribution_id"` Domain types.String `tfsdk:"domain"` - Domains fwtypes.SetValueOf[types.String] `tfsdk:"domains"` + Domains fwtypes.SetNestedObjectValueOf[domainItemDataSourceModel] `tfsdk:"domains"` Enabled types.Bool `tfsdk:"enabled"` ETag types.String `tfsdk:"etag"` ID types.String `tfsdk:"id"` @@ -264,8 +283,23 @@ type customizationsDataSourceModel struct { WebAcl fwtypes.ListNestedObjectValueOf[webAclDataSourceModel] `tfsdk:"web_acl"` } +// Implement fwflex.Flattener interface +var ( + _ fwflex.Flattener = &customizationsDataSourceModel{} + _ fwflex.Flattener = &geoRestrictionDataSourceModel{} + _ fwflex.Flattener = &certificateDataSourceModel{} + _ fwflex.Flattener = &webAclDataSourceModel{} + _ fwflex.Flattener = &managedCertificateRequestDataSourceModel{} + _ fwflex.Flattener = ¶meterDataSourceModel{} + _ fwflex.Flattener = &domainItemDataSourceModel{} +) + +type domainItemDataSourceModel struct { + Domain types.String `tfsdk:"domain"` +} + type geoRestrictionDataSourceModel struct { - Locations fwtypes.SetValueOf[types.String] `tfsdk:"locations"` + Locations fwtypes.SetOfString `tfsdk:"locations"` RestrictionType fwtypes.StringEnum[awstypes.GeoRestrictionType] `tfsdk:"restriction_type"` } @@ -315,3 +349,233 @@ func findDistributionTenantByDomain(ctx context.Context, conn *cloudfront.Client return output, nil } + +// Implement fwflex.Flattener interface methods +func (m *customizationsDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.Customizations); ok { + if t.GeoRestrictions != nil { + var geoModel geoRestrictionDataSourceModel + diags.Append(geoModel.Flatten(ctx, t.GeoRestrictions)...) + if diags.HasError() { + return diags + } + geoList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &geoModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.GeoRestriction = geoList + } else { + m.GeoRestriction = fwtypes.NewListNestedObjectValueOfNull[geoRestrictionDataSourceModel](ctx) + } + + if t.Certificate != nil { + var certModel certificateDataSourceModel + diags.Append(certModel.Flatten(ctx, t.Certificate)...) + if diags.HasError() { + return diags + } + certList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &certModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.Certificate = certList + } else { + m.Certificate = fwtypes.NewListNestedObjectValueOfNull[certificateDataSourceModel](ctx) + } + + if t.WebAcl != nil { + var webAclModel webAclDataSourceModel + diags.Append(webAclModel.Flatten(ctx, t.WebAcl)...) + if diags.HasError() { + return diags + } + webAclList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &webAclModel) + diags.Append(d...) + if diags.HasError() { + return diags + } + m.WebAcl = webAclList + } else { + m.WebAcl = fwtypes.NewListNestedObjectValueOfNull[webAclDataSourceModel](ctx) + } + } + + return diags +} + +func (m *geoRestrictionDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.GeoRestrictionCustomization); ok { + m.RestrictionType = fwtypes.StringEnumValue(t.RestrictionType) + + // Convert locations slice to SetOfString + if len(t.Locations) > 0 { + // Filter out empty strings + filteredLocations := make([]string, 0, len(t.Locations)) + for _, location := range t.Locations { + if location != "" { + filteredLocations = append(filteredLocations, location) + } + } + + if len(filteredLocations) > 0 { + // Convert strings to attr.Value slice + elements := make([]attr.Value, len(filteredLocations)) + for i, location := range filteredLocations { + elements[i] = basetypes.NewStringValue(location) + } + setVal, d := fwtypes.NewSetValueOf[basetypes.StringValue](ctx, elements) + diags.Append(d...) + m.Locations = setVal + } else { + m.Locations = fwtypes.NewSetValueOfNull[basetypes.StringValue](ctx) + } + } else { + m.Locations = fwtypes.NewSetValueOfNull[basetypes.StringValue](ctx) + } + } + + return diags +} + +func (m *certificateDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.Certificate); ok { + m.ARN = fwtypes.ARNValue(aws.ToString(t.Arn)) + } + + return diags +} + +func (m *webAclDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.WebAclCustomization); ok { + m.Action = fwtypes.StringEnumValue(t.Action) + m.ARN = fwtypes.ARNValue(aws.ToString(t.Arn)) + } + + return diags +} + +func (m *managedCertificateRequestDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.ManagedCertificateRequest); ok { + m.CertificateTransparencyLoggingPreference = fwtypes.StringEnumValue(t.CertificateTransparencyLoggingPreference) + m.PrimaryDomainName = fwflex.StringToFramework(ctx, t.PrimaryDomainName) + m.ValidationTokenHost = fwtypes.StringEnumValue(t.ValidationTokenHost) + } + + return diags +} + +func (m *parameterDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.Parameter); ok { + m.Name = fwflex.StringToFramework(ctx, t.Name) + m.Value = fwflex.StringToFramework(ctx, t.Value) + } + + return diags +} + +func (m *domainItemDataSourceModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + if v == nil { + return diags + } + + if t, ok := v.(*awstypes.DomainResult); ok { + m.Domain = fwflex.StringToFramework(ctx, t.Domain) + } + + return diags +} + +// flattenDomainsDataSource converts AWS SDK DomainResult slice to framework ListNestedObjectValueOf for data source +func flattenDomainsDataSource(ctx context.Context, domains []awstypes.DomainResult) (fwtypes.SetNestedObjectValueOf[domainItemDataSourceModel], diag.Diagnostics) { + var diags diag.Diagnostics + + domainModels := make([]*domainItemDataSourceModel, 0, len(domains)) + for _, domainResult := range domains { + domainModels = append(domainModels, &domainItemDataSourceModel{ + Domain: fwflex.StringToFramework(ctx, domainResult.Domain), + }) + } + + domainsSet, d := fwtypes.NewSetNestedObjectValueOfSlice(ctx, domainModels, nil) + diags.Append(d...) + + return domainsSet, diags +} + +// flattenParametersDataSource converts AWS SDK Parameter slice to framework ListNestedObjectValueOf for data source +func flattenParametersDataSource(ctx context.Context, parameters []awstypes.Parameter) (fwtypes.SetNestedObjectValueOf[parameterDataSourceModel], diag.Diagnostics) { + var diags diag.Diagnostics + + parameterModels := make([]*parameterDataSourceModel, 0, len(parameters)) + for _, param := range parameters { + parameterModels = append(parameterModels, ¶meterDataSourceModel{ + Name: fwflex.StringToFramework(ctx, param.Name), + Value: fwflex.StringToFramework(ctx, param.Value), + }) + } + + parametersSet, d := fwtypes.NewSetNestedObjectValueOfSlice(ctx, parameterModels, nil) + diags.Append(d...) + + return parametersSet, diags +} + +// flattenCustomizationsDataSource converts AWS SDK Customizations to framework ListNestedObjectValueOf for data source +func flattenCustomizationsDataSource(ctx context.Context, customizations *awstypes.Customizations) (fwtypes.ListNestedObjectValueOf[customizationsDataSourceModel], diag.Diagnostics) { + var diags diag.Diagnostics + + if customizations == nil { + return fwtypes.NewListNestedObjectValueOfNull[customizationsDataSourceModel](ctx), diags + } + + var customModel customizationsDataSourceModel + diags.Append(customModel.Flatten(ctx, customizations)...) + if diags.HasError() { + return fwtypes.NewListNestedObjectValueOfNull[customizationsDataSourceModel](ctx), diags + } + + customList, d := fwtypes.NewListNestedObjectValueOfPtr(ctx, &customModel) + diags.Append(d...) + + return customList, diags +} diff --git a/internal/service/cloudfront/distribution_tenant_data_source_test.go b/internal/service/cloudfront/distribution_tenant_data_source_test.go index 535f5ec002f7..da860b1b531b 100644 --- a/internal/service/cloudfront/distribution_tenant_data_source_test.go +++ b/internal/service/cloudfront/distribution_tenant_data_source_test.go @@ -28,7 +28,7 @@ func TestAccCloudFrontDistributionTenantDataSource_basic(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDistributionTenantDataSourceConfig_basic(rName), + Config: testAccDistributionTenantDataSourceConfig_basic(t, rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), @@ -41,7 +41,7 @@ func TestAccCloudFrontDistributionTenantDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), - resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + resource.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), ), }, }, @@ -63,7 +63,7 @@ func TestAccCloudFrontDistributionTenantDataSource_byARN(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDistributionTenantDataSourceConfig_byARN(rName), + Config: testAccDistributionTenantDataSourceConfig_byARN(t, rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), @@ -76,7 +76,7 @@ func TestAccCloudFrontDistributionTenantDataSource_byARN(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), - resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + resource.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), ), }, }, @@ -98,7 +98,7 @@ func TestAccCloudFrontDistributionTenantDataSource_byName(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDistributionTenantDataSourceConfig_byName(rName), + Config: testAccDistributionTenantDataSourceConfig_byName(t, rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), @@ -111,7 +111,7 @@ func TestAccCloudFrontDistributionTenantDataSource_byName(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), - resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + resource.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), ), }, }, @@ -133,7 +133,7 @@ func TestAccCloudFrontDistributionTenantDataSource_byDomain(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccDistributionTenantDataSourceConfig_byDomain(rName), + Config: testAccDistributionTenantDataSourceConfig_byDomain(t, rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN), resource.TestCheckResourceAttrPair(dataSourceName, "connection_group_id", resourceName, "connection_group_id"), @@ -146,26 +146,25 @@ func TestAccCloudFrontDistributionTenantDataSource_byDomain(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "managed_certificate_request.#", resourceName, "managed_certificate_request.#"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrName, resourceName, names.AttrName), resource.TestCheckResourceAttrPair(dataSourceName, "parameters.#", resourceName, "parameters.#"), - resource.TestCheckResourceAttrPair(dataSourceName, names.AttrStatus, resourceName, names.AttrStatus), + resource.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), ), }, }, }) } -func testAccDistributionTenantDataSourceConfig_basic(rName string) string { +func testAccDistributionTenantDataSourceConfig_basic(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } -resource "aws_cloudfront_connection_group" "test" { - name = %[1]q -} - resource "aws_cloudfront_cache_policy" "test" { name = %[1]q comment = "test tenant cache policy" @@ -223,31 +222,31 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] + domains { + domain = %[2]q + } name = %[1]q enabled = false - connection_group_id = aws_cloudfront_connection_group.test.id } data "aws_cloudfront_distribution_tenant" "test" { id = aws_cloudfront_distribution_tenant.test.id } -`, rName)) +`, rName, tenantDomain, certDomain)) } -func testAccDistributionTenantDataSourceConfig_byARN(rName string) string { +func testAccDistributionTenantDataSourceConfig_byARN(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } -resource "aws_cloudfront_connection_group" "test" { - name = %[1]q -} - resource "aws_cloudfront_cache_policy" "test" { name = %[1]q comment = "test tenant cache policy" @@ -305,31 +304,31 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] + domains { + domain = %[2]q + } name = %[1]q enabled = false - connection_group_id = aws_cloudfront_connection_group.test.id } data "aws_cloudfront_distribution_tenant" "test" { arn = aws_cloudfront_distribution_tenant.test.arn } -`, rName)) +`, rName, tenantDomain, certDomain)) } -func testAccDistributionTenantDataSourceConfig_byName(rName string) string { +func testAccDistributionTenantDataSourceConfig_byName(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } -resource "aws_cloudfront_connection_group" "test" { - name = %[1]q -} - resource "aws_cloudfront_cache_policy" "test" { name = %[1]q comment = "test tenant cache policy" @@ -387,31 +386,31 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] + domains { + domain = %[2]q + } name = %[1]q enabled = false - connection_group_id = aws_cloudfront_connection_group.test.id } data "aws_cloudfront_distribution_tenant" "test" { name = aws_cloudfront_distribution_tenant.test.name } -`, rName)) +`, rName, tenantDomain, certDomain)) } -func testAccDistributionTenantDataSourceConfig_byDomain(rName string) string { +func testAccDistributionTenantDataSourceConfig_byDomain(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } -resource "aws_cloudfront_connection_group" "test" { - name = %[1]q -} - resource "aws_cloudfront_cache_policy" "test" { name = %[1]q comment = "test tenant cache policy" @@ -469,14 +468,17 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example-%[1]s.com"] + domains { + domain = %[2]q + } name = %[1]q enabled = false - connection_group_id = aws_cloudfront_connection_group.test.id } data "aws_cloudfront_distribution_tenant" "test" { - domain = "example-%[1]s.com" + domain = %[2]q + + depends_on = [aws_cloudfront_distribution_tenant.test] } -`, rName)) +`, rName, tenantDomain, certDomain)) } diff --git a/internal/service/cloudfront/distribution_tenant_test.go b/internal/service/cloudfront/distribution_tenant_test.go index d28e1ab4b6de..73dce7083a24 100644 --- a/internal/service/cloudfront/distribution_tenant_test.go +++ b/internal/service/cloudfront/distribution_tenant_test.go @@ -36,7 +36,7 @@ func TestAccCloudFrontDistributionTenant_basic(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_basic(rName), + Config: testAccDistributionTenantConfig_basic(t, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), @@ -79,7 +79,7 @@ func TestAccCloudFrontDistributionTenant_disappears(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_basic(rName), + Config: testAccDistributionTenantConfig_basic(t, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfcloudfront.ResourceDistributionTenant, resourceName), @@ -106,7 +106,7 @@ func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_customCertificate(rName), + Config: testAccDistributionTenantConfig_customCertificate(t, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), @@ -117,6 +117,16 @@ func TestAccCloudFrontDistributionTenant_customCertificate(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "customizations.0.certificate.0.arn", "data.aws_acm_certificate.test", names.AttrARN), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + "status", + }, + }, }, }) } @@ -137,7 +147,7 @@ func TestAccCloudFrontDistributionTenant_customCertificateWithWebACL(t *testing. CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_customCertificateWithWebACL(rName), + Config: testAccDistributionTenantConfig_customCertificateWithWebACL(t, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "customizations.#", "1"), @@ -166,7 +176,7 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_parameters(rName), + Config: testAccDistributionTenantConfig_parameters(t, rName), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, "parameters.#", "2"), @@ -183,42 +193,13 @@ func TestAccCloudFrontDistributionTenant_parameters(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "wait_for_deployment", + "status", }, }, }, }) } -func TestAccCloudFrontDistributionTenant_managedCertificateRequest(t *testing.T) { - ctx := acctest.Context(t) - var tenant awstypes.DistributionTenant - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_cloudfront_distribution_tenant.test" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(ctx, t) - acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) - }, - ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), - Steps: []resource.TestStep{ - { - Config: testAccDistributionTenantConfig_managedCertificateRequest(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), - resource.TestCheckResourceAttrSet(resourceName, "connection_group_id"), - resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.#", "1"), - resource.TestCheckResourceAttrSet(resourceName, "managed_certificate_request.0.primary_domain_name"), - resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.0.validation_token_host", "cloudfront"), - resource.TestCheckResourceAttr(resourceName, "managed_certificate_request.0.certificate_transparency_logging_preference", "disabled"), - ), - }, - }, - }) -} - func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ctx := acctest.Context(t) var tenant awstypes.DistributionTenant @@ -235,7 +216,7 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { CheckDestroy: testAccCheckDistributionTenantDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDistributionTenantConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Config: testAccDistributionTenantConfig_tags1(t, rName, acctest.CtKey1, acctest.CtValue1), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), @@ -243,7 +224,7 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ), }, { - Config: testAccDistributionTenantConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Config: testAccDistributionTenantConfig_tags2(t, rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), @@ -252,13 +233,23 @@ func TestAccCloudFrontDistributionTenant_tags(t *testing.T) { ), }, { - Config: testAccDistributionTenantConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), + Config: testAccDistributionTenantConfig_tags1(t, rName, acctest.CtKey2, acctest.CtValue2), Check: resource.ComposeTestCheckFunc( testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + "status", + }, + }, }, }) } @@ -310,12 +301,13 @@ func testAccCheckDistributionTenantExists(ctx context.Context, n string, v *awst } } -func testAccDistributionTenantConfig_basic(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_basic(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = %[1]q + domain = %[3]q + region = "us-east-1" most_recent = true } @@ -376,18 +368,22 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[2]q + } + name = %[1]q + enabled = false } -`, rName)) +`, rName, tenantDomain, certDomain) } -func testAccDistributionTenantConfig_customCertificate(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_customCertificate(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` + data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } @@ -447,9 +443,11 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[2]q + } + name = %[1]q + enabled = false customizations { geo_restriction { @@ -462,22 +460,24 @@ resource "aws_cloudfront_distribution_tenant" "test" { } } } -`, rName)) +`, rName, tenantDomain, certDomain) } -func testAccDistributionTenantConfig_customCertificateWithWebACL(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_customCertificateWithWebACL(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } resource "aws_wafv2_web_acl" "test" { name = %[1]q - description = "test web acl" + description = "tftest" scope = "CLOUDFRONT" + region = "us-east-1" default_action { allow { @@ -553,9 +553,11 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[2]q + } + name = %[1]q + enabled = false customizations { geo_restriction { @@ -573,15 +575,16 @@ resource "aws_cloudfront_distribution_tenant" "test" { } } } -`, rName)) +`, rName, tenantDomain, certDomain) } -func testAccDistributionTenantConfig_parameters(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_parameters(t *testing.T, rName string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[3]q + region = "us-east-1" most_recent = true } @@ -642,9 +645,11 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[2]q + } + name = %[1]q + enabled = false parameters { name = "tenantid" @@ -656,15 +661,16 @@ resource "aws_cloudfront_distribution_tenant" "test" { value = "na" } } -`, rName)) +`, rName, tenantDomain, certDomain) } -func testAccDistributionTenantConfig_tags1(rName, tagKey1, tagValue1 string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_tags1(t *testing.T, rName, tagKey1, tagValue1 string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[5]q + region = "us-east-1" most_recent = true } @@ -725,23 +731,26 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[4]q + } + name = %[1]q + enabled = false tags = { %[2]q = %[3]q } } -`, rName, tagKey1, tagValue1)) +`, rName, tagKey1, tagValue1, tenantDomain, certDomain) } -func testAccDistributionTenantConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` +func testAccDistributionTenantConfig_tags2(t *testing.T, rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + certDomain := "*.tf." + acctest.ACMCertificateDomainFromEnv(t) + tenantDomain := rName + ".tf." + acctest.ACMCertificateDomainFromEnv(t) + return fmt.Sprintf(` data "aws_acm_certificate" "test" { - domain = "example.com" + domain = %[7]q + region = "us-east-1" most_recent = true } @@ -802,107 +811,16 @@ resource "aws_cloudfront_distribution" "test" { resource "aws_cloudfront_distribution_tenant" "test" { distribution_id = aws_cloudfront_distribution.test.id - domains = ["example.com"] - name = %[1]q - enabled = false + domains { + domain = %[6]q + } + name = %[1]q + enabled = false tags = { %[2]q = %[3]q %[4]q = %[5]q } } -`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) -} - -func testAccDistributionTenantConfig_managedCertificateRequest(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` -data "aws_route53_zone" "test" { - name = "example.com" - private_zone = false -} - -resource "aws_cloudfront_connection_group" "test" { - name = %[1]q -} - -resource "aws_route53_record" "test" { - zone_id = data.aws_route53_zone.test.id - type = "CNAME" - ttl = 300 - name = "tf.example.com" - records = [aws_cloudfront_connection_group.test.routing_endpoint] -} - -resource "aws_cloudfront_cache_policy" "test" { - name = %[1]q - comment = "test tenant cache policy" - default_ttl = 50 - max_ttl = 100 - min_ttl = 1 - parameters_in_cache_key_and_forwarded_to_origin { - cookies_config { - cookie_behavior = "none" - } - headers_config { - header_behavior = "none" - } - query_strings_config { - query_string_behavior = "none" - } - } -} - -resource "aws_cloudfront_distribution" "test" { - connection_mode = "tenant-only" - enabled = true - - origin { - domain_name = "www.example.com" - origin_id = "test" - - custom_origin_config { - http_port = 80 - https_port = 443 - origin_protocol_policy = "http-only" - origin_ssl_protocols = ["TLSv1.2"] - } - } - - default_cache_behavior { - allowed_methods = ["GET", "HEAD"] - cached_methods = ["GET", "HEAD"] - target_origin_id = "test" - viewer_protocol_policy = "allow-all" - cache_policy_id = aws_cloudfront_cache_policy.test.id - } - - restrictions { - geo_restriction { - restriction_type = "none" - } - } - - viewer_certificate { - cloudfront_default_certificate = true - } - - depends_on = [aws_route53_record.test] -} - -resource "aws_cloudfront_distribution_tenant" "test" { - distribution_id = aws_cloudfront_distribution.test.id - domains = ["tf.example.com"] - name = %[1]q - enabled = false - connection_group_id = aws_cloudfront_connection_group.test.id - - managed_certificate_request { - primary_domain_name = "tf.example.com" - validation_token_host = "cloudfront" - certificate_transparency_logging_preference = "disabled" - } -} -`, rName)) +`, rName, tagKey1, tagValue1, tagKey2, tagValue2, tenantDomain, certDomain) } From c990572c5af53fdcd90d339812b4325fb238529b Mon Sep 17 00:00:00 2001 From: Jason Kim Date: Fri, 14 Nov 2025 12:03:47 -0800 Subject: [PATCH 14/15] fix: add linter changes --- internal/service/cloudfront/distribution_tenant.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/service/cloudfront/distribution_tenant.go b/internal/service/cloudfront/distribution_tenant.go index b369683119d3..26a31f4278ec 100644 --- a/internal/service/cloudfront/distribution_tenant.go +++ b/internal/service/cloudfront/distribution_tenant.go @@ -49,7 +49,8 @@ func newDistributionTenantResource(context.Context) (resource.ResourceWithConfig } const ( - ResNameDistributionTenant = "Distribution Tenant" + ResNameDistributionTenant = "Distribution Tenant" + distributionTenantPollInterval = 30 * time.Second ) type distributionTenantResource struct { @@ -641,15 +642,13 @@ func (r *distributionTenantResource) Delete(ctx context.Context, req resource.De return } - const timeout = 30 * time.Second - _, err = tfresource.RetryWhenIsA[any, *awstypes.ResourceNotDisabled](ctx, timeout, func(ctx context.Context) (any, error) { + _, err = tfresource.RetryWhenIsA[any, *awstypes.ResourceNotDisabled](ctx, distributionTenantPollInterval, func(ctx context.Context) (any, error) { return nil, deleteDistributionTenant(ctx, conn, id) }) } if errs.IsA[*awstypes.PreconditionFailed](err) || errs.IsA[*awstypes.InvalidIfMatchVersion](err) { - const timeout = 30 * time.Second - _, err = tfresource.RetryWhenIsOneOf2[any, *awstypes.PreconditionFailed, *awstypes.InvalidIfMatchVersion](ctx, timeout, func(ctx context.Context) (any, error) { + _, err = tfresource.RetryWhenIsOneOf2[any, *awstypes.PreconditionFailed, *awstypes.InvalidIfMatchVersion](ctx, distributionTenantPollInterval, func(ctx context.Context) (any, error) { return nil, deleteDistributionTenant(ctx, conn, id) }) } From 04e1b804d2b9b1d4cb3d8df6fa29e68fe09af4ac Mon Sep 17 00:00:00 2001 From: Jason Kim Date: Fri, 14 Nov 2025 15:10:44 -0800 Subject: [PATCH 15/15] update change log --- .changelog/44181.txt | 3 --- .changelog/{44183.txt => 45088.txt} | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 .changelog/44181.txt rename .changelog/{44183.txt => 45088.txt} (50%) diff --git a/.changelog/44181.txt b/.changelog/44181.txt deleted file mode 100644 index a89fe4cfb97e..000000000000 --- a/.changelog/44181.txt +++ /dev/null @@ -1,3 +0,0 @@ -```release-note:new-resource -aws_cloudfront_distribution_tenant -``` \ No newline at end of file diff --git a/.changelog/44183.txt b/.changelog/45088.txt similarity index 50% rename from .changelog/44183.txt rename to .changelog/45088.txt index 62eba15aa10d..79bf91f96e16 100644 --- a/.changelog/44183.txt +++ b/.changelog/45088.txt @@ -1,3 +1,7 @@ +```release-note:new-resource +aws_cloudfront_distribution_tenant +``` + ```release-note:new-data-source aws_cloudfront_distribution_tenant ``` \ No newline at end of file