diff --git a/go.mod b/go.mod index f19b0a9e7989..4b2434acd9c4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/YakDriver/go-version v0.1.0 github.com/YakDriver/regexache v0.25.0 github.com/YakDriver/smarterr v0.8.0 + github.com/aws/aws-sdk-go v1.55.8 github.com/aws/aws-sdk-go-v2 v1.40.1 github.com/aws/aws-sdk-go-v2/config v1.32.3 github.com/aws/aws-sdk-go-v2/credentials v1.19.3 @@ -285,6 +286,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go v0.23.0 github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.68 + github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2 v2.0.0-beta.69 github.com/hashicorp/awspolicyequivalence v1.7.0 github.com/hashicorp/cli v1.1.7 github.com/hashicorp/go-cleanhttp v0.5.2 diff --git a/go.sum b/go.sum index e50b647ba0b8..ea6ba62f188c 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= @@ -639,6 +641,8 @@ github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go v0.23.0 h1:l16/Vr github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go v0.23.0/go.mod h1:HAmscHyzSOfB1Dr16KLc177KNbn83wscnZC+N7WyaM8= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.68 h1:az84QLx3MwAMrOMjRpGjKQFY9/JkXveoc5TPbr8XHDQ= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.68/go.mod h1:naAMUZYs95/rPa/UchEe9VR/wa3RdTO5mQC5lKUWTmM= +github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2 v2.0.0-beta.69 h1:jIeLQWz27dBB/zbh6NXlnKUY82TZfVtOkZFNXB9+olc= +github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2 v2.0.0-beta.69/go.mod h1:OSQssuqo5JkYi9jwA/bNwnx8oG3HPRVe5Co9+8IZdmU= github.com/hashicorp/awspolicyequivalence v1.7.0 h1:HxwPEw2/31BqQa73PinGciTfG2uJ/ATelvDG8X1gScU= github.com/hashicorp/awspolicyequivalence v1.7.0/go.mod h1:+oCTxQEYt+GcRalqrqTCBcJf100SQYiWQ4aENNYxYe0= github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= @@ -902,7 +906,8 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/service/redshift/exports_test.go b/internal/service/redshift/exports_test.go index e080f7b94c77..0a129c3062f4 100644 --- a/internal/service/redshift/exports_test.go +++ b/internal/service/redshift/exports_test.go @@ -17,6 +17,7 @@ var ( ResourceHSMClientCertificate = resourceHSMClientCertificate ResourceHSMConfiguration = resourceHSMConfiguration ResourceIntegration = newIntegrationResource + ResourceIdcApplication = resourceIdcApplication ResourceLogging = newLoggingResource ResourceParameterGroup = resourceParameterGroup ResourcePartner = resourcePartner @@ -40,6 +41,7 @@ var ( FindHSMClientCertificateByID = findHSMClientCertificateByID FindHSMConfigurationByID = findHSMConfigurationByID FindIntegrationByARN = findIntegrationByARN + FindIDCApplicationByARN = findIDCApplicationByARN FindLoggingByID = findLoggingByID FindParameterGroupByName = findParameterGroupByName FindPartnerByID = findPartnerByID diff --git a/internal/service/redshift/find.go b/internal/service/redshift/find.go index aaeef95ba3b9..f808853825c9 100644 --- a/internal/service/redshift/find.go +++ b/internal/service/redshift/find.go @@ -480,3 +480,32 @@ func findIntegrationByARN(ctx context.Context, conn *redshift.Client, arn string return findIntegration(ctx, conn, &input) } + +func findIDCApplicationByARN(ctx context.Context, conn *redshift.Redshift, arn string) (*redshift.RedshiftIdcApplication, error) { + input := &redshift.DescribeRedshiftIdcApplicationsInput{ + RedshiftIdcApplicationArn: aws.String(arn), + } + + output, err := conn.DescribeRedshiftIdcApplicationsWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, redshift.ErrCodeRedshiftIdcApplicationNotExistsFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.RedshiftIdcApplications) == 0 || output.RedshiftIdcApplications[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.RedshiftIdcApplications); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.RedshiftIdcApplications[0], nil +} diff --git a/internal/service/redshift/idc_application.go b/internal/service/redshift/idc_application.go new file mode 100644 index 000000000000..8bf3770ca4c4 --- /dev/null +++ b/internal/service/redshift/idc_application.go @@ -0,0 +1,496 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package redshift + +import ( + "context" + "errors" + "time" + + "github.com/YakDriver/regexache" + "github.com/YakDriver/smarterr" + "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-validators/listvalidator" + "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/types" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" + sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" + 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_redshift_idc_application", name="IDC Application") +func newResourceIDCApplication(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceIDCApplication{} + + return r, nil +} + +const ( + ResNameIDCApplication = "IDC Application" +) + +type resourceIDCApplication struct { + framework.ResourceWithModel[resourceIDCApplicationModel] +} + +func (r *resourceIDCApplication) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrDescription: schema.StringAttribute{ + Optional: true, + }, + names.AttrIAMRoleARN: schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + }, + "idc_display_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 127), + stringvalidator.RegexMatches(regexache.MustCompile(`[\w+=,.@-]+`), "must match [\\w+=,.@-]"), + }, + }, + "idc_instance_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "identity_namespace": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 127), + stringvalidator.RegexMatches(regexache.MustCompile(`^[a-zA-Z0-9_+.#@$-]+$`), "must match ^[a-zA-Z0-9_+.#@$-]+$"), + }, + }, + "redshift_idc_application_name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 63), + stringvalidator.RegexMatches(regexache.MustCompile(`[a-z][a-z0-9]*(-[a-z0-9]+)*`), "must match [a-z][a-z0-9]*(-[a-z0-9]+)*"), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "authorized_token_issuer_list": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[authorizedTokenIssuerListModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "authorized_audiences_list": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + }, + "trusted_token_issuer_arn": schema.StringAttribute{ + Optional: true, + CustomType: fwtypes.ARNType, + }, + }, + }, + }, + "service_integrations": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[serviceIntegrationsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "lake_formation": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[lakeFormationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "lake_formation_scope": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[lakeFormationScopeModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "lake_formation_query": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[lakeFormationQueryModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "authorization": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ServiceAuthorization](), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "redshift": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[redshiftModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "connect": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[connectModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "authorization": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ServiceAuthorization](), + }, + }, + }, + }, + }, + }, + }, + "s3_access_grants": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[s3AccessGrantsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "read_write_access": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[readWriteAccessModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "authorization": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ServiceAuthorization](), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r *resourceIDCApplication) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().RedshiftClient(ctx) + + var plan resourceIDCApplicationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + var input redshift.CreateRedshiftIdcApplicationInput + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input)) + if resp.Diagnostics.HasError() { + return + } + + out, err := conn.CreateRedshiftIdcApplication(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.RedshiftIDCApplicationName.String()) + return + } + if out == nil || out.RedshiftIdcApplication == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.RedshiftIDCApplicationName.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *resourceIDCApplication) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().RedshiftClient(ctx) + + var state resourceIDCApplicationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + out, err := findIDCApplicationByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &state)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *resourceIDCApplication) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().RedshiftClient(ctx) + + var plan, state resourceIDCApplicationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Diff(ctx, plan, state) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + var input redshift.UpdateIDCApplicationInput + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("Test"))) + if resp.Diagnostics.HasError() { + return + } + + out, err := conn.UpdateIDCApplication(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + if out == nil || out.IDCApplication == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ID.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) + if resp.Diagnostics.HasError() { + return + } + } + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, err := waitIDCApplicationUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) +} + +func (r *resourceIDCApplication) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().RedshiftClient(ctx) + + var state resourceIDCApplicationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + input := redshift.DeleteIDCApplicationInput{ + IDCApplicationId: state.ID.ValueStringPointer(), + } + + _, err := conn.DeleteIDCApplication(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitIDCApplicationDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } +} + +func (r *resourceIDCApplication) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrARN), req, resp) +} + +func findIDCApplicationByID(ctx context.Context, conn *redshift.Client, id string) (*awstypes.RedshiftIdcApplication, error) { + input := redshift.DescribeRedshiftIdcApplicationsInput{ + RedshiftIdcApplicationArn: aws.String(id), + } + + out, err := conn.DescribeRedshiftIdcApplications(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.RedshiftIdcApplicationNotExistsFault](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + LastRequest: &input, + }) + } + + return nil, smarterr.NewError(err) + } + + if out == nil || out.RedshiftIdcApplications == nil { + return nil, smarterr.NewError(tfresource.NewEmptyResultError(&input)) + } + + return &out.RedshiftIdcApplications[0], nil +} + +type resourceIDCApplicationModel struct { + framework.WithRegionModel + ARN fwtypes.ARN `tfsdk:"arn"` + AuthorizedTokenIssuerList fwtypes.ListNestedObjectValueOf[authorizedTokenIssuerListModel] `tfsdk:"authorized_token_issuer_list"` + Description types.String `tfsdk:"description"` + IAMRoleARN fwtypes.ARN `tfsdk:"iam_role_arn"` + IDCDisplayName types.String `tfsdk:"idc_display_name"` + IDCInstanceARN fwtypes.ARN `tfsdk:"idc_instance_arn"` + IdentityNamespace types.String `tfsdk:"identity_namespace"` + RedshiftIDCApplicationName types.String `tfsdk:"redshift_idc_application_name"` + ServiceIntegrations fwtypes.ListNestedObjectValueOf[serviceIntegrationsModel] `tfsdk:"service_integrations"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` +} + +type authorizedTokenIssuerListModel struct { + AuthorizedAudiencesList fwtypes.ListOfString `tfsdk:"authorized_audiences_list"` + TrustedTokenIssuerARN fwtypes.ARN `tfsdk:"trusted_token_issuer_arn"` +} + +type serviceIntegrationsModel struct { + LakeFormation fwtypes.ListNestedObjectValueOf[lakeFormationModel] `tfsdk:"lake_formation"` + Redshift fwtypes.ListNestedObjectValueOf[redshiftModel] `tfsdk:"redshift"` + S3AccessGrants fwtypes.ListNestedObjectValueOf[s3AccessGrantsModel] `tfsdk:"s3_access_grants"` +} + +func (m serviceIntegrationsModel) Expand(ctx context.Context) (result any, diags diag.Diagnostics) { + switch { + case !m.LakeFormation.IsNull(): + lakeFormationData, d := m.LakeFormation.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + var r awstypes.ServiceIntegrationsUnionMemberLakeFormation + diags.Append(flex.Expand(ctx, lakeFormationData, &r.Value)...) + if diags.HasError() { + return nil, diags + } + + return &r, diags + + case !m.Redshift.IsNull(): + redshiftData, d := m.Redshift.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + var r awstypes.ServiceIntegrationsUnionMemberRedshift + diags.Append(flex.Expand(ctx, redshiftData, &r.Value)...) + if diags.HasError() { + return nil, diags + } + + return &r, diags + + case !m.S3AccessGrants.IsNull(): + s3AccessGrants, d := m.S3AccessGrants.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + var r awstypes.ServiceIntegrationsUnionMemberS3AccessGrants + diags.Append(flex.Expand(ctx, s3AccessGrants, &r.Value)...) + if diags.HasError() { + return nil, diags + } + + return &r, diags + } + + return nil, diags +} + +func (m *serviceIntegrationsModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + switch t := v.(type) { + case awstypes. + } +} + +type lakeFormationModel struct { + LakeFormationScope fwtypes.ListNestedObjectValueOf[lakeFormationScopeModel] `tfsdk:"lake_formation_scope"` +} + +type lakeFormationScopeModel struct { + LakeFormationQuery fwtypes.ListNestedObjectValueOf[lakeFormationQueryModel] `tfsdk:"lake_formation_query"` +} + +type lakeFormationQueryModel struct { + Authorization fwtypes.StringEnum[awstypes.ServiceAuthorization] `tfsdk:"authorization"` +} + +type redshiftModel struct { + Connect fwtypes.ListNestedObjectValueOf[connectModel] `tfsdk:"connect"` +} + +type connectModel struct { + Authorization fwtypes.StringEnum[awstypes.ServiceAuthorization] `tfsdk:"authorization"` +} + +type s3AccessGrantsModel struct { + ReadWriteAccess fwtypes.ListNestedObjectValueOf[readWriteAccessModel] `tfsdk:"read_write_access"` +} + +type readWriteAccessModel struct { + Authorization fwtypes.StringEnum[awstypes.ServiceAuthorization] `tfsdk:"authorization"` +} diff --git a/internal/service/redshift/idc_application_test.go b/internal/service/redshift/idc_application_test.go new file mode 100644 index 000000000000..db8888673db0 --- /dev/null +++ b/internal/service/redshift/idc_application_test.go @@ -0,0 +1,291 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package redshift_test + +import ( + "context" + "fmt" + "testing" + + 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" + tfredshift "github.com/hashicorp/terraform-provider-aws/internal/service/redshift" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccRedshiftIdcApplication_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_redshift_idc_application.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIdcApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIdcApplicationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIdcApplicationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "iam_role_arn", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "idc_display_name", rName), + resource.TestCheckResourceAttrPair(resourceName, "idc_instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttr(resourceName, "redshift_idc_application_name", rName), + resource.TestCheckResourceAttr(resourceName, "identity_namespace", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRedshiftIdcApplication_disappears(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_redshift_idc_application.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIdcApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIdcApplicationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIdcApplicationExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfredshift.ResourceIdcApplication(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccRedshiftIdcApplication_authorizedTokenIssuerList(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_redshift_idc_application.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIdcApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIdcApplicationConfig_authorizedTokenIssuerList(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIdcApplicationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "redshift_idc_application_name", rName), + resource.TestCheckResourceAttr(resourceName, "authorized_token_issuer_list.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "authorized_token_issuer_list.0.trusted_token_issuer_arn", "aws_ssoadmin_trusted_token_issuer.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "authorized_token_issuer_list.0.authorized_audiences_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "authorized_token_issuer_list.0.authorized_audiences_list.0", "client_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRedshiftIdcApplication_serviceIntegrations(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_redshift_idc_application.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RedshiftServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIdcApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIdcApplicationConfig_serviceIntegrations(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIdcApplicationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "redshift_idc_application_name", rName), + resource.TestCheckResourceAttr(resourceName, "service_integrations.#", "1"), + resource.TestCheckResourceAttr(resourceName, "service_integrations.0.lake_formation.#", "1"), + resource.TestCheckResourceAttr(resourceName, "service_integrations.0.lake_formation.0.lake_formation_query.authorization", "Enabled"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckIdcApplicationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_idc_application" { + continue + } + _, err := tfredshift.FindIDCApplicationByARN(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Redshift IDC Application %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckIdcApplicationExists(ctx context.Context, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Redshift IDC Application is not set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftConn(ctx) + + _, err := tfredshift.FindIDCApplicationByARN(ctx, conn, rs.Primary.ID) + + return err + } +} +func testAccIdcApplicationConfig_baseIAMRole(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_role" "test" { + name = %[1]q + path = "/service-role/" + + assume_role_policy = < 0 && v[0] != "" { + authorizedTokenIssuer.AuthorizedAudiencesList = expandAuthorizedAudiences(v) + } + if vTrustedTokenIssuerArn, ok := m["trusted_token_issuer_arn"].(string); ok && vTrustedTokenIssuerArn != "" { + authorizedTokenIssuer.TrustedTokenIssuerArn = aws.String(vTrustedTokenIssuerArn) + } + authorizedTokenIssuerList = append(authorizedTokenIssuerList, authorizedTokenIssuer) + } + return authorizedTokenIssuerList +} + +func expandAuthorizedAudiences(v []interface{}) []*string { + var authorizedAudiencesList []*string + for _, v := range v { + authorizedAudiencesList = append(authorizedAudiencesList, aws.String(v.(string))) + } + return authorizedAudiencesList +} + +func flattenAuthorizedTokenIssuerList(v []*redshift.AuthorizedTokenIssuer) *schema.Set { + s := &schema.Set{F: authorizedTokenIssuerListHash} + if len(v) == 0 { + return nil + } + + for _, v := range v { + var authorizedToeknIsuser interface{} + authorizedAudiences := flatternAuthorizedAudiences(v.AuthorizedAudiencesList) + authorizedToeknIsuser = map[string]interface{}{ + "authorized_audiences_list": authorizedAudiences, + "trusted_token_issuer_arn": aws.StringValue(v.TrustedTokenIssuerArn), + } + + s.Add(authorizedToeknIsuser) + } + + return s +} + +func expandServiceIntegrations(v []interface{}) []*redshift.ServiceIntegrationsUnion { + if len(v) == 0 || v[0] == nil { + return nil + } + + serviceIntegrations := []*redshift.ServiceIntegrationsUnion{} + + for _, v := range v { + serviceIntegration := &redshift.ServiceIntegrationsUnion{} + m := v.(map[string]interface{}) + if v, ok := m["lake_formation"].(*schema.Set); ok && v.Len() > 0 { + serviceIntegration.LakeFormation = expandLakeFormation(v.List()) + } + serviceIntegrations = append(serviceIntegrations, serviceIntegration) + } + return serviceIntegrations +} + +func expandLakeFormation(v []interface{}) []*redshift.LakeFormationScopeUnion { + if len(v) == 0 || v[0] == nil { + return nil + } + + lakeFormation := []*redshift.LakeFormationScopeUnion{} + for _, v := range v { + lakeFormationScopeUnion := expandLakeFormationScopeUnion(v.(map[string]interface{})) + lakeFormation = append(lakeFormation, lakeFormationScopeUnion) + } + return lakeFormation +} + +func expandLakeFormationScopeUnion(v map[string]interface{}) *redshift.LakeFormationScopeUnion { + lakeFormationScopeUnion := &redshift.LakeFormationScopeUnion{} + if v, ok := v["lake_formation_query"].(map[string]interface{}); ok { + lakeFormationScopeUnion.LakeFormationQuery = expandLakeFormationQuery(v) + } + return lakeFormationScopeUnion +} + +func expandLakeFormationQuery(v map[string]interface{}) *redshift.LakeFormationQuery { + lakeFormationQuery := &redshift.LakeFormationQuery{} + if v, ok := v["authorization"].(string); ok && v != "" { + lakeFormationQuery.Authorization = aws.String(v) + } + return lakeFormationQuery +} + +func flatternAuthorizedAudiences(v []*string) []interface{} { + var authorizedAudiencesList []interface{} + for _, v := range v { + authorizedAudiencesList = append(authorizedAudiencesList, v) + } + return authorizedAudiencesList +} + +func flatternServiceIntegrations(v []*redshift.ServiceIntegrationsUnion) *schema.Set { + serviceIntegrations := []interface{}{} + if len(v) == 0 { + return nil + } + + for _, v := range v { + serviceIntegrationsUnion := flatternServiceIntegrationsUnion(v) + serviceIntegrations = append(serviceIntegrations, serviceIntegrationsUnion) + } + return schema.NewSet(serviceIntegrationsHash, serviceIntegrations) +} + +func flatternServiceIntegrationsUnion(v *redshift.ServiceIntegrationsUnion) map[string]interface{} { + mServiceIntegrationsUnion := make(map[string]interface{}) + if lakeFormation := v.LakeFormation; lakeFormation != nil { + mServiceIntegrationsUnion["lake_formation"] = flatternLakeFormation(v.LakeFormation) + } + return mServiceIntegrationsUnion +} + +func flatternLakeFormation(v []*redshift.LakeFormationScopeUnion) []interface{} { + lakeFormation := []interface{}{} + if len(v) == 0 { + return nil + } + for _, v := range v { + lakeFormationScopeUnion := flatternLakeFormationScopeUnion(v) + lakeFormation = append(lakeFormation, lakeFormationScopeUnion) + } + + return lakeFormation +} + +func flatternLakeFormationScopeUnion(v *redshift.LakeFormationScopeUnion) map[string]interface{} { + mLakeFormationScopeUnion := make(map[string]interface{}) + if lakeFormationQuery := v.LakeFormationQuery; lakeFormationQuery != nil { + mLakeFormationScopeUnion["lake_formation_query"] = flatternLakeFormationQuery(v.LakeFormationQuery) + } + return mLakeFormationScopeUnion +} + +func flatternLakeFormationQuery(v *redshift.LakeFormationQuery) map[string]interface{} { + lakeFormationQuery := make(map[string]interface{}) + lakeFormationQuery["authorization"] = aws.StringValue(v.Authorization) + + return lakeFormationQuery +} + +func authorizedTokenIssuerListHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["authorized_audiences_list"].([]interface{}))) + buf.WriteString(fmt.Sprintf("%s-", m["trusted_token_issuer_arn"].(string))) + + return create.StringHashcode(buf.String()) +} + +func serviceIntegrationsHash(vServiceIntegrations interface{}) int { + var buf bytes.Buffer + + if vLakeformation, ok := vServiceIntegrations.(map[string]interface{})["lake_formation"].([]map[string]interface{}); ok && len(vLakeformation) > 0 && vLakeformation[0] != nil { + for _, v := range vLakeformation { + if vLakeFormationQuery, ok := v["lake_formation_query"].(map[string]interface{}); ok && len(vLakeFormationQuery) > 0 { + if v, ok := vLakeFormationQuery["authorization"].(string); ok && v != "" { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + } + } + + return create.StringHashcode(buf.String()) +} diff --git a/internal/service/redshift/service_package_gen.go b/internal/service/redshift/service_package_gen.go index 3051901dcc55..d43d300361d6 100644 --- a/internal/service/redshift/service_package_gen.go +++ b/internal/service/redshift/service_package_gen.go @@ -178,6 +178,14 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*inttypes.ServicePa }), Region: unique.Make(inttypes.ResourceRegionDefault()), }, + { + Factory: resourceIdcApplication, + TypeName: "aws_redshift_idc_application", + Name: "IDC Application", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, { Factory: resourceParameterGroup, TypeName: "aws_redshift_parameter_group", diff --git a/internal/service/redshift/sweep.go b/internal/service/redshift/sweep.go index ae593fde87db..2586b3077661 100644 --- a/internal/service/redshift/sweep.go +++ b/internal/service/redshift/sweep.go @@ -481,3 +481,25 @@ func sweepAuthenticationProfiles(region string) error { return nil } + +func sweepIDCApplications(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + input := redshift.ListIDCApplicationsInput{} + conn := client.RedshiftClient(ctx) + var sweepResources []sweep.Sweepable + + pages := redshift.NewListIDCApplicationsPaginator(conn, &input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, smarterr.NewError(err) + } + + for _, v := range page.IDCApplications { + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceIDCApplication, client, + sweepfw.NewAttribute(names.AttrID, aws.ToString(v.IDCApplicationId))), + ) + } + } + + return sweepResources, nil +} diff --git a/website/docs/r/redshift_idc_application.html.markdown b/website/docs/r/redshift_idc_application.html.markdown new file mode 100644 index 000000000000..ec4bc3bbdce4 --- /dev/null +++ b/website/docs/r/redshift_idc_application.html.markdown @@ -0,0 +1,81 @@ +--- +subcategory: "Redshift" +layout: "aws" +page_title: "AWS: aws_redshift_idc_application" +description: |- + Provides a Redshift IDC Application resource. +--- + +# Resource: aws_redshift_idc_application + +Creates a new Amazon Redshift IDC application. + +## Example Usage + +```terraform +resource "aws_redshift_idc_application" "example" { + iam_role_arn = aws_iam_role.example.arn + idc_display_name = "example" + idc_instance_arn = tolist(data.aws_ssoadmin_instances.example.arns)[0] + identity_namespace = "example" + redshift_idc_application_name = "example" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `authorized_token_issuer_list` - (Optional) The token issuer list for the Amazon Redshift IAM Identity Center application instance. Documented below. +* `iam_role_arn` - (Required) The IAM role ARN for the Amazon Redshift IAM Identity Center application instance. +* `idc_display_name` - (Required) The display name for the Amazon Redshift IAM Identity Center application instance. +* `idc_instance_arn` - (Required) The Amazon resource name (ARN) of the IAM Identity Center instance where Amazon Redshift creates a new managed application. +* `identity_namespace` - (Optional) The namespace for the Amazon Redshift IAM Identity Center application instance. +* `redshift_idc_application_name` - (Required) The name of the Redshift application in IAM Identity Center. +* `service_integrations` - (Optional) A collection of service integrations for the Redshift IAM Identity Center application. Documented below. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - The ARN for the Redshift application that integrates with IAM Identity Center. +* `authorized_token_issuer_list` - The authorized token issuer list for the Amazon Redshift IAM Identity Center application. +* `iam_role_arn` - The ARN for the Amazon Redshift IAM Identity Center application. +* `idc_display_name` - The display name for the Amazon Redshift IAM Identity Center application. +* `identity_namespace` - The identity namespace for the Amazon Redshift IAM Identity Center application. +* `redshift_idc_application_name` - The name of the Redshift application in IAM Identity Center. +* `service_integrations` - A list of service integrations for the Redshift IAM Identity Center application. + +### AuthorizedTokenIssuerList + +* `authorized_audiences_list` - One or more network interfaces of the endpoint. Also known as an interface endpoint. See details below. +* `trusted_token_issuer_arn` - The connection endpoint ID for connecting an Amazon Redshift cluster through the proxy. + +### ServiceIntegrations + +* `lake_formation` - (Optional) A list of scopes set up for Lake Formation integration. + +### LakeFormation + +* `lake_formation_query` - (Optional) The Lake Formation scope. + +### LakeFormationQuery + +* `authorization` - (Required) Determines whether the query scope is enabled or disabled. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Redshift IDC Application using the `arn`. For example: + +```terraform +import { + to = aws_redshift_idc_application.example + id = "example" +} +``` + +Using `terraform import`, import Redshift endpoint access using the `arn`. For example: + +```console +% terraform import aws_redshift_idc_application.example example +```