diff --git a/.changelog/35311.txt b/.changelog/35311.txt new file mode 100644 index 00000000000..a2cdda5a420 --- /dev/null +++ b/.changelog/35311.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_m2_environment +``` \ No newline at end of file diff --git a/.changelog/35399.txt b/.changelog/35399.txt new file mode 100644 index 00000000000..a6430d00102 --- /dev/null +++ b/.changelog/35399.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_m2_application +``` diff --git a/.changelog/35408.txt b/.changelog/35408.txt new file mode 100644 index 00000000000..541d3dde981 --- /dev/null +++ b/.changelog/35408.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_m2_deployment +``` diff --git a/internal/framework/flex/auto_expand.go b/internal/framework/flex/auto_expand.go index a65a1aa1b30..9f81d45c8b8 100644 --- a/internal/framework/flex/auto_expand.go +++ b/internal/framework/flex/auto_expand.go @@ -622,10 +622,24 @@ func (expander autoExpander) nestedObjectCollection(ctx context.Context, vFrom f diags.Append(expander.nestedObjectToSlice(ctx, vFrom, tTo, tElem, vTo)...) return diags } + + case reflect.Interface: + // + // types.List(OfObject) -> []interface. + // + // Smithy union type handling not yet implemented. Silently skip. + return diags } + + case reflect.Interface: + // + // types.List(OfObject) -> interface. + // + // Smithy union type handling not yet implemented. Silently skip. + return diags } - diags.AddError("Incompatible types", fmt.Sprintf("nestedObject[%s] cannot be expanded to %s", vFrom.Type(ctx).(attr.TypeWithElementType).ElementType(), vTo.Kind())) + diags.AddError("Incompatible types", fmt.Sprintf("nestedObjectCollection[%s] cannot be expanded to %s", vFrom.Type(ctx).(attr.TypeWithElementType).ElementType(), vTo.Kind())) return diags } diff --git a/internal/framework/flex/auto_flatten.go b/internal/framework/flex/auto_flatten.go index 541183c81d1..6f5f224f7ea 100644 --- a/internal/framework/flex/auto_flatten.go +++ b/internal/framework/flex/auto_flatten.go @@ -89,6 +89,10 @@ func (flattener autoFlattener) convert(ctx context.Context, vFrom, vTo reflect.V case reflect.Struct: diags.Append(flattener.struct_(ctx, vFrom, false, tTo, vTo)...) return diags + + case reflect.Interface: + // Smithy union type handling not yet implemented. Silently skip. + return diags } tflog.Info(ctx, "AutoFlex Flatten; incompatible types", map[string]interface{}{ @@ -494,6 +498,10 @@ func (flattener autoFlattener) slice(ctx context.Context, vFrom reflect.Value, t diags.Append(flattener.sliceOfStructNestedObjectCollection(ctx, vFrom, tTo, vTo)...) return diags } + + case reflect.Interface: + // Smithy union type handling not yet implemented. Silently skip. + return diags } tflog.Info(ctx, "AutoFlex Flatten; incompatible types", map[string]interface{}{ diff --git a/internal/framework/types/once_a_week_window.go b/internal/framework/types/once_a_week_window.go new file mode 100644 index 00000000000..81d282de1f5 --- /dev/null +++ b/internal/framework/types/once_a_week_window.go @@ -0,0 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package types + +import ( + "context" + "fmt" + "strings" + + "github.com/YakDriver/regexache" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ xattr.TypeWithValidate = (*onceAWeekWindowType)(nil) + _ basetypes.StringTypable = (*onceAWeekWindowType)(nil) + _ basetypes.StringValuable = (*OnceAWeekWindow)(nil) + _ basetypes.StringValuableWithSemanticEquals = (*OnceAWeekWindow)(nil) +) + +type onceAWeekWindowType struct { + basetypes.StringType +} + +var ( + OnceAWeekWindowType = onceAWeekWindowType{} +) + +func (t onceAWeekWindowType) Equal(o attr.Type) bool { + other, ok := o.(onceAWeekWindowType) + if !ok { + return false + } + + return t.StringType.Equal(other.StringType) +} + +func (onceAWeekWindowType) String() string { + return "OnceAWeekWindowType" +} + +func (t onceAWeekWindowType) ValueFromString(_ context.Context, in types.String) (basetypes.StringValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + if in.IsNull() { + return OnceAWeekWindowNull(), diags + } + if in.IsUnknown() { + return OnceAWeekWindowUnknown(), diags + } + + return OnceAWeekWindowValue(in.ValueString()), diags +} + +func (t onceAWeekWindowType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} + +func (onceAWeekWindowType) ValueType(context.Context) attr.Value { + return OnceAWeekWindow{} +} + +func (t onceAWeekWindowType) Validate(ctx context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { + var diags diag.Diagnostics + + if !in.IsKnown() || in.IsNull() { + return diags + } + + var value string + err := in.As(&value) + if err != nil { + diags.AddAttributeError( + path, + "OnceAWeekWindowType Validation Error", + ProviderErrorDetailPrefix+fmt.Sprintf("Cannot convert value to string: %s", err), + ) + return diags + } + + // Valid time format is "ddd:hh24:mi". + validTimeFormat := "(sun|mon|tue|wed|thu|fri|sat):([0-1][0-9]|2[0-3]):([0-5][0-9])" + validTimeFormatConsolidated := "^(" + validTimeFormat + "-" + validTimeFormat + "|)$" + + if v := strings.ToLower(value); !regexache.MustCompile(validTimeFormatConsolidated).MatchString(v) { + diags.AddAttributeError( + path, + "OnceAWeekWindowType Validation Error", + fmt.Sprintf("Value %q must satisfy the format of \"ddd:hh24:mi-ddd:hh24:mi\".", value), + ) + return diags + } + + return diags +} + +type OnceAWeekWindow struct { + basetypes.StringValue +} + +func OnceAWeekWindowNull() OnceAWeekWindow { + return OnceAWeekWindow{StringValue: basetypes.NewStringNull()} +} + +func OnceAWeekWindowUnknown() OnceAWeekWindow { + return OnceAWeekWindow{StringValue: basetypes.NewStringUnknown()} +} + +func OnceAWeekWindowValue(value string) OnceAWeekWindow { + return OnceAWeekWindow{StringValue: basetypes.NewStringValue(value)} +} + +func (v OnceAWeekWindow) Equal(o attr.Value) bool { + other, ok := o.(OnceAWeekWindow) + if !ok { + return false + } + + return v.StringValue.Equal(other.StringValue) +} + +func (OnceAWeekWindow) Type(context.Context) attr.Type { + return OnceAWeekWindowType +} + +func (v OnceAWeekWindow) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(OnceAWeekWindow) + if !ok { + return false, diags + } + + old, d := v.ToStringValue(ctx) + diags.Append(d...) + if diags.HasError() { + return false, diags + } + + new, d := newValue.ToStringValue(ctx) + diags.Append(d...) + if diags.HasError() { + return false, diags + } + + // Case insensitive comparison. + return strings.EqualFold(old.ValueString(), new.ValueString()), diags +} diff --git a/internal/framework/types/once_a_week_window_test.go b/internal/framework/types/once_a_week_window_test.go new file mode 100644 index 00000000000..498e32e43f6 --- /dev/null +++ b/internal/framework/types/once_a_week_window_test.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package types_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" +) + +func TestOnceAWeekWindowTypeValidate(t *testing.T) { + t.Parallel() + + type testCase struct { + val tftypes.Value + expectError bool + } + tests := map[string]testCase{ + "not a string": { + val: tftypes.NewValue(tftypes.Bool, true), + expectError: true, + }, + "unknown string": { + val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + "null string": { + val: tftypes.NewValue(tftypes.String, nil), + }, + "valid string lowercase": { + val: tftypes.NewValue(tftypes.String, "thu:07:44-thu:09:44"), + }, + "valid string uppercase": { + val: tftypes.NewValue(tftypes.String, "THU:07:44-THU:09:44"), + }, + "invalid string": { + val: tftypes.NewValue(tftypes.String, "thu:25:44-zat:09:88"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + diags := fwtypes.OnceAWeekWindowType.Validate(ctx, test.val, path.Root("test")) + + if !diags.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if diags.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %#v", diags) + } + }) + } +} + +func TestOnceAWeekWindowStringSemanticEquals(t *testing.T) { + t.Parallel() + + type testCase struct { + val1, val2 fwtypes.OnceAWeekWindow + equals bool + } + tests := map[string]testCase{ + "both lowercase, equal": { + val1: fwtypes.OnceAWeekWindowValue("thu:07:44-thu:09:44"), + val2: fwtypes.OnceAWeekWindowValue("thu:07:44-thu:09:44"), + equals: true, + }, + "both uppercase, equal": { + val1: fwtypes.OnceAWeekWindowValue("THU:07:44-THU:09:44"), + val2: fwtypes.OnceAWeekWindowValue("THU:07:44-THU:09:44"), + equals: true, + }, + "first uppercase, second lowercase, equal": { + val1: fwtypes.OnceAWeekWindowValue("THU:07:44-THU:09:44"), + val2: fwtypes.OnceAWeekWindowValue("thu:07:44-thu:09:44"), + equals: true, + }, + "first lowercase, second uppercase, equal": { + val1: fwtypes.OnceAWeekWindowValue("thu:07:44-thu:09:44"), + val2: fwtypes.OnceAWeekWindowValue("THU:07:44-THU:09:44"), + equals: true, + }, + "not equal": { + val1: fwtypes.OnceAWeekWindowValue("thu:07:44-thu:09:44"), + val2: fwtypes.OnceAWeekWindowValue("thu:07:44-fri:11:09"), + equals: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + equals, _ := test.val1.StringSemanticEquals(ctx, test.val2) + + if got, want := equals, test.equals; got != want { + t.Errorf("StringSemanticEquals(%q, %q) = %v, want %v", test.val1, test.val2, got, want) + } + }) + } +} diff --git a/internal/service/elbv2/export_test.go b/internal/service/elbv2/exports_test.go similarity index 100% rename from internal/service/elbv2/export_test.go rename to internal/service/elbv2/exports_test.go diff --git a/internal/service/m2/application.go b/internal/service/m2/application.go new file mode 100644 index 00000000000..f6998fab422 --- /dev/null +++ b/internal/service/m2/application.go @@ -0,0 +1,642 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2 + +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/m2" + awstypes "github.com/aws/aws-sdk-go-v2/service/m2/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/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" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Application") +// @Tags(identifierAttribute="arn") +func newApplicationResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &applicationResource{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +type applicationResource struct { + framework.ResourceWithConfigure + framework.WithImportByID + framework.WithTimeouts +} + +func (*applicationResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_m2_application" +} + +func (r *applicationResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "application_id": framework.IDAttribute(), + names.AttrARN: framework.ARNAttributeComputedOnly(), + "current_version": schema.Int64Attribute{ + Computed: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(500), + }, + }, + "engine_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.EngineType](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrID: framework.IDAttribute(), + "kms_key_id": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_\-]{1,59}$`), ""), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "definition": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[definitionModel](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "content": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 65000), + stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("content"), + path.MatchRelative().AtParent().AtName("s3_location"), + ), + }, + }, + "s3_location": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *applicationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data applicationResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + name := data.Name.ValueString() + input := &m2.CreateApplicationInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + // AutoFlEx doesn't yet handle union types. + if !data.Definition.IsNull() { + definitionData, diags := data.Definition.ToPtr(ctx) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + input.Definition = expandDefinition(definitionData) + } + + // Additional fields. + input.ClientToken = aws.String(sdkid.UniqueId()) + input.Tags = getTagsIn(ctx) + + outputRaw, err := tfresource.RetryWhenIsAErrorMessageContains[*awstypes.AccessDeniedException](ctx, propagationTimeout, func() (interface{}, error) { + return conn.CreateApplication(ctx, input) + }, "does not have proper Trust Policy for M2 service") + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating Mainframe Modernization Application (%s)", name), err.Error()) + + return + } + + // Set values for unknowns. + data.ApplicationID = fwflex.StringToFramework(ctx, outputRaw.(*m2.CreateApplicationOutput).ApplicationId) + data.setID() + + app, err := waitApplicationCreated(ctx, conn, data.ID.ValueString(), r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.State.SetAttribute(ctx, path.Root(names.AttrID), data.ID) // Set 'id' so as to taint the resource. + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Application (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, app, &data)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + data.CurrentVersion = fwflex.Int32ToFramework(ctx, app.LatestVersion.ApplicationVersion) + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *applicationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data applicationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().M2Client(ctx) + + outputGA, err := findApplicationByID(ctx, conn, data.ID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Mainframe Modernization Application (%s)", data.ID.ValueString()), err.Error()) + + return + } + + applicationVersion := aws.ToInt32(outputGA.LatestVersion.ApplicationVersion) + outputGAV, err := findApplicationVersionByTwoPartKey(ctx, conn, data.ID.ValueString(), applicationVersion) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Mainframe Modernization Application (%s) version (%d)", data.ID.ValueString(), applicationVersion), err.Error()) + + return + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, outputGA, &data)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + data.CurrentVersion = fwflex.Int32ToFramework(ctx, outputGAV.ApplicationVersion) + data.Definition = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &definitionModel{ + Content: fwflex.StringToFramework(ctx, outputGAV.DefinitionContent), + S3Location: types.StringNull(), + }) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *applicationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new applicationResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + if !new.Definition.Equal(old.Definition) || !new.Description.Equal(old.Description) { + input := &m2.UpdateApplicationInput{ + ApplicationId: fwflex.StringFromFramework(ctx, new.ID), + CurrentApplicationVersion: fwflex.Int32FromFramework(ctx, old.CurrentVersion), + } + + if !new.Definition.Equal(old.Definition) { + // AutoFlEx doesn't yet handle union types. + if !new.Definition.IsNull() { + definitionData, diags := new.Definition.ToPtr(ctx) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + input.Definition = expandDefinition(definitionData) + } + } + + if !new.Description.Equal(old.Description) { + input.Description = fwflex.StringFromFramework(ctx, new.Description) + } + + outputUA, err := conn.UpdateApplication(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Mainframe Modernization Application (%s)", new.ID.ValueString()), err.Error()) + + return + } + + applicationVersion := aws.ToInt32(outputUA.ApplicationVersion) + if _, err := waitApplicationUpdated(ctx, conn, new.ID.ValueString(), applicationVersion, r.UpdateTimeout(ctx, new.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Application (%s) update", new.ID.ValueString()), err.Error()) + + return + } + + new.CurrentVersion = types.Int64Value(int64(applicationVersion)) + } else { + new.CurrentVersion = old.CurrentVersion + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *applicationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data applicationResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + _, err := conn.DeleteApplication(ctx, &m2.DeleteApplicationInput{ + ApplicationId: aws.String(data.ID.ValueString()), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Mainframe Modernization Application (%s)", data.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitApplicationDeleted(ctx, conn, data.ID.ValueString(), r.DeleteTimeout(ctx, data.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Application (%s) delete", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *applicationResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func startApplication(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { //nolint:unparam + input := &m2.StartApplicationInput{ + ApplicationId: aws.String(id), + } + + _, err := conn.StartApplication(ctx, input) + + if err != nil { + return nil, err + } + + return waitApplicationRunning(ctx, conn, id, timeout) +} + +func stopApplicationIfRunning(ctx context.Context, conn *m2.Client, id string, forceStop bool, timeout time.Duration) (*m2.GetApplicationOutput, error) { //nolint:unparam + app, err := findApplicationByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, nil + } + + if err != nil { + return nil, err + } + + if app.Status != awstypes.ApplicationLifecycleRunning { + return nil, nil + } + + input := &m2.StopApplicationInput{ + ApplicationId: aws.String(id), + ForceStop: forceStop, + } + + _, err = conn.StopApplication(ctx, input) + + if err != nil { + return nil, err + } + + return waitApplicationStopped(ctx, conn, id, timeout) +} + +func findApplicationByID(ctx context.Context, conn *m2.Client, id string) (*m2.GetApplicationOutput, error) { + input := &m2.GetApplicationInput{ + ApplicationId: aws.String(id), + } + + output, err := conn.GetApplication(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ApplicationId == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func findApplicationVersionByTwoPartKey(ctx context.Context, conn *m2.Client, id string, version int32) (*m2.GetApplicationVersionOutput, error) { + input := &m2.GetApplicationVersionInput{ + ApplicationId: aws.String(id), + ApplicationVersion: aws.Int32(version), + } + + output, err := conn.GetApplicationVersion(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.ApplicationVersion == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func statusApplication(ctx context.Context, conn *m2.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findApplicationByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func statusApplicationVersion(ctx context.Context, conn *m2.Client, id string, version int32) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findApplicationVersionByTwoPartKey(ctx, conn, id, version) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitApplicationCreated(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationLifecycleCreating), + Target: enum.Slice(awstypes.ApplicationLifecycleCreated, awstypes.ApplicationLifecycleAvailable), + Refresh: statusApplication(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitApplicationUpdated(ctx context.Context, conn *m2.Client, id string, version int32, timeout time.Duration) (*m2.GetApplicationVersionOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationVersionLifecycleCreating), + Target: enum.Slice(awstypes.ApplicationVersionLifecycleAvailable), + Refresh: statusApplicationVersion(ctx, conn, id, version), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationVersionOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitApplicationDeleted(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationLifecycleDeleting, awstypes.ApplicationLifecycleDeletingFromEnvironment), + Target: []string{}, + Refresh: statusApplication(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitApplicationDeletedFromEnvironment(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationLifecycleDeletingFromEnvironment), + Target: enum.Slice(awstypes.ApplicationLifecycleAvailable), + Refresh: statusApplication(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitApplicationStopped(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationLifecycleStopping), + Target: enum.Slice(awstypes.ApplicationLifecycleStopped), + Refresh: statusApplication(ctx, conn, id), + Timeout: timeout, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitApplicationRunning(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetApplicationOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ApplicationLifecycleStarting), + Target: enum.Slice(awstypes.ApplicationLifecycleRunning), + Refresh: statusApplication(ctx, conn, id), + Timeout: timeout, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetApplicationOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +type applicationResourceModel struct { + ApplicationID types.String `tfsdk:"application_id"` + ApplicationARN types.String `tfsdk:"arn"` + CurrentVersion types.Int64 `tfsdk:"current_version"` + Definition fwtypes.ListNestedObjectValueOf[definitionModel] `tfsdk:"definition"` + Description types.String `tfsdk:"description"` + EngineType fwtypes.StringEnum[awstypes.EngineType] `tfsdk:"engine_type"` + ID types.String `tfsdk:"id"` + KmsKeyID types.String `tfsdk:"kms_key_id"` + Name types.String `tfsdk:"name"` + RoleARN fwtypes.ARN `tfsdk:"role_arn"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (model *applicationResourceModel) InitFromID() error { + model.ApplicationID = model.ID + + return nil +} + +func (model *applicationResourceModel) setID() { + model.ID = model.ApplicationID +} + +type definitionModel struct { + Content types.String `tfsdk:"content"` + S3Location types.String `tfsdk:"s3_location"` +} + +func expandDefinition(definitionData *definitionModel) awstypes.Definition { + if !definitionData.Content.IsNull() { + return &awstypes.DefinitionMemberContent{ + Value: definitionData.Content.ValueString(), + } + } + + if !definitionData.S3Location.IsNull() { + return &awstypes.DefinitionMemberS3Location{ + Value: definitionData.S3Location.ValueString(), + } + } + + return nil +} diff --git a/internal/service/m2/application_test.go b/internal/service/m2/application_test.go new file mode 100644 index 00000000000..c01b4a0b39f --- /dev/null +++ b/internal/service/m2/application_test.go @@ -0,0 +1,450 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/m2" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfm2 "github.com/hashicorp/terraform-provider-aws/internal/service/m2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccM2Application_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var application m2.GetApplicationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_application.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccApplicationConfig_basic(rName, "bluage"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttrSet(resourceName, "application_id"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "m2", regexache.MustCompile(`app/.+`)), + resource.TestCheckResourceAttr(resourceName, "current_version", "1"), + resource.TestCheckResourceAttr(resourceName, "definition.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "definition.0.content"), + resource.TestCheckNoResourceAttr(resourceName, "definition.0.s3_location"), + resource.TestCheckNoResourceAttr(resourceName, "description"), + resource.TestCheckResourceAttr(resourceName, "engine_type", "bluage"), + resource.TestCheckNoResourceAttr(resourceName, "kms_key_id"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckNoResourceAttr(resourceName, "role_arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Application_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var application m2.GetApplicationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_application.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccApplicationPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccApplicationConfig_basic(rName, "bluage"), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfm2.ResourceApplication, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccM2Application_tags(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_application.test" + var application m2.GetApplicationOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.M2), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccCheckApplicationDestroy(ctx), + ), + Steps: []resource.TestStep{ + { + Config: testAccApplicationConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccApplicationConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccApplicationConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccM2Application_full(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_application.test" + var application m2.GetApplicationOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.M2), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccCheckApplicationDestroy(ctx), + ), + Steps: []resource.TestStep{ + { + Config: testAccApplicationConfig_full(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttrSet(resourceName, "application_id"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "m2", regexache.MustCompile(`app/.+`)), + resource.TestCheckResourceAttr(resourceName, "current_version", "1"), + resource.TestCheckResourceAttr(resourceName, "definition.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "definition.0.content"), + resource.TestCheckNoResourceAttr(resourceName, "definition.0.s3_location"), + resource.TestCheckResourceAttr(resourceName, "description", "testing"), + resource.TestCheckResourceAttr(resourceName, "engine_type", "bluage"), + resource.TestCheckResourceAttrSet(resourceName, "kms_key_id"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "role_arn"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Application_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var application m2.GetApplicationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_application.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckApplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccApplicationConfig_versioned(rName, "bluage", 1, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "current_version", "1"), + ), + }, + { + Config: testAccApplicationConfig_versioned(rName, "bluage", 2, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckApplicationExists(ctx, resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "current_version", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckApplicationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_m2_application" { + continue + } + + _, err := tfm2.FindEnvironmentByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Mainframe Modernization Application %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckApplicationExists(ctx context.Context, n string, v *m2.GetApplicationOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + output, err := tfm2.FindApplicationByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccApplicationPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + input := &m2.ListApplicationsInput{} + _, err := conn.ListApplications(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccApplicationConfig_basic(rName, engineType string) string { + return testAccApplicationConfig_versioned(rName, engineType, 1, 1) +} + +func testAccApplicationConfig_versioned(rName, engineType string, version, versions int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + count = %[4]d + + bucket = aws_s3_bucket.test.id + key = "v${count.index + 1}/PlanetsDemo-v${count.index + 1}.zip" + source = "test-fixtures/PlanetsDemo-v1.zip" +} + +resource "aws_m2_application" "test" { + name = %[1]q + engine_type = %[2]q + definition { + content = templatefile("test-fixtures/application-definition.json", { s3_bucket = aws_s3_bucket.test.id, version = %[3]d }) + } + + depends_on = [aws_s3_object.test] +} +`, rName, engineType, version, versions) +} + +func testAccApplicationConfig_full(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.id + key = "v1/PlanetsDemo-v1.zip" + source = "test-fixtures/PlanetsDemo-v1.zip" +} + +resource "aws_m2_application" "test" { + name = %[1]q + engine_type = "bluage" + description = "testing" + kms_key_id = aws_kms_key.test.arn + role_arn = aws_iam_role.test.arn + definition { + content = templatefile("test-fixtures/application-definition.json", { s3_bucket = aws_s3_bucket.test.id, version = "v1" }) + } + + depends_on = [aws_s3_object.test, aws_iam_role_policy.test] +} + +resource "aws_kms_key" "test" { + description = %[1]q +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "m2.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy" "test" { + name = %[1]q + role = aws_iam_role.test.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue", + "kms:Decrypt", + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} +`, rName) +} + +func testAccApplicationConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.id + key = "v1/PlanetsDemo-v1.zip" + source = "test-fixtures/PlanetsDemo-v1.zip" +} + +resource "aws_m2_application" "test" { + name = %[1]q + engine_type = "bluage" + definition { + content = templatefile("test-fixtures/application-definition.json", { s3_bucket = aws_s3_bucket.test.id, version = "v1" }) + } + + tags = { + %[2]q = %[3]q + } + + depends_on = [aws_s3_object.test] +} +`, rName, tagKey1, tagValue1) +} + +func testAccApplicationConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.id + key = "v1/PlanetsDemo-v1.zip" + source = "test-fixtures/PlanetsDemo-v1.zip" +} + +resource "aws_m2_application" "test" { + name = %[1]q + engine_type = "bluage" + definition { + content = templatefile("test-fixtures/application-definition.json", { s3_bucket = aws_s3_bucket.test.id, version = "v1" }) + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } + + depends_on = [aws_s3_object.test] +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/m2/consts.go b/internal/service/m2/consts.go new file mode 100644 index 00000000000..ace006bc5d1 --- /dev/null +++ b/internal/service/m2/consts.go @@ -0,0 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2 + +import ( + "time" +) + +const ( + propagationTimeout = 2 * time.Minute +) diff --git a/internal/service/m2/deployment.go b/internal/service/m2/deployment.go new file mode 100644 index 00000000000..178a8f72bb4 --- /dev/null +++ b/internal/service/m2/deployment.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2 + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/m2" + awstypes "github.com/aws/aws-sdk-go-v2/service/m2/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "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/types" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Deployment") +func newDeploymentResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &deploymentResource{} + + r.SetDefaultCreateTimeout(60 * time.Minute) + r.SetDefaultUpdateTimeout(60 * time.Minute) + r.SetDefaultDeleteTimeout(60 * time.Minute) + + return r, nil +} + +type deploymentResource struct { + framework.ResourceWithConfigure + framework.WithImportByID + framework.WithTimeouts +} + +func (*deploymentResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_m2_deployment" +} + +func (r *deploymentResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "application_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "application_version": schema.Int64Attribute{ + Required: true, + }, + "deployment_id": schema.StringAttribute{ + Computed: true, + }, + "environment_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "force_stop": schema.BoolAttribute{ + Optional: true, + }, + names.AttrID: framework.IDAttribute(), + "start": schema.BoolAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *deploymentResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data deploymentResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + input := &m2.CreateDeploymentInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + input.ClientToken = aws.String(sdkid.UniqueId()) + + output, err := conn.CreateDeployment(ctx, input) + + if err != nil { + response.Diagnostics.AddError("creating Mainframe Modernization Deployment", err.Error()) + + return + } + + // Set values for unknowns. + data.DeploymentID = fwflex.StringToFramework(ctx, output.DeploymentId) + data.setID() + + timeout := r.CreateTimeout(ctx, data.Timeouts) + if _, err := waitDeploymentCreated(ctx, conn, data.ApplicationID.ValueString(), data.DeploymentID.ValueString(), timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Deployment (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + if data.Start.ValueBool() { + applicationID := data.ApplicationID.ValueString() + if _, err := startApplication(ctx, conn, applicationID, timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("starting Mainframe Modernization Application (%s)", applicationID), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *deploymentResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data deploymentResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().M2Client(ctx) + + outputGD, err := findDeploymentByTwoPartKey(ctx, conn, data.ApplicationID.ValueString(), data.DeploymentID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Mainframe Modernization Deployment (%s)", data.ID.ValueString()), err.Error()) + + return + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, outputGD, &data)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + outputGA, err := findApplicationByID(ctx, conn, data.ApplicationID.ValueString()) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Mainframe Modernization Application (%s)", data.ApplicationID.ValueString()), err.Error()) + + return + } + + data.Start = types.BoolValue(outputGA.Status == awstypes.ApplicationLifecycleRunning) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *deploymentResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new deploymentResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + timeout := r.UpdateTimeout(ctx, new.Timeouts) + if !new.ApplicationVersion.Equal(old.ApplicationVersion) { + applicationID := new.ApplicationID.ValueString() + + // Stop the application if it was running. + if old.Start.ValueBool() { + if _, err := stopApplicationIfRunning(ctx, conn, applicationID, new.ForceStop.ValueBool(), timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("stopping Mainframe Modernization Application (%s)", applicationID), err.Error()) + + return + } + } + + input := &m2.CreateDeploymentInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + if response.Diagnostics.HasError() { + return + } + + // Additional fields. + input.ClientToken = aws.String(sdkid.UniqueId()) + + output, err := conn.CreateDeployment(ctx, input) + + if err != nil { + response.Diagnostics.AddError("creating Mainframe Modernization Deployment", err.Error()) + + return + } + + // Set values for unknowns. + new.DeploymentID = fwflex.StringToFramework(ctx, output.DeploymentId) + new.setID() + + if _, err := waitDeploymentUpdated(ctx, conn, new.ApplicationID.ValueString(), new.DeploymentID.ValueString(), timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Deployment (%s) update", new.ID.ValueString()), err.Error()) + + return + } + } + + // Start the application if plan says to. + if new.Start.ValueBool() { + applicationID := new.ApplicationID.ValueString() + if _, err := startApplication(ctx, conn, applicationID, timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("starting Mainframe Modernization Application (%s)", applicationID), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, new)...) +} + +func (r *deploymentResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data deploymentResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + timeout := r.DeleteTimeout(ctx, data.Timeouts) + if data.Start.ValueBool() { + applicationID := data.ApplicationID.ValueString() + if _, err := stopApplicationIfRunning(ctx, conn, applicationID, data.ForceStop.ValueBool(), timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("stopping Mainframe Modernization Application (%s)", applicationID), err.Error()) + + return + } + } + + _, err := conn.DeleteApplicationFromEnvironment(ctx, &m2.DeleteApplicationFromEnvironmentInput{ + ApplicationId: fwflex.StringFromFramework(ctx, data.ApplicationID), + EnvironmentId: fwflex.StringFromFramework(ctx, data.EnvironmentID), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Mainframe Modernization Deployment (%s)", data.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitApplicationDeletedFromEnvironment(ctx, conn, data.ApplicationID.ValueString(), timeout); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Deployment (%s) delete", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *deploymentResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + if !request.State.Raw.IsNull() && !request.Plan.Raw.IsNull() { + var plan, state deploymentResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return + } + + if !plan.ApplicationVersion.Equal(state.ApplicationVersion) { + // If the ApplicationVersion changes, ID becomes unknown. + plan.ID = types.StringUnknown() + } + + response.Diagnostics.Append(response.Plan.Set(ctx, &plan)...) + } +} + +func findDeploymentByTwoPartKey(ctx context.Context, conn *m2.Client, applicationID, deploymentID string) (*m2.GetDeploymentOutput, error) { + input := &m2.GetDeploymentInput{ + ApplicationId: aws.String(applicationID), + DeploymentId: aws.String(deploymentID), + } + + output, err := conn.GetDeployment(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func statusDeployment(ctx context.Context, conn *m2.Client, applicationID, deploymentID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findDeploymentByTwoPartKey(ctx, conn, applicationID, deploymentID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitDeploymentCreated(ctx context.Context, conn *m2.Client, applicationID, deploymentID string, timeout time.Duration) (*m2.GetDeploymentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.DeploymentLifecycleDeploying), + Target: enum.Slice(awstypes.DeploymentLifecycleSucceeded), + Refresh: statusDeployment(ctx, conn, applicationID, deploymentID), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetDeploymentOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitDeploymentUpdated(ctx context.Context, conn *m2.Client, applicationID, deploymentID string, timeout time.Duration) (*m2.GetDeploymentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.DeploymentLifecycleDeployUpdate), + Target: enum.Slice(awstypes.DeploymentLifecycleSucceeded), + Refresh: statusDeployment(ctx, conn, applicationID, deploymentID), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetDeploymentOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +type deploymentResourceModel struct { + ApplicationID types.String `tfsdk:"application_id"` + ApplicationVersion types.Int64 `tfsdk:"application_version"` + DeploymentID types.String `tfsdk:"deployment_id"` + EnvironmentID types.String `tfsdk:"environment_id"` + ForceStop types.Bool `tfsdk:"force_stop"` + ID types.String `tfsdk:"id"` + Start types.Bool `tfsdk:"start"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +const ( + deploymentResourceIDPartCount = 2 +) + +func (data *deploymentResourceModel) InitFromID() error { + id := data.ID.ValueString() + parts, err := flex.ExpandResourceId(id, deploymentResourceIDPartCount, false) + + if err != nil { + return err + } + + data.ApplicationID = types.StringValue(parts[0]) + data.DeploymentID = types.StringValue(parts[1]) + + return nil +} + +func (data *deploymentResourceModel) setID() { + data.ID = types.StringValue(errs.Must(flex.FlattenResourceId([]string{data.ApplicationID.ValueString(), data.DeploymentID.ValueString()}, deploymentResourceIDPartCount, false))) +} diff --git a/internal/service/m2/deployment_test.go b/internal/service/m2/deployment_test.go new file mode 100644 index 00000000000..95c6cbc314c --- /dev/null +++ b/internal/service/m2/deployment_test.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/m2" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfm2 "github.com/hashicorp/terraform-provider-aws/internal/service/m2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccM2Deployment_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var deployment m2.GetDeploymentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_deployment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDeploymentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentConfig_basic(rName, "bluage", 1, 1, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resourceName, &deployment), + resource.TestCheckResourceAttr(resourceName, "application_version", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Deployment_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var deployment m2.GetDeploymentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_deployment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDeploymentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentConfig_basic(rName, "bluage", 1, 1, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resourceName, &deployment), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfm2.ResourceDeployment, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccM2Deployment_nostart(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var deployment m2.GetDeploymentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_deployment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDeploymentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentConfig_basic(rName, "bluage", 1, 1, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resourceName, &deployment), + resource.TestCheckResourceAttr(resourceName, "application_version", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Deployment_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var deployment m2.GetDeploymentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_deployment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDeploymentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDeploymentConfig_basic(rName, "bluage", 1, 1, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resourceName, &deployment), + resource.TestCheckResourceAttr(resourceName, "application_version", "1"), + ), + }, + { + Config: testAccDeploymentConfig_basic(rName, "bluage", 2, 2, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resourceName, &deployment), + resource.TestCheckResourceAttr(resourceName, "application_version", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckDeploymentDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_m2_deployment" { + continue + } + + _, err := tfm2.FindDeploymentByTwoPartKey(ctx, conn, rs.Primary.Attributes["application_id"], rs.Primary.Attributes["deployment_id"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Mainframe Modernization Deployment %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckDeploymentExists(ctx context.Context, n string, v *m2.GetDeploymentOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + output, err := tfm2.FindDeploymentByTwoPartKey(ctx, conn, rs.Primary.Attributes["application_id"], rs.Primary.Attributes["deployment_id"]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccDeploymentConfig_basic(rName, engineType string, appVersion, deployVersion int, start bool) string { + return acctest.ConfigCompose(testAccEnvironmentConfig_base(rName), testAccApplicationConfig_versioned(rName, engineType, appVersion, 2), fmt.Sprintf(` +resource "aws_m2_environment" "test" { + name = %[1]q + engine_type = %[2]q + instance_type = "M2.m5.large" + + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id +} + +data "aws_region" "current" {} + +resource "aws_vpc_endpoint" "secretsmanager" { + vpc_id = aws_vpc.test.id + service_name = "com.amazonaws.${data.aws_region.current.name}.secretsmanager" + vpc_endpoint_type = "Interface" + + security_group_ids = [ + aws_security_group.test.id, + ] + subnet_ids = aws_subnet.test[*].id + + private_dns_enabled = true + + tags = { + Name = %[1]q + } +} + +resource "aws_m2_deployment" "test" { + environment_id = aws_m2_environment.test.id + application_id = aws_m2_application.test.id + application_version = %[3]d + start = %[4]t + depends_on = [aws_vpc_endpoint.secretsmanager] +} +`, rName, engineType, deployVersion, start)) +} diff --git a/internal/service/m2/environment.go b/internal/service/m2/environment.go new file mode 100644 index 00000000000..ea74ae2cd48 --- /dev/null +++ b/internal/service/m2/environment.go @@ -0,0 +1,715 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2 + +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/m2" + awstypes "github.com/aws/aws-sdk-go-v2/service/m2/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Environment") +// @Tags(identifierAttribute="arn") +func newEnvironmentResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &environmentResource{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +type environmentResource struct { + framework.ResourceWithConfigure + framework.WithImportByID + framework.WithTimeouts +} + +func (*environmentResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_m2_environment" +} + +func (r *environmentResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "apply_changes_during_maintenance_window": schema.BoolAttribute{ + Optional: true, + }, + names.AttrARN: framework.ARNAttributeComputedOnly(), + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(500), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "engine_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.EngineType](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "engine_version": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^\S{1,10}$`), ""), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "environment_id": framework.IDAttribute(), + "force_update": schema.BoolAttribute{ + Optional: true, + }, + names.AttrID: framework.IDAttribute(), + "instance_type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^\S{1,20}$`), ""), + }, + }, + "kms_key_id": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "load_balancer_arn": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_\-]{1,59}$`), ""), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "preferred_maintenance_window": schema.StringAttribute{ + CustomType: fwtypes.OnceAWeekWindowType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "publicly_accessible": schema.BoolAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + boolplanmodifier.UseStateForUnknown(), + }, + }, + "security_group_ids": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + setplanmodifier.UseStateForUnknown(), + }, + }, + "subnet_ids": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(2), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + setplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "high_availability_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[highAvailabilityConfigModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "desired_capacity": schema.Int64Attribute{ + Required: true, + Validators: []validator.Int64{ + int64validator.Between(1, 100), + }, + }, + }, + }, + }, + "storage_configuration": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[storageConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "efs": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[efsStorageConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("efs"), + path.MatchRelative().AtParent().AtName("fsx"), + ), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "file_system_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "mount_point": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "fsx": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[fsxStorageConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "file_system_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "mount_point": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + }, + }, + }, + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *environmentResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data environmentResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + name := data.Name.ValueString() + input := &m2.CreateEnvironmentInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + // AutoFlEx doesn't yet handle union types. + if !data.StorageConfigurations.IsNull() { + storageConfigurationsData, diags := data.StorageConfigurations.ToSlice(ctx) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + storageConfigurations, diags := expandStorageConfigurations(ctx, storageConfigurationsData) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + input.StorageConfigurations = storageConfigurations + } + + // Additional fields. + input.ClientToken = aws.String(sdkid.UniqueId()) + input.Tags = getTagsIn(ctx) + + output, err := conn.CreateEnvironment(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating Mainframe Modernization Environment (%s)", name), err.Error()) + + return + } + + // Set values for unknowns. + data.EnvironmentID = fwflex.StringToFramework(ctx, output.EnvironmentId) + data.setID() + + env, err := waitEnvironmentCreated(ctx, conn, data.ID.ValueString(), r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.State.SetAttribute(ctx, path.Root(names.AttrID), data.ID) // Set 'id' so as to taint the resource. + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Environment (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, env, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *environmentResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data environmentResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().M2Client(ctx) + + output, err := findEnvironmentByID(ctx, conn, data.ID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Mainframe Modernization Environment (%s)", data.ID.ValueString()), err.Error()) + + return + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + // AutoFlEx doesn't yet handle union types. + if output.StorageConfigurations != nil { + storageConfigurationsData, diags := flattenStorageConfigurations(ctx, output.StorageConfigurations) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + data.StorageConfigurations = fwtypes.NewListNestedObjectValueOfSliceMust(ctx, storageConfigurationsData) + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *environmentResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new environmentResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + if !new.EngineVersion.Equal(old.EngineVersion) || + !new.HighAvailabilityConfig.Equal(old.HighAvailabilityConfig) || + !new.InstanceType.Equal(old.InstanceType) || + !new.PreferredMaintenanceWindow.Equal(old.PreferredMaintenanceWindow) { + input := &m2.UpdateEnvironmentInput{ + EnvironmentId: fwflex.StringFromFramework(ctx, new.ID), + } + + if !new.ForceUpdate.IsNull() { + input.ForceUpdate = new.ForceUpdate.ValueBool() + } + + // https://docs.aws.amazon.com/m2/latest/APIReference/API_UpdateEnvironment.html#m2-UpdateEnvironment-request-applyDuringMaintenanceWindow. + // "Currently, AWS Mainframe Modernization accepts the engineVersion parameter only if applyDuringMaintenanceWindow is true. If any parameter other than engineVersion is provided in UpdateEnvironmentRequest, it will fail if applyDuringMaintenanceWindow is set to true." + if new.ApplyDuringMaintenanceWindow.ValueBool() && !new.EngineVersion.Equal(old.EngineVersion) { + input.ApplyDuringMaintenanceWindow = true + input.EngineVersion = fwflex.StringFromFramework(ctx, new.EngineVersion) + } else { + if !new.EngineVersion.Equal(old.EngineVersion) { + input.EngineVersion = fwflex.StringFromFramework(ctx, new.EngineVersion) + } + if !new.HighAvailabilityConfig.Equal(old.HighAvailabilityConfig) { + highAvailabilityConfigData, diags := new.HighAvailabilityConfig.ToPtr(ctx) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + input.DesiredCapacity = fwflex.Int32FromFramework(ctx, highAvailabilityConfigData.DesiredCapacity) + } + if !new.InstanceType.Equal(old.InstanceType) { + input.InstanceType = fwflex.StringFromFramework(ctx, new.InstanceType) + } + if !new.PreferredMaintenanceWindow.Equal(old.PreferredMaintenanceWindow) { + input.PreferredMaintenanceWindow = fwflex.StringFromFramework(ctx, new.PreferredMaintenanceWindow) + } + } + _, err := conn.UpdateEnvironment(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Mainframe Modernization Environment (%s)", new.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitEnvironmentUpdated(ctx, conn, new.ID.ValueString(), r.UpdateTimeout(ctx, new.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Environment (%s) update", new.ID.ValueString()), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *environmentResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data environmentResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().M2Client(ctx) + + _, err := conn.DeleteEnvironment(ctx, &m2.DeleteEnvironmentInput{ + EnvironmentId: aws.String(data.ID.ValueString()), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Mainframe Modernization Environment (%s)", data.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitEnvironmentDeleted(ctx, conn, data.ID.ValueString(), r.DeleteTimeout(ctx, data.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Mainframe Modernization Environment (%s) delete", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *environmentResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func findEnvironmentByID(ctx context.Context, conn *m2.Client, id string) (*m2.GetEnvironmentOutput, error) { + input := &m2.GetEnvironmentInput{ + EnvironmentId: aws.String(id), + } + + output, err := conn.GetEnvironment(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func statusEnvironment(ctx context.Context, conn *m2.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findEnvironmentByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitEnvironmentCreated(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.EnvironmentLifecycleCreating), + Target: enum.Slice(awstypes.EnvironmentLifecycleAvailable), + Refresh: statusEnvironment(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetEnvironmentOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitEnvironmentUpdated(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.EnvironmentLifecycleUpdating), + Target: enum.Slice(awstypes.EnvironmentLifecycleAvailable), + Refresh: statusEnvironment(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetEnvironmentOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +func waitEnvironmentDeleted(ctx context.Context, conn *m2.Client, id string, timeout time.Duration) (*m2.GetEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.EnvironmentLifecycleAvailable, awstypes.EnvironmentLifecycleCreating, awstypes.EnvironmentLifecycleDeleting), + Target: []string{}, + Refresh: statusEnvironment(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*m2.GetEnvironmentOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +type environmentResourceModel struct { + ApplyDuringMaintenanceWindow types.Bool `tfsdk:"apply_changes_during_maintenance_window"` + Description types.String `tfsdk:"description"` + EngineType fwtypes.StringEnum[awstypes.EngineType] `tfsdk:"engine_type"` + EngineVersion types.String `tfsdk:"engine_version"` + EnvironmentARN types.String `tfsdk:"arn"` + EnvironmentID types.String `tfsdk:"environment_id"` + ForceUpdate types.Bool `tfsdk:"force_update"` + HighAvailabilityConfig fwtypes.ListNestedObjectValueOf[highAvailabilityConfigModel] `tfsdk:"high_availability_config"` + ID types.String `tfsdk:"id"` + InstanceType types.String `tfsdk:"instance_type"` + KmsKeyID fwtypes.ARN `tfsdk:"kms_key_id"` + LoadBalancerArn types.String `tfsdk:"load_balancer_arn"` + Name types.String `tfsdk:"name"` + PreferredMaintenanceWindow fwtypes.OnceAWeekWindow `tfsdk:"preferred_maintenance_window"` + PubliclyAccessible types.Bool `tfsdk:"publicly_accessible"` + SecurityGroupIDs fwtypes.SetValueOf[types.String] `tfsdk:"security_group_ids"` + StorageConfigurations fwtypes.ListNestedObjectValueOf[storageConfigurationModel] `tfsdk:"storage_configuration"` + SubnetIDs fwtypes.SetValueOf[types.String] `tfsdk:"subnet_ids"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (model *environmentResourceModel) InitFromID() error { + model.EnvironmentID = model.ID + + return nil +} + +func (model *environmentResourceModel) setID() { + model.ID = model.EnvironmentID +} + +type storageConfigurationModel struct { + EFS fwtypes.ListNestedObjectValueOf[efsStorageConfigurationModel] `tfsdk:"efs"` + FSX fwtypes.ListNestedObjectValueOf[fsxStorageConfigurationModel] `tfsdk:"fsx"` +} + +type efsStorageConfigurationModel struct { + FileSystemID types.String `tfsdk:"file_system_id"` + MountPoint types.String `tfsdk:"mount_point"` +} + +type fsxStorageConfigurationModel struct { + FileSystemID types.String `tfsdk:"file_system_id"` + MountPoint types.String `tfsdk:"mount_point"` +} + +type highAvailabilityConfigModel struct { + DesiredCapacity types.Int64 `tfsdk:"desired_capacity"` +} + +func expandStorageConfigurations(ctx context.Context, storageConfigurationsData []*storageConfigurationModel) ([]awstypes.StorageConfiguration, diag.Diagnostics) { + var diags diag.Diagnostics + apiObjects := []awstypes.StorageConfiguration{} + + for _, item := range storageConfigurationsData { + if !item.EFS.IsNull() { + efsStorageConfigurationData, d := item.EFS.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + apiObject := &awstypes.StorageConfigurationMemberEfs{} + diags.Append(fwflex.Expand(ctx, efsStorageConfigurationData, &apiObject.Value)...) + if diags.HasError() { + return nil, diags + } + + apiObjects = append(apiObjects, apiObject) + } + if !item.FSX.IsNull() { + fsxStorageConfigurationData, d := item.FSX.ToPtr(ctx) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + apiObject := &awstypes.StorageConfigurationMemberFsx{} + diags.Append(fwflex.Expand(ctx, fsxStorageConfigurationData, &apiObject.Value)...) + if diags.HasError() { + return nil, diags + } + + apiObjects = append(apiObjects, apiObject) + } + } + + return apiObjects, diags +} + +func flattenStorageConfigurations(ctx context.Context, apiObjects []awstypes.StorageConfiguration) ([]*storageConfigurationModel, diag.Diagnostics) { + var diags diag.Diagnostics + var storageConfigurationsData []*storageConfigurationModel + + for _, apiObject := range apiObjects { + switch v := apiObject.(type) { + case *awstypes.StorageConfigurationMemberEfs: + var efsStorageConfigurationData efsStorageConfigurationModel + d := fwflex.Flatten(ctx, v.Value, &efsStorageConfigurationData) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + storageConfigurationsData = append(storageConfigurationsData, &storageConfigurationModel{ + EFS: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &efsStorageConfigurationData), + FSX: fwtypes.NewListNestedObjectValueOfNull[fsxStorageConfigurationModel](ctx), + }) + + case *awstypes.StorageConfigurationMemberFsx: + var fsxStorageConfigurationData fsxStorageConfigurationModel + d := fwflex.Flatten(ctx, v.Value, &fsxStorageConfigurationData) + diags.Append(d...) + if diags.HasError() { + return nil, diags + } + + storageConfigurationsData = append(storageConfigurationsData, &storageConfigurationModel{ + EFS: fwtypes.NewListNestedObjectValueOfNull[efsStorageConfigurationModel](ctx), + FSX: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &fsxStorageConfigurationData), + }) + } + } + + return storageConfigurationsData, diags +} diff --git a/internal/service/m2/environment_test.go b/internal/service/m2/environment_test.go new file mode 100644 index 00000000000..2f68f563318 --- /dev/null +++ b/internal/service/m2/environment_test.go @@ -0,0 +1,588 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/m2" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfm2 "github.com/hashicorp/terraform-provider-aws/internal/service/m2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccM2Environment_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var environment m2.GetEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_basic(rName, "bluage"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckNoResourceAttr(resourceName, "apply_changes_during_maintenance_window"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "m2", regexache.MustCompile(`env/+.`)), + resource.TestCheckNoResourceAttr(resourceName, "description"), + resource.TestCheckResourceAttr(resourceName, "engine_type", "bluage"), + resource.TestCheckResourceAttrSet(resourceName, "engine_version"), + resource.TestCheckResourceAttrSet(resourceName, "environment_id"), + resource.TestCheckNoResourceAttr(resourceName, "force_update"), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "instance_type", "M2.m5.large"), + resource.TestCheckNoResourceAttr(resourceName, "kms_key_id"), + resource.TestCheckResourceAttrSet(resourceName, "load_balancer_arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "preferred_maintenance_window"), + resource.TestCheckResourceAttr(resourceName, "publicly_accessible", "false"), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "security_group_ids.#", 0), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.#", "0"), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "subnet_ids.#", 0), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Environment_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var environment m2.GetEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_basic(rName, "bluage"), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfm2.ResourceEnvironment, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccM2Environment_tags(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + var environment m2.GetEnvironmentOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.M2), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEnvironmentConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccEnvironmentConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccM2Environment_full(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + var environment m2.GetEnvironmentOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.M2), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_full(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckNoResourceAttr(resourceName, "apply_changes_during_maintenance_window"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "m2", regexache.MustCompile(`env/+.`)), + resource.TestCheckResourceAttr(resourceName, "description", "Test-1"), + resource.TestCheckResourceAttr(resourceName, "engine_type", "microfocus"), + resource.TestCheckResourceAttr(resourceName, "engine_version", "8.0.10"), + resource.TestCheckResourceAttrSet(resourceName, "environment_id"), + resource.TestCheckNoResourceAttr(resourceName, "force_update"), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.0.desired_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "instance_type", "M2.m5.large"), + resource.TestCheckResourceAttrSet(resourceName, "kms_key_id"), + resource.TestCheckResourceAttrSet(resourceName, "load_balancer_arn"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "preferred_maintenance_window"), + resource.TestCheckResourceAttr(resourceName, "publicly_accessible", "false"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccM2Environment_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var environment m2.GetEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_update(rName, "M2.m5.large", 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.0.desired_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "instance_type", "M2.m5.large"), + resource.TestCheckResourceAttr(resourceName, "preferred_maintenance_window", "sat:03:35-sat:05:35"), + ), + }, + { + Config: testAccEnvironmentConfig_update(rName, "M2.m6i.large", 3), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "high_availability_config.0.desired_capacity", "3"), + resource.TestCheckResourceAttr(resourceName, "instance_type", "M2.m6i.large"), + resource.TestCheckResourceAttr(resourceName, "preferred_maintenance_window", "sat:03:35-sat:05:35"), + ), + }, + }, + }) +} + +func TestAccM2Environment_efs(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var environment m2.GetEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_efsComplete(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "storage_configuration.0.efs.0.file_system_id"), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.0.efs.0.mount_point", "/m2/mount/efsexample"), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.0.fsx.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} +func TestAccM2Environment_fsx(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var environment m2.GetEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_m2_environment.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.M2EndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.M2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEnvironmentConfig_fsxComplete(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEnvironmentExists(ctx, resourceName, &environment), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.0.efs.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, "storage_configuration.0.fsx.0.file_system_id"), + resource.TestCheckResourceAttr(resourceName, "storage_configuration.0.fsx.0.mount_point", "/m2/mount/fsxexample"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckEnvironmentDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_m2_environment" { + continue + } + + _, err := tfm2.FindEnvironmentByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Mainframe Modernization Environment %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckEnvironmentExists(ctx context.Context, n string, v *m2.GetEnvironmentOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + output, err := tfm2.FindEnvironmentByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).M2Client(ctx) + + input := &m2.ListEnvironmentsInput{} + _, err := conn.ListEnvironments(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccEnvironmentConfig_base(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptInDefaultExclude(), fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + count = 2 + + vpc_id = aws_vpc.test.id + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [aws_vpc.test.cidr_block] + } +} +`, rName)) +} + +func testAccEnvironmentConfig_basic(rName, engineType string) string { + return fmt.Sprintf(` +resource "aws_m2_environment" "test" { + name = %[1]q + engine_type = %[2]q + instance_type = "M2.m5.large" +} +`, rName, engineType) +} + +func testAccEnvironmentConfig_full(rName string) string { + return acctest.ConfigCompose(testAccEnvironmentConfig_base(rName), fmt.Sprintf(` +resource "aws_m2_environment" "test" { + description = "Test-1" + engine_type = "microfocus" + engine_version = "8.0.10" + + high_availability_config { + desired_capacity = 5 + } + + instance_type = "M2.m5.large" + kms_key_id = aws_kms_key.test.arn + name = %[1]q + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_kms_key" "test" { + description = %[1]q +} +`, rName)) +} + +func testAccEnvironmentConfig_update(rName, instanceType string, desiredCapacity int32) string { + return acctest.ConfigCompose(testAccEnvironmentConfig_base(rName), fmt.Sprintf(` +resource "aws_m2_environment" "test" { + name = %[1]q + engine_type = "bluage" + engine_version = "3.7.0" + instance_type = %[2]q + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id + + preferred_maintenance_window = "sat:03:35-sat:05:35" + + high_availability_config { + desired_capacity = %[3]d + } +} +`, rName, instanceType, desiredCapacity)) +} + +func testAccEnvironmentConfig_efsComplete(rName string) string { + return acctest.ConfigCompose(testAccEnvironmentConfig_base(rName), + fmt.Sprintf(` +resource "aws_efs_file_system" "test" { + tags = { + Name = %[1]q + } +} + +resource "aws_efs_access_point" "test" { + file_system_id = aws_efs_file_system.test.id + + root_directory { + path = "/" + } + + tags = { + Name = %[1]q + } +} + +resource "aws_efs_mount_target" "test" { + count = 2 + + file_system_id = aws_efs_file_system.test.id + subnet_id = aws_subnet.test[count.index].id + security_groups = [aws_security_group.test.id] +} + +resource "aws_m2_environment" "test" { + name = %[1]q + engine_type = "bluage" + engine_version = "3.7.0" + instance_type = "M2.m5.large" + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id + + storage_configuration { + efs { + file_system_id = aws_efs_file_system.test.id + mount_point = "/m2/mount/efsexample" + } + } +} +`, rName)) +} + +func testAccEnvironmentConfig_fsxComplete(rName string) string { + return acctest.ConfigCompose(testAccEnvironmentConfig_base(rName), fmt.Sprintf(` +resource "aws_fsx_lustre_file_system" "test" { + storage_capacity = 1200 + subnet_ids = [aws_subnet.test[0].id] + security_group_ids = [aws_security_group.test.id] + + tags = { + Name = %[1]q + } +} + +resource "aws_m2_environment" "test" { + name = %[1]q + engine_type = "bluage" + engine_version = "3.7.0" + instance_type = "M2.m5.large" + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id + + storage_configuration { + fsx { + file_system_id = aws_fsx_lustre_file_system.test.id + mount_point = "/m2/mount/fsxexample" + } + } +} +`, rName)) +} + +func testAccEnvironmentConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_m2_environment" "test" { + engine_type = "microfocus" + instance_type = "M2.m5.large" + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccEnvironmentConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_m2_environment" "test" { + engine_type = "microfocus" + instance_type = "M2.m5.large" + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/m2/exports_test.go b/internal/service/m2/exports_test.go new file mode 100644 index 00000000000..98ccd25e91c --- /dev/null +++ b/internal/service/m2/exports_test.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package m2 + +// Exports for use in tests only. +var ( + ResourceApplication = newApplicationResource + ResourceDeployment = newDeploymentResource + ResourceEnvironment = newEnvironmentResource + + FindApplicationByID = findApplicationByID + FindDeploymentByTwoPartKey = findDeploymentByTwoPartKey + FindEnvironmentByID = findEnvironmentByID +) diff --git a/internal/service/m2/generate.go b/internal/service/m2/generate.go index d8fa11d9053..e3b3812b734 100644 --- a/internal/service/m2/generate.go +++ b/internal/service/m2/generate.go @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 //go:generate go run ../../generate/servicepackage/main.go +//go:generate go run ../../generate/tags/main.go -ListTags -ServiceTagsMap -UpdateTags -KVTValues -AWSSDKVersion=2 -SkipTypesImp // ONLY generate directives and package declaration! Do not add anything else to this file package m2 diff --git a/internal/service/m2/service_package_gen.go b/internal/service/m2/service_package_gen.go index ef6d98b3311..bc6f58f47f2 100644 --- a/internal/service/m2/service_package_gen.go +++ b/internal/service/m2/service_package_gen.go @@ -19,7 +19,26 @@ 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: newApplicationResource, + Name: "Application", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, + { + Factory: newDeploymentResource, + Name: "Deployment", + }, + { + Factory: newEnvironmentResource, + Name: "Environment", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/internal/service/m2/tags_gen.go b/internal/service/m2/tags_gen.go new file mode 100644 index 00000000000..ed7c2bed720 --- /dev/null +++ b/internal/service/m2/tags_gen.go @@ -0,0 +1,128 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package m2 + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/m2" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/logging" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/types/option" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// listTags lists m2 service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func listTags(ctx context.Context, conn *m2.Client, identifier string, optFns ...func(*m2.Options)) (tftags.KeyValueTags, error) { + input := &m2.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(ctx, input, optFns...) + + if err != nil { + return tftags.New(ctx, nil), err + } + + return KeyValueTags(ctx, output.Tags), nil +} + +// ListTags lists m2 service tags and set them in Context. +// It is called from outside this package. +func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier string) error { + tags, err := listTags(ctx, meta.(*conns.AWSClient).M2Client(ctx), identifier) + + if err != nil { + return err + } + + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(tags) + } + + return nil +} + +// map[string]string handling + +// Tags returns m2 service tags. +func Tags(tags tftags.KeyValueTags) map[string]string { + return tags.Map() +} + +// KeyValueTags creates tftags.KeyValueTags from m2 service tags. +func KeyValueTags(ctx context.Context, tags map[string]string) tftags.KeyValueTags { + return tftags.New(ctx, tags) +} + +// getTagsIn returns m2 service tags from Context. +// nil is returned if there are no input tags. +func getTagsIn(ctx context.Context) map[string]string { + if inContext, ok := tftags.FromContext(ctx); ok { + if tags := Tags(inContext.TagsIn.UnwrapOrDefault()); len(tags) > 0 { + return tags + } + } + + return nil +} + +// setTagsOut sets m2 service tags in Context. +func setTagsOut(ctx context.Context, tags map[string]string) { + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(KeyValueTags(ctx, tags)) + } +} + +// updateTags updates m2 service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func updateTags(ctx context.Context, conn *m2.Client, identifier string, oldTagsMap, newTagsMap any, optFns ...func(*m2.Options)) error { + oldTags := tftags.New(ctx, oldTagsMap) + newTags := tftags.New(ctx, newTagsMap) + + ctx = tflog.SetField(ctx, logging.KeyResourceId, identifier) + + removedTags := oldTags.Removed(newTags) + removedTags = removedTags.IgnoreSystem(names.M2) + if len(removedTags) > 0 { + input := &m2.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: removedTags.Keys(), + } + + _, err := conn.UntagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + updatedTags := oldTags.Updated(newTags) + updatedTags = updatedTags.IgnoreSystem(names.M2) + if len(updatedTags) > 0 { + input := &m2.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags), + } + + _, err := conn.TagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// UpdateTags updates m2 service tags. +// It is called from outside this package. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + return updateTags(ctx, meta.(*conns.AWSClient).M2Client(ctx), identifier, oldTags, newTags) +} diff --git a/internal/service/m2/test-fixtures/PlanetsDemo-v1.zip b/internal/service/m2/test-fixtures/PlanetsDemo-v1.zip new file mode 100644 index 00000000000..a143a066168 Binary files /dev/null and b/internal/service/m2/test-fixtures/PlanetsDemo-v1.zip differ diff --git a/internal/service/m2/test-fixtures/application-definition.json b/internal/service/m2/test-fixtures/application-definition.json new file mode 100644 index 00000000000..76f9a75b5b8 --- /dev/null +++ b/internal/service/m2/test-fixtures/application-definition.json @@ -0,0 +1,24 @@ +{ + "template-version": "2.0", + "source-locations": [ + { + "source-id": "s3-source", + "source-type": "s3", + "properties": { + "s3-bucket": "${s3_bucket}", + "s3-key-prefix": "v${version}" + } + } + ], + "definition": { + "listeners": [ + { + "port": 8196, + "type": "http" + } + ], + "ba-application": { + "app-location": "$${s3-source}/PlanetsDemo-v${version}.zip" + } + } +} \ No newline at end of file diff --git a/internal/service/mediapackage/export_test.go b/internal/service/mediapackage/exports_test.go similarity index 100% rename from internal/service/mediapackage/export_test.go rename to internal/service/mediapackage/exports_test.go diff --git a/internal/service/timestreamwrite/export_test.go b/internal/service/timestreamwrite/exports_test.go similarity index 100% rename from internal/service/timestreamwrite/export_test.go rename to internal/service/timestreamwrite/exports_test.go diff --git a/names/names.go b/names/names.go index 40937ea6a21..4b67e7c95b2 100644 --- a/names/names.go +++ b/names/names.go @@ -53,6 +53,8 @@ const ( IVSChatEndpointID = "ivschat" KendraEndpointID = "kendra" LexV2ModelsEndpointID = "models-v2-lex" + M2EndpointID = "m2" + MediaConvertEndpointID = "mediaconvert" MediaLiveEndpointID = "medialive" MQEndpointID = "mq" ObservabilityAccessManagerEndpointID = "oam" diff --git a/website/docs/r/m2_application.html.markdown b/website/docs/r/m2_application.html.markdown new file mode 100644 index 00000000000..3dddb73cc50 --- /dev/null +++ b/website/docs/r/m2_application.html.markdown @@ -0,0 +1,108 @@ +--- +subcategory: "Mainframe Modernization" +layout: "aws" +page_title: "AWS: aws_m2_application" +description: |- + Terraform resource for managing an AWS Mainframe Modernization Application. +--- +# Resource: aws_m2_application + +Terraform resource for managing an [AWS Mainframe Modernization Application](https://docs.aws.amazon.com/m2/latest/userguide/applications-m2.html). + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_m2_application" "example" { + name = "Example" + engine_type = "bluage" + definition { + content = <