diff --git a/.changelog/47410.txt b/.changelog/47410.txt new file mode 100644 index 000000000000..bee256b959b6 --- /dev/null +++ b/.changelog/47410.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_redshift_custom_domain_association +``` diff --git a/internal/service/redshift/custom_domain_association.go b/internal/service/redshift/custom_domain_association.go new file mode 100644 index 000000000000..5e350ffc632a --- /dev/null +++ b/internal/service/redshift/custom_domain_association.go @@ -0,0 +1,376 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// DONOTCOPY: Copying old resources spreads bad habits. Use skaff instead. + +package redshift + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/redshift" + awstypes "github.com/aws/aws-sdk-go-v2/service/redshift/types" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + inttypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +var errCustomDomainAssociationNotUpdated = errors.New("redshift custom domain association not updated yet") + +// @FrameworkResource("aws_redshift_custom_domain_association", name="Custom Domain Association") +// @IdentityAttribute("cluster_identifier") +// @IdentityAttribute("custom_domain_name") +// @ImportIDHandler("customDomainAssociationImportID", setIDAttribute=true) +// @Testing(hasNoPreExistingResource=true) +// @Testing(preCheck="testAccPreCheckCustomDomainAssociation") +// @Testing(requireEnvVarValue="ACM_CERTIFICATE_ROOT_DOMAIN") +// @Testing(importStateIdAttributes="cluster_identifier;custom_domain_name", importStateIdAttributesSep="flex.ResourceIdSeparator") +func newCustomDomainAssociationResource(context.Context) (resource.ResourceWithConfigure, error) { + return &customDomainAssociationResource{}, nil +} + +type customDomainAssociationResource struct { + framework.ResourceWithModel[customDomainAssociationResourceModel] + framework.WithImportByIdentity +} + +func (r *customDomainAssociationResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrClusterIdentifier: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "custom_domain_certificate_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + }, + "custom_domain_certificate_expiry_time": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + "custom_domain_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 253), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrID: framework.IDAttribute(), + }, + } +} + +func (r *customDomainAssociationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data customDomainAssociationResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().RedshiftClient(ctx) + + _, err := conn.CreateCustomDomainAssociation(ctx, &redshift.CreateCustomDomainAssociationInput{ + ClusterIdentifier: data.ClusterIdentifier.ValueStringPointer(), + CustomDomainCertificateArn: data.CustomDomainCertificateARN.ValueStringPointer(), + CustomDomainName: data.CustomDomainName.ValueStringPointer(), + }) + if err != nil { + response.Diagnostics.AddError("creating Redshift Custom Domain Association", err.Error()) + return + } + + id, err := data.setID() + if err != nil { + response.Diagnostics.AddError("creating Redshift Custom Domain Association", err.Error()) + return + } + data.ID = types.StringValue(id) + + output, err := tfresource.RetryWhenNotFound(ctx, propagationTimeout, func(ctx context.Context) (*customDomainAssociation, error) { + return findCustomDomainAssociationByTwoPartKey(ctx, conn, data.ClusterIdentifier.ValueString(), data.CustomDomainName.ValueString()) + }) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Redshift Custom Domain Association (%s) create", data.ID.ValueString()), err.Error()) + return + } + + if err := data.setFromAPIObject(output); err != nil { + response.Diagnostics.AddError("creating Redshift Custom Domain Association", err.Error()) + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *customDomainAssociationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data customDomainAssociationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + return + } + + conn := r.Meta().RedshiftClient(ctx) + + output, err := findCustomDomainAssociationByTwoPartKey(ctx, conn, data.ClusterIdentifier.ValueString(), data.CustomDomainName.ValueString()) + if retry.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + return + } + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Redshift Custom Domain Association (%s)", data.ID.ValueString()), err.Error()) + return + } + + if err := data.setFromAPIObject(output); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Redshift Custom Domain Association (%s)", data.ID.ValueString()), err.Error()) + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *customDomainAssociationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new customDomainAssociationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().RedshiftClient(ctx) + + _, err := conn.ModifyCustomDomainAssociation(ctx, &redshift.ModifyCustomDomainAssociationInput{ + ClusterIdentifier: new.ClusterIdentifier.ValueStringPointer(), + CustomDomainCertificateArn: new.CustomDomainCertificateARN.ValueStringPointer(), + CustomDomainName: new.CustomDomainName.ValueStringPointer(), + }) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Redshift Custom Domain Association (%s)", old.ID.ValueString()), err.Error()) + return + } + + output, err := tfresource.RetryWhen(ctx, propagationTimeout, + func(ctx context.Context) (*customDomainAssociation, error) { + output, err := findCustomDomainAssociationByTwoPartKey(ctx, conn, new.ClusterIdentifier.ValueString(), new.CustomDomainName.ValueString()) + if err != nil { + return nil, err + } + + if aws.ToString(output.CustomDomainCertificateArn) != new.CustomDomainCertificateARN.ValueString() { + return nil, errCustomDomainAssociationNotUpdated + } + + return output, nil + }, + func(err error) (bool, error) { + if retry.NotFound(err) || errors.Is(err, errCustomDomainAssociationNotUpdated) { + return true, err + } + + return false, err + }, + ) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Redshift Custom Domain Association (%s) update", old.ID.ValueString()), err.Error()) + return + } + + if err := new.setFromAPIObject(output); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Redshift Custom Domain Association (%s)", old.ID.ValueString()), err.Error()) + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *customDomainAssociationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data customDomainAssociationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().RedshiftClient(ctx) + + _, err := conn.DeleteCustomDomainAssociation(ctx, &redshift.DeleteCustomDomainAssociationInput{ + ClusterIdentifier: data.ClusterIdentifier.ValueStringPointer(), + CustomDomainName: data.CustomDomainName.ValueStringPointer(), + }) + if errs.IsA[*awstypes.ClusterNotFoundFault](err) || errs.IsA[*awstypes.CustomDomainAssociationNotFoundFault](err) { + return + } + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Redshift Custom Domain Association (%s)", data.ID.ValueString()), err.Error()) + return + } + + _, err = tfresource.RetryUntilNotFound(ctx, propagationTimeout, func(ctx context.Context) (any, error) { + return findCustomDomainAssociationByTwoPartKey(ctx, conn, data.ClusterIdentifier.ValueString(), data.CustomDomainName.ValueString()) + }) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Redshift Custom Domain Association (%s) delete", data.ID.ValueString()), err.Error()) + return + } +} + +func findCustomDomainAssociationByTwoPartKey(ctx context.Context, conn *redshift.Client, clusterIdentifier, customDomainName string) (*customDomainAssociation, error) { + input := &redshift.DescribeCustomDomainAssociationsInput{ + CustomDomainName: aws.String(customDomainName), + } + + for { + output, err := conn.DescribeCustomDomainAssociations(ctx, input) + if errs.IsA[*awstypes.CustomDomainAssociationNotFoundFault](err) { + return nil, &retry.NotFoundError{ + LastError: err, + } + } + if err != nil { + return nil, err + } + + for _, association := range output.Associations { + for _, certificateAssociation := range association.CertificateAssociations { + if aws.ToString(certificateAssociation.ClusterIdentifier) == clusterIdentifier && + aws.ToString(certificateAssociation.CustomDomainName) == customDomainName { + return &customDomainAssociation{ + ClusterIdentifier: certificateAssociation.ClusterIdentifier, + CustomDomainCertificateArn: association.CustomDomainCertificateArn, + CustomDomainCertificateExpiryDate: association.CustomDomainCertificateExpiryDate, + CustomDomainName: certificateAssociation.CustomDomainName, + }, nil + } + } + } + + if aws.ToString(output.Marker) == "" { + break + } + input.Marker = output.Marker + } + + return nil, tfresource.NewEmptyResultError() +} + +type customDomainAssociation struct { + ClusterIdentifier *string + CustomDomainCertificateArn *string + CustomDomainCertificateExpiryDate *time.Time + CustomDomainName *string +} + +type customDomainAssociationResourceModel struct { + framework.WithRegionModel + ClusterIdentifier types.String `tfsdk:"cluster_identifier"` + CustomDomainCertificateARN fwtypes.ARN `tfsdk:"custom_domain_certificate_arn"` + CustomDomainCertificateExpiryTime timetypes.RFC3339 `tfsdk:"custom_domain_certificate_expiry_time"` + CustomDomainName types.String `tfsdk:"custom_domain_name"` + ID types.String `tfsdk:"id"` +} + +const ( + customDomainAssociationResourceIDPartCount = 2 +) + +var ( + _ inttypes.ImportIDParser = customDomainAssociationImportID{} + _ inttypes.FrameworkImportIDCreator = customDomainAssociationImportID{} +) + +type customDomainAssociationImportID struct{} + +func (customDomainAssociationImportID) Parse(id string) (string, map[string]any, error) { + parts, err := flex.ExpandResourceId(id, customDomainAssociationResourceIDPartCount, false) + if err != nil { + return "", nil, err + } + + result := map[string]any{ + names.AttrClusterIdentifier: parts[0], + "custom_domain_name": parts[1], + } + + return id, result, nil +} + +func (customDomainAssociationImportID) Create(ctx context.Context, state tfsdk.State) string { + parts := make([]string, 0, customDomainAssociationResourceIDPartCount) + + var attrVal types.String + + state.GetAttribute(ctx, path.Root(names.AttrClusterIdentifier), &attrVal) + parts = append(parts, attrVal.ValueString()) + + state.GetAttribute(ctx, path.Root("custom_domain_name"), &attrVal) + parts = append(parts, attrVal.ValueString()) + + return fmt.Sprintf("%s%s%s", parts[0], flex.ResourceIdSeparator, parts[1]) +} + +func (data *customDomainAssociationResourceModel) InitFromID() error { + parts, err := flex.ExpandResourceId(data.ID.ValueString(), customDomainAssociationResourceIDPartCount, false) + if err != nil { + return err + } + + data.ClusterIdentifier = types.StringValue(parts[0]) + data.CustomDomainName = types.StringValue(parts[1]) + + return nil +} + +func (data *customDomainAssociationResourceModel) setFromAPIObject(apiObject *customDomainAssociation) error { + data.ClusterIdentifier = types.StringPointerValue(apiObject.ClusterIdentifier) + data.CustomDomainCertificateARN = fwtypes.ARNValue(aws.ToString(apiObject.CustomDomainCertificateArn)) + data.CustomDomainCertificateExpiryTime = timetypes.NewRFC3339TimePointerValue(apiObject.CustomDomainCertificateExpiryDate) + data.CustomDomainName = types.StringPointerValue(apiObject.CustomDomainName) + + id, err := data.setID() + if err != nil { + return err + } + + data.ID = types.StringValue(id) + + return nil +} + +func (data *customDomainAssociationResourceModel) setID() (string, error) { + parts := []string{ + data.ClusterIdentifier.ValueString(), + data.CustomDomainName.ValueString(), + } + + return flex.FlattenResourceId(parts, customDomainAssociationResourceIDPartCount, false) +} diff --git a/internal/service/redshift/custom_domain_association_identity_gen_test.go b/internal/service/redshift/custom_domain_association_identity_gen_test.go new file mode 100644 index 000000000000..5c827df1851c --- /dev/null +++ b/internal/service/redshift/custom_domain_association_identity_gen_test.go @@ -0,0 +1,221 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package redshift_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfknownvalue "github.com/hashicorp/terraform-provider-aws/internal/acctest/knownvalue" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccRedshiftCustomDomainAssociation_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN") + resourceName := "aws_redshift_custom_domain_association.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckCustomDomainAssociation(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + CheckDestroy: testAccCheckCustomDomainAssociationDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomDomainAssociationExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.Region()), + names.AttrClusterIdentifier: knownvalue.NotNull(), + "custom_domain_name": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrClusterIdentifier)), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("custom_domain_name")), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, names.AttrClusterIdentifier, "custom_domain_name"), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrClusterIdentifier, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, names.AttrClusterIdentifier, "custom_domain_name"), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrClusterIdentifier), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("custom_domain_name"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrClusterIdentifier), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("custom_domain_name"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccRedshiftCustomDomainAssociation_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN") + resourceName := "aws_redshift_custom_domain_association.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckCustomDomainAssociation(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrAccountID: tfknownvalue.AccountID(), + names.AttrRegion: knownvalue.StringExact(acctest.AlternateRegion()), + names.AttrClusterIdentifier: knownvalue.NotNull(), + "custom_domain_name": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrClusterIdentifier)), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("custom_domain_name")), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionAttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, names.AttrClusterIdentifier, "custom_domain_name"), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrClusterIdentifier, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionAttrsImportStateIdFunc(resourceName, flex.ResourceIdSeparator, names.AttrClusterIdentifier, "custom_domain_name"), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrClusterIdentifier), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("custom_domain_name"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/CustomDomainAssociation/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + "ACM_CERTIFICATE_ROOT_DOMAIN": config.StringVariable(acctest.SkipIfEnvVarNotSet(t, "ACM_CERTIFICATE_ROOT_DOMAIN")), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrClusterIdentifier), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("custom_domain_name"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} diff --git a/internal/service/redshift/custom_domain_association_test.go b/internal/service/redshift/custom_domain_association_test.go new file mode 100644 index 000000000000..deae9c240ddb --- /dev/null +++ b/internal/service/redshift/custom_domain_association_test.go @@ -0,0 +1,297 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package redshift_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/redshift" + "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/retry" + tfredshift "github.com/hashicorp/terraform-provider-aws/internal/service/redshift" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccRedshiftCustomDomainAssociation_basic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_redshift_custom_domain_association.test" + rootDomain := acctest.ACMCertificateDomainFromEnv(t) + domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckCustomDomainAssociation(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDomainAssociationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCustomDomainAssociationConfig_basic(rName, rootDomain, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDomainAssociationExists(ctx, t, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrClusterIdentifier, "aws_redshift_cluster.test", names.AttrClusterIdentifier), + resource.TestCheckResourceAttrPair(resourceName, "custom_domain_certificate_arn", "aws_acm_certificate_validation.test1", "certificate_arn"), + resource.TestCheckResourceAttr(resourceName, "custom_domain_name", domain), + resource.TestCheckResourceAttrSet(resourceName, "custom_domain_certificate_expiry_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRedshiftCustomDomainAssociation_certificateARN(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_redshift_custom_domain_association.test" + rootDomain := acctest.ACMCertificateDomainFromEnv(t) + domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckCustomDomainAssociation(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDomainAssociationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCustomDomainAssociationConfig_basic(rName, rootDomain, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDomainAssociationExists(ctx, t, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "custom_domain_certificate_arn", "aws_acm_certificate_validation.test1", "certificate_arn"), + ), + }, + { + Config: testAccCustomDomainAssociationConfig_certificateARN(rName, rootDomain, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDomainAssociationExists(ctx, t, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "custom_domain_certificate_arn", "aws_acm_certificate_validation.test2", "certificate_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRedshiftCustomDomainAssociation_disappears(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_redshift_custom_domain_association.test" + rootDomain := acctest.ACMCertificateDomainFromEnv(t) + domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckCustomDomainAssociation(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDomainAssociationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCustomDomainAssociationConfig_basic(rName, rootDomain, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDomainAssociationExists(ctx, t, resourceName), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfredshift.ResourceCustomDomainAssociation, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckCustomDomainAssociationDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).RedshiftClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_custom_domain_association" { + continue + } + + _, err := tfredshift.FindCustomDomainAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes[names.AttrClusterIdentifier], rs.Primary.Attributes["custom_domain_name"]) + + if retry.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Redshift Custom Domain Association %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckCustomDomainAssociationExists(ctx context.Context, t *testing.T, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no Redshift Custom Domain Association ID is set") + } + + conn := acctest.ProviderMeta(ctx, t).RedshiftClient(ctx) + + _, err := tfredshift.FindCustomDomainAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes[names.AttrClusterIdentifier], rs.Primary.Attributes["custom_domain_name"]) + + return err + } +} + +func testAccPreCheckCustomDomainAssociation(ctx context.Context, t *testing.T) { + acctest.PreCheckPartitionHasService(t, names.RedshiftEndpointID) + + conn := acctest.ProviderMeta(ctx, t).RedshiftClient(ctx) + + _, err := conn.DescribeCustomDomainAssociations(ctx, &redshift.DescribeCustomDomainAssociationsInput{}) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCustomDomainAssociationConfigBase(rName, rootDomain string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 2), fmt.Sprintf(` +resource "aws_redshift_subnet_group" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_redshift_cluster" "test" { + cluster_identifier = %[1]q + cluster_subnet_group_name = aws_redshift_subnet_group.test.name + database_name = "mydb" + master_username = "foo_test" + master_password = "Mustbe8characters" + node_type = "ra3.large" + automated_snapshot_retention_period = 1 + allow_version_upgrade = false + skip_final_snapshot = true + availability_zone_relocation_enabled = true + publicly_accessible = false +} + +data "aws_route53_zone" "test" { + name = %[2]q + private_zone = false +} +`, rName, rootDomain)) +} + +func testAccCustomDomainAssociationConfig_basic(rName, rootDomain, domain string) string { + return acctest.ConfigCompose(testAccCustomDomainAssociationConfigBase(rName, rootDomain), fmt.Sprintf(` +resource "aws_acm_certificate" "test1" { + domain_name = %[1]q + validation_method = "DNS" +} + +resource "aws_route53_record" "test1" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test1" { + certificate_arn = aws_acm_certificate.test1.arn + validation_record_fqdns = [aws_route53_record.test1.fqdn] +} + +resource "aws_redshift_custom_domain_association" "test" { + cluster_identifier = aws_redshift_cluster.test.cluster_identifier + custom_domain_name = %[1]q + custom_domain_certificate_arn = aws_acm_certificate_validation.test1.certificate_arn +} +`, domain)) +} + +func testAccCustomDomainAssociationConfig_certificateARN(rName, rootDomain, domain string) string { + return acctest.ConfigCompose(testAccCustomDomainAssociationConfigBase(rName, rootDomain), fmt.Sprintf(` +resource "aws_acm_certificate" "test1" { + domain_name = %[1]q + validation_method = "DNS" +} + +resource "aws_route53_record" "test1" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test1.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test1" { + certificate_arn = aws_acm_certificate.test1.arn + validation_record_fqdns = [aws_route53_record.test1.fqdn] +} + +resource "aws_acm_certificate" "test2" { + domain_name = %[1]q + validation_method = "DNS" +} + +resource "aws_route53_record" "test2" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test2.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test2.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test2.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test2" { + certificate_arn = aws_acm_certificate.test2.arn + validation_record_fqdns = [aws_route53_record.test2.fqdn] +} + +resource "aws_redshift_custom_domain_association" "test" { + cluster_identifier = aws_redshift_cluster.test.cluster_identifier + custom_domain_name = %[1]q + custom_domain_certificate_arn = aws_acm_certificate_validation.test2.certificate_arn +} +`, domain)) +} diff --git a/internal/service/redshift/exports_test.go b/internal/service/redshift/exports_test.go index c188623303f1..8a9a595d960b 100644 --- a/internal/service/redshift/exports_test.go +++ b/internal/service/redshift/exports_test.go @@ -9,6 +9,7 @@ var ( ResourceCluster = resourceCluster ResourceClusterIAMRoles = resourceClusterIAMRoles ResourceClusterSnapshot = resourceClusterSnapshot + ResourceCustomDomainAssociation = newCustomDomainAssociationResource ResourceDataShareAuthorization = newDataShareAuthorizationResource ResourceDataShareConsumerAssociation = newDataShareConsumerAssociationResource ResourceEndpointAccess = resourceEndpointAccess @@ -33,6 +34,7 @@ var ( FindAuthenticationProfileByID = findAuthenticationProfileByID FindClusterByID = findClusterByID FindClusterSnapshotByID = findClusterSnapshotByID + FindCustomDomainAssociationByTwoPartKey = findCustomDomainAssociationByTwoPartKey FindDataShareAuthorizationByTwoPartKey = findDataShareAuthorizationByTwoPartKey FindDataShareConsumerAssociationByFourPartKey = findDataShareConsumerAssociationByFourPartKey FindEndpointAccessByName = findEndpointAccessByName diff --git a/internal/service/redshift/service_package_gen.go b/internal/service/redshift/service_package_gen.go index 6a1a815fc519..e7126f598ed0 100644 --- a/internal/service/redshift/service_package_gen.go +++ b/internal/service/redshift/service_package_gen.go @@ -39,6 +39,21 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.S func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.ServicePackageFrameworkResource { return []*inttypes.ServicePackageFrameworkResource{ + { + Factory: newCustomDomainAssociationResource, + TypeName: "aws_redshift_custom_domain_association", + Name: "Custom Domain Association", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalParameterizedIdentity([]inttypes.IdentityAttribute{ + inttypes.StringIdentityAttribute(names.AttrClusterIdentifier, true), + inttypes.StringIdentityAttribute("custom_domain_name", true), + }), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + ImportID: customDomainAssociationImportID{}, + SetIDAttr: true, + }, + }, { Factory: newDataShareAuthorizationResource, TypeName: "aws_redshift_data_share_authorization", diff --git a/internal/service/redshift/testdata/CustomDomainAssociation/basic/main_gen.tf b/internal/service/redshift/testdata/CustomDomainAssociation/basic/main_gen.tf new file mode 100644 index 000000000000..773404b0b70a --- /dev/null +++ b/internal/service/redshift/testdata/CustomDomainAssociation/basic/main_gen.tf @@ -0,0 +1,98 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +locals { + custom_domain_name = "${var.rName}.${trimsuffix(var.ACM_CERTIFICATE_ROOT_DOMAIN, ".")}" +} + +resource "aws_redshift_subnet_group" "test" { + name = var.rName + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_redshift_cluster" "test" { + cluster_identifier = var.rName + cluster_subnet_group_name = aws_redshift_subnet_group.test.name + database_name = "mydb" + master_username = "foo_test" + master_password = "Mustbe8characters" + node_type = "ra3.large" + automated_snapshot_retention_period = 1 + allow_version_upgrade = false + skip_final_snapshot = true + availability_zone_relocation_enabled = true + publicly_accessible = false +} + +data "aws_route53_zone" "test" { + name = var.ACM_CERTIFICATE_ROOT_DOMAIN + private_zone = false +} + +resource "aws_acm_certificate" "test" { + domain_name = local.custom_domain_name + validation_method = "DNS" +} + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn + validation_record_fqdns = [aws_route53_record.test.fqdn] +} + +resource "aws_redshift_custom_domain_association" "test" { + cluster_identifier = aws_redshift_cluster.test.cluster_identifier + custom_domain_name = local.custom_domain_name + custom_domain_certificate_arn = aws_acm_certificate_validation.test.certificate_arn +} + +# acctest.ConfigVPCWithSubnets(rName, 2) + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +# acctest.ConfigSubnets(rName, 2) + +resource "aws_subnet" "test" { + count = 2 + + vpc_id = aws_vpc.test.id + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) +} + +# acctest.ConfigAvailableAZsNoOptInDefaultExclude + +data "aws_availability_zones" "available" { + exclude_zone_ids = local.default_exclude_zone_ids + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +locals { + default_exclude_zone_ids = ["usw2-az4", "usgw1-az2"] +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "ACM_CERTIFICATE_ROOT_DOMAIN" { + type = string + nullable = false +} diff --git a/internal/service/redshift/testdata/CustomDomainAssociation/region_override/main_gen.tf b/internal/service/redshift/testdata/CustomDomainAssociation/region_override/main_gen.tf new file mode 100644 index 000000000000..664d1b65fc9a --- /dev/null +++ b/internal/service/redshift/testdata/CustomDomainAssociation/region_override/main_gen.tf @@ -0,0 +1,120 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +locals { + custom_domain_name = "${var.rName}.${trimsuffix(var.ACM_CERTIFICATE_ROOT_DOMAIN, ".")}" +} + +resource "aws_redshift_subnet_group" "test" { + region = var.region + + name = var.rName + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_redshift_cluster" "test" { + region = var.region + + cluster_identifier = var.rName + cluster_subnet_group_name = aws_redshift_subnet_group.test.name + database_name = "mydb" + master_username = "foo_test" + master_password = "Mustbe8characters" + node_type = "ra3.large" + automated_snapshot_retention_period = 1 + allow_version_upgrade = false + skip_final_snapshot = true + availability_zone_relocation_enabled = true + publicly_accessible = false +} + +data "aws_route53_zone" "test" { + name = var.ACM_CERTIFICATE_ROOT_DOMAIN + private_zone = false +} + +resource "aws_acm_certificate" "test" { + region = var.region + + domain_name = local.custom_domain_name + validation_method = "DNS" +} + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test" { + region = var.region + + certificate_arn = aws_acm_certificate.test.arn + validation_record_fqdns = [aws_route53_record.test.fqdn] +} + +resource "aws_redshift_custom_domain_association" "test" { + region = var.region + + cluster_identifier = aws_redshift_cluster.test.cluster_identifier + custom_domain_name = local.custom_domain_name + custom_domain_certificate_arn = aws_acm_certificate_validation.test.certificate_arn +} + +# acctest.ConfigVPCWithSubnets(rName, 2) + +resource "aws_vpc" "test" { + region = var.region + + cidr_block = "10.0.0.0/16" +} + +# acctest.ConfigSubnets(rName, 2) + +resource "aws_subnet" "test" { + region = var.region + + count = 2 + + vpc_id = aws_vpc.test.id + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) +} + +# acctest.ConfigAvailableAZsNoOptInDefaultExclude + +data "aws_availability_zones" "available" { + region = var.region + + exclude_zone_ids = local.default_exclude_zone_ids + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +locals { + default_exclude_zone_ids = ["usw2-az4", "usgw1-az2"] +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "ACM_CERTIFICATE_ROOT_DOMAIN" { + type = string + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/redshift/testdata/tmpl/custom_domain_association_basic.gtpl b/internal/service/redshift/testdata/tmpl/custom_domain_association_basic.gtpl new file mode 100644 index 000000000000..2bb39883b432 --- /dev/null +++ b/internal/service/redshift/testdata/tmpl/custom_domain_association_basic.gtpl @@ -0,0 +1,59 @@ +locals { + custom_domain_name = "${var.rName}.${trimsuffix(var.ACM_CERTIFICATE_ROOT_DOMAIN, ".")}" +} + +resource "aws_redshift_subnet_group" "test" { +{{- template "region" }} + name = var.rName + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_redshift_cluster" "test" { +{{- template "region" }} + cluster_identifier = var.rName + cluster_subnet_group_name = aws_redshift_subnet_group.test.name + database_name = "mydb" + master_username = "foo_test" + master_password = "Mustbe8characters" + node_type = "ra3.large" + automated_snapshot_retention_period = 1 + allow_version_upgrade = false + skip_final_snapshot = true + availability_zone_relocation_enabled = true + publicly_accessible = false +} + +data "aws_route53_zone" "test" { + name = var.ACM_CERTIFICATE_ROOT_DOMAIN + private_zone = false +} + +resource "aws_acm_certificate" "test" { +{{- template "region" }} + domain_name = local.custom_domain_name + validation_method = "DNS" +} + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] + ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id +} + +resource "aws_acm_certificate_validation" "test" { +{{- template "region" }} + certificate_arn = aws_acm_certificate.test.arn + validation_record_fqdns = [aws_route53_record.test.fqdn] +} + +resource "aws_redshift_custom_domain_association" "test" { +{{- template "region" }} + cluster_identifier = aws_redshift_cluster.test.cluster_identifier + custom_domain_name = local.custom_domain_name + custom_domain_certificate_arn = aws_acm_certificate_validation.test.certificate_arn +} + +{{ template "acctest.ConfigVPCWithSubnets" 2 }} diff --git a/website/docs/r/redshift_custom_domain_association.html.markdown b/website/docs/r/redshift_custom_domain_association.html.markdown new file mode 100644 index 000000000000..73a1a9908de4 --- /dev/null +++ b/website/docs/r/redshift_custom_domain_association.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "Redshift" +layout: "aws" +page_title: "AWS: aws_redshift_custom_domain_association" +description: |- + Terraform resource for managing an AWS Redshift Custom Domain Association. +--- +# Resource: aws_redshift_custom_domain_association + +Terraform resource for managing an AWS Redshift Custom Domain Association. + +## Example Usage + +```terraform +resource "aws_acm_certificate" "example" { + domain_name = "redshift.example.com" + # ... +} + +resource "aws_redshift_cluster" "example" { + cluster_identifier = "example" + database_name = "example" + master_username = "exampleuser" + master_password = "Mustbe8characters" + node_type = "ra3.xlplus" + skip_final_snapshot = true +} + +resource "aws_redshift_custom_domain_association" "example" { + cluster_identifier = aws_redshift_cluster.example.cluster_identifier + custom_domain_name = "redshift.example.com" + custom_domain_certificate_arn = aws_acm_certificate.example.arn +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `cluster_identifier` - (Required) Identifier of the Redshift cluster. +* `custom_domain_name` - (Required) Custom domain to associate with the cluster. +* `custom_domain_certificate_arn` - (Required) ARN of the certificate for the custom domain association. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `custom_domain_certificate_expiry_time` - Expiration time for the certificate. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Redshift Custom Domain Association using the `cluster_identifier` and `custom_domain_name`, separated by a comma. For example: + +```terraform +import { + to = aws_redshift_custom_domain_association.example + id = "example-cluster,redshift.example.com" +} +``` + +Using `terraform import`, import Redshift Custom Domain Association using the `cluster_identifier` and `custom_domain_name`, separated by a comma. For example: + +```console +% terraform import aws_redshift_custom_domain_association.example example-cluster,redshift.example.com +```