diff --git a/.changelog/45088.txt b/.changelog/45088.txt new file mode 100644 index 000000000000..79bf91f96e16 --- /dev/null +++ b/.changelog/45088.txt @@ -0,0 +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 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..26a31f4278ec --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant.go @@ -0,0 +1,1233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront + +import ( + "context" + "fmt" + "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-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/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" + "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-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" + "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/names" +) + +// @FrameworkResource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") +// @Tags(identifierAttribute="arn") +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" + distributionTenantPollInterval = 30 * time.Second +) + +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, + }, + "distribution_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + 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, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + }, + names.AttrStatus: schema.StringAttribute{ + Computed: true, + }, + "wait_for_deployment": schema.BoolAttribute{ + Optional: true, + 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[geoRestrictionCustomizationModel](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": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.GeoRestrictionType](), + }, + }, + }, + }, + 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": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[webAclCustomizationModel](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: schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.ARNType, + }, + }, + }, + }, + }, + }, + }, + "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": schema.StringAttribute{ + Optional: true, + }, + "validation_token_host": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.StringEnumType[awstypes.ValidationTokenHost](), + }, + }, + }, + }, + "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{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ + Required: true, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} + +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.SetNestedObjectValueOf[domainItemModel] `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"` +} + +type customizationsModel struct { + 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 geoRestrictionCustomizationModel struct { + Locations fwtypes.SetOfString `tfsdk:"locations"` + RestrictionType fwtypes.StringEnum[awstypes.GeoRestrictionType] `tfsdk:"restriction_type"` +} + +type certificateModel struct { + ARN fwtypes.ARN `tfsdk:"arn"` +} + +type webAclCustomizationModel 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 + } + + conn := r.Meta().CloudFrontClient(ctx) + + input := &cloudfront.CreateDistributionTenantInput{} + resp.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if resp.Diagnostics.HasError() { + return + } + + if tags := getTagsIn(ctx); len(tags) > 0 { + input.Tags = &awstypes.Tags{ + Items: tags, + } + } + + 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 + } + + // Use create response directly - no extra read needed + resp.Diagnostics.Append(fwflex.Flatten(ctx, output.DistributionTenant, &data)...) + if resp.Diagnostics.HasError() { + 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) + data.ARN = fwflex.StringToFramework(ctx, output.DistributionTenant.Arn) + data.ETag = fwflex.StringToFramework(ctx, output.ETag) + + 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 + } + + // 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 + } + + // Update the data model with refreshed information + resp.Diagnostics.Append(fwflex.Flatten(ctx, refreshedOutput.DistributionTenant, &data)...) + if resp.Diagnostics.HasError() { + 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) + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +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 + } + + conn := r.Meta().CloudFrontClient(ctx) + + output, err := findDistributionTenantByIdentifier(ctx, conn, data.ID.ValueString()) + if ret.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionReading, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + tenant := output.DistributionTenant + + // Flatten the distribution tenant data into the model + resp.Diagnostics.Append(fwflex.Flatten(ctx, tenant, &data)...) + if resp.Diagnostics.HasError() { + 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) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +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 + } + + 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 + } + + // 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, new.ID.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionUpdating, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + input.IfMatch = aws.String(etag) + output, err = conn.UpdateDistributionTenant(ctx, input) + } + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionUpdating, ResNameDistributionTenant, new.ID.String(), err), + err.Error(), + ) + return + } + + 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 + } + + // 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) + } + } + + new.LastModifiedTime = fwflex.TimeToFramework(ctx, output.DistributionTenant.LastModifiedTime) + } else { + new.LastModifiedTime = old.LastModifiedTime + } + + // 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 + } + + // 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 + // 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 + } + + // 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) + } + + 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 + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + err := deleteDistributionTenant(ctx, conn, id) + + if err == nil || ret.NotFound(err) || errs.IsA[*awstypes.EntityNotFound](err) { + return + } + + // Disable distribution tenant if it is not yet disabled and attempt deletion again. + if errs.IsA[*awstypes.ResourceNotDisabled](err) { + if err := disableDistributionTenant(ctx, conn, id); err != nil { + if ret.NotFound(err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + _, 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) { + _, err = tfresource.RetryWhenIsOneOf2[any, *awstypes.PreconditionFailed, *awstypes.InvalidIfMatchVersion](ctx, distributionTenantPollInterval, func(ctx context.Context) (any, error) { + return nil, deleteDistributionTenant(ctx, conn, id) + }) + } + + if errs.IsA[*awstypes.ResourceNotDisabled](err) { + if err := disableDistributionTenant(ctx, conn, id); err != nil { + if ret.NotFound(err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } + + err = deleteDistributionTenant(ctx, conn, id) + } + + if errs.IsA[*awstypes.EntityNotFound](err) { + return + } + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.CloudFront, create.ErrActionDeleting, ResNameDistributionTenant, data.ID.String(), err), + err.Error(), + ) + return + } +} +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) + + 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 := findDistributionTenantByIdentifier(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 Tenant (%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 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 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, + } + + 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: 30 * 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 := findDistributionTenantByIdentifier(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 distributionTenantETag(ctx context.Context, conn *cloudfront.Client, id string) (string, error) { + output, err := findDistributionTenantByIdentifier(ctx, conn, id) + + if err != nil { + return "", fmt.Errorf("reading CloudFront Distribution Tenant (%s): %w", id, err) + } + + return aws.ToString(output.ETag), 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 + } + + // 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) + } + + // 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) + } + + // 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) + } + + // Step 3: Update distribution tenant with the issued certificate + return updateDistributionTenantWithManagedCertificate(ctx, conn, dtOutput, mcOutput) +} + +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) + } + + if aws.ToString(dtOutput.DistributionTenant.Status) == distributionTenantStatusDeployed { + return dtOutput, nil + } + + time.Sleep(30 * time.Second) + } +} + +func waitForDNSConfiguration(ctx context.Context, conn *cloudfront.Client, dtOutput *cloudfront.GetDistributionTenantOutput) error { + timeout := 15 * time.Minute + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + if err := verifyDNSConfiguration(ctx, conn, dtOutput); err == nil { + return nil // DNS is valid + } + time.Sleep(30 * time.Second) + } + + return fmt.Errorf("CloudFront Distribution Tenant (%s) timeout after 15 minutes waiting for expected DNS configuration", aws.ToString(dtOutput.DistributionTenant.Id)) +} + +func waitForManagedCertificateIssued(ctx context.Context, conn *cloudfront.Client, id string) (*cloudfront.GetManagedCertificateDetailsOutput, error) { + timeout := 3 * time.Hour + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + mcInput := &cloudfront.GetManagedCertificateDetailsInput{ + Identifier: aws.String(id), + } + + 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) + } + + // Check certificate status + switch mcOutput.ManagedCertificateDetails.CertificateStatus { + case awstypes.ManagedCertificateStatusIssued: + return mcOutput, nil + + case awstypes.ManagedCertificateStatusPendingValidation: + // Certificate still being validated, continue waiting + time.Sleep(1 * time.Minute) // Longer sleep for certificate issuance + continue + + default: + return nil, fmt.Errorf("CloudFront Distribution Tenant (%s) managed certificate failed with status: %s", id, mcOutput.ManagedCertificateDetails.CertificateStatus) + } + } + + return nil, fmt.Errorf("CloudFront Distribution Tenant (%s) timeout after 3 hours waiting for managed certificate to be issued", id) +} + +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 + } + + // 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) + } + + // 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, + }, + }, + } + + // 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 + + _, 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) + } + + // 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 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, + } + + 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 nil +} + +func convertDomainResultsToDomainItems(domainResults []awstypes.DomainResult) []awstypes.DomainItem { + if len(domainResults) == 0 { + return nil + } + + domainItems := make([]awstypes.DomainItem, len(domainResults)) + for i, domainResult := range domainResults { + domainItems[i] = awstypes.DomainItem{ + Domain: domainResult.Domain, + } + } + + 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 new file mode 100644 index 000000000000..a3774cda8664 --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_data_source.go @@ -0,0 +1,581 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" + 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" +) + +// @FrameworkDataSource("aws_cloudfront_distribution_tenant", name="Distribution Tenant") +// @Tags(identifierAttribute="arn") +func newDistributionTenantDataSource(_ context.Context) (datasource.DataSourceWithConfigure, error) { + d := &distributionTenantDataSource{} + return d, nil +} + +const ( + DSNameDistributionTenant = "Distribution Tenant Data Source" +) + +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": schema.StringAttribute{ + Computed: true, + }, + "distribution_id": schema.StringAttribute{ + Computed: true, + }, + names.AttrDomain: schema.StringAttribute{ + Optional: true, + }, + names.AttrEnabled: schema.BoolAttribute{ + Computed: true, + }, + "etag": schema.StringAttribute{ + Computed: true, + }, + names.AttrID: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "last_modified_time": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + names.AttrName: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + names.AttrStatus: schema.StringAttribute{ + 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](), + }, + }, + }, + }, + "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{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ + Computed: true, + }, + names.AttrValue: schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + } +} + +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 using config values + 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 + + // 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 + } + + // Flatten the distribution tenant data into the model + response.Diagnostics.Append(fwflex.Flatten(ctx, tenant, &data)...) + if response.Diagnostics.HasError() { + 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) + if tenant.LastModifiedTime != nil { + data.LastModifiedTime = fwflex.TimeToFramework(ctx, tenant.LastModifiedTime) + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +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.SetNestedObjectValueOf[domainItemDataSourceModel] `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"` +} + +type customizationsDataSourceModel struct { + GeoRestriction fwtypes.ListNestedObjectValueOf[geoRestrictionDataSourceModel] `tfsdk:"geo_restriction"` + Certificate fwtypes.ListNestedObjectValueOf[certificateDataSourceModel] `tfsdk:"certificate"` + 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.SetOfString `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 { + return nil, tfresource.NewEmptyResultError(input) + } + + 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 new file mode 100644 index 000000000000..da860b1b531b --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_data_source_test.go @@ -0,0 +1,484 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +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) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + 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"), + 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.TestCheckResourceAttrSet(dataSourceName, 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(t, 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.TestCheckResourceAttrSet(dataSourceName, 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_byName(t, 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.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), + ), + }, + }, + }) +} + +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(t, 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.TestCheckResourceAttrSet(dataSourceName, names.AttrStatus), + ), + }, + }, + }) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false +} + +data "aws_cloudfront_distribution_tenant" "test" { + id = aws_cloudfront_distribution_tenant.test.id +} +`, rName, tenantDomain, certDomain)) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false +} + +data "aws_cloudfront_distribution_tenant" "test" { + arn = aws_cloudfront_distribution_tenant.test.arn +} +`, rName, tenantDomain, certDomain)) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false +} + +data "aws_cloudfront_distribution_tenant" "test" { + name = aws_cloudfront_distribution_tenant.test.name +} +`, rName, tenantDomain, certDomain)) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false +} + +data "aws_cloudfront_distribution_tenant" "test" { + domain = %[2]q + + depends_on = [aws_cloudfront_distribution_tenant.test] +} +`, rName, tenantDomain, certDomain)) +} diff --git a/internal/service/cloudfront/distribution_tenant_test.go b/internal/service/cloudfront/distribution_tenant_test.go new file mode 100644 index 000000000000..73dce7083a24 --- /dev/null +++ b/internal/service/cloudfront/distribution_tenant_test.go @@ -0,0 +1,826 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront_test + +import ( + "context" + "fmt" + "testing" + + "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" + "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 + 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_basic(t, 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.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), + ), + }, + { + 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{ + 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, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfcloudfront.ResourceDistributionTenant, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +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{ + 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, rName), + 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), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + "status", + }, + }, + }, + }) +} + +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{ + 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, 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.test", names.AttrARN), + ), + }, + }, + }) +} + +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{ + 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, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionTenantExists(ctx, resourceName, &tenant), + resource.TestCheckResourceAttr(resourceName, "parameters.#", "2"), + 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"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrName), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "wait_for_deployment", + "status", + }, + }, + }, + }) +} + +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{ + 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, rName, 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, rName, 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, 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", + }, + }, + }, + }) +} + +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.FindDistributionTenantByIdentifier(ctx, conn, rs.Primary.ID) + + if retry.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.FindDistributionTenantByIdentifier(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output.DistributionTenant + + return nil + } +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false +} +`, rName, tenantDomain, certDomain) +} +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains { + domain = %[2]q + } + name = %[1]q + enabled = false + + customizations { + geo_restriction { + locations = ["US", "CA"] + restriction_type = "whitelist" + } + + certificate { + arn = data.aws_acm_certificate.test.arn + } + } +} +`, rName, tenantDomain, certDomain) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +resource "aws_wafv2_web_acl" "test" { + name = %[1]q + description = "tftest" + scope = "CLOUDFRONT" + region = "us-east-1" + + 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" "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 + } +} + +resource "aws_cloudfront_distribution_tenant" "test" { + distribution_id = aws_cloudfront_distribution.test.id + domains { + domain = %[2]q + } + name = %[1]q + 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.test.arn + } + } +} +`, rName, tenantDomain, certDomain) +} + +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 = %[3]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[2]q + } + name = %[1]q + enabled = false + + parameters { + name = "tenantid" + value = "tenant-123" + } + + parameters { + name = "place" + value = "na" + } +} +`, rName, tenantDomain, certDomain) +} + +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 = %[5]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[4]q + } + name = %[1]q + enabled = false + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1, tenantDomain, certDomain) +} + +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 = %[7]q + region = "us-east-1" + most_recent = true +} + +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 { + domain = %[6]q + } + name = %[1]q + enabled = false + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2, tenantDomain, certDomain) +} diff --git a/internal/service/cloudfront/exports_test.go b/internal/service/cloudfront/exports_test.go index 2243d6a6f6fa..fd0680f17a9b 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 = newDistributionTenantResource ResourceFieldLevelEncryptionConfig = resourceFieldLevelEncryptionConfig ResourceFieldLevelEncryptionProfile = resourceFieldLevelEncryptionProfile ResourceFunction = resourceFunction @@ -23,6 +24,7 @@ var ( FindCachePolicyByID = findCachePolicyByID FindContinuousDeploymentPolicyByID = findContinuousDeploymentPolicyByID + 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 1db1f6705adc..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", 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. 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 +```