diff --git a/.changelog/38143.txt b/.changelog/38143.txt new file mode 100644 index 000000000000..49e60f8c3b3e --- /dev/null +++ b/.changelog/38143.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_drs_replication_configuration_template: Fix issues preventing creation and deletion +``` \ No newline at end of file diff --git a/examples/drs-initialize/README.md b/examples/drs-initialize/README.md new file mode 100644 index 000000000000..88be993caed7 --- /dev/null +++ b/examples/drs-initialize/README.md @@ -0,0 +1,9 @@ +# DRS Initialize Example + +This is an example of using the Terraform AWS Provider to manually initialize your account to use DRS. For more information see the [AWS instructions](https://docs.aws.amazon.com/drs/latest/userguide/getting-started-initializing.html). + +Running the example: + +1. Run `terraform apply` +2. Assume the `AWSElasticDisasterRecoveryInitializerRole` role +3. Use the AWS CLI to initialize: `aws drs initialize-service` diff --git a/examples/drs-initialize/main.tf b/examples/drs-initialize/main.tf new file mode 100644 index 000000000000..8b56ff3d4db0 --- /dev/null +++ b/examples/drs-initialize/main.tf @@ -0,0 +1,291 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.12" +} + +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryAgentRole" { + name = "AWSElasticDisasterRecoveryAgentRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "drs.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:SetSourceIdentity" + ] + Condition = { + StringLike = { + "sts:SourceIdentity" = "s-*", + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + } + ] + }) +} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryFailbackRole" { + name = "AWSElasticDisasterRecoveryFailbackRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "drs.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:SetSourceIdentity" + ] + Condition = { + StringLike = { + "sts:SourceIdentity" = "i-*", + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + } + ] + }) +} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryConversionServerRole" { + name = "AWSElasticDisasterRecoveryConversionServerRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryRecoveryInstanceRole" { + name = "AWSElasticDisasterRecoveryRecoveryInstanceRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryReplicationServerRole" { + name = "AWSElasticDisasterRecoveryReplicationServerRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role" "AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole" { + name = "AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole" + path = "/service-role/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) +} + +data "aws_iam_policy" "AWSElasticDisasterRecoveryAgentPolicy" { + name = "AWSElasticDisasterRecoveryAgentPolicy" +} + +data "aws_iam_policy" "AWSElasticDisasterRecoveryFailbackPolicy" { + name = "AWSElasticDisasterRecoveryFailbackPolicy" +} + +data "aws_iam_policy" "AWSElasticDisasterRecoveryConversionServerPolicy" { + name = "AWSElasticDisasterRecoveryConversionServerPolicy" +} + +data "aws_iam_policy" "AWSElasticDisasterRecoveryRecoveryInstancePolicy" { + name = "AWSElasticDisasterRecoveryRecoveryInstancePolicy" +} + +data "aws_iam_policy" "AWSElasticDisasterRecoveryReplicationServerPolicy" { + name = "AWSElasticDisasterRecoveryReplicationServerPolicy" +} + +data "aws_iam_policy" "AmazonSSMManagedInstanceCore" { + name = "AmazonSSMManagedInstanceCore" +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryAgentRole" { + role = aws_iam_role.AWSElasticDisasterRecoveryAgentRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryAgentPolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryFailbackRole" { + role = aws_iam_role.AWSElasticDisasterRecoveryFailbackRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryFailbackPolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryConversionServerRole" { + role = aws_iam_role.AWSElasticDisasterRecoveryConversionServerRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryConversionServerPolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryRecoveryInstanceRole" { + role = aws_iam_role.AWSElasticDisasterRecoveryRecoveryInstanceRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryRecoveryInstancePolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryReplicationServerRole" { + role = aws_iam_role.AWSElasticDisasterRecoveryReplicationServerRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryReplicationServerPolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole1" { + role = aws_iam_role.AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole.name + policy_arn = data.aws_iam_policy.AWSElasticDisasterRecoveryRecoveryInstancePolicy.arn +} + +resource "aws_iam_role_policy_attachment" "AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole2" { + role = aws_iam_role.AWSElasticDisasterRecoveryRecoveryInstanceWithLaunchActionsRole.name + policy_arn = data.aws_iam_policy.AmazonSSMManagedInstanceCore.arn +} + + +resource "aws_iam_role" "AWSElasticDisasterRecoveryInitializerRole" { + name = "AWSElasticDisasterRecoveryInitializerRole" + path = "/" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = data.aws_caller_identity.current.user_id + } + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_policy" "InitializePolicy" { + name = "InitializePolicy" + description = "Policy for initializing the AWS Elastic Disaster Recovery service" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "iam:AttachRolePolicy" + Resource = "*" + Condition = { + "ForAnyValue:ArnEquals" = { + "iam:PolicyARN" = [ + data.aws_iam_policy.AWSElasticDisasterRecoveryAgentPolicy.arn, + data.aws_iam_policy.AWSElasticDisasterRecoveryFailbackPolicy.arn, + data.aws_iam_policy.AWSElasticDisasterRecoveryConversionServerPolicy.arn, + data.aws_iam_policy.AWSElasticDisasterRecoveryRecoveryInstancePolicy.arn, + data.aws_iam_policy.AWSElasticDisasterRecoveryReplicationServerPolicy.arn + ] + } + } + }, + { + Effect = "Allow" + Action = "iam:PassRole" + Resource = "arn:aws:iam::*:role/*" + Condition = { + "ForAnyValue:StringLike" = { + "iam:PassedToService" = [ + "ec2.amazonaws.com", + "drs.amazonaws.com" + ] + } + } + }, + { + Effect = "Allow" + Action = [ + "drs:InitializeService", + "drs:ListTagsForResource", + "drs:GetReplicationConfiguration", + "drs:CreateLaunchConfigurationTemplate", + "drs:GetLaunchConfiguration", + "drs:CreateReplicationConfigurationTemplate", + "drs:*ReplicationConfigurationTemplate*", + "iam:TagRole", + "iam:CreateRole", + "iam:GetServiceLinkedRoleDeletionStatus", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:GetRole", + "iam:DeleteRole", + "iam:DeleteServiceLinkedRole", + "ec2:*", + "sts:DecodeAuthorizationMessage", + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = "iam:CreateServiceLinkedRole" + Resource = "arn:aws:iam::*:role/aws-service-role/drs.amazonaws.com/AWSServiceRoleForElasticDisasterRecovery" + }, + { + Effect = "Allow" + Action = [ + "iam:CreateInstanceProfile", + "iam:ListInstanceProfilesForRole", + "iam:GetInstanceProfile", + "iam:ListInstanceProfiles", + "iam:AddRoleToInstanceProfile" + ] + Resource = [ + "arn:aws:iam::*:instance-profile/*", + "arn:aws:iam::*:role/*" + ] + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "Initializer" { + role = aws_iam_role.AWSElasticDisasterRecoveryInitializerRole.name + policy_arn = aws_iam_policy.InitializePolicy.arn +} diff --git a/internal/create/errors.go b/internal/create/errors.go index 41dfd1961d35..d739ba9154c7 100644 --- a/internal/create/errors.go +++ b/internal/create/errors.go @@ -53,6 +53,13 @@ func ProblemStandardMessage(service, action, resource, id string, gotError error return fmt.Sprintf("%s %s %s (%s): %s", action, hf, resource, id, gotError) } +func AddError(d *fwdiag.Diagnostics, service, action, resource, id string, gotError error) { + d.AddError( + ProblemStandardMessage(service, action, resource, id, nil), + gotError.Error(), + ) +} + // Error returns an errors.Error with a standardized error message func Error(service, action, resource, id string, gotError error) error { return errors.New(ProblemStandardMessage(service, action, resource, id, gotError)) diff --git a/internal/service/drs/consts.go b/internal/service/drs/consts.go new file mode 100644 index 000000000000..f9c32fcac45a --- /dev/null +++ b/internal/service/drs/consts.go @@ -0,0 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package drs + +// Exports for use in tests only. +const ( + ResNameReplicationConfigurationTemplate = "Replication Configuration Template" + ResPrefixReplicationConfigurationTemplate = "ReplicationConfigurationTemplate" +) diff --git a/internal/service/drs/exports_test.go b/internal/service/drs/exports_test.go index 432b2c2a6c2a..810adaafe6cb 100644 --- a/internal/service/drs/exports_test.go +++ b/internal/service/drs/exports_test.go @@ -5,5 +5,7 @@ package drs // Exports for use in tests only. var ( + ResourceReplicationConfigurationTemplate = newReplicationConfigurationTemplateResource + FindReplicationConfigurationTemplateByID = findReplicationConfigurationTemplateByID ) diff --git a/internal/service/drs/replication_configuration_template.go b/internal/service/drs/replication_configuration_template.go index 500e1a1947d7..2a6101467022 100644 --- a/internal/service/drs/replication_configuration_template.go +++ b/internal/service/drs/replication_configuration_template.go @@ -5,7 +5,6 @@ package drs import ( "context" - "fmt" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -15,14 +14,17 @@ import ( "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/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "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/create" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" "github.com/hashicorp/terraform-provider-aws/internal/framework" - fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "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" @@ -61,7 +63,11 @@ func (r *replicationConfigurationTemplateResource) Schema(ctx context.Context, r Required: true, }, "auto_replicate_new_disks": schema.BoolAttribute{ + Computed: true, Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "bandwidth_throttling": schema.Int64Attribute{ Required: true, @@ -152,7 +158,7 @@ func (r *replicationConfigurationTemplateResource) Create(ctx context.Context, r conn := r.Meta().DRSClient(ctx) input := &drs.CreateReplicationConfigurationTemplateInput{} - response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + response.Diagnostics.Append(flex.Expand(context.WithValue(ctx, flex.ResourcePrefix, ResPrefixReplicationConfigurationTemplate), data, input)...) if response.Diagnostics.HasError() { return } @@ -161,7 +167,7 @@ func (r *replicationConfigurationTemplateResource) Create(ctx context.Context, r _, err := conn.CreateReplicationConfigurationTemplate(ctx, input) if err != nil { - response.Diagnostics.AddError("creating DRS Replication Configuration Template", err.Error()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionCreating, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err) return } @@ -169,12 +175,12 @@ func (r *replicationConfigurationTemplateResource) Create(ctx context.Context, r 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()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionWaitingForCreation, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err) return } - response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + response.Diagnostics.Append(flex.Flatten(context.WithValue(ctx, flex.ResourcePrefix, ResPrefixReplicationConfigurationTemplate), output, &data)...) if response.Diagnostics.HasError() { return } @@ -201,12 +207,12 @@ func (r *replicationConfigurationTemplateResource) Read(ctx context.Context, req } if err != nil { - response.Diagnostics.AddError(fmt.Sprintf("reading Replication Configuration Template (%s)", data.ID.ValueString()), err.Error()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionReading, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err) return } - response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + response.Diagnostics.Append(flex.Flatten(context.WithValue(ctx, flex.ResourcePrefix, ResPrefixReplicationConfigurationTemplate), output, &data)...) if response.Diagnostics.HasError() { return } @@ -230,20 +236,20 @@ func (r *replicationConfigurationTemplateResource) Update(ctx context.Context, r if replicationConfigurationTemplateHasChanges(ctx, new, old) { input := &drs.UpdateReplicationConfigurationTemplateInput{} - response.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + response.Diagnostics.Append(flex.Expand(context.WithValue(ctx, flex.ResourcePrefix, ResPrefixReplicationConfigurationTemplate), 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()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionUpdating, ResNameReplicationConfigurationTemplate, new.ID.ValueString(), err) 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()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionWaitingForUpdate, ResNameReplicationConfigurationTemplate, new.ID.ValueString(), err) return } @@ -251,12 +257,12 @@ func (r *replicationConfigurationTemplateResource) Update(ctx context.Context, r 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()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionUpdating, ResNameReplicationConfigurationTemplate, old.ID.ValueString(), err) return } - response.Diagnostics.Append(fwflex.Flatten(ctx, output, &new)...) + response.Diagnostics.Append(flex.Flatten(context.WithValue(ctx, flex.ResourcePrefix, ResPrefixReplicationConfigurationTemplate), output, &new)...) if response.Diagnostics.HasError() { return } @@ -290,13 +296,13 @@ func (r *replicationConfigurationTemplateResource) Delete(ctx context.Context, r } if err != nil { - response.Diagnostics.AddError(fmt.Sprintf("deleting DRS Replication Configuration Template (%s)", data.ID.ValueString()), err.Error()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionDeleting, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err) 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()) + create.AddError(&response.Diagnostics, names.DRS, create.ErrActionWaitingForDeletion, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err) return } @@ -342,7 +348,7 @@ func findReplicationConfigurationTemplates(ctx context.Context, conn *drs.Client func findReplicationConfigurationTemplateByID(ctx context.Context, conn *drs.Client, id string) (*awstypes.ReplicationConfigurationTemplate, error) { input := &drs.DescribeReplicationConfigurationTemplatesInput{ - ReplicationConfigurationTemplateIDs: []string{id}, + //ReplicationConfigurationTemplateIDs: []string{id}, // Uncomment when SDK supports this, currently MAX of 1 so you find it anyway } return findReplicationConfigurationTemplate(ctx, conn, input) diff --git a/internal/service/drs/replication_configuration_template_test.go b/internal/service/drs/replication_configuration_template_test.go index 28a213c75de3..1a9ea805a498 100644 --- a/internal/service/drs/replication_configuration_template_test.go +++ b/internal/service/drs/replication_configuration_template_test.go @@ -7,9 +7,9 @@ import ( "context" "fmt" "testing" + "time" 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" @@ -20,13 +20,26 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -func TestAccDRSReplicationConfigurationTemplate_basic(t *testing.T) { +// TestAccDRSReplicationConfigurationTemplate_serial serializes the tests +// since the account limit tends to be 1. +func TestAccDRSReplicationConfigurationTemplate_serial(t *testing.T) { + t.Parallel() + + testCases := map[string]func(t *testing.T){ + acctest.CtBasic: testAccReplicationConfigurationTemplate_basic, + acctest.CtDisappears: testAccReplicationConfigurationTemplate_disappears, + } + + acctest.RunSerialTests1Level(t, testCases, 5*time.Second) +} + +func testAccReplicationConfigurationTemplate_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{ + resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.DRSServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -48,14 +61,28 @@ func TestAccDRSReplicationConfigurationTemplate_basic(t *testing.T) { 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", + resource.TestCheckResourceAttrPair(resourceName, "staging_area_subnet_id", "aws_subnet.test.0", names.AttrID), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "pit_policy.*", map[string]string{ + names.AttrEnabled: acctest.CtTrue, + names.AttrInterval: acctest.Ct10, + "retention_duration": "60", + "units": "MINUTE", "rule_id": acctest.Ct1, }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "pit_policy.*", map[string]string{ + names.AttrEnabled: acctest.CtTrue, + names.AttrInterval: acctest.Ct1, + "retention_duration": "24", + "units": "HOUR", + "rule_id": acctest.Ct2, + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "pit_policy.*", map[string]string{ + names.AttrEnabled: acctest.CtTrue, + names.AttrInterval: acctest.Ct1, + "retention_duration": acctest.Ct3, + "units": "DAY", + "rule_id": acctest.Ct3, + }), resource.TestCheckResourceAttr(resourceName, "staging_area_tags.%", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "staging_area_tags.Name", rName), ), @@ -69,6 +96,30 @@ func TestAccDRSReplicationConfigurationTemplate_basic(t *testing.T) { }) } +func testAccReplicationConfigurationTemplate_disappears(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_drs_replication_configuration_template.test" + var rct awstypes.ReplicationConfigurationTemplate + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DRSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckReplicationConfigurationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccReplicationConfigurationTemplateConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckReplicationConfigurationTemplateExists(ctx, resourceName, &rct), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfdrs.ResourceReplicationConfigurationTemplate, resourceName), + ), + ExpectNonEmptyPlan: 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] @@ -145,13 +196,29 @@ resource "aws_drs_replication_configuration_template" "test" { staging_area_subnet_id = aws_subnet.test[0].id pit_policy { - enabled = false - interval = 14 - retention_duration = 21 - units = "DAY" + enabled = true + interval = 10 + retention_duration = 60 + units = "MINUTE" rule_id = 1 } + pit_policy { + enabled = true + interval = 1 + retention_duration = 24 + units = "HOUR" + rule_id = 2 + } + + pit_policy { + enabled = true + interval = 1 + retention_duration = 3 + units = "DAY" + rule_id = 3 + } + staging_area_tags = { Name = %[1]q } diff --git a/website/docs/r/drs_replication_configuration_template.html.markdown b/website/docs/r/drs_replication_configuration_template.html.markdown index e3b91516c6c1..a0b4a7408e4b 100644 --- a/website/docs/r/drs_replication_configuration_template.html.markdown +++ b/website/docs/r/drs_replication_configuration_template.html.markdown @@ -8,9 +8,9 @@ description: |- # Resource: aws_drs_replication_configuration_template -Provides an Elastic Disaster Recovery replication configuration template resource. +Provides an Elastic Disaster Recovery replication configuration template resource. Before using DRS, your account must be [initialized](https://docs.aws.amazon.com/drs/latest/userguide/getting-started-initializing.html). -~> **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). +~> **NOTE:** Your configuration must use the PIT policy shown in the [basic configuration](#basic-configuration) due to AWS rules. The only value that you can change is the `retention_duration` of `rule_id` 3. ## Example Usage @@ -30,11 +30,28 @@ resource "aws_drs_replication_configuration_template" "example" { staging_area_subnet_id = aws_subnet.example.id use_dedicated_replication_server = false + pit_policy { + enabled = true + interval = 10 + retention_duration = 60 + units = "MINUTE" + rule_id = 1 + } + + pit_policy { + enabled = true + interval = 1 + retention_duration = 24 + units = "HOUR" + rule_id = 2 + } + pit_policy { enabled = true interval = 1 - retention_duration = 1 + retention_duration = 3 units = "DAY" + rule_id = 3 } } ``` @@ -64,6 +81,8 @@ The following arguments are optional: ### `pit_policy` +The PIT policies _must_ be specified as shown in the [basic configuration example](#basic-configuration) above. The only value that you can change is the `retention_duration` of `rule_id` 3. + * `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`.