diff --git a/.changelog/37039.txt b/.changelog/37039.txt new file mode 100644 index 000000000000..eef1a0014812 --- /dev/null +++ b/.changelog/37039.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_backup_restore_testing_plan +``` + +```release-note:new-resource +aws_backup_restore_testing_selection +``` \ No newline at end of file diff --git a/internal/framework/types/mapof.go b/internal/framework/types/mapof.go index 21a53b29b2b9..693727088c98 100644 --- a/internal/framework/types/mapof.go +++ b/internal/framework/types/mapof.go @@ -100,6 +100,10 @@ type MapValueOf[T attr.Value] struct { basetypes.MapValue } +type ( + MapOfString = MapValueOf[basetypes.StringValue] +) + func (v MapValueOf[T]) Equal(o attr.Value) bool { other, ok := o.(MapValueOf[T]) diff --git a/internal/framework/types/setof.go b/internal/framework/types/setof.go index 357f1f63f8bf..4cf81f00b2ad 100644 --- a/internal/framework/types/setof.go +++ b/internal/framework/types/setof.go @@ -25,7 +25,11 @@ type setTypeOf[T attr.Value] struct { } var ( + // SetOfStringType is a custom type used for defining a Set of strings. SetOfStringType = setTypeOf[basetypes.StringValue]{basetypes.SetType{ElemType: basetypes.StringType{}}} + + // SetOfARNType is a custom type used for defining a Set of ARNs. + SetOfARNType = setTypeOf[ARN]{basetypes.SetType{ElemType: ARNType}} ) func NewSetTypeOf[T attr.Value](ctx context.Context) setTypeOf[T] { @@ -97,6 +101,11 @@ type SetValueOf[T attr.Value] struct { basetypes.SetValue } +type ( + SetOfString = SetValueOf[basetypes.StringValue] + SetOfARN = SetValueOf[ARN] +) + func (v SetValueOf[T]) Equal(o attr.Value) bool { other, ok := o.(SetValueOf[T]) diff --git a/internal/framework/validators/arn.go b/internal/framework/validators/arn.go new file mode 100644 index 000000000000..0ea1add39e29 --- /dev/null +++ b/internal/framework/validators/arn.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validators + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +type arnValidator struct{} + +func (validator arnValidator) Description(_ context.Context) string { + return "An Amazon Resource Name" +} + +func (validator arnValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +func (validator arnValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + if !arn.IsARN(request.ConfigValue.ValueString()) { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.Path, + validator.Description(ctx), + "value must be a valid ARN", + )) + return + } +} + +func ARN() validator.String { + return arnValidator{} +} diff --git a/internal/framework/validators/arn_test.go b/internal/framework/validators/arn_test.go new file mode 100644 index 000000000000..c1acb45a455c --- /dev/null +++ b/internal/framework/validators/arn_test.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validators_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + fwvalidators "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" +) + +func TestARNValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + + tests := map[string]testCase{ + "unknown String": { + val: types.StringUnknown(), + }, + "null String": { + val: types.StringNull(), + }, + "valid arn": { + val: types.StringValue("arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess"), + }, + "invalid_arn": { + val: types.StringValue("arn"), + expectError: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.StringResponse{} + fwvalidators.ARN().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/internal/service/backup/exports_test.go b/internal/service/backup/exports_test.go index 48ad42d68b37..8f8652efe7ca 100644 --- a/internal/service/backup/exports_test.go +++ b/internal/service/backup/exports_test.go @@ -11,6 +11,8 @@ var ( ResourcePlan = resourcePlan ResourceRegionSettings = resourceRegionSettings ResourceReportPlan = resourceReportPlan + ResourceRestoreTestingPlan = newRestoreTestingPlanResource + ResourceRestoreTestingSelection = newRestoreTestingSelectionResource ResourceSelection = resourceSelection ResourceVault = resourceVault ResourceVaultLockConfiguration = resourceVaultLockConfiguration @@ -24,6 +26,8 @@ var ( FindPlanByID = findPlanByID FindRegionSettings = findRegionSettings FindReportPlanByName = findReportPlanByName + FindRestoreTestingPlanByName = findRestoreTestingPlanByName + FindRestoreTestingSelectionByTwoPartKey = findRestoreTestingSelectionByTwoPartKey FindSelectionByTwoPartKey = findSelectionByTwoPartKey FindVaultAccessPolicyByName = findVaultAccessPolicyByName FindVaultNotificationsByName = findVaultNotificationsByName diff --git a/internal/service/backup/logically_air_gapped_vault.go b/internal/service/backup/logically_air_gapped_vault.go index 56ceaef5c99a..a11e7022b1f3 100644 --- a/internal/service/backup/logically_air_gapped_vault.go +++ b/internal/service/backup/logically_air_gapped_vault.go @@ -206,9 +206,9 @@ type logicallyAirGappedVaultResourceModel struct { ID types.String `tfsdk:"id"` MaxRetentionDays types.Int64 `tfsdk:"max_retention_days"` MinRetentionDays types.Int64 `tfsdk:"min_retention_days"` - Timeouts timeouts.Value `tfsdk:"timeouts"` Tags tftags.Map `tfsdk:"tags"` TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } func findLogicallyAirGappedBackupVaultByName(ctx context.Context, conn *backup.Client, name string) (*backup.DescribeBackupVaultOutput, error) { // nosemgrep:ci.backup-in-func-name diff --git a/internal/service/backup/restore_testing_plan.go b/internal/service/backup/restore_testing_plan.go new file mode 100644 index 000000000000..b324c80f3657 --- /dev/null +++ b/internal/service/backup/restore_testing_plan.go @@ -0,0 +1,356 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package backup + +import ( + "context" + "fmt" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/backup" + awstypes "github.com/aws/aws-sdk-go-v2/service/backup/types" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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/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" + "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" + 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="Restore Testing Plan") +// @Tags(identifierAttribute="arn") +func newRestoreTestingPlanResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &restoreTestingPlanResource{} + + return r, nil +} + +type restoreTestingPlanResource struct { + framework.ResourceWithConfigure +} + +func (*restoreTestingPlanResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_backup_restore_testing_plan" +} + +func (r *restoreTestingPlanResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 50), + stringvalidator.RegexMatches(regexache.MustCompile(`^[0-9A-Za-z_]+$`), "must contain only alphanumeric characters, and underscores"), + }, + }, + names.AttrScheduleExpression: schema.StringAttribute{ + Required: true, + }, + "schedule_expression_timezone": schema.StringAttribute{ + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "start_window_hours": schema.Int64Attribute{ + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(0, 168), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "recovery_point_selection": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[restoreRecoveryPointSelectionModel](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.RestoreTestingRecoveryPointSelectionAlgorithm](), + Required: true, + }, + "exclude_vaults": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.Any( + validators.ARN(), + stringvalidator.OneOf("*"), + ), + ), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + "include_vaults": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.Any( + validators.ARN(), + stringvalidator.OneOf("*"), + ), + ), + }, + }, + "recovery_point_types": schema.SetAttribute{ + CustomType: fwtypes.NewSetTypeOf[fwtypes.StringEnum[awstypes.RestoreTestingRecoveryPointType]](ctx), + Required: true, + ElementType: fwtypes.StringEnumType[awstypes.RestoreTestingRecoveryPointType](), + }, + "selection_window_days": schema.Int64Attribute{ + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(1, 365), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + } +} + +func (r *restoreTestingPlanResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data restoreTestingPlanResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + name := data.RestoreTestingPlanName.ValueString() + input := &backup.CreateRestoreTestingPlanInput{ + CreatorRequestId: aws.String(sdkid.UniqueId()), + RestoreTestingPlan: &awstypes.RestoreTestingPlanForCreate{}, + Tags: getTagsIn(ctx), + } + response.Diagnostics.Append(fwflex.Expand(ctx, data, input.RestoreTestingPlan)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.CreateRestoreTestingPlan(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating Backup Restore Testing Plan (%s)", name), err.Error()) + + return + } + + // Set values for unknowns. + restoreTestingPlan, err := findRestoreTestingPlanByName(ctx, conn, name) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Backup Restore Testing Plan (%s)", name), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, restoreTestingPlan, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *restoreTestingPlanResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data restoreTestingPlanResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + name := data.RestoreTestingPlanName.ValueString() + restoreTestingPlan, err := findRestoreTestingPlanByName(ctx, conn, name) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Backup Restore Testing Plan (%s)", name), err.Error()) + + return + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, restoreTestingPlan, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *restoreTestingPlanResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new restoreTestingPlanResourceModel + 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().BackupClient(ctx) + + if !old.RecoveryPointSelection.Equal(new.RecoveryPointSelection) || + !old.ScheduleExpression.Equal(new.ScheduleExpression) || + !old.ScheduleExpressionTimezone.Equal(new.ScheduleExpressionTimezone) || + !old.StartWindowHours.Equal(new.StartWindowHours) { + name := new.RestoreTestingPlanName.ValueString() + input := &backup.UpdateRestoreTestingPlanInput{ + RestoreTestingPlan: &awstypes.RestoreTestingPlanForUpdate{}, + RestoreTestingPlanName: aws.String(name), + } + response.Diagnostics.Append(fwflex.Expand(ctx, new, input.RestoreTestingPlan)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.UpdateRestoreTestingPlan(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Backup Restore Testing Plan (%s)", name), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *restoreTestingPlanResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data restoreTestingPlanResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + name := data.RestoreTestingPlanName.ValueString() + _, err := conn.DeleteRestoreTestingPlan(ctx, &backup.DeleteRestoreTestingPlanInput{ + RestoreTestingPlanName: aws.String(name), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Backup Restore Testing Plan (%s)", name), err.Error()) + + return + } +} + +func (r *restoreTestingPlanResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func (r *restoreTestingPlanResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root(names.AttrName), request.ID)...) +} + +func findRestoreTestingPlanByName(ctx context.Context, conn *backup.Client, name string) (*awstypes.RestoreTestingPlanForGet, error) { + input := &backup.GetRestoreTestingPlanInput{ + RestoreTestingPlanName: aws.String(name), + } + + return findRestoreTestingPlan(ctx, conn, input) +} + +func findRestoreTestingPlan(ctx context.Context, conn *backup.Client, input *backup.GetRestoreTestingPlanInput) (*awstypes.RestoreTestingPlanForGet, error) { + output, err := conn.GetRestoreTestingPlan(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.RestoreTestingPlan == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.RestoreTestingPlan, nil +} + +type restoreTestingPlanResourceModel struct { + RecoveryPointSelection fwtypes.ListNestedObjectValueOf[restoreRecoveryPointSelectionModel] `tfsdk:"recovery_point_selection"` + RestoreTestingPlanARN types.String `tfsdk:"arn"` + RestoreTestingPlanName types.String `tfsdk:"name"` + ScheduleExpression types.String `tfsdk:"schedule_expression"` + ScheduleExpressionTimezone types.String `tfsdk:"schedule_expression_timezone"` + StartWindowHours types.Int64 `tfsdk:"start_window_hours"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` +} + +type restoreRecoveryPointSelectionModel struct { + Algorithm fwtypes.StringEnum[awstypes.RestoreTestingRecoveryPointSelectionAlgorithm] `tfsdk:"algorithm"` + ExcludeVaults fwtypes.SetOfString `tfsdk:"exclude_vaults"` + IncludeVaults fwtypes.SetOfString `tfsdk:"include_vaults"` + RecoveryPointTypes fwtypes.SetValueOf[fwtypes.StringEnum[awstypes.RestoreTestingRecoveryPointType]] `tfsdk:"recovery_point_types"` + SelectionWindowDays types.Int64 `tfsdk:"selection_window_days"` +} diff --git a/internal/service/backup/restore_testing_plan_test.go b/internal/service/backup/restore_testing_plan_test.go new file mode 100644 index 000000000000..55f4e570afbc --- /dev/null +++ b/internal/service/backup/restore_testing_plan_test.go @@ -0,0 +1,482 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package backup_test + +import ( + "context" + "fmt" + "strings" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/backup/types" + 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" + tfbackup "github.com/hashicorp/terraform-provider-aws/internal/service/backup" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccBackupRestoreTestingPlan_basic(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.include_vaults.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 12 ? * * *)"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), // no tags + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_disappears(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfbackup.ResourceRestoreTestingPlan, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_tags(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + { + Config: testAccRestoreTestingPlanConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + { + Config: testAccRestoreTestingPlanConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_includeVaults(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_includeVaults(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.include_vaults.#", acctest.Ct1), + acctest.CheckResourceAttrRegionalARN(resourceName, "recovery_point_selection.0.include_vaults.0", "backup", fmt.Sprintf("backup-vault:%s", rName)), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 12 ? * * *)"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_excludeVaults(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_excludeVaults(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.exclude_vaults.#", acctest.Ct1), + acctest.CheckResourceAttrRegionalARN(resourceName, "recovery_point_selection.0.exclude_vaults.0", "backup", fmt.Sprintf("backup-vault:%s", rName)), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 12 ? * * *)"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_additionals(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_additionals("365", "cron(0 12 ? * * *)", rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.include_vaults.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.exclude_vaults.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.selection_window_days", "365"), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 12 ? * * *)"), + resource.TestCheckResourceAttr(resourceName, "start_window_hours", "168"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + }, + }) +} + +func TestAccBackupRestoreTestingPlan_additionalsWithUpdate(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingPlanForGet + resourceName := "aws_backup_restore_testing_plan.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingPlanDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingPlanConfig_additionals("365", "cron(0 1 ? * * *)", rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.include_vaults.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.exclude_vaults.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.selection_window_days", "365"), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 1 ? * * *)"), + resource.TestCheckResourceAttr(resourceName, "start_window_hours", "168"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: rName, + ImportStateVerifyIdentifierAttribute: names.AttrName, + }, + { + Config: testAccRestoreTestingPlanConfig_additionals(acctest.Ct1, "cron(0 12 ? * * *)", rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingPlanExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttrSet(resourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.algorithm", "LATEST_WITHIN_WINDOW"), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.include_vaults.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.exclude_vaults.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.recovery_point_types.#", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "recovery_point_selection.0.selection_window_days", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrScheduleExpression, "cron(0 12 ? * * *)"), + resource.TestCheckResourceAttr(resourceName, "start_window_hours", "168"), + ), + }, + }, + }) +} + +func testAccCheckRestoreTestingPlanDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).BackupClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_backup_restore_testing_plan" { + continue + } + + _, err := tfbackup.FindRestoreTestingPlanByName(ctx, conn, rs.Primary.Attributes[names.AttrName]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Backup Restore Testing Plan %s still exists", rs.Primary.Attributes[names.AttrName]) + } + + return nil + } +} + +func testAccCheckRestoreTestingPlanExists(ctx context.Context, n string, v *awstypes.RestoreTestingPlanForGet) 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).BackupClient(ctx) + + output, err := tfbackup.FindRestoreTestingPlanByName(ctx, conn, rs.Primary.Attributes[names.AttrName]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccRestoreTestingPlanConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[1]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 +} +`, rName) +} + +func testAccRestoreTestingPlanConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[1]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccRestoreTestingPlanConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[1]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccRestoreTestingPlanConfig_additionals(selectionWindowDays, scheduleExpression, rName string) string { + return fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[3]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS", "SNAPSHOT"] + selection_window_days = %[1]s + } + + schedule_expression = %[2]q + start_window_hours = 168 +} +`, selectionWindowDays, scheduleExpression, rName) +} + +func testAccRestoreTestingPlanConfig_baseVaults(rName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + enable_key_rotation = true + description = %[1]q + deletion_window_in_days = 7 +} + +resource "aws_backup_vault" "test" { + name = %[1]q + kms_key_arn = aws_kms_key.test.arn +} +`, rName) +} + +func testAccRestoreTestingPlanConfig_includeVaults(rName string) string { + return acctest.ConfigCompose( + testAccRestoreTestingPlanConfig_baseVaults(rName), + fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[1]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = [resource.aws_backup_vault.test.arn] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 +} +`, rName)) +} + +func testAccRestoreTestingPlanConfig_excludeVaults(rName string) string { + return acctest.ConfigCompose( + testAccRestoreTestingPlanConfig_baseVaults(rName), + fmt.Sprintf(` +resource "aws_backup_restore_testing_plan" "test" { + name = %[1]q + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + exclude_vaults = [resource.aws_backup_vault.test.arn] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 +} +`, rName)) +} diff --git a/internal/service/backup/restore_testing_selection.go b/internal/service/backup/restore_testing_selection.go new file mode 100644 index 000000000000..a6e5969749de --- /dev/null +++ b/internal/service/backup/restore_testing_selection.go @@ -0,0 +1,416 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package backup + +import ( + "context" + "fmt" + "strings" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/backup" + awstypes "github.com/aws/aws-sdk-go-v2/service/backup/types" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "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/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" + "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Restore Testing Plan Selection") +func newRestoreTestingSelectionResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &restoreTestingSelectionResource{} + + return r, nil +} + +type restoreTestingSelectionResource struct { + framework.ResourceWithConfigure +} + +func (*restoreTestingSelectionResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_backup_restore_testing_selection" +} + +func (r *restoreTestingSelectionResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrIAMRoleARN: schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 50), + stringvalidator.RegexMatches(regexache.MustCompile(`^[0-9A-Za-z_]+$`), "must contain only alphanumeric characters, and underscores"), + }, + }, + "protected_resource_arns": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.Any( + validators.ARN(), + stringvalidator.OneOf("*"), + ), + ), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + "protected_resource_type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "restore_metadata_overrides": schema.MapAttribute{ + CustomType: fwtypes.MapOfStringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "restore_testing_plan_name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "validation_window_hours": schema.Int64Attribute{ + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.Between(1, 168), + }, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "protected_resource_conditions": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[protectedResourceConditionsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "string_equals": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[keyValueModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrKey: schema.StringAttribute{ + Required: true, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "string_not_equals": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[keyValueModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrKey: schema.StringAttribute{ + Required: true, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r *restoreTestingSelectionResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data restoreTestingSelectionResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + restoreTestingPlanName := data.RestoreTestingPlanName.ValueString() + name := data.RestoreTestingSelectionName.ValueString() + input := &backup.CreateRestoreTestingSelectionInput{ + CreatorRequestId: aws.String(sdkid.UniqueId()), + RestoreTestingPlanName: aws.String(restoreTestingPlanName), + RestoreTestingSelection: &awstypes.RestoreTestingSelectionForCreate{}, + } + response.Diagnostics.Append(fwflex.Expand(ctx, data, input.RestoreTestingSelection)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.CreateRestoreTestingSelection(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating Backup Restore Testing Selection (%s)", name), err.Error()) + + return + } + + // Set values for unknowns. + restoreTestingSelection, err := findRestoreTestingSelectionByTwoPartKey(ctx, conn, restoreTestingPlanName, name) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Backup Restore Testing Selection (%s)", name), err.Error()) + + return + } + + if v := restoreTestingSelection.ProtectedResourceConditions; v != nil { + // The default is + // + // "ProtectedResourceConditions": { + // "StringEquals": [], + // "StringNotEquals": [] + // }, + if len(v.StringEquals) == 0 { + v.StringEquals = nil + } + if len(v.StringNotEquals) == 0 { + v.StringNotEquals = nil + } + if v.StringEquals == nil && v.StringNotEquals == nil { + restoreTestingSelection.ProtectedResourceConditions = nil + } + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, restoreTestingSelection, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *restoreTestingSelectionResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data restoreTestingSelectionResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + restoreTestingPlanName := data.RestoreTestingPlanName.ValueString() + name := data.RestoreTestingSelectionName.ValueString() + restoreTestingSelection, err := findRestoreTestingSelectionByTwoPartKey(ctx, conn, restoreTestingPlanName, name) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Backup Restore Testing Selection (%s)", name), err.Error()) + + return + } + + if v := restoreTestingSelection.ProtectedResourceConditions; v != nil { + // The default is + // + // "ProtectedResourceConditions": { + // "StringEquals": [], + // "StringNotEquals": [] + // }, + if len(v.StringEquals) == 0 { + v.StringEquals = nil + } + if len(v.StringNotEquals) == 0 { + v.StringNotEquals = nil + } + if v.StringEquals == nil && v.StringNotEquals == nil { + restoreTestingSelection.ProtectedResourceConditions = nil + } + } + + // Set attributes for import. + response.Diagnostics.Append(fwflex.Flatten(ctx, restoreTestingSelection, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *restoreTestingSelectionResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new restoreTestingSelectionResourceModel + 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().BackupClient(ctx) + + if !old.IAMRoleARN.Equal(new.IAMRoleARN) || + !old.ProtectedResourceConditions.Equal(new.ProtectedResourceConditions) || + !old.RestoreMetadataOverrides.Equal(new.RestoreMetadataOverrides) || + !old.ValidationWindowHours.Equal(new.ValidationWindowHours) { + restoreTestingPlanName := new.RestoreTestingPlanName.ValueString() + name := new.RestoreTestingSelectionName.ValueString() + input := &backup.UpdateRestoreTestingSelectionInput{ + RestoreTestingPlanName: aws.String(restoreTestingPlanName), + RestoreTestingSelection: &awstypes.RestoreTestingSelectionForUpdate{}, + RestoreTestingSelectionName: aws.String(name), + } + response.Diagnostics.Append(fwflex.Expand(ctx, new, input.RestoreTestingSelection)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.UpdateRestoreTestingSelection(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Backup Restore Testing Selection (%s)", name), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *restoreTestingSelectionResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data restoreTestingSelectionResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BackupClient(ctx) + + restoreTestingPlanName := data.RestoreTestingPlanName.ValueString() + name := data.RestoreTestingSelectionName.ValueString() + _, err := conn.DeleteRestoreTestingSelection(ctx, &backup.DeleteRestoreTestingSelectionInput{ + RestoreTestingPlanName: aws.String(restoreTestingPlanName), + RestoreTestingSelectionName: aws.String(name), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Backup Restore Testing Selection (%s)", name), err.Error()) + + return + } +} + +func (r *restoreTestingSelectionResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + parts := strings.Split(request.ID, ":") + if len(parts) != 2 { + response.Diagnostics.AddError("Resource Import Invalid ID", fmt.Sprintf(`Unexpected format for import ID (%s), use: "RestoreTestingSelectionName:RestoreTestingPlanName"`, request.ID)) + return + } + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root(names.AttrName), parts[0])...) + response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("restore_testing_plan_name"), parts[1])...) +} + +func (r *restoreTestingSelectionResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("protected_resource_arns"), + path.MatchRoot("protected_resource_conditions"), + ), + } +} + +func findRestoreTestingSelectionByTwoPartKey(ctx context.Context, conn *backup.Client, restoreTestingPlanName, restoreTestingSelectionName string) (*awstypes.RestoreTestingSelectionForGet, error) { + input := &backup.GetRestoreTestingSelectionInput{ + RestoreTestingPlanName: aws.String(restoreTestingPlanName), + RestoreTestingSelectionName: aws.String(restoreTestingSelectionName), + } + + return findRestoreTestingSelection(ctx, conn, input) +} + +func findRestoreTestingSelection(ctx context.Context, conn *backup.Client, input *backup.GetRestoreTestingSelectionInput) (*awstypes.RestoreTestingSelectionForGet, error) { + output, err := conn.GetRestoreTestingSelection(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.RestoreTestingSelection == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.RestoreTestingSelection, nil +} + +type restoreTestingSelectionResourceModel struct { + IAMRoleARN fwtypes.ARN `tfsdk:"iam_role_arn"` + ProtectedResourceARNs fwtypes.SetOfString `tfsdk:"protected_resource_arns"` + ProtectedResourceConditions fwtypes.ListNestedObjectValueOf[protectedResourceConditionsModel] `tfsdk:"protected_resource_conditions"` + ProtectedResourceType types.String `tfsdk:"protected_resource_type"` + RestoreMetadataOverrides fwtypes.MapOfString `tfsdk:"restore_metadata_overrides"` + RestoreTestingSelectionName types.String `tfsdk:"name"` + RestoreTestingPlanName types.String `tfsdk:"restore_testing_plan_name"` + ValidationWindowHours types.Int64 `tfsdk:"validation_window_hours"` +} + +type protectedResourceConditionsModel struct { + StringEquals fwtypes.ListNestedObjectValueOf[keyValueModel] `tfsdk:"string_equals"` + StringNotEquals fwtypes.ListNestedObjectValueOf[keyValueModel] `tfsdk:"string_not_equals"` +} + +type keyValueModel struct { + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` +} diff --git a/internal/service/backup/restore_testing_selection_test.go b/internal/service/backup/restore_testing_selection_test.go new file mode 100644 index 000000000000..150eec5d062c --- /dev/null +++ b/internal/service/backup/restore_testing_selection_test.go @@ -0,0 +1,269 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package backup_test + +import ( + "context" + "fmt" + "strings" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/backup/types" + 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" + tfbackup "github.com/hashicorp/terraform-provider-aws/internal/service/backup" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccBackupRestoreTestingSelection_basic(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingSelectionForGet + resourceName := "aws_backup_restore_testing_selection.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingSelectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingSelectionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingSelectionExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "restore_testing_plan_name", rName+"_plan"), + resource.TestCheckResourceAttr(resourceName, "protected_resource_type", "EC2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s:%s", rName, rName+"_plan"), + ImportStateVerifyIdentifierAttribute: names.AttrName, + ImportStateVerifyIgnore: []string{names.AttrApplyImmediately, "user"}, + }, + }, + }) +} + +func TestAccBackupRestoreTestingSelection_disappears(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingselection awstypes.RestoreTestingSelectionForGet + resourceName := "aws_backup_restore_testing_selection.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingSelectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingSelectionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingSelectionExists(ctx, resourceName, &restoretestingselection), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfbackup.ResourceRestoreTestingSelection, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccBackupRestoreTestingSelection_updates(t *testing.T) { + ctx := acctest.Context(t) + var restoretestingplan awstypes.RestoreTestingSelectionForGet + resourceName := "aws_backup_restore_testing_selection.test" + rName := strings.ReplaceAll(sdkacctest.RandomWithPrefix(acctest.ResourcePrefix), "-", "_") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BackupServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRestoreTestingSelectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRestoreTestingSelectionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingSelectionExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "restore_testing_plan_name", rName+"_plan"), + resource.TestCheckResourceAttr(resourceName, "protected_resource_type", "EC2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateId: fmt.Sprintf("%s:%s", rName, rName+"_plan"), + ImportStateVerifyIdentifierAttribute: names.AttrName, + ImportStateVerifyIgnore: []string{names.AttrApplyImmediately, "user"}, + }, + { + Config: testAccRestoreTestingSelectionConfig_updates(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRestoreTestingSelectionExists(ctx, resourceName, &restoretestingplan), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "restore_testing_plan_name", rName+"_plan"), + resource.TestCheckResourceAttr(resourceName, "protected_resource_type", "EC2"), + ), + }, + }, + }) +} + +func testAccCheckRestoreTestingSelectionDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_backup_restore_testing_selection" { + continue + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).BackupClient(ctx) + + _, err := tfbackup.FindRestoreTestingSelectionByTwoPartKey(ctx, conn, rs.Primary.Attributes["restore_testing_plan_name"], rs.Primary.Attributes[names.AttrName]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Backup Restore Testing Selection %s still exists", rs.Primary.Attributes[names.AttrName]) + } + + return nil + } +} + +func testAccCheckRestoreTestingSelectionExists(ctx context.Context, n string, v *awstypes.RestoreTestingSelectionForGet) 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).BackupClient(ctx) + + output, err := tfbackup.FindRestoreTestingSelectionByTwoPartKey(ctx, conn, rs.Primary.Attributes["restore_testing_plan_name"], rs.Primary.Attributes[names.AttrName]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccRestoreTestingSelectionConfig_base(rName string) string { + return fmt.Sprintf(` +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 = "ec2.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_kms_key" "test" { + enable_key_rotation = true + description = %[1]q + deletion_window_in_days = 7 +} + +resource "aws_kms_alias" "a" { + name = "alias/%[1]s" + target_key_id = aws_kms_key.test.key_id +} + +resource "aws_backup_vault" "test" { + name = %[1]q + kms_key_arn = aws_kms_key.test.arn +} + +resource "aws_backup_restore_testing_plan" "test" { + name = "%[1]s_plan" + + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 +} +`, rName) +} + +func testAccRestoreTestingSelectionConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccRestoreTestingSelectionConfig_base(rName), + fmt.Sprintf(` +resource "aws_backup_restore_testing_selection" "test" { + name = %[1]q + + restore_testing_plan_name = aws_backup_restore_testing_plan.test.name + protected_resource_type = "EC2" + iam_role_arn = aws_iam_role.test.arn + + protected_resource_arns = ["*"] +} +`, rName)) +} + +func testAccRestoreTestingSelectionConfig_updates(rName string) string { + return acctest.ConfigCompose( + testAccRestoreTestingSelectionConfig_base(rName), + fmt.Sprintf(` +resource "aws_backup_restore_testing_selection" "test" { + name = %[1]q + + restore_testing_plan_name = aws_backup_restore_testing_plan.test.name + protected_resource_type = "EC2" + iam_role_arn = aws_iam_role.test.arn + + protected_resource_conditions { + string_equals { + key = "aws:ResourceTag/backup" + value = true + } + } + + validation_window_hours = 10 + + restore_metadata_overrides = { + instanceType = "t2.micro" + } +} +`, rName)) +} diff --git a/internal/service/backup/service_package_gen.go b/internal/service/backup/service_package_gen.go index 148ded0ae7e4..8bc5ccc40c59 100644 --- a/internal/service/backup/service_package_gen.go +++ b/internal/service/backup/service_package_gen.go @@ -27,6 +27,17 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic IdentifierAttribute: names.AttrARN, }, }, + { + Factory: newRestoreTestingPlanResource, + Name: "Restore Testing Plan", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, + { + Factory: newRestoreTestingSelectionResource, + Name: "Restore Testing Plan Selection", + }, } } diff --git a/internal/service/backup/sweep.go b/internal/service/backup/sweep.go index 12ea9a415b6a..c1f8e4045707 100644 --- a/internal/service/backup/sweep.go +++ b/internal/service/backup/sweep.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/sweep" "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -26,6 +27,19 @@ func RegisterSweepers() { F: sweepReportPlan, }) + resource.AddTestSweepers("aws_backup_restore_testing_plan", &resource.Sweeper{ + Name: "aws_backup_restore_testing_plan", + F: sweepRestoreTestingPlans, + Dependencies: []string{ + "aws_backup_restore_testing_selection", + }, + }) + + resource.AddTestSweepers("aws_backup_restore_testing_selection", &resource.Sweeper{ + Name: "aws_backup_restore_testing_selection", + F: sweepRestoreTestingSelections, + }) + resource.AddTestSweepers("aws_backup_vault_lock_configuration", &resource.Sweeper{ Name: "aws_backup_vault_lock_configuration", F: sweepVaultLockConfiguration, @@ -106,7 +120,7 @@ func sweepReportPlan(region string) error { page, err := pages.NextPage(ctx) if awsv2.SkipSweepError(err) { - log.Printf("[WARN] Skipping Backup Report Plans sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping Backup Report Plan sweep for %s: %s", region, err) return nil } @@ -130,6 +144,95 @@ func sweepReportPlan(region string) error { return nil } +func sweepRestoreTestingPlans(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.BackupClient(ctx) + input := &backup.ListRestoreTestingPlansInput{} + sweepResources := make([]sweep.Sweepable, 0) + + pages := backup.NewListRestoreTestingPlansPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping Backup Restore Testing Plan sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Backup Restore Testing Plans for %s: %w", region, err) + } + + for _, v := range page.RestoreTestingPlans { + sweepResources = append(sweepResources, framework.NewSweepResource(newRestoreTestingPlanResource, client, + framework.NewAttribute(names.AttrName, aws.ToString(v.RestoreTestingPlanName)))) + } + } + + if err := sweep.SweepOrchestrator(ctx, sweepResources); err != nil { + return fmt.Errorf("error sweeping Backup Restore Testing Plans for %s: %w", region, err) + } + + return nil +} + +func sweepRestoreTestingSelections(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.BackupClient(ctx) + input := &backup.ListRestoreTestingPlansInput{} + sweepResources := make([]sweep.Sweepable, 0) + + pages := backup.NewListRestoreTestingPlansPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping Backup Restore Testing Plan sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Backup Restore Testing Plans for %s: %w", region, err) + } + + for _, v := range page.RestoreTestingPlans { + restoreTestingPlanName := aws.ToString(v.RestoreTestingPlanName) + input := &backup.ListRestoreTestingSelectionsInput{ + RestoreTestingPlanName: aws.String(restoreTestingPlanName), + } + + pages := backup.NewListRestoreTestingSelectionsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if err != nil { + continue + } + + for _, v := range page.RestoreTestingSelections { + sweepResources = append(sweepResources, framework.NewSweepResource(newRestoreTestingSelectionResource, client, + framework.NewAttribute(names.AttrName, aws.ToString(v.RestoreTestingSelectionName)), + framework.NewAttribute("restore_testing_plan_name", restoreTestingPlanName))) + } + } + } + } + + if err := sweep.SweepOrchestrator(ctx, sweepResources); err != nil { + return fmt.Errorf("error sweeping Backup Restore Testing Selections for %s: %w", region, err) + } + + return nil +} + func sweepVaultLockConfiguration(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) @@ -163,7 +266,7 @@ func sweepVaultLockConfiguration(region string) error { } if err = sweep.SweepOrchestrator(ctx, sweepResources); err != nil { - return fmt.Errorf("error sweeping Backup Vault Lock Configuration for %s: %w", region, err) + return fmt.Errorf("error sweeping Backup Vault Lock Configurations for %s: %w", region, err) } return nil @@ -223,7 +326,7 @@ func sweepVaultPolicies(region string) error { page, err := pages.NextPage(ctx) if awsv2.SkipSweepError(err) { - log.Printf("[WARN] Skipping Backup Vault Policies sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping Backup Vault Policy sweep for %s: %s", region, err) return nil } @@ -262,7 +365,7 @@ func sweepVaults(region string) error { page, err := pages.NextPage(ctx) if awsv2.SkipSweepError(err) { - log.Printf("[WARN] Skipping Backup Vaults sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping Backup Vault sweep for %s: %s", region, err) return nil } diff --git a/website/docs/r/backup_restore_testing_plan.html.markdown b/website/docs/r/backup_restore_testing_plan.html.markdown new file mode 100644 index 000000000000..5e100c5969b1 --- /dev/null +++ b/website/docs/r/backup_restore_testing_plan.html.markdown @@ -0,0 +1,68 @@ +--- +subcategory: "Backup" +layout: "aws" +page_title: "AWS: aws_backup_restore_testing_plan" +description: |- + Terraform resource for managing an AWS Backup Restore Testing Plan. +--- +# Resource: aws_backup_restore_testing_plan + +Terraform resource for managing an AWS Backup Restore Testing Plan. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_backup_restore_testing_plan" "example" { + recovery_point_selection { + algorithm = "LATEST_WITHIN_WINDOW" + include_vaults = ["*"] + recovery_point_types = ["CONTINUOUS"] + } + + schedule_expression = "cron(0 12 ? * * *)" # Daily at 12:00 +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` (Required): The name of the restore testing plan. Must be between 1 and 50 characters long and contain only alphanumeric characters and underscores. +* `schedule_expression` (Required): The schedule expression for the restore testing plan. +* `schedule_expression_timezone` (Optional): The timezone for the schedule expression. If not provided, the state value will be used. +* `start_window_hours` (Optional): The number of hours in the start window for the restore testing plan. Must be between 1 and 168. +* `recovery_point_selection` (Required): Specifies the recovery point selection configuration. See [RecoveryPointSelection](#recoverypointselection) section for more details. + +### RecoveryPointSelection + +* `algorithm` (Required): Specifies the algorithm used for selecting recovery points. Valid values are "RANDOM_WITHIN_WINDOW" and "LATEST_WITHIN_WINDOW". +* `include_vaults` (Required): Specifies the backup vaults to include in the recovery point selection. Each value must be a valid AWS ARN for a backup vault or "*" to include all backup vaults. +* `recovery_point_types` (Required): Specifies the types of recovery points to include in the selection. Valid values are "CONTINUOUS" and "SNAPSHOT". +* `exclude_vaults` (Optional): Specifies the backup vaults to exclude from the recovery point selection. Each value must be a valid AWS ARN for a backup vault or "*" to exclude all backup vaults. +* `selection_window_days` (Optional): Specifies the number of days within which the recovery points should be selected. Must be a value between 1 and 365. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Restore Testing Plan. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Backup Restore Testing Plan using the `name`. For example: + +```terraform +import { + to = aws_backup_restore_testing_plan.example + id = "my_testing_plan" +} +``` + +Using `terraform import`, import Backup Restore Testing Plan using the `name`. For example: + +```console +% terraform import aws_backup_restore_testing_plan.example my_testing_plan +``` diff --git a/website/docs/r/backup_restore_testing_selection.html.markdown b/website/docs/r/backup_restore_testing_selection.html.markdown new file mode 100644 index 000000000000..454af639818a --- /dev/null +++ b/website/docs/r/backup_restore_testing_selection.html.markdown @@ -0,0 +1,89 @@ +--- +subcategory: "Backup" +layout: "aws" +page_title: "AWS: aws_backup_restore_testing_selection" +description: |- + Terraform resource for managing an AWS Backup Restore Testing Selection. +--- + +# Resource: aws_backup_restore_testing_selection + +Terraform resource for managing an AWS Backup Restore Testing Selection. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_backup_restore_testing_selection" "example" { + name = "ec2_selection" + + restore_testing_plan_name = aws_backup_restore_testing_plan.example.name + protected_resource_type = "EC2" + iam_role_arn = aws_iam_role.example.arn + + protected_resource_arns = ["*"] +} +``` + +### Advanced Usage + +```terraform +resource "aws_backup_restore_testing_selection" "example" { + name = "ec2_selection" + + restore_testing_plan_name = aws_backup_restore_testing_plan.example.name + protected_resource_type = "EC2" + iam_role_arn = aws_iam_role.example.arn + + protected_resource_conditions { + string_equals { + key = "aws:ResourceTag/backup" + value = true + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the backup restore testing selection. +* `restore_testing_plan_name` - (Required) The name of the restore testing plan. +* `protected_resource_type` - (Required) The type of the protected resource. +* `iam_role_arn` - (Required) The ARN of the IAM role. +* `protected_resource_arns` - (Optional) The ARNs for the protected resources. +* `protected_resource_conditions` - (Optional) The conditions for the protected resource. +* `restore_metadata_overrides` - (Optional) Override certain restore metadata keys. See the complete list of [restore testing inferred metadata](https://docs.aws.amazon.com/aws-backup/latest/devguide/restore-testing-inferred-metadata.html) . + +The `protected_resource_conditions` block supports the following arguments: + +* `string_equals` - (Optional) The list of string equals conditions for resource tags. Filters the values of your tagged resources for only those resources that you tagged with the same value. Also called "exact matching.". See [the structure for details](#keyvalues) +* `string_not_equals` - (Optional) The list of string not equals conditions for resource tags. Filters the values of your tagged resources for only those resources that you tagged that do not have the same value. Also called "negated matching.". See [the structure for details](#keyvalues) + +### KeyValues + +* `key` - (Required) The Tag name, must start with one of the following prefixes: [aws:ResourceTag/] with a Minimum length of 1. Maximum length of 128, and can contain characters that are letters, white space, and numbers that can be represented in UTF-8 and the following characters: `+ - = . _ : /`. +* `value` - (Required) The value of the Tag. Maximum length of 256. + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Backup Restore Testing Selection using `name:restore_testing_plan_name`. For example: + +```terraform +import { + to = aws_backup_restore_testing_selection.example + id = "my_testing_selection:my_testing_plan" +} +``` + +Using `terraform import`, import Backup Restore Testing Selection using `name:restore_testing_plan_name`. For example: + +```console +% terraform import aws_backup_restore_testing_selection.example restore_testing_selection_12345678:restore_testing_plan_12345678 +```