From a06c925e1ffd387d8960b76dea1e72cfa1c08503 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:23:23 +0900 Subject: [PATCH 01/17] add aws_transfer_web_app resource --- internal/service/transfer/web_app.go | 557 +++++++++++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 internal/service/transfer/web_app.go diff --git a/internal/service/transfer/web_app.go b/internal/service/transfer/web_app.go new file mode 100644 index 000000000000..42a4baed4c01 --- /dev/null +++ b/internal/service/transfer/web_app.go @@ -0,0 +1,557 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package transfer + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/transfer" + awstypes "github.com/aws/aws-sdk-go-v2/service/transfer/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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-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/enum" + "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/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_transfer_web_app", name="Web App") +// @Tags(identifierAttribute="arn") +func newResourceWebApp(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceWebApp{} + + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) + + return r, nil +} + +const ( + ResNameWebApp = "Web App" +) + +type resourceWebApp struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceWebApp) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "access_endpoint": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(1024), + }, + }, + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrID: framework.IDAttribute(), + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "web_app_endpoint_policy": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.WebAppEndpointPolicy](), + Optional: true, + Computed: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.WebAppEndpointPolicy](), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "web_app_units": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[webAppUnitsModel](ctx), + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "provisioned": types.Int64Type, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "identity_provider_details": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[identityProviderDetailsModel](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "identity_center_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[identityCenterConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "application_arn": schema.StringAttribute{ + Computed: true, + }, + "instance_arn": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(10, 1024), + stringvalidator.RegexMatches(regexache.MustCompile(`^arn:[\w-]+:sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}$`), ""), + }, + }, + "role": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(20, 2048), + stringvalidator.RegexMatches(regexache.MustCompile(`^arn:.*role/\S+$`), ""), + }, + }, + }, + }, + }, + }, + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: false, + Delete: true, + }), + }, + } +} + +func (r *resourceWebApp) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().TransferClient(ctx) + + var plan resourceWebAppModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var input transfer.CreateWebAppInput + resp.Diagnostics.Append(flex.Expand(ctx, plan, &input)...) + if resp.Diagnostics.HasError() { + return + } + + input.Tags = getTagsIn(ctx) + + out, err := conn.CreateWebApp(ctx, &input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionCreating, ResNameWebApp, "", err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionCreating, ResNameWebApp, "", nil), + errors.New("empty output").Error(), + ) + return + } + + plan.WebAppId = flex.StringToFramework(ctx, out.WebAppId) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + _, err = waitWebAppCreated(ctx, conn, plan.WebAppId.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionWaitingForCreation, ResNameWebApp, plan.WebAppId.String(), err), + err.Error(), + ) + return + } + + rout, err := findWebAppByID(ctx, conn, plan.WebAppId.ValueString()) + resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan, flex.WithFieldNamePrefix("Described"))...) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceWebApp) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().TransferClient(ctx) + + var state resourceWebAppModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findWebAppByID(ctx, conn, state.WebAppId.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionReading, ResNameWebApp, state.WebAppId.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state, flex.WithFieldNamePrefix("Described"))...) + if resp.Diagnostics.HasError() { + return + } + + setTagsOut(ctx, out.Tags) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceWebApp) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + needUpdate := false + conn := r.Meta().TransferClient(ctx) + + var plan, state resourceWebAppModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Diff(ctx, plan, state) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + input := transfer.UpdateWebAppInput{ + WebAppId: state.WebAppId.ValueStringPointer(), + } + + if !state.AccessEndpoint.Equal(plan.AccessEndpoint) { + if v := plan.AccessEndpoint.ValueStringPointer(); v != nil && *v != "" { + input.AccessEndpoint = v + } + needUpdate = true + } + if !state.IdentityProviderDetails.Equal(plan.IdentityProviderDetails) { + if v, diags := plan.IdentityProviderDetails.ToPtr(ctx); v != nil && !diags.HasError() { + if v, diags := v.IdentityCenterConfig.ToPtr(ctx); v != nil && !diags.HasError() { + input.IdentityProviderDetails = &awstypes.UpdateWebAppIdentityProviderDetailsMemberIdentityCenterConfig{ + Value: awstypes.UpdateWebAppIdentityCenterConfig{ + Role: v.Role.ValueStringPointer(), + }, + } + needUpdate = true + } + } + } + if !state.WebAppUnits.Equal(plan.WebAppUnits) { + if v, diags := plan.WebAppUnits.ToPtr(ctx); v != nil && !diags.HasError() { + if v, diags := plan.WebAppUnits.ToPtr(ctx); v != nil && !diags.HasError() { + input.WebAppUnits = &awstypes.WebAppUnitsMemberProvisioned{ + Value: flex.Int32ValueFromFrameworkInt64(ctx, v.Provisioned), + } + needUpdate = true + } + } + } + + if needUpdate { + out, err := conn.UpdateWebApp(ctx, &input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionUpdating, ResNameWebApp, plan.WebAppId.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionUpdating, ResNameWebApp, plan.WebAppId.String(), nil), + errors.New("empty output").Error(), + ) + return + } + } + + if !state.Tags.Equal(plan.Tags) { + if err := updateTags(ctx, conn, plan.ARN.ValueString(), state.Tags, plan.Tags); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionUpdating, ResNameWebApp, plan.WebAppId.String(), err), + err.Error(), + ) + return + } + } + + if resp.Diagnostics.HasError() { + return + } + } + + rout, _ := findWebAppByID(ctx, conn, plan.WebAppId.ValueString()) + resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan, flex.WithFieldNamePrefix("Described"))...) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceWebApp) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().TransferClient(ctx) + + var state resourceWebAppModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + input := transfer.DeleteWebAppInput{ + WebAppId: state.WebAppId.ValueStringPointer(), + } + + _, err := conn.DeleteWebApp(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionDeleting, ResNameWebApp, state.WebAppId.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitWebAppDeleted(ctx, conn, state.WebAppId.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionWaitingForDeletion, ResNameWebApp, state.WebAppId.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceWebApp) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), req, resp) +} + +const ( + statusNormal = "Normal" +) + +func waitWebAppCreated(ctx context.Context, conn *transfer.Client, id string, timeout time.Duration) (*awstypes.DescribedWebApp, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + Refresh: statusWebApp(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.DescribedWebApp); ok { + return out, err + } + + return nil, err +} + +func waitWebAppDeleted(ctx context.Context, conn *transfer.Client, id string, timeout time.Duration) (*awstypes.DescribedWebApp, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusNormal}, + Target: []string{}, + Refresh: statusWebApp(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.DescribedWebApp); ok { + return out, err + } + + return nil, err +} + +func statusWebApp(ctx context.Context, conn *transfer.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + out, err := findWebAppByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, statusNormal, nil + } +} + +func findWebAppByID(ctx context.Context, conn *transfer.Client, id string) (*awstypes.DescribedWebApp, error) { + input := transfer.DescribeWebAppInput{ + WebAppId: aws.String(id), + } + + out, err := conn.DescribeWebApp(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: &input, + } + } + + return nil, err + } + + if out == nil || out.WebApp == nil { + return nil, tfresource.NewEmptyResultError(&input) + } + + return out.WebApp, nil +} + +type resourceWebAppModel struct { + AccessEndpoint types.String `tfsdk:"access_endpoint"` + ARN types.String `tfsdk:"arn"` + IdentityProviderDetails fwtypes.ListNestedObjectValueOf[identityProviderDetailsModel] `tfsdk:"identity_provider_details"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + WebAppEndpointPolicy fwtypes.StringEnum[awstypes.WebAppEndpointPolicy] `tfsdk:"web_app_endpoint_policy"` + WebAppUnits fwtypes.ListNestedObjectValueOf[webAppUnitsModel] `tfsdk:"web_app_units"` + WebAppId types.String `tfsdk:"id"` +} + +type identityProviderDetailsModel struct { + IdentityCenterConfig fwtypes.ListNestedObjectValueOf[identityCenterConfigModel] `tfsdk:"identity_center_config"` +} + +type identityCenterConfigModel struct { + ApplicationArn types.String `tfsdk:"application_arn"` + InstanceArn types.String `tfsdk:"instance_arn"` + Role types.String `tfsdk:"role"` +} + +type webAppUnitsModel struct { + Provisioned types.Int64 `tfsdk:"provisioned"` +} + +func sweepWebApps(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + input := transfer.ListWebAppsInput{} + conn := client.TransferClient(ctx) + var sweepResources []sweep.Sweepable + + pages := transfer.NewListWebAppsPaginator(conn, &input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, v := range page.WebApps { + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceWebApp, client, + sweepfw.NewAttribute(names.AttrID, aws.ToString(v.WebAppId))), + ) + } + } + + return sweepResources, nil +} + +var ( + _ flex.Expander = webAppUnitsModel{} + _ flex.Flattener = &webAppUnitsModel{} + _ flex.Expander = identityProviderDetailsModel{} + _ flex.Flattener = &identityProviderDetailsModel{} +) + +func (m webAppUnitsModel) Expand(ctx context.Context) (any, diag.Diagnostics) { + var diags diag.Diagnostics + var v awstypes.WebAppUnits + + switch { + case !m.Provisioned.IsNull(): + var apiObject awstypes.WebAppUnitsMemberProvisioned + apiObject.Value = *flex.Int32FromFrameworkInt64(ctx, &m.Provisioned) + v = &apiObject + } + + return v, diags +} + +func (m *webAppUnitsModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + switch t := v.(type) { + case awstypes.WebAppUnitsMemberProvisioned: + m.Provisioned = flex.Int32ToFrameworkInt64(ctx, &t.Value) + } + return diags +} + +func (m identityProviderDetailsModel) Expand(ctx context.Context) (any, diag.Diagnostics) { + var diags diag.Diagnostics + var v awstypes.WebAppIdentityProviderDetails + + switch { + case !m.IdentityCenterConfig.IsNull(): + data, d := m.IdentityCenterConfig.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + var apiObject awstypes.WebAppIdentityProviderDetailsMemberIdentityCenterConfig + diags.Append(flex.Expand(ctx, data, &apiObject.Value)...) + if diags.HasError() { + return nil, diags + } + v = &apiObject + } + + return v, diags +} + +func (m *identityProviderDetailsModel) Flatten(ctx context.Context, v any) diag.Diagnostics { + var diags diag.Diagnostics + + switch t := v.(type) { + case awstypes.DescribedWebAppIdentityProviderDetailsMemberIdentityCenterConfig: + var data identityCenterConfigModel + diags.Append(flex.Flatten(ctx, t.Value, &data)...) + if diags.HasError() { + return diags + } + m.IdentityCenterConfig = fwtypes.NewListNestedObjectValueOfPtrMust[identityCenterConfigModel](ctx, &data) + default: + diags.AddError("Interface Conversion Error", fmt.Sprintf("cannot flatten %T into %T", v, m)) + } + return diags +} From 2779b8dd7378c0103a19b1941d377b1f3d288192 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:24:13 +0900 Subject: [PATCH 02/17] add aws_trasfer_web_app_customization --- .../service/transfer/web_app_customization.go | 433 ++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 internal/service/transfer/web_app_customization.go diff --git a/internal/service/transfer/web_app_customization.go b/internal/service/transfer/web_app_customization.go new file mode 100644 index 000000000000..214ff2812651 --- /dev/null +++ b/internal/service/transfer/web_app_customization.go @@ -0,0 +1,433 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package transfer + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/transfer" + awstypes "github.com/aws/aws-sdk-go-v2/service/transfer/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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-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/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + itypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_transfer_web_app_customization", name="Web App Customization") +func newResourceWebAppCustomization(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceWebAppCustomization{} + + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) + + return r, nil +} + +const ( + ResNameWebAppCustomization = "Web App Customization" +) + +type resourceWebAppCustomization struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceWebAppCustomization) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + "favicon_file": schema.StringAttribute{ + // If faviconFile is not specified when calling the UpdateWebAppCustomization API, + // the existing favicon remains unchanged. + // Therefore, this field is marked as Optional and Computed. + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 20960), + }, + }, + names.AttrID: framework.IDAttribute(), + "logo_file": schema.StringAttribute{ + // Same as favicon_file + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 51200), + }, + }, + "title": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(0, 100), + }, + }, + "web_app_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: false, + Delete: true, + }), + }, + } +} + +func (r *resourceWebAppCustomization) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().TransferClient(ctx) + + var plan resourceWebAppCustomizationModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var input transfer.UpdateWebAppCustomizationInput + resp.Diagnostics.Append(flex.Expand(ctx, plan, &input)...) + if resp.Diagnostics.HasError() { + return + } + + // Empty string values are not allowed for FaviconFile and LogoFile. + if v := plan.FaviconFile.ValueString(); v == "" { + input.FaviconFile = nil + } + if v := plan.LogoFile.ValueString(); v == "" { + input.LogoFile = nil + } + + out, err := conn.UpdateWebAppCustomization(ctx, &input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionCreating, ResNameWebAppCustomization, plan.ID.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionCreating, ResNameWebAppCustomization, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + plan.ID = flex.StringToFramework(ctx, out.WebAppId) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + _, err = waitWebAppCustomizationCreated(ctx, conn, plan.ID.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionWaitingForCreation, ResNameWebAppCustomization, plan.ID.String(), err), + err.Error(), + ) + return + } + + rout, _ := findWebAppCustomizationByID(ctx, conn, plan.ID.ValueString()) + resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan)...) + + // Set values for unknowns after creation is complete because they are marked as Computed. + if rout.FaviconFile == nil { + plan.FaviconFile = types.StringNull() + } + if rout.LogoFile == nil { + plan.LogoFile = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceWebAppCustomization) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().TransferClient(ctx) + + var state resourceWebAppCustomizationModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findWebAppCustomizationByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionReading, ResNameWebAppCustomization, state.ID.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state)...) + if resp.Diagnostics.HasError() { + return + } + + state.ID = flex.StringToFramework(ctx, out.WebAppId) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceWebAppCustomization) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().TransferClient(ctx) + + var plan, state resourceWebAppCustomizationModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Diff(ctx, plan, state) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + var input transfer.UpdateWebAppCustomizationInput + resp.Diagnostics.Append(flex.Expand(ctx, plan, &input)...) + if resp.Diagnostics.HasError() { + return + } + if v := plan.FaviconFile.ValueString(); v == "" { + input.FaviconFile = nil + } + if v := plan.LogoFile.ValueString(); v == "" { + input.LogoFile = nil + } + + out, err := conn.UpdateWebAppCustomization(ctx, &input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionUpdating, ResNameWebAppCustomization, plan.ID.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionUpdating, ResNameWebAppCustomization, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &plan)...) + if resp.Diagnostics.HasError() { + return + } + } + + rout, _ := findWebAppCustomizationByID(ctx, conn, plan.ID.ValueString()) + resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan)...) + if rout.FaviconFile == nil { + plan.FaviconFile = types.StringNull() + } + if rout.LogoFile == nil { + plan.LogoFile = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceWebAppCustomization) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().TransferClient(ctx) + + var state resourceWebAppCustomizationModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + input := transfer.DeleteWebAppCustomizationInput{ + WebAppId: state.ID.ValueStringPointer(), + } + + _, err := conn.DeleteWebAppCustomization(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionDeleting, ResNameWebAppCustomization, state.ID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitWebAppCustomizationDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Transfer, create.ErrActionWaitingForDeletion, ResNameWebAppCustomization, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceWebAppCustomization) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), req, resp) +} + +func waitWebAppCustomizationCreated(ctx context.Context, conn *transfer.Client, id string, timeout time.Duration) (*awstypes.DescribedWebAppCustomization, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + Refresh: statusWebAppCustomization(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.DescribedWebAppCustomization); ok { + return out, err + } + + return nil, err +} + +func waitWebAppCustomizationDeleted(ctx context.Context, conn *transfer.Client, id string, timeout time.Duration) (*awstypes.DescribedWebAppCustomization, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusNormal}, + Target: []string{}, + Refresh: statusWebAppCustomization(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.DescribedWebAppCustomization); ok { + return out, err + } + + return nil, err +} + +func statusWebAppCustomization(ctx context.Context, conn *transfer.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + out, err := findWebAppCustomizationByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, statusNormal, nil + } +} + +func findWebAppCustomizationByID(ctx context.Context, conn *transfer.Client, id string) (*awstypes.DescribedWebAppCustomization, error) { + input := transfer.DescribeWebAppCustomizationInput{ + WebAppId: aws.String(id), + } + + out, err := conn.DescribeWebAppCustomization(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: &input, + } + } + + return nil, err + } + + if out == nil || out.WebAppCustomization == nil { + return nil, tfresource.NewEmptyResultError(&input) + } + + return out.WebAppCustomization, nil +} + +type resourceWebAppCustomizationModel struct { + ARN types.String `tfsdk:"arn"` + FaviconFile types.String `tfsdk:"favicon_file"` + ID types.String `tfsdk:"id"` + LogoFile types.String `tfsdk:"logo_file"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Title types.String `tfsdk:"title"` + WebAppID types.String `tfsdk:"web_app_id"` +} + +var ( + _ flex.Expander = resourceWebAppCustomizationModel{} + _ flex.Flattener = &resourceWebAppCustomizationModel{} +) + +func (m resourceWebAppCustomizationModel) Expand(ctx context.Context) (any, diag.Diagnostics) { + var input transfer.UpdateWebAppCustomizationInput + input.WebAppId = m.WebAppID.ValueStringPointer() + if !m.FaviconFile.IsNull() { + input.FaviconFile = itypes.MustBase64Decode(m.FaviconFile.ValueString()) + } + if !m.LogoFile.IsNull() { + input.LogoFile = itypes.MustBase64Decode(m.LogoFile.ValueString()) + } + if !m.Title.IsNull() { + input.Title = m.Title.ValueStringPointer() + } + return &input, nil +} + +func (m *resourceWebAppCustomizationModel) Flatten(ctx context.Context, in any) diag.Diagnostics { + var diags diag.Diagnostics + switch t := in.(type) { + case awstypes.DescribedWebAppCustomization: + m.ARN = flex.StringToFramework(ctx, t.Arn) + if t.FaviconFile != nil { + m.FaviconFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.FaviconFile))) + } + m.ID = flex.StringToFramework(ctx, t.WebAppId) + if t.LogoFile != nil { + m.LogoFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.LogoFile))) + } + if t.Title != nil { + m.Title = flex.StringToFramework(ctx, t.Title) + } + m.WebAppID = flex.StringToFramework(ctx, t.WebAppId) + case transfer.UpdateWebAppCustomizationOutput: + m.WebAppID = flex.StringToFramework(ctx, t.WebAppId) + default: + diags.AddError("Interface Conversion Error", fmt.Sprintf("cannot flatten %T into %T", in, m)) + } + return diags +} From ef12f3490bcd6e82df5f18e842bfc3a1e4c28c61 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:24:57 +0900 Subject: [PATCH 03/17] make gen --- internal/service/transfer/service_package_gen.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/service/transfer/service_package_gen.go b/internal/service/transfer/service_package_gen.go index 8d84829bf2a5..5b1823df704b 100644 --- a/internal/service/transfer/service_package_gen.go +++ b/internal/service/transfer/service_package_gen.go @@ -25,7 +25,21 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceWebApp, + TypeName: "aws_transfer_web_app", + Name: "Web App", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, + { + Factory: newResourceWebAppCustomization, + TypeName: "aws_transfer_web_app_customization", + Name: "Web App Customization", + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { From 8a482eeff99f010638492f2b763b9407456eada5 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:25:10 +0900 Subject: [PATCH 04/17] add sweeper of aws_transfer_web_app --- internal/service/transfer/sweep.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/service/transfer/sweep.go b/internal/service/transfer/sweep.go index b56ba5747fb6..c8ce8844690b 100644 --- a/internal/service/transfer/sweep.go +++ b/internal/service/transfer/sweep.go @@ -28,6 +28,8 @@ func RegisterSweepers() { "aws_transfer_server", }, }) + + awsv2.Register("aws_transfer_web_app", sweepWebApps) } func sweepServers(region string) error { From aee2f5a8fc6d811b81ffdd22080ebb592f22a1f9 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:26:12 +0900 Subject: [PATCH 05/17] update exports_test --- internal/service/transfer/exports_test.go | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/service/transfer/exports_test.go b/internal/service/transfer/exports_test.go index bee2d12e1deb..b6031ba3b137 100644 --- a/internal/service/transfer/exports_test.go +++ b/internal/service/transfer/exports_test.go @@ -5,16 +5,18 @@ package transfer // Exports for use in tests only. var ( - ResourceAccess = resourceAccess - ResourceAgreement = resourceAgreement - ResourceCertificate = resourceCertificate - ResourceConnector = resourceConnector - ResourceProfile = resourceProfile - ResourceServer = resourceServer - ResourceSSHKey = resourceSSHKey - ResourceTag = resourceTag - ResourceUser = resourceUser - ResourceWorkflow = resourceWorkflow + ResourceAccess = resourceAccess + ResourceAgreement = resourceAgreement + ResourceCertificate = resourceCertificate + ResourceConnector = resourceConnector + ResourceProfile = resourceProfile + ResourceServer = resourceServer + ResourceSSHKey = resourceSSHKey + ResourceTag = resourceTag + ResourceUser = resourceUser + ResourceWebApp = newResourceWebApp + ResourceWebAppCustomization = newResourceWebAppCustomization + ResourceWorkflow = resourceWorkflow FindAccessByTwoPartKey = findAccessByTwoPartKey FindAgreementByTwoPartKey = findAgreementByTwoPartKey @@ -26,4 +28,6 @@ var ( FindUserByTwoPartKey = findUserByTwoPartKey FindUserSSHKeyByThreePartKey = findUserSSHKeyByThreePartKey FindWorkflowByID = findWorkflowByID + FindWebAppByID = findWebAppByID + FindWebAppCustomizationByID = findWebAppCustomizationByID ) From caea69ffcf60fd6127458e460cf8c6a65050477c Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:26:35 +0900 Subject: [PATCH 06/17] add acctests for aws_transfer_web_app --- internal/service/transfer/web_app_test.go | 538 ++++++++++++++++++++++ 1 file changed, 538 insertions(+) create mode 100644 internal/service/transfer/web_app_test.go diff --git a/internal/service/transfer/web_app_test.go b/internal/service/transfer/web_app_test.go new file mode 100644 index 000000000000..44643c888cf1 --- /dev/null +++ b/internal/service/transfer/web_app_test.go @@ -0,0 +1,538 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package transfer_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + awstypes "github.com/aws/aws-sdk-go-v2/service/transfer/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "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/create" + tftransfer "github.com/hashicorp/terraform-provider-aws/internal/service/transfer" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccTransferWebApp_basic(t *testing.T) { + ctx := acctest.Context(t) + + var webappBefore, webappAfter awstypes.DescribedWebApp + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappBefore), + resource.TestMatchResourceAttr(resourceName, "access_endpoint", regexache.MustCompile(`^https:\/\/.*.aws$`)), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppConfig_basic(rName+"-tag-changed", rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestMatchResourceAttr(resourceName, "access_endpoint", regexache.MustCompile(`^https:\/\/.*.aws$`)), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName+"-tag-changed"), + ), + }, + { + Config: testAccWebAppConfig_basic(rName+"-tag-changed", rName+"-tag-changed"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestMatchResourceAttr(resourceName, "access_endpoint", regexache.MustCompile(`^https:\/\/.*.aws$`)), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName+"-tag-changed"), + ), + }, + }, + }) +} + +func TestAccTransferWebApp_webAppUnits(t *testing.T) { + ctx := acctest.Context(t) + + var webappBefore, webappAfter awstypes.DescribedWebApp + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppConfig_webAppUnits(rName, 1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappBefore), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppConfig_webAppUnits(rName, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "2"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + }, + }) +} + +func TestAccTransferWebApp_accessEndpoint(t *testing.T) { + ctx := acctest.Context(t) + + var webappBefore, webappAfter awstypes.DescribedWebApp + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppConfig_accessEndPoint(rName, "https://example.com"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappBefore), + resource.TestCheckResourceAttr(resourceName, "access_endpoint", "https://example.com"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppConfig_accessEndPoint(rName, "https://example2.com"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "access_endpoint", "https://example2.com"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + }, + }) +} + +func TestAccTransferWebApp_tags(t *testing.T) { + ctx := acctest.Context(t) + + var webappBefore, webappAfter awstypes.DescribedWebApp + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + acctest.PreCheckSSOAdminInstances(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappBefore), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + { + Config: testAccWebAppConfig_noTags(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + { + Config: testAccWebAppConfig_multipleTags(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.Env", rName), + ), + }, + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, + }, + }) +} + +func TestAccTransferWebApp_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var webapp awstypes.DescribedWebApp + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webapp), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tftransfer.ResourceWebApp, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccCheckWebAppDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).TransferClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_transfer_web_app" { + continue + } + + _, err := tftransfer.FindWebAppByID(ctx, conn, rs.Primary.ID) + if tfresource.NotFound(err) { + return nil + } + if err != nil { + return create.Error(names.Transfer, create.ErrActionCheckingDestroyed, tftransfer.ResNameWebApp, rs.Primary.ID, err) + } + + return create.Error(names.Transfer, create.ErrActionCheckingDestroyed, tftransfer.ResNameWebApp, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckWebAppExists(ctx context.Context, name string, webapp *awstypes.DescribedWebApp) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebApp, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebApp, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).TransferClient(ctx) + + resp, err := tftransfer.FindWebAppByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebApp, rs.Primary.ID, err) + } + + *webapp = *resp + + return nil + } +} + +func testAccCheckWebAppNotRecreated(before, after *awstypes.DescribedWebApp) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.WebAppId), aws.ToString(after.WebAppId); before != after { + return create.Error(names.Transfer, create.ErrActionCheckingNotRecreated, tftransfer.ResNameWebApp, before, errors.New("recreated")) + } + + return nil + } +} + +func testAccWebAppConfig_base(roleName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +data "aws_ssoadmin_instances" "test" {} + +data "aws_iam_policy_document" "assume_role_transfer" { + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:SetContext" + ] + principals { + type = "Service" + identifiers = ["transfer.amazonaws.com"] + } + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "aws:SourceAccount" + } + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = data.aws_iam_policy_document.assume_role_transfer.json +} + +data "aws_iam_policy_document" "web_app_identity_bearer" { + statement { + effect = "Allow" + actions = [ + "s3:GetDataAccess", + "s3:ListCallerAccessGrants", + ] + resources = [ + "arn:aws:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" + ] + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "s3:ResourceAccount" + } + } + statement { + effect = "Allow" + actions = [ + "s3:ListAccessGrantsInstances" + ] + resources = ["*"] + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "s3:ResourceAccount" + } + } +} + +resource "aws_iam_role_policy" "web_app_identity_bearer" { + policy = data.aws_iam_policy_document.web_app_identity_bearer.json + role = aws_iam_role.test.name +} +`, roleName) +} + +func testAccWebAppConfig_basic(rName, roleName string) string { + return fmt.Sprintf(acctest.ConfigCompose( + testAccWebAppConfig_base(roleName), fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } + tags = { + Name = %[1]q + } +} +`, rName))) +} + +func testAccWebAppConfig_webAppUnits(rName string, webAppUnitsProvisioned int) string { + return fmt.Sprintf(acctest.ConfigCompose( + testAccWebAppConfig_base(rName), fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } + web_app_units { + provisioned = %[2]d + } + + tags = { + Name = %[1]q + } +} +`, rName, webAppUnitsProvisioned))) +} + +func testAccWebAppConfig_accessEndPoint(rName, accessEndPoint string) string { + return fmt.Sprintf(acctest.ConfigCompose( + testAccWebAppConfig_base(rName), fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } + access_endpoint = %[2]q + + tags = { + Name = %[1]q + } +} +`, rName, accessEndPoint))) +} + +func testAccWebAppConfig_noTags(rName string) string { + return fmt.Sprintf(acctest.ConfigCompose( + testAccWebAppConfig_base(rName), fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } +} +`))) +} + +func testAccWebAppConfig_multipleTags(rName string) string { + return fmt.Sprintf(acctest.ConfigCompose( + testAccWebAppConfig_base(rName), fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } + + tags = { + Name = %[1]q + Env = %[1]q + } + +} +`, rName))) +} From f6859274cf1714236b6f4f304c0bd6106ec153b6 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:27:00 +0900 Subject: [PATCH 07/17] add acctests for aws_transfer_web_app_customization --- .../Terraform-LogoMark_onDark.png | Bin 0 -> 4438 bytes .../Terraform-LogoMark_onLight.png | Bin 0 -> 4422 bytes .../transfer/web_app_customization_test.go | 248 ++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 internal/service/transfer/test-fixtures/Terraform-LogoMark_onDark.png create mode 100644 internal/service/transfer/test-fixtures/Terraform-LogoMark_onLight.png create mode 100644 internal/service/transfer/web_app_customization_test.go diff --git a/internal/service/transfer/test-fixtures/Terraform-LogoMark_onDark.png b/internal/service/transfer/test-fixtures/Terraform-LogoMark_onDark.png new file mode 100644 index 0000000000000000000000000000000000000000..f727329c4f717a6f5a55bb6637b9208726e49e83 GIT binary patch literal 4438 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 H18@8nCIv%e literal 0 HcmV?d00001 diff --git a/internal/service/transfer/web_app_customization_test.go b/internal/service/transfer/web_app_customization_test.go new file mode 100644 index 000000000000..3e6f853c6853 --- /dev/null +++ b/internal/service/transfer/web_app_customization_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package transfer_test + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/transfer/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "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/create" + tftransfer "github.com/hashicorp/terraform-provider-aws/internal/service/transfer" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + itypes "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccTransferWebAppCustomization_basic(t *testing.T) { + ctx := acctest.Context(t) + + var webappcustomization awstypes.DescribedWebAppCustomization + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app_customization.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppCustomizationConfig_basic(rName, "test2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test2"), + ), + }, + }, + }) +} + +func TestAccTransferWebAppCustomization_files(t *testing.T) { + ctx := acctest.Context(t) + + var webappcustomization awstypes.DescribedWebAppCustomization + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app_customization.test" + darkBytes, _ := os.ReadFile("test-fixtures/Terraform-LogoMark_onDark.png") + lightBytes, _ := os.ReadFile("test-fixtures/Terraform-LogoMark_onLight.png") + darkFileBase64Encoded := itypes.Base64Encode(darkBytes) + lightFileBase64Encoded := itypes.Base64Encode(lightBytes) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppCustomizationConfig_files(rName, "test", "Dark", "Light"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test"), + resource.TestCheckResourceAttr(resourceName, "logo_file", darkFileBase64Encoded), + resource.TestCheckResourceAttr(resourceName, "favicon_file", lightFileBase64Encoded), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppCustomizationConfig_files(rName, "test", "Light", "Dark"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test"), + resource.TestCheckResourceAttr(resourceName, "logo_file", lightFileBase64Encoded), + resource.TestCheckResourceAttr(resourceName, "favicon_file", darkFileBase64Encoded), + ), + }, + { + Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test"), + resource.TestCheckResourceAttr(resourceName, "logo_file", lightFileBase64Encoded), + resource.TestCheckResourceAttr(resourceName, "favicon_file", darkFileBase64Encoded), + ), + }, + }, + }) +} + +func TestAccTransferWebAppCustomization_disappears(t *testing.T) { + ctx := acctest.Context(t) + + var webappcustomization awstypes.DescribedWebAppCustomization + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app_customization.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tftransfer.ResourceWebAppCustomization, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccCheckWebAppCustomizationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).TransferClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_transfer_web_app_customization" { + continue + } + + _, err := tftransfer.FindWebAppCustomizationByID(ctx, conn, rs.Primary.ID) + if tfresource.NotFound(err) { + return nil + } + if err != nil { + return create.Error(names.Transfer, create.ErrActionCheckingDestroyed, tftransfer.ResNameWebAppCustomization, rs.Primary.ID, err) + } + + return create.Error(names.Transfer, create.ErrActionCheckingDestroyed, tftransfer.ResNameWebAppCustomization, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckWebAppCustomizationExists(ctx context.Context, name string, webappcustomization *awstypes.DescribedWebAppCustomization) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebAppCustomization, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebAppCustomization, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).TransferClient(ctx) + + resp, err := tftransfer.FindWebAppCustomizationByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.Transfer, create.ErrActionCheckingExistence, tftransfer.ResNameWebAppCustomization, rs.Primary.ID, err) + } + + *webappcustomization = *resp + + return nil + } +} + +func testAccWebAppCustomizationConfig_base(rName string) string { + return acctest.ConfigCompose( + testAccWebAppConfig_base(rName), + fmt.Sprintf(` +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } +} +`)) +} + +func testAccWebAppCustomizationConfig_basic(rName, title string) string { + return acctest.ConfigCompose( + testAccWebAppCustomizationConfig_base(rName), + fmt.Sprintf(` +resource "aws_transfer_web_app_customization" "test" { + web_app_id = aws_transfer_web_app.test.id + title = %[1]q +} +`, title)) +} + +func testAccWebAppCustomizationConfig_files(rName, title, logoFileSuffix, faviconFileSuffix string) string { + return acctest.ConfigCompose( + testAccWebAppCustomizationConfig_base(rName), + fmt.Sprintf(` +resource "aws_transfer_web_app_customization" "test" { + web_app_id = aws_transfer_web_app.test.id + title = %[1]q + logo_file = filebase64("test-fixtures/Terraform-LogoMark_on%[2]s.png") + favicon_file = filebase64("test-fixtures/Terraform-LogoMark_on%[3]s.png") +} +`, title, logoFileSuffix, faviconFileSuffix)) +} From 1cd797d5eacde464f13e62bac54fd1602f715450 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:27:37 +0900 Subject: [PATCH 08/17] add documentation for aws_trasfer_web_app --- website/docs/r/transfer_web_app.html.markdown | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 website/docs/r/transfer_web_app.html.markdown diff --git a/website/docs/r/transfer_web_app.html.markdown b/website/docs/r/transfer_web_app.html.markdown new file mode 100644 index 000000000000..7e8846a6a374 --- /dev/null +++ b/website/docs/r/transfer_web_app.html.markdown @@ -0,0 +1,150 @@ +--- +subcategory: "Transfer Family" +layout: "aws" +page_title: "AWS: aws_transfer_web_app" +description: |- + Terraform resource for managing an AWS Transfer Family Web App. +--- + +# Resource: aws_transfer_web_app + +Terraform resource for managing an AWS Transfer Family Web App. + +## Example Usage + +### Basic Usage + +```terraform +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +data "aws_ssoadmin_instances" "example" {} + +data "aws_iam_policy_document" "assume_role_transfer" { + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:SetContext" + ] + principals { + type = "Service" + identifiers = ["transfer.amazonaws.com"] + } + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "aws:SourceAccount" + } + } +} + +resource "aws_iam_role" "example" { + name = "example" + assume_role_policy = data.aws_iam_policy_document.assume_role_transfer.json +} + +data "aws_iam_policy_document" "example" { + statement { + effect = "Allow" + actions = [ + "s3:GetDataAccess", + "s3:ListCallerAccessGrants", + ] + resources = [ + "arn:aws:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" + ] + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "s3:ResourceAccount" + } + } + statement { + effect = "Allow" + actions = [ + "s3:ListAccessGrantsInstances" + ] + resources = ["*"] + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "s3:ResourceAccount" + } + } +} + +resource "aws_iam_role_policy" "example" { + policy = data.aws_iam_policy_document.example.json + role = aws_iam_role.example.name +} + +resource "aws_transfer_web_app" "example" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.example.arns)[0] + role = aws_iam_role.example.arn + } + } + web_app_units { + provisioned = 1 + } + tags = { + Name = "test" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `identity_provider_details` - (Required) Block for details of the identity provider to use with the web app. See [Identity provider details](#identity-provider-details) below. + +The following arguments are optional: + +* `access_endpoint` - (Optional) URL provided to interact with the Transfer Family web app. +* `tags` - (Optional) Key-value pairs that can be used to group and search for web apps. +* `web_app_endpoint_policy` - (Optional) Type of endpoint policy for the web app. Valid values are: `STANDARD`(default) or `FIPS`. +* `web_app_units` - (Optional) Block for number of concurrent connections or the user sessions on the web app. + * provisioned - (Optional) Number of units of concurrent connections. + +### Identity provider details + +* `identity_center_config` - (Optional) Block that describes the values to use for the IAM Identity Center settings. See [Identity center config](#identity-center-config) below. + +### Identity center config + +* instance_arn - (Optional) ARN of the IAM Identity Center used for the web app. +* role - (Optional) ARN of an identity bearer role for your web app. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Web App. +* `id` - ID of the Wep App resource. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `10m`) +* `delete` - (Default `10m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Transfer Family Web App using the `id`. For example: + +```terraform +import { + to = aws_transfer_web_app.example + id = "web_app-id-12345678" +} +``` + +Using `terraform import`, import Transfer Family Web App using the `example_id_arg`. For example: + +```console +% terraform import aws_transfer_web_app.example web_app-id-12345678 +``` From 74d66f6a85bfc3dd5a6d62234ff77e1e8062f108 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:28:08 +0900 Subject: [PATCH 09/17] add documentation for aws_transfer_web_app_customization --- ...ansfer_web_app_customization.html.markdown | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 website/docs/r/transfer_web_app_customization.html.markdown diff --git a/website/docs/r/transfer_web_app_customization.html.markdown b/website/docs/r/transfer_web_app_customization.html.markdown new file mode 100644 index 000000000000..bd40baf0f34d --- /dev/null +++ b/website/docs/r/transfer_web_app_customization.html.markdown @@ -0,0 +1,81 @@ +--- +subcategory: "Transfer Family" +layout: "aws" +page_title: "AWS: aws_transfer_web_app_customization" +description: |- + Terraform resource for managing an AWS Transfer Family Web App Customization. +--- + +# Resource: aws_transfer_web_app_customization + +Terraform resource for managing an AWS Transfer Family Web App Customization. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_transfer_web_app" "test" { + identity_provider_details { + identity_center_config { + instance_arn = tolist(data.aws_ssoadmin_instances.test.arns)[0] + role = aws_iam_role.test.arn + } + } + web_app_units { + provisioned = 1 + } + tags = { + Name = "test" + } +} + +resource "aws_transfer_web_app_customization" "test" { + web_app_id = aws_transfer_web_app.test.id + favicon_file = filebase64("${path.module}/favicon.png") + logo_file = filebase64("${path.module}/logo.png") + title = "test" +} +``` + +## Argument Reference + +The following arguments are required: + +* `web_app_id` - (Required) The identifier of the web app to be customized. + +The following arguments are optional: + +* `favicon_file` - (Optional) Base64-encoded string representing the favicon image. Terraform will detect drift only if this argument is specified. To remove the favicon, recreate the resource. +* `logo_file` - (Optional) Base64-encoded string representing the logo image. Terraform will detect drift only if this argument is specified. To remove the logo, recreate the resource. +* `title` – (Optional) Title of the web app. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - Same as `web_app_id`. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Transfer Family Web App Customization using the `web_app_id`. For example: + +```terraform +import { + to = aws_transfer_web_app_customization.example + id = "webapp-12345678901234567890" +} +``` + +Using `terraform import`, import Transfer Family Web App Customization using the `web_app_id`. For example: + +```console +% terraform import aws_transfer_web_app_customization.example webapp-12345678901234567890 +``` From 20793285fd8eba3f7b6d52445d2d2929e490e372 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:35:59 +0900 Subject: [PATCH 10/17] add changelog --- .changelog/42708.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/42708.txt diff --git a/.changelog/42708.txt b/.changelog/42708.txt new file mode 100644 index 000000000000..64d58e83abaf --- /dev/null +++ b/.changelog/42708.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_transfer_web_app +``` + +```release-note:new-resource +aws_transfer_web_app_customization +``` From 8b3f48e477ec5befc3d20c2d4582c29260ba1b36 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 01:38:41 +0900 Subject: [PATCH 11/17] fixed issues reported by linters --- internal/service/transfer/web_app.go | 8 +-- .../transfer/web_app_customization_test.go | 9 ++-- internal/service/transfer/web_app_test.go | 49 ++++++++++--------- website/docs/r/transfer_web_app.html.markdown | 5 +- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/internal/service/transfer/web_app.go b/internal/service/transfer/web_app.go index 42a4baed4c01..2999726cf654 100644 --- a/internal/service/transfer/web_app.go +++ b/internal/service/transfer/web_app.go @@ -126,7 +126,7 @@ func (r *resourceWebApp) Schema(ctx context.Context, req resource.SchemaRequest, stringvalidator.RegexMatches(regexache.MustCompile(`^arn:[\w-]+:sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}$`), ""), }, }, - "role": schema.StringAttribute{ + names.AttrRole: schema.StringAttribute{ Optional: true, Validators: []validator.String{ stringvalidator.LengthBetween(20, 2048), @@ -193,7 +193,7 @@ func (r *resourceWebApp) Create(ctx context.Context, req resource.CreateRequest, return } - rout, err := findWebAppByID(ctx, conn, plan.WebAppId.ValueString()) + rout, _ := findWebAppByID(ctx, conn, plan.WebAppId.ValueString()) resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan, flex.WithFieldNamePrefix("Described"))...) resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -254,7 +254,7 @@ func (r *resourceWebApp) Update(ctx context.Context, req resource.UpdateRequest, } if !state.AccessEndpoint.Equal(plan.AccessEndpoint) { - if v := plan.AccessEndpoint.ValueStringPointer(); v != nil && *v != "" { + if v := plan.AccessEndpoint.ValueStringPointer(); v != nil && aws.ToString(v) != "" { input.AccessEndpoint = v } needUpdate = true @@ -501,7 +501,7 @@ func (m webAppUnitsModel) Expand(ctx context.Context) (any, diag.Diagnostics) { switch { case !m.Provisioned.IsNull(): var apiObject awstypes.WebAppUnitsMemberProvisioned - apiObject.Value = *flex.Int32FromFrameworkInt64(ctx, &m.Provisioned) + apiObject.Value = aws.ToInt32(flex.Int32FromFrameworkInt64(ctx, &m.Provisioned)) v = &apiObject } diff --git a/internal/service/transfer/web_app_customization_test.go b/internal/service/transfer/web_app_customization_test.go index 3e6f853c6853..4b3ae7af87b3 100644 --- a/internal/service/transfer/web_app_customization_test.go +++ b/internal/service/transfer/web_app_customization_test.go @@ -210,8 +210,7 @@ func testAccCheckWebAppCustomizationExists(ctx context.Context, name string, web func testAccWebAppCustomizationConfig_base(rName string) string { return acctest.ConfigCompose( - testAccWebAppConfig_base(rName), - fmt.Sprintf(` + testAccWebAppConfig_base(rName), ` resource "aws_transfer_web_app" "test" { identity_provider_details { identity_center_config { @@ -220,7 +219,7 @@ resource "aws_transfer_web_app" "test" { } } } -`)) +`) } func testAccWebAppCustomizationConfig_basic(rName, title string) string { @@ -228,8 +227,8 @@ func testAccWebAppCustomizationConfig_basic(rName, title string) string { testAccWebAppCustomizationConfig_base(rName), fmt.Sprintf(` resource "aws_transfer_web_app_customization" "test" { - web_app_id = aws_transfer_web_app.test.id - title = %[1]q + web_app_id = aws_transfer_web_app.test.id + title = %[1]q } `, title)) } diff --git a/internal/service/transfer/web_app_test.go b/internal/service/transfer/web_app_test.go index 44643c888cf1..d3d918c352fc 100644 --- a/internal/service/transfer/web_app_test.go +++ b/internal/service/transfer/web_app_test.go @@ -52,7 +52,7 @@ func TestAccTransferWebApp_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -73,7 +73,7 @@ func TestAccTransferWebApp_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName+"-tag-changed"), ), }, @@ -89,7 +89,7 @@ func TestAccTransferWebApp_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName+"-tag-changed"), ), }, @@ -125,7 +125,7 @@ func TestAccTransferWebApp_webAppUnits(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -146,7 +146,7 @@ func TestAccTransferWebApp_webAppUnits(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "2"), resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -183,7 +183,7 @@ func TestAccTransferWebApp_accessEndpoint(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -205,7 +205,7 @@ func TestAccTransferWebApp_accessEndpoint(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -234,7 +234,7 @@ func TestAccTransferWebApp_tags(t *testing.T) { Config: testAccWebAppConfig_basic(rName, rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppExists(ctx, resourceName, &webappBefore), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -244,7 +244,7 @@ func TestAccTransferWebApp_tags(t *testing.T) { testAccCheckWebAppExists(ctx, resourceName, &webappAfter), testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), ), }, { @@ -257,7 +257,7 @@ func TestAccTransferWebApp_tags(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppExists(ctx, resourceName, &webappAfter), testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -266,7 +266,7 @@ func TestAccTransferWebApp_tags(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppExists(ctx, resourceName, &webappAfter), testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), resource.TestCheckResourceAttr(resourceName, "tags.Env", rName), ), @@ -276,7 +276,7 @@ func TestAccTransferWebApp_tags(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppExists(ctx, resourceName, &webappAfter), testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, @@ -382,6 +382,7 @@ func testAccWebAppConfig_base(roleName string) string { return fmt.Sprintf(` data "aws_caller_identity" "current" {} data "aws_region" "current" {} +data "aws_partition" "current" {} data "aws_ssoadmin_instances" "test" {} @@ -417,7 +418,7 @@ data "aws_iam_policy_document" "web_app_identity_bearer" { "s3:ListCallerAccessGrants", ] resources = [ - "arn:aws:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" + "arn:${data.aws_partition.current.partition}:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" ] condition { test = "StringEquals" @@ -447,7 +448,7 @@ resource "aws_iam_role_policy" "web_app_identity_bearer" { } func testAccWebAppConfig_basic(rName, roleName string) string { - return fmt.Sprintf(acctest.ConfigCompose( + return acctest.ConfigCompose( testAccWebAppConfig_base(roleName), fmt.Sprintf(` resource "aws_transfer_web_app" "test" { identity_provider_details { @@ -460,11 +461,11 @@ resource "aws_transfer_web_app" "test" { Name = %[1]q } } -`, rName))) +`, rName)) } func testAccWebAppConfig_webAppUnits(rName string, webAppUnitsProvisioned int) string { - return fmt.Sprintf(acctest.ConfigCompose( + return acctest.ConfigCompose( testAccWebAppConfig_base(rName), fmt.Sprintf(` resource "aws_transfer_web_app" "test" { identity_provider_details { @@ -481,11 +482,11 @@ resource "aws_transfer_web_app" "test" { Name = %[1]q } } -`, rName, webAppUnitsProvisioned))) +`, rName, webAppUnitsProvisioned)) } func testAccWebAppConfig_accessEndPoint(rName, accessEndPoint string) string { - return fmt.Sprintf(acctest.ConfigCompose( + return acctest.ConfigCompose( testAccWebAppConfig_base(rName), fmt.Sprintf(` resource "aws_transfer_web_app" "test" { identity_provider_details { @@ -500,12 +501,12 @@ resource "aws_transfer_web_app" "test" { Name = %[1]q } } -`, rName, accessEndPoint))) +`, rName, accessEndPoint)) } func testAccWebAppConfig_noTags(rName string) string { - return fmt.Sprintf(acctest.ConfigCompose( - testAccWebAppConfig_base(rName), fmt.Sprintf(` + return acctest.ConfigCompose( + testAccWebAppConfig_base(rName), ` resource "aws_transfer_web_app" "test" { identity_provider_details { identity_center_config { @@ -514,11 +515,11 @@ resource "aws_transfer_web_app" "test" { } } } -`))) +`) } func testAccWebAppConfig_multipleTags(rName string) string { - return fmt.Sprintf(acctest.ConfigCompose( + return acctest.ConfigCompose( testAccWebAppConfig_base(rName), fmt.Sprintf(` resource "aws_transfer_web_app" "test" { identity_provider_details { @@ -534,5 +535,5 @@ resource "aws_transfer_web_app" "test" { } } -`, rName))) +`, rName)) } diff --git a/website/docs/r/transfer_web_app.html.markdown b/website/docs/r/transfer_web_app.html.markdown index 7e8846a6a374..b582c324a5e2 100644 --- a/website/docs/r/transfer_web_app.html.markdown +++ b/website/docs/r/transfer_web_app.html.markdown @@ -17,6 +17,7 @@ Terraform resource for managing an AWS Transfer Family Web App. ```terraform data "aws_caller_identity" "current" {} data "aws_region" "current" {} +data "aws_partition" "current" {} data "aws_ssoadmin_instances" "example" {} @@ -52,7 +53,7 @@ data "aws_iam_policy_document" "example" { "s3:ListCallerAccessGrants", ] resources = [ - "arn:aws:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" + "arn:${data.aws_partition.current.partition}:s3:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:access-grants/*" ] condition { test = "StringEquals" @@ -90,7 +91,7 @@ resource "aws_transfer_web_app" "example" { provisioned = 1 } tags = { - Name = "test" + Name = "test" } } ``` From 59dbea7283196150689d187ad6fdee1d916a1483 Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 20:38:20 +0900 Subject: [PATCH 12/17] add test cases for aws_transfer_web_app --- internal/service/transfer/web_app_test.go | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/service/transfer/web_app_test.go b/internal/service/transfer/web_app_test.go index d3d918c352fc..9fe9fbdd2427 100644 --- a/internal/service/transfer/web_app_test.go +++ b/internal/service/transfer/web_app_test.go @@ -150,6 +150,22 @@ func TestAccTransferWebApp_webAppUnits(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "2"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, }, }) } @@ -209,6 +225,23 @@ func TestAccTransferWebApp_accessEndpoint(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), ), }, + { + Config: testAccWebAppConfig_basic(rName, rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppExists(ctx, resourceName, &webappAfter), + testAccCheckWebAppNotRecreated(&webappBefore, &webappAfter), + resource.TestCheckResourceAttr(resourceName, "access_endpoint", "https://example2.com"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "identity_provider_details.0.identity_center_config.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.instance_arn", "data.aws_ssoadmin_instances.test", "arns.0"), + resource.TestCheckResourceAttrPair(resourceName, "identity_provider_details.0.identity_center_config.0.role", "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "web_app_units.#", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_units.0.provisioned", "1"), + resource.TestCheckResourceAttr(resourceName, "web_app_endpoint_policy", "STANDARD"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + ), + }, }, }) } From ad1d51d96b9f395435369ac07279af2eb780c5ec Mon Sep 17 00:00:00 2001 From: tabito Date: Thu, 22 May 2025 20:39:01 +0900 Subject: [PATCH 13/17] fix acctests --- .../transfer/web_app_customization_test.go | 85 +++++++++++++++---- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/internal/service/transfer/web_app_customization_test.go b/internal/service/transfer/web_app_customization_test.go index 4b3ae7af87b3..985682b25868 100644 --- a/internal/service/transfer/web_app_customization_test.go +++ b/internal/service/transfer/web_app_customization_test.go @@ -42,11 +42,10 @@ func TestAccTransferWebAppCustomization_basic(t *testing.T) { CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Config: testAccWebAppCustomizationConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", "test"), ), }, { @@ -54,14 +53,63 @@ func TestAccTransferWebAppCustomization_basic(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + }, + }) +} + +func TestAccTransferWebAppCustomization_title(t *testing.T) { + ctx := acctest.Context(t) + + var webappcustomization awstypes.DescribedWebAppCustomization + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_transfer_web_app_customization.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.TransferEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.TransferServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), + Steps: []resource.TestStep{ { - Config: testAccWebAppCustomizationConfig_basic(rName, "test2"), + Config: testAccWebAppCustomizationConfig_title(rName, "test"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccWebAppCustomizationConfig_title(rName, "test2"), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), resource.TestCheckResourceAttr(resourceName, "title", "test2"), ), }, + { + Config: testAccWebAppCustomizationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test2"), + ), + }, + { + Config: testAccWebAppCustomizationConfig_title(rName, ""), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), + resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", ""), + ), + }, }, }) } @@ -88,11 +136,10 @@ func TestAccTransferWebAppCustomization_files(t *testing.T) { CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccWebAppCustomizationConfig_files(rName, "test", "Dark", "Light"), + Config: testAccWebAppCustomizationConfig_files(rName, "Dark", "Light"), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", "test"), resource.TestCheckResourceAttr(resourceName, "logo_file", darkFileBase64Encoded), resource.TestCheckResourceAttr(resourceName, "favicon_file", lightFileBase64Encoded), ), @@ -103,21 +150,19 @@ func TestAccTransferWebAppCustomization_files(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccWebAppCustomizationConfig_files(rName, "test", "Light", "Dark"), + Config: testAccWebAppCustomizationConfig_files(rName, "Light", "Dark"), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", "test"), resource.TestCheckResourceAttr(resourceName, "logo_file", lightFileBase64Encoded), resource.TestCheckResourceAttr(resourceName, "favicon_file", darkFileBase64Encoded), ), }, { - Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Config: testAccWebAppCustomizationConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", "test"), resource.TestCheckResourceAttr(resourceName, "logo_file", lightFileBase64Encoded), resource.TestCheckResourceAttr(resourceName, "favicon_file", darkFileBase64Encoded), ), @@ -144,7 +189,7 @@ func TestAccTransferWebAppCustomization_disappears(t *testing.T) { CheckDestroy: testAccCheckWebAppCustomizationDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccWebAppCustomizationConfig_basic(rName, "test"), + Config: testAccWebAppCustomizationConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tftransfer.ResourceWebAppCustomization, resourceName), @@ -222,7 +267,16 @@ resource "aws_transfer_web_app" "test" { `) } -func testAccWebAppCustomizationConfig_basic(rName, title string) string { +func testAccWebAppCustomizationConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccWebAppCustomizationConfig_base(rName), ` +resource "aws_transfer_web_app_customization" "test" { + web_app_id = aws_transfer_web_app.test.id +} +`) +} + +func testAccWebAppCustomizationConfig_title(rName, title string) string { return acctest.ConfigCompose( testAccWebAppCustomizationConfig_base(rName), fmt.Sprintf(` @@ -233,15 +287,14 @@ resource "aws_transfer_web_app_customization" "test" { `, title)) } -func testAccWebAppCustomizationConfig_files(rName, title, logoFileSuffix, faviconFileSuffix string) string { +func testAccWebAppCustomizationConfig_files(rName, logoFileSuffix, faviconFileSuffix string) string { return acctest.ConfigCompose( testAccWebAppCustomizationConfig_base(rName), fmt.Sprintf(` resource "aws_transfer_web_app_customization" "test" { web_app_id = aws_transfer_web_app.test.id - title = %[1]q - logo_file = filebase64("test-fixtures/Terraform-LogoMark_on%[2]s.png") - favicon_file = filebase64("test-fixtures/Terraform-LogoMark_on%[3]s.png") + logo_file = filebase64("test-fixtures/Terraform-LogoMark_on%[1]s.png") + favicon_file = filebase64("test-fixtures/Terraform-LogoMark_on%[2]s.png") } -`, title, logoFileSuffix, faviconFileSuffix)) +`, logoFileSuffix, faviconFileSuffix)) } From b80fece9032a66cb15a2134e2e884b5b9fa6f40e Mon Sep 17 00:00:00 2001 From: "tabito.hara" Date: Thu, 22 May 2025 16:57:19 +0900 Subject: [PATCH 14/17] tidy up Expand and Flatten processes and fix tests --- .../service/transfer/web_app_customization.go | 48 +++++++------------ .../transfer/web_app_customization_test.go | 11 +---- 2 files changed, 18 insertions(+), 41 deletions(-) diff --git a/internal/service/transfer/web_app_customization.go b/internal/service/transfer/web_app_customization.go index 214ff2812651..87dd3882e25c 100644 --- a/internal/service/transfer/web_app_customization.go +++ b/internal/service/transfer/web_app_customization.go @@ -78,7 +78,7 @@ func (r *resourceWebAppCustomization) Schema(ctx context.Context, req resource.S "title": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - stringvalidator.LengthBetween(0, 100), + stringvalidator.LengthBetween(1, 100), }, }, "web_app_id": schema.StringAttribute{ @@ -113,14 +113,6 @@ func (r *resourceWebAppCustomization) Create(ctx context.Context, req resource.C return } - // Empty string values are not allowed for FaviconFile and LogoFile. - if v := plan.FaviconFile.ValueString(); v == "" { - input.FaviconFile = nil - } - if v := plan.LogoFile.ValueString(); v == "" { - input.LogoFile = nil - } - out, err := conn.UpdateWebAppCustomization(ctx, &input) if err != nil { resp.Diagnostics.AddError( @@ -157,14 +149,6 @@ func (r *resourceWebAppCustomization) Create(ctx context.Context, req resource.C rout, _ := findWebAppCustomizationByID(ctx, conn, plan.ID.ValueString()) resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan)...) - // Set values for unknowns after creation is complete because they are marked as Computed. - if rout.FaviconFile == nil { - plan.FaviconFile = types.StringNull() - } - if rout.LogoFile == nil { - plan.LogoFile = types.StringNull() - } - resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -223,12 +207,6 @@ func (r *resourceWebAppCustomization) Update(ctx context.Context, req resource.U if resp.Diagnostics.HasError() { return } - if v := plan.FaviconFile.ValueString(); v == "" { - input.FaviconFile = nil - } - if v := plan.LogoFile.ValueString(); v == "" { - input.LogoFile = nil - } out, err := conn.UpdateWebAppCustomization(ctx, &input) if err != nil { @@ -254,12 +232,6 @@ func (r *resourceWebAppCustomization) Update(ctx context.Context, req resource.U rout, _ := findWebAppCustomizationByID(ctx, conn, plan.ID.ValueString()) resp.Diagnostics.Append(flex.Flatten(ctx, rout, &plan)...) - if rout.FaviconFile == nil { - plan.FaviconFile = types.StringNull() - } - if rout.LogoFile == nil { - plan.LogoFile = types.StringNull() - } resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } @@ -396,14 +368,20 @@ var ( func (m resourceWebAppCustomizationModel) Expand(ctx context.Context) (any, diag.Diagnostics) { var input transfer.UpdateWebAppCustomizationInput input.WebAppId = m.WebAppID.ValueStringPointer() - if !m.FaviconFile.IsNull() { + if !m.FaviconFile.IsNull() && m.FaviconFile.ValueString() != "" { input.FaviconFile = itypes.MustBase64Decode(m.FaviconFile.ValueString()) + } else { + input.FaviconFile = nil } - if !m.LogoFile.IsNull() { + if !m.LogoFile.IsNull() && m.LogoFile.ValueString() != "" { input.LogoFile = itypes.MustBase64Decode(m.LogoFile.ValueString()) + } else { + input.LogoFile = nil } - if !m.Title.IsNull() { + if !m.Title.IsNull() && m.Title.ValueString() != "" { input.Title = m.Title.ValueStringPointer() + } else { + input.Title = aws.String("") } return &input, nil } @@ -415,13 +393,19 @@ func (m *resourceWebAppCustomizationModel) Flatten(ctx context.Context, in any) m.ARN = flex.StringToFramework(ctx, t.Arn) if t.FaviconFile != nil { m.FaviconFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.FaviconFile))) + } else { + m.FaviconFile = types.StringNull() } m.ID = flex.StringToFramework(ctx, t.WebAppId) if t.LogoFile != nil { m.LogoFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.LogoFile))) + } else { + m.LogoFile = types.StringNull() } if t.Title != nil { m.Title = flex.StringToFramework(ctx, t.Title) + } else { + m.Title = types.StringNull() } m.WebAppID = flex.StringToFramework(ctx, t.WebAppId) case transfer.UpdateWebAppCustomizationOutput: diff --git a/internal/service/transfer/web_app_customization_test.go b/internal/service/transfer/web_app_customization_test.go index 985682b25868..0ed7e29ea7ef 100644 --- a/internal/service/transfer/web_app_customization_test.go +++ b/internal/service/transfer/web_app_customization_test.go @@ -79,6 +79,7 @@ func TestAccTransferWebAppCustomization_title(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), + resource.TestCheckResourceAttr(resourceName, "title", "test"), ), }, { @@ -99,15 +100,7 @@ func TestAccTransferWebAppCustomization_title(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", "test2"), - ), - }, - { - Config: testAccWebAppCustomizationConfig_title(rName, ""), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckWebAppCustomizationExists(ctx, resourceName, &webappcustomization), - resource.TestCheckResourceAttrPair(resourceName, "web_app_id", "aws_transfer_web_app.test", names.AttrID), - resource.TestCheckResourceAttr(resourceName, "title", ""), + resource.TestCheckNoResourceAttr(resourceName, "title"), ), }, }, From 6325253972a3d821eb26fc6e02c964fdc84a3fb2 Mon Sep 17 00:00:00 2001 From: tabito Date: Fri, 23 May 2025 00:26:28 +0900 Subject: [PATCH 15/17] add note about "title" in `aws_transfer_web_app_customization` --- website/docs/r/transfer_web_app_customization.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/transfer_web_app_customization.html.markdown b/website/docs/r/transfer_web_app_customization.html.markdown index bd40baf0f34d..d78922465383 100644 --- a/website/docs/r/transfer_web_app_customization.html.markdown +++ b/website/docs/r/transfer_web_app_customization.html.markdown @@ -48,7 +48,7 @@ The following arguments are optional: * `favicon_file` - (Optional) Base64-encoded string representing the favicon image. Terraform will detect drift only if this argument is specified. To remove the favicon, recreate the resource. * `logo_file` - (Optional) Base64-encoded string representing the logo image. Terraform will detect drift only if this argument is specified. To remove the logo, recreate the resource. -* `title` – (Optional) Title of the web app. +* `title` – (Optional) Title of the web app. Must be between 1 and 100 characters in length (an empty string is not allowed). To remove the title, omit this argument from your configuration. ## Attribute Reference From de751556dd0576a32e954c1e5b0cd5ed839f9f68 Mon Sep 17 00:00:00 2001 From: tabito Date: Sat, 24 May 2025 01:51:28 +0900 Subject: [PATCH 16/17] Simplified and add error checks for Base64Decode --- .../service/transfer/web_app_customization.go | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/internal/service/transfer/web_app_customization.go b/internal/service/transfer/web_app_customization.go index 87dd3882e25c..c839fffbb8b2 100644 --- a/internal/service/transfer/web_app_customization.go +++ b/internal/service/transfer/web_app_customization.go @@ -65,6 +65,9 @@ func (r *resourceWebAppCustomization) Schema(ctx context.Context, req resource.S Validators: []validator.String{ stringvalidator.LengthBetween(1, 20960), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, names.AttrID: framework.IDAttribute(), "logo_file": schema.StringAttribute{ @@ -74,6 +77,9 @@ func (r *resourceWebAppCustomization) Schema(ctx context.Context, req resource.S Validators: []validator.String{ stringvalidator.LengthBetween(1, 51200), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "title": schema.StringAttribute{ Optional: true, @@ -367,14 +373,31 @@ var ( func (m resourceWebAppCustomizationModel) Expand(ctx context.Context) (any, diag.Diagnostics) { var input transfer.UpdateWebAppCustomizationInput + var diags diag.Diagnostics input.WebAppId = m.WebAppID.ValueStringPointer() + if !m.FaviconFile.IsNull() && m.FaviconFile.ValueString() != "" { - input.FaviconFile = itypes.MustBase64Decode(m.FaviconFile.ValueString()) + if v, err := itypes.Base64Decode(m.FaviconFile.ValueString()); err != nil { + diags.AddError( + "Favicon File Decode Error", + "An unexpected error occurred while decoding the Favicon File. ", + ) + + } else { + input.FaviconFile = v + } } else { input.FaviconFile = nil } if !m.LogoFile.IsNull() && m.LogoFile.ValueString() != "" { - input.LogoFile = itypes.MustBase64Decode(m.LogoFile.ValueString()) + if v, err := itypes.Base64Decode(m.LogoFile.ValueString()); err != nil { + diags.AddError( + "Logo File Decode Error", + "An unexpected error occurred while decoding the Logo File. ", + ) + } else { + input.LogoFile = v + } } else { input.LogoFile = nil } @@ -391,22 +414,10 @@ func (m *resourceWebAppCustomizationModel) Flatten(ctx context.Context, in any) switch t := in.(type) { case awstypes.DescribedWebAppCustomization: m.ARN = flex.StringToFramework(ctx, t.Arn) - if t.FaviconFile != nil { - m.FaviconFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.FaviconFile))) - } else { - m.FaviconFile = types.StringNull() - } + m.FaviconFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.FaviconFile))) m.ID = flex.StringToFramework(ctx, t.WebAppId) - if t.LogoFile != nil { - m.LogoFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.LogoFile))) - } else { - m.LogoFile = types.StringNull() - } - if t.Title != nil { - m.Title = flex.StringToFramework(ctx, t.Title) - } else { - m.Title = types.StringNull() - } + m.LogoFile = flex.StringToFramework(ctx, aws.String(itypes.Base64Encode(t.LogoFile))) + m.Title = flex.StringToFramework(ctx, t.Title) m.WebAppID = flex.StringToFramework(ctx, t.WebAppId) case transfer.UpdateWebAppCustomizationOutput: m.WebAppID = flex.StringToFramework(ctx, t.WebAppId) From 136eb512900e2cbcb005becff943f262a78ba208 Mon Sep 17 00:00:00 2001 From: tabito Date: Sat, 24 May 2025 02:27:48 +0900 Subject: [PATCH 17/17] fixed an issue reported by linter --- internal/service/transfer/web_app_customization.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/service/transfer/web_app_customization.go b/internal/service/transfer/web_app_customization.go index c839fffbb8b2..f0a326964ab7 100644 --- a/internal/service/transfer/web_app_customization.go +++ b/internal/service/transfer/web_app_customization.go @@ -382,7 +382,6 @@ func (m resourceWebAppCustomizationModel) Expand(ctx context.Context) (any, diag "Favicon File Decode Error", "An unexpected error occurred while decoding the Favicon File. ", ) - } else { input.FaviconFile = v }