diff --git a/.changelog/26399.txt b/.changelog/26399.txt new file mode 100644 index 000000000000..19345acf8fd7 --- /dev/null +++ b/.changelog/26399.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_drs_replication_configuration_template +``` \ No newline at end of file diff --git a/internal/service/drs/exports_test.go b/internal/service/drs/exports_test.go new file mode 100644 index 000000000000..432b2c2a6c2a --- /dev/null +++ b/internal/service/drs/exports_test.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package drs + +// Exports for use in tests only. +var ( + FindReplicationConfigurationTemplateByID = findReplicationConfigurationTemplateByID +) diff --git a/internal/service/drs/generate.go b/internal/service/drs/generate.go index 590623b58dcb..04beee97cc05 100644 --- a/internal/service/drs/generate.go +++ b/internal/service/drs/generate.go @@ -1,8 +1,8 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 +//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ListTags -ListTagsInIDElem=ResourceArn -ServiceTagsMap -SkipTypesImp -KVTValues -TagOp=TagResource -TagInIDElem=ResourceArn -UntagOp=UntagResource -CreateTags -ListTags -UpdateTags //go:generate go run ../../generate/servicepackage/main.go -//go:generate go run ../../generate/tagstests/main.go // ONLY generate directives and package declaration! Do not add anything else to this file. package drs diff --git a/internal/service/drs/replication_configuration_template.go b/internal/service/drs/replication_configuration_template.go new file mode 100644 index 000000000000..500e1a1947d7 --- /dev/null +++ b/internal/service/drs/replication_configuration_template.go @@ -0,0 +1,453 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package drs + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/drs" + awstypes "github.com/aws/aws-sdk-go-v2/service/drs/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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="Replication Configuration Template") +// @Tags(identifierAttribute="arn") +func newReplicationConfigurationTemplateResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &replicationConfigurationTemplateResource{} + + r.SetDefaultCreateTimeout(20 * time.Minute) + r.SetDefaultUpdateTimeout(20 * time.Minute) + r.SetDefaultDeleteTimeout(20 * time.Minute) + + return r, nil +} + +type replicationConfigurationTemplateResource struct { + framework.ResourceWithConfigure + framework.WithImportByID + framework.WithTimeouts +} + +func (r *replicationConfigurationTemplateResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_drs_replication_configuration_template" +} + +func (r *replicationConfigurationTemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + Computed: true, + }, + "associate_default_security_group": schema.BoolAttribute{ + Required: true, + }, + "auto_replicate_new_disks": schema.BoolAttribute{ + Optional: true, + }, + "bandwidth_throttling": schema.Int64Attribute{ + Required: true, + }, + "create_public_ip": schema.BoolAttribute{ + Required: true, + }, + "data_plane_routing": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ReplicationConfigurationDataPlaneRouting](), + }, + "default_large_staging_disk_type": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ReplicationConfigurationDefaultLargeStagingDiskType](), + }, + "ebs_encryption": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ReplicationConfigurationEbsEncryption](), + }, + "ebs_encryption_key_arn": schema.StringAttribute{ + Optional: true, + }, + names.AttrID: schema.StringAttribute{ + Computed: true, + }, + "replication_server_instance_type": schema.StringAttribute{ + Required: true, + }, + "replication_servers_security_groups_ids": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + "staging_area_subnet_id": schema.StringAttribute{ + Required: true, + }, + + "staging_area_tags": tftags.TagsAttribute(), + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + + "use_dedicated_replication_server": schema.BoolAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "pit_policy": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[pitPolicy](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrEnabled: schema.BoolAttribute{ + Optional: true, + }, + names.AttrInterval: schema.Int64Attribute{ + Required: true, + }, + "retention_duration": schema.Int64Attribute{ + Required: true, + }, + "rule_id": schema.Int64Attribute{ + Optional: true, + }, + "units": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.PITPolicyRuleUnits](), + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *replicationConfigurationTemplateResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data replicationConfigurationTemplateResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().DRSClient(ctx) + + input := &drs.CreateReplicationConfigurationTemplateInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + input.Tags = getTagsIn(ctx) + + _, err := conn.CreateReplicationConfigurationTemplate(ctx, input) + if err != nil { + response.Diagnostics.AddError("creating DRS Replication Configuration Template", err.Error()) + + return + } + + output, err := waitReplicationConfigurationTemplateAvailable(ctx, conn, data.ID.ValueString(), r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for DRS Replication Configuration Template (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *replicationConfigurationTemplateResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data replicationConfigurationTemplateResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().DRSClient(ctx) + + output, err := findReplicationConfigurationTemplateByID(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 Replication Configuration Template (%s)", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *replicationConfigurationTemplateResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new replicationConfigurationTemplateResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().DRSClient(ctx) + + if replicationConfigurationTemplateHasChanges(ctx, new, old) { + input := &drs.UpdateReplicationConfigurationTemplateInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.UpdateReplicationConfigurationTemplate(ctx, input) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating DRS Replication Configuration Template (%s)", new.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitReplicationConfigurationTemplateAvailable(ctx, conn, old.ID.ValueString(), r.UpdateTimeout(ctx, new.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for DRS Replication Configuration Template (%s) update", new.ID.ValueString()), err.Error()) + + return + } + } + + output, err := findReplicationConfigurationTemplateByID(ctx, conn, old.ID.ValueString()) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading DRS Replication Configuration Template (%s)", old.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &new)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *replicationConfigurationTemplateResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data replicationConfigurationTemplateResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().DRSClient(ctx) + + tflog.Debug(ctx, "deleting DRS Replication Configuration Template", map[string]interface{}{ + names.AttrID: data.ID.ValueString(), + }) + + input := &drs.DeleteReplicationConfigurationTemplateInput{ + ReplicationConfigurationTemplateID: aws.String(data.ID.ValueString()), + } + + _, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, 5*time.Minute, func() (interface{}, error) { + return conn.DeleteReplicationConfigurationTemplate(ctx, input) + }, "DependencyViolation") + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting DRS Replication Configuration Template (%s)", data.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitReplicationConfigurationTemplateDeleted(ctx, conn, data.ID.ValueString(), r.DeleteTimeout(ctx, data.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for DRS Replication Configuration Template (%s) delete", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *replicationConfigurationTemplateResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func findReplicationConfigurationTemplate(ctx context.Context, conn *drs.Client, input *drs.DescribeReplicationConfigurationTemplatesInput) (*awstypes.ReplicationConfigurationTemplate, error) { + output, err := findReplicationConfigurationTemplates(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSingleValueResult(output) +} + +func findReplicationConfigurationTemplates(ctx context.Context, conn *drs.Client, input *drs.DescribeReplicationConfigurationTemplatesInput) ([]awstypes.ReplicationConfigurationTemplate, error) { + var output []awstypes.ReplicationConfigurationTemplate + + pages := drs.NewDescribeReplicationConfigurationTemplatesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + output = append(output, page.Items...) + } + + return output, nil +} + +func findReplicationConfigurationTemplateByID(ctx context.Context, conn *drs.Client, id string) (*awstypes.ReplicationConfigurationTemplate, error) { + input := &drs.DescribeReplicationConfigurationTemplatesInput{ + ReplicationConfigurationTemplateIDs: []string{id}, + } + + return findReplicationConfigurationTemplate(ctx, conn, input) +} + +const ( + replicationConfigurationTemplateAvailable = "AVAILABLE" +) + +func statusReplicationConfigurationTemplate(ctx context.Context, conn *drs.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findReplicationConfigurationTemplateByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + + return output, replicationConfigurationTemplateAvailable, nil + } +} + +func waitReplicationConfigurationTemplateAvailable(ctx context.Context, conn *drs.Client, id string, timeout time.Duration) (*awstypes.ReplicationConfigurationTemplate, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{replicationConfigurationTemplateAvailable}, + Refresh: statusReplicationConfigurationTemplate(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.ReplicationConfigurationTemplate); ok { + return output, err + } + + return nil, err +} + +func waitReplicationConfigurationTemplateDeleted(ctx context.Context, conn *drs.Client, id string, timeout time.Duration) (*awstypes.ReplicationConfigurationTemplate, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{replicationConfigurationTemplateAvailable}, + Target: []string{}, + Refresh: statusReplicationConfigurationTemplate(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.ReplicationConfigurationTemplate); ok { + return output, err + } + + return nil, err +} + +type replicationConfigurationTemplateResourceModel struct { + ARN types.String `tfsdk:"arn"` + AssociateDefaultSecurityGroup types.Bool `tfsdk:"associate_default_security_group"` + AutoReplicateNewDisks types.Bool `tfsdk:"auto_replicate_new_disks"` + BandwidthThrottling types.Int64 `tfsdk:"bandwidth_throttling"` + CreatePublicIP types.Bool `tfsdk:"create_public_ip"` + DataPlaneRouting fwtypes.StringEnum[awstypes.ReplicationConfigurationDataPlaneRouting] `tfsdk:"data_plane_routing"` + DefaultLargeStagingDiskType fwtypes.StringEnum[awstypes.ReplicationConfigurationDefaultLargeStagingDiskType] `tfsdk:"default_large_staging_disk_type"` + EBSEncryption fwtypes.StringEnum[awstypes.ReplicationConfigurationEbsEncryption] `tfsdk:"ebs_encryption"` + EBSEncryptionKeyARN types.String `tfsdk:"ebs_encryption_key_arn"` + ID types.String `tfsdk:"id"` + PitPolicy fwtypes.ListNestedObjectValueOf[pitPolicy] `tfsdk:"pit_policy"` + ReplicationServerInstanceType types.String `tfsdk:"replication_server_instance_type"` + ReplicationServersSecurityGroupsIDs types.List `tfsdk:"replication_servers_security_groups_ids"` + StagingAreaSubnetID types.String `tfsdk:"staging_area_subnet_id"` + UseDedicatedReplicationServer types.Bool `tfsdk:"use_dedicated_replication_server"` + StagingAreaTags types.Map `tfsdk:"staging_area_tags"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type pitPolicy struct { + Enabled types.Bool `tfsdk:"enabled"` + Interval types.Int64 `tfsdk:"interval"` + RetentionDuration types.Int64 `tfsdk:"retention_duration"` + RuleID types.Int64 `tfsdk:"rule_id"` + Units fwtypes.StringEnum[awstypes.PITPolicyRuleUnits] `tfsdk:"units"` +} + +func replicationConfigurationTemplateHasChanges(_ context.Context, plan, state replicationConfigurationTemplateResourceModel) bool { + return !plan.AssociateDefaultSecurityGroup.Equal(state.AssociateDefaultSecurityGroup) || + !plan.AutoReplicateNewDisks.Equal(state.AutoReplicateNewDisks) || + !plan.BandwidthThrottling.Equal(state.BandwidthThrottling) || + !plan.CreatePublicIP.Equal(state.CreatePublicIP) || + !plan.DataPlaneRouting.Equal(state.DataPlaneRouting) || + !plan.DefaultLargeStagingDiskType.Equal(state.DefaultLargeStagingDiskType) || + !plan.EBSEncryption.Equal(state.EBSEncryption) || + !plan.EBSEncryptionKeyARN.Equal(state.EBSEncryptionKeyARN) || + !plan.ID.Equal(state.ID) || + !plan.PitPolicy.Equal(state.PitPolicy) || + !plan.ReplicationServerInstanceType.Equal(state.ReplicationServerInstanceType) || + !plan.ReplicationServersSecurityGroupsIDs.Equal(state.ReplicationServersSecurityGroupsIDs) || + !plan.StagingAreaSubnetID.Equal(state.StagingAreaSubnetID) || + !plan.UseDedicatedReplicationServer.Equal(state.UseDedicatedReplicationServer) +} diff --git a/internal/service/drs/replication_configuration_template_test.go b/internal/service/drs/replication_configuration_template_test.go new file mode 100644 index 000000000000..28a213c75de3 --- /dev/null +++ b/internal/service/drs/replication_configuration_template_test.go @@ -0,0 +1,160 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package drs_test + +import ( + "context" + "fmt" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/drs/types" + "github.com/aws/aws-sdk-go/aws" + 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" + tfdrs "github.com/hashicorp/terraform-provider-aws/internal/service/drs" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccDRSReplicationConfigurationTemplate_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_drs_replication_configuration_template.test" + var rct awstypes.ReplicationConfigurationTemplate + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DRSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccCheckReplicationConfigurationTemplateDestroy(ctx), + ), + Steps: []resource.TestStep{ + { + Config: testAccReplicationConfigurationTemplateConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckReplicationConfigurationTemplateExists(ctx, resourceName, &rct), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "associate_default_security_group", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "bandwidth_throttling", "12"), + resource.TestCheckResourceAttr(resourceName, "create_public_ip", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "data_plane_routing", "PRIVATE_IP"), + resource.TestCheckResourceAttr(resourceName, "default_large_staging_disk_type", "GP2"), + resource.TestCheckResourceAttr(resourceName, "ebs_encryption", "NONE"), + resource.TestCheckResourceAttr(resourceName, "use_dedicated_replication_server", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "replication_server_instance_type", "t3.small"), + resource.TestCheckResourceAttr(resourceName, "replication_servers_security_groups_ids.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "staging_area_subnet_id", aws.StringValue(rct.StagingAreaSubnetId)), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "pit_policy", map[string]string{ + names.AttrEnabled: acctest.CtFalse, + names.AttrInterval: "14", + "retention_duration": "21", + "units": "DAY", + "rule_id": acctest.Ct1, + }), + resource.TestCheckResourceAttr(resourceName, "staging_area_tags.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "staging_area_tags.Name", rName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckReplicationConfigurationTemplateExists(ctx context.Context, n string, v *awstypes.ReplicationConfigurationTemplate) 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).DRSClient(ctx) + + output, err := tfdrs.FindReplicationConfigurationTemplateByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckReplicationConfigurationTemplateDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DRSClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_drs_replication_configuration_template" { + continue + } + + _, err := tfdrs.FindReplicationConfigurationTemplateByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + if err != nil { + return err + } + + return fmt.Errorf("DRS Replication Configuration Template (%s) still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccReplicationConfigurationTemplateConfig_basic(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 1), + fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q + description = %[1]q + vpc_id = aws_vpc.test.id + + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_drs_replication_configuration_template" "test" { + associate_default_security_group = false + bandwidth_throttling = 12 + create_public_ip = false + data_plane_routing = "PRIVATE_IP" + default_large_staging_disk_type = "GP2" + ebs_encryption = "NONE" + use_dedicated_replication_server = false + replication_server_instance_type = "t3.small" + replication_servers_security_groups_ids = [aws_security_group.test.id] + staging_area_subnet_id = aws_subnet.test[0].id + + pit_policy { + enabled = false + interval = 14 + retention_duration = 21 + units = "DAY" + rule_id = 1 + } + + staging_area_tags = { + Name = %[1]q + } +} +`, rName)) +} diff --git a/internal/service/drs/service_package_gen.go b/internal/service/drs/service_package_gen.go index 93ba7aec234c..60cf41df0564 100644 --- a/internal/service/drs/service_package_gen.go +++ b/internal/service/drs/service_package_gen.go @@ -20,7 +20,15 @@ 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: newReplicationConfigurationTemplateResource, + Name: "Replication Configuration Template", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/internal/service/drs/tags_gen.go b/internal/service/drs/tags_gen.go new file mode 100644 index 000000000000..cf6f91a7d0fe --- /dev/null +++ b/internal/service/drs/tags_gen.go @@ -0,0 +1,137 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package drs + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/drs" + "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 drs 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 *drs.Client, identifier string, optFns ...func(*drs.Options)) (tftags.KeyValueTags, error) { + input := &drs.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 drs 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).DRSClient(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 drs service tags. +func Tags(tags tftags.KeyValueTags) map[string]string { + return tags.Map() +} + +// KeyValueTags creates tftags.KeyValueTags from drs service tags. +func KeyValueTags(ctx context.Context, tags map[string]string) tftags.KeyValueTags { + return tftags.New(ctx, tags) +} + +// getTagsIn returns drs 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 drs 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)) + } +} + +// createTags creates drs service tags for new resources. +func createTags(ctx context.Context, conn *drs.Client, identifier string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + + return updateTags(ctx, conn, identifier, nil, tags) +} + +// updateTags updates drs 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 *drs.Client, identifier string, oldTagsMap, newTagsMap any, optFns ...func(*drs.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.DRS) + if len(removedTags) > 0 { + input := &drs.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.DRS) + if len(updatedTags) > 0 { + input := &drs.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 drs 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).DRSClient(ctx), identifier, oldTags, newTags) +} diff --git a/website/docs/r/drs_replication_configuration_template.html.markdown b/website/docs/r/drs_replication_configuration_template.html.markdown new file mode 100644 index 000000000000..e3b91516c6c1 --- /dev/null +++ b/website/docs/r/drs_replication_configuration_template.html.markdown @@ -0,0 +1,104 @@ +--- +subcategory: "DRS (Elastic Disaster Recovery)" +layout: "aws" +page_title: "AWS: drs_replication_configuration_template" +description: |- + Provides an Elastic Disaster Recovery replication configuration template resource. +--- + +# Resource: aws_drs_replication_configuration_template + +Provides an Elastic Disaster Recovery replication configuration template resource. + +~> **NOTE:** This resource is provided on a best-effort basis and may not function as intended. Due to challenges with DRS permissions, it has not been fully tested. We are collaborating with AWS to enhance its functionality and [welcome your feedback](https://github.com/hashicorp/terraform-provider-aws/issues/new/choose). + +## Example Usage + +### Basic configuration + +```terraform +resource "aws_drs_replication_configuration_template" "example" { + associate_default_security_group = false + bandwidth_throttling = 12 + create_public_ip = false + data_plane_routing = "PRIVATE_IP" + default_large_staging_disk_type = "GP2" + ebs_ecryption = "DEFAULT" + ebs_encryption_key_arn = "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + replication_server_instance_type = "t3.small" + replication_servers_security_groups_ids = aws_security_group.example[*].id + staging_area_subnet_id = aws_subnet.example.id + use_dedicated_replication_server = false + + pit_policy { + enabled = true + interval = 1 + retention_duration = 1 + units = "DAY" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `associate_default_security_group` - (Required) Whether to associate the default Elastic Disaster Recovery Security group with the Replication Configuration Template. +* `bandwidth_throttling` - (Required) Configure bandwidth throttling for the outbound data transfer rate of the Source Server in Mbps. +* `create_public_ip` - (Required) Whether to create a Public IP for the Recovery Instance by default. +* `data_plane_routing` - (Required) Data plane routing mechanism that will be used for replication. Valid values are `PUBLIC_IP` and `PRIVATE_IP`. +* `default_large_staging_disk_type` - (Required) Staging Disk EBS volume type to be used during replication. Valid values are `GP2`, `GP3`, `ST1`, or `AUTO`. +* `ebs_encryption` - (Required) Type of EBS encryption to be used during replication. Valid values are `DEFAULT` and `CUSTOM`. +* `ebs_encryption_key_arn` - (Required) ARN of the EBS encryption key to be used during replication. +* `pit_policy` - (Required) Configuration block for Point in time (PIT) policy to manage snapshots taken during replication. [See below](#pit_policy). +* `replication_server_instance_type` - (Required) Instance type to be used for the replication server. +* `replication_servers_security_groups_ids` - (Required) Security group IDs that will be used by the replication server. +* `staging_area_subnet_id` - (Required) Subnet to be used by the replication staging area. +* `staging_area_tags` - (Required) Set of tags to be associated with all resources created in the replication staging area: EC2 replication server, EBS volumes, EBS snapshots, etc. +* `use_dedicated_replication_server` - (Required) Whether to use a dedicated Replication Server in the replication staging area. + +The following arguments are optional: + +* `auto_replicate_new_disks` - (Optional) Whether to allow the AWS replication agent to automatically replicate newly added disks. +* `tags` - (Optional) Set of tags to be associated with the Replication Configuration Template resource. + +### `pit_policy` + +* `enabled` - (Optional) Whether this rule is enabled or not. +* `interval` - (Required) How often, in the chosen units, a snapshot should be taken. +* `retention_duration` - (Required) Duration to retain a snapshot for, in the chosen `units`. +* `rule_id` - (Optional) ID of the rule. Valid values are integers. +* `units` - (Required) Units used to measure the `interval` and `retention_duration`. Valid values are `MINUTE`, `HOUR`, and `DAY`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - Replication configuration template ARN. +* `id` - Replication configuration template ID. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `20m`) +- `update` - (Default `20m`) +- `delete` - (Default `20m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import DRS Replication Configuration Template using the `id`. For example: + +```terraform +import { + to = aws_drs_replication_configuration_template.example + id = "templateid" +} +``` + +Using `terraform import`, import DRS Replication Configuration Template using the `id`. For example: + +```console +% terraform import aws_drs_replication_configuration_template.example templateid +```