diff --git a/.changelog/38913.txt b/.changelog/38913.txt new file mode 100644 index 000000000000..6be39fcbbf4f --- /dev/null +++ b/.changelog/38913.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_resiliencehub_resiliency_policy +``` diff --git a/.teamcity/components/generated/services_all.kt b/.teamcity/components/generated/services_all.kt index c1b84d0d0433..90ac07084298 100644 --- a/.teamcity/components/generated/services_all.kt +++ b/.teamcity/components/generated/services_all.kt @@ -186,7 +186,7 @@ val services = mapOf( "redshiftdata" to ServiceSpec("Redshift Data"), "redshiftserverless" to ServiceSpec("Redshift Serverless"), "rekognition" to ServiceSpec("Rekognition"), - "resiliencehub" to ServiceSpec("Resilience Hub"), + "resiliencehub" to ServiceSpec("Resilience Hub", parallelismOverride = 10), "resourceexplorer2" to ServiceSpec("Resource Explorer"), "resourcegroups" to ServiceSpec("Resource Groups"), "resourcegroupstaggingapi" to ServiceSpec("Resource Groups Tagging"), diff --git a/internal/generate/teamcity/acctest_services.hcl b/internal/generate/teamcity/acctest_services.hcl index 93d0712638d9..02371378470f 100644 --- a/internal/generate/teamcity/acctest_services.hcl +++ b/internal/generate/teamcity/acctest_services.hcl @@ -219,6 +219,10 @@ service "redshift" { vpc_lock = true } +service "resiliencehub" { + parallelism = 10 +} + service "route53" { vpc_lock = true } diff --git a/internal/service/resiliencehub/exports_test.go b/internal/service/resiliencehub/exports_test.go new file mode 100644 index 000000000000..d52ff9d16f7a --- /dev/null +++ b/internal/service/resiliencehub/exports_test.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resiliencehub + +// Exports for use in tests only. +var ( + ResourceResiliencyPolicy = newResourceResiliencyPolicy +) diff --git a/internal/service/resiliencehub/generate.go b/internal/service/resiliencehub/generate.go index fa6c2d1f1ed6..fe43e1481b65 100644 --- a/internal/service/resiliencehub/generate.go +++ b/internal/service/resiliencehub/generate.go @@ -1,6 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 +//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ServiceTagsMap -KVTValues -SkipTypesImp -ListTags -ListTagsInIDElem=ResourceArn -ListTagsOutTagsElem=Tags -TagOp=TagResource -TagInIDElem=ResourceArn -UntagOp=UntagResource -UpdateTags //go:generate go run ../../generate/servicepackage/main.go // ONLY generate directives and package declaration! Do not add anything else to this file. diff --git a/internal/service/resiliencehub/resiliency_policy.go b/internal/service/resiliencehub/resiliency_policy.go new file mode 100644 index 000000000000..5c7571d5019f --- /dev/null +++ b/internal/service/resiliencehub/resiliency_policy.go @@ -0,0 +1,616 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resiliencehub + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/resiliencehub" + awstypes "github.com/aws/aws-sdk-go-v2/service/resiliencehub/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_resiliencehub_resiliency_policy", name="Resiliency Policy") +// @Tags(identifierAttribute="arn") +func newResourceResiliencyPolicy(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceResiliencyPolicy{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameResiliencyPolicy = "Resiliency Policy" +) + +type resourceResiliencyPolicy struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceResiliencyPolicy) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_resiliencehub_resiliency_policy" +} + +func (r *resourceResiliencyPolicy) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + requiredObjAttrs := map[string]schema.Attribute{ + "rto": schema.StringAttribute{ + Description: "Recovery Time Objective (RTO) as a Go duration.", + CustomType: timetypes.GoDurationType{}, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "rpo": schema.StringAttribute{ + Description: "Recovery Point Objective (RPO) as a Go duration.", + CustomType: timetypes.GoDurationType{}, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + } + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + "data_location_constraint": schema.StringAttribute{ + Description: "Specifies a high-level geographical location constraint for where resilience policy data can be stored.", + CustomType: fwtypes.StringEnumType[awstypes.DataLocationConstraint](), + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrDescription: schema.StringAttribute{ + Description: "The description for the policy.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtMost(500), + }, + }, + names.AttrName: schema.StringAttribute{ + Description: "The name of the policy.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 60), + stringvalidator.RegexMatches(regexache.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]+$`), "Must start with an alphanumeric character and contain alphanumeric characters, underscores, or hyphens"), + }, + }, + "tier": schema.StringAttribute{ + Description: "The tier for the resiliency policy, ranging from the highest severity (MissionCritical) to lowest (NonCritical).", + CustomType: fwtypes.StringEnumType[awstypes.ResiliencyPolicyTier](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "estimated_cost_tier": schema.StringAttribute{ + Description: "Specifies the estimated cost tier of the resiliency policy.", + CustomType: fwtypes.StringEnumType[awstypes.EstimatedCostTier](), + Computed: true, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + names.AttrPolicy: schema.SingleNestedBlock{ + Description: "The resiliency failure policy.", + CustomType: fwtypes.NewObjectTypeOf[resourceResiliencyPolicyModel](ctx), + Validators: []validator.Object{ + objectvalidator.IsRequired(), + }, + Blocks: map[string]schema.Block{ + "az": schema.SingleNestedBlock{ + CustomType: fwtypes.NewObjectTypeOf[resourceResiliencyObjectiveModel](ctx), + Description: "The RTO and RPO target to measure resiliency for potential availability zone disruptions.", + Validators: []validator.Object{ + objectvalidator.IsRequired(), + }, + Attributes: requiredObjAttrs, + }, + "hardware": schema.SingleNestedBlock{ + CustomType: fwtypes.NewObjectTypeOf[resourceResiliencyObjectiveModel](ctx), + Description: "The RTO and RPO target to measure resiliency for potential infrastructure disruptions.", + Validators: []validator.Object{ + objectvalidator.IsRequired(), + }, + Attributes: requiredObjAttrs, + }, + "software": schema.SingleNestedBlock{ + CustomType: fwtypes.NewObjectTypeOf[resourceResiliencyObjectiveModel](ctx), + Description: "The RTO and RPO target to measure resiliency for potential application disruptions.", + Validators: []validator.Object{ + objectvalidator.IsRequired(), + }, + Attributes: requiredObjAttrs, + }, + names.AttrRegion: schema.SingleNestedBlock{ + CustomType: fwtypes.NewObjectTypeOf[resourceResiliencyObjectiveModel](ctx), + Description: "The RTO and RPO target to measure resiliency for potential region disruptions.", + Validators: []validator.Object{ + objectvalidator.AlsoRequires( + path.MatchRelative().AtName("rto"), + path.MatchRelative().AtName("rpo"), + ), + }, + Attributes: map[string]schema.Attribute{ + "rto": schema.StringAttribute{ + Description: "Recovery Time Objective (RTO) as a Go duration.", + CustomType: timetypes.GoDurationType{}, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "rpo": schema.StringAttribute{ + Description: "Recovery Point Objective (RPO) as a Go duration.", + CustomType: timetypes.GoDurationType{}, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceResiliencyPolicy) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceResiliencyPolicyData + + conn := r.Meta().ResilienceHubClient(ctx) + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // "Policy" cannot be handled by AutoFlex, since the parameter in the AWS API is a map + planPolicyModel, diags := plan.Policy.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + var in resiliencehub.CreateResiliencyPolicyInput + resp.Diagnostics.Append(flex.Expand(ctx, plan, &in, flex.WithIgnoredFieldNamesAppend("Policy"))...) + if resp.Diagnostics.HasError() { + return + } + + p, d := planPolicyModel.expandPolicy(ctx) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + in.Policy = p + + in.Tags = getTagsIn(ctx) + + out, err := conn.CreateResiliencyPolicy(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionCreating, ResNameResiliencyPolicy, "", err), + err.Error(), + ) + return + } + if out == nil || out.Policy == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionCreating, ResNameResiliencyPolicy, "", nil), + errors.New("empty output").Error(), + ) + return + } + + arn := aws.ToString(out.Policy.PolicyArn) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + created, err := waitResiliencyPolicyCreated(ctx, conn, arn, createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionWaitingForCreation, ResNameResiliencyPolicy, plan.PolicyARN.ValueString(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, created, &plan, flex.WithIgnoredFieldNamesAppend("Policy"))...) + if resp.Diagnostics.HasError() { + return + } + + plan.flattenPolicy(ctx, created.Policy) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceResiliencyPolicy) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state resourceResiliencyPolicyData + + conn := r.Meta().ResilienceHubClient(ctx) + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findResiliencyPolicyByARN(ctx, conn, state.PolicyARN.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionSetting, ResNameResiliencyPolicy, state.PolicyARN.ValueString(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out, &state, flex.WithIgnoredFieldNamesAppend("Policy"))...) + if resp.Diagnostics.HasError() { + return + } + + state.flattenPolicy(ctx, out.Policy) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceResiliencyPolicy) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state resourceResiliencyPolicyData + + conn := r.Meta().ResilienceHubClient(ctx) + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Calculate(ctx, plan, state) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + in := resiliencehub.UpdateResiliencyPolicyInput{ + PolicyArn: flex.StringFromFramework(ctx, plan.PolicyARN), + } + + if !plan.PolicyDescription.Equal(state.PolicyDescription) { + in.PolicyDescription = flex.StringFromFramework(ctx, plan.PolicyDescription) + } + + if !plan.DataLocationConstraint.Equal(state.DataLocationConstraint) { + in.DataLocationConstraint = plan.DataLocationConstraint.ValueEnum() + } + + if !plan.Policy.Equal(state.Policy) { + planPolicyModel, d := plan.Policy.ToPtr(ctx) + resp.Diagnostics.Append(d...) + if d.HasError() { + return + } + p, d := planPolicyModel.expandPolicy(ctx) + resp.Diagnostics.Append(d...) + if d.HasError() { + return + } + in.Policy = p + } + + if !plan.PolicyName.Equal(state.PolicyName) { + in.PolicyName = flex.StringFromFramework(ctx, plan.PolicyName) + } + + if !plan.Tier.Equal(state.Tier) { + in.Tier = plan.Tier.ValueEnum() + } + + _, err := conn.UpdateResiliencyPolicy(ctx, &in) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("reading Resilience Hub policy ID (%s)", plan.PolicyARN.String()), err.Error()) + return + } + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + updated, err := waitResiliencyPolicyUpdated(ctx, conn, plan.PolicyARN.ValueString(), updateTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionWaitingForUpdate, ResNameResiliencyPolicy, plan.PolicyARN.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, updated, &plan, flex.WithIgnoredFieldNamesAppend("Policy"))...) + if resp.Diagnostics.HasError() { + return + } + + state.flattenPolicy(ctx, updated.Policy) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceResiliencyPolicy) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state resourceResiliencyPolicyData + + conn := r.Meta().ResilienceHubClient(ctx) + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.DeleteResiliencyPolicy(ctx, &resiliencehub.DeleteResiliencyPolicyInput{ + PolicyArn: flex.StringFromFramework(ctx, state.PolicyARN), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("deleting Resilience Hub policy name (%s)", state.PolicyName.String()), err.Error()) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitResiliencyPolicyDeleted(ctx, conn, state.PolicyARN.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ResilienceHub, create.ErrActionWaitingForDeletion, ResNameResiliencyPolicy, state.PolicyARN.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceResiliencyPolicy) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrARN), req, resp) +} + +func (r *resourceResiliencyPolicy) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +const ( + statusChangePending = "Pending" + statusDeleting = "Deleting" + statusNormal = "Normal" + statusUpdated = "Updated" + statusCompleted = "Completed" +) + +func waitResiliencyPolicyCreated(ctx context.Context, conn *resiliencehub.Client, arn string, timeout time.Duration) (*awstypes.ResiliencyPolicy, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusCompleted}, + Refresh: statusResiliencyPolicy(ctx, conn, arn), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.ResiliencyPolicy); ok { + return out, err + } + + return nil, err +} + +func waitResiliencyPolicyUpdated(ctx context.Context, conn *resiliencehub.Client, arn string, timeout time.Duration) (*awstypes.ResiliencyPolicy, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusCompleted}, + Refresh: statusResiliencyPolicy(ctx, conn, arn), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.ResiliencyPolicy); ok { + return out, err + } + + return nil, err +} + +func waitResiliencyPolicyDeleted(ctx context.Context, conn *resiliencehub.Client, arn string, timeout time.Duration) (*awstypes.ResiliencyPolicy, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{}, + Refresh: statusResiliencyPolicy(ctx, conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.ResiliencyPolicy); ok { + return out, err + } + + return nil, err +} + +func statusResiliencyPolicy(ctx context.Context, conn *resiliencehub.Client, arn string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findResiliencyPolicyByARN(ctx, conn, arn) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, statusCompleted, nil + } +} + +func findResiliencyPolicyByARN(ctx context.Context, conn *resiliencehub.Client, arn string) (*awstypes.ResiliencyPolicy, error) { + in := &resiliencehub.DescribeResiliencyPolicyInput{ + PolicyArn: aws.String(arn), + } + + out, err := conn.DescribeResiliencyPolicy(ctx, in) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.Policy == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.Policy, nil +} + +func (m *resourceResiliencyPolicyModel) expandPolicy(ctx context.Context) (result map[string]awstypes.FailurePolicy, diags diag.Diagnostics) { + failurePolicy := make(map[string]awstypes.FailurePolicy) + + // Policy key case must be modified to align with key case in CreateResiliencyPolicy API documentation. + // See https://docs.aws.amazon.com/resilience-hub/latest/APIReference/API_CreateResiliencyPolicy.html + failurePolicyKeyMap := map[awstypes.TestType]fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel]{ + awstypes.TestTypeAz: m.AZ, + awstypes.TestTypeHardware: m.Hardware, + awstypes.TestTypeSoftware: m.Software, + awstypes.TestTypeRegion: m.Region, + } + for k, v := range failurePolicyKeyMap { + if !v.IsNull() { + resObjModel, d := v.ToPtr(ctx) + diags.Append(d...) + if d.HasError() { + return result, diags + } + rpo, d := resObjModel.Rpo.ValueGoDuration() + if d.HasError() { + return result, diags + } + rto, d := resObjModel.Rto.ValueGoDuration() + if d.HasError() { + return result, diags + } + failurePolicy[string(k)] = awstypes.FailurePolicy{ + RpoInSecs: int32(rpo.Seconds()), + RtoInSecs: int32(rto.Seconds()), + } + } + } + + result = failurePolicy + + return result, diags +} + +func (m *resourceResiliencyPolicyData) flattenPolicy(ctx context.Context, failurePolicy map[string]awstypes.FailurePolicy) { + if len(failurePolicy) == 0 { + m.Policy = fwtypes.NewObjectValueOfNull[resourceResiliencyPolicyModel](ctx) + } + + newResObjModel := func(policyType awstypes.TestType, failurePolicy map[string]awstypes.FailurePolicy) fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel] { + if pv, exists := failurePolicy[string(policyType)]; exists { + return fwtypes.NewObjectValueOfMust(ctx, &resourceResiliencyObjectiveModel{ + Rpo: timetypes.NewGoDurationValue(time.Duration(pv.RpoInSecs) * time.Second), + Rto: timetypes.NewGoDurationValue(time.Duration(pv.RtoInSecs) * time.Second), + }) + } else { + return fwtypes.NewObjectValueOfNull[resourceResiliencyObjectiveModel](ctx) + } + } + + m.Policy = fwtypes.NewObjectValueOfMust(ctx, &resourceResiliencyPolicyModel{ + newResObjModel(awstypes.TestTypeAz, failurePolicy), + newResObjModel(awstypes.TestTypeHardware, failurePolicy), + newResObjModel(awstypes.TestTypeSoftware, failurePolicy), + newResObjModel(awstypes.TestTypeRegion, failurePolicy), + }) +} + +type resourceResiliencyPolicyData struct { + DataLocationConstraint fwtypes.StringEnum[awstypes.DataLocationConstraint] `tfsdk:"data_location_constraint"` + EstimatedCostTier fwtypes.StringEnum[awstypes.EstimatedCostTier] `tfsdk:"estimated_cost_tier"` + Policy fwtypes.ObjectValueOf[resourceResiliencyPolicyModel] `tfsdk:"policy"` + PolicyARN types.String `tfsdk:"arn"` + PolicyDescription types.String `tfsdk:"description"` + PolicyName types.String `tfsdk:"name"` + Tier fwtypes.StringEnum[awstypes.ResiliencyPolicyTier] `tfsdk:"tier"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type resourceResiliencyPolicyModel struct { + AZ fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel] `tfsdk:"az"` + Hardware fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel] `tfsdk:"hardware"` + Software fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel] `tfsdk:"software"` + Region fwtypes.ObjectValueOf[resourceResiliencyObjectiveModel] `tfsdk:"region"` +} + +type resourceResiliencyObjectiveModel struct { + Rpo timetypes.GoDuration `tfsdk:"rpo"` + Rto timetypes.GoDuration `tfsdk:"rto"` +} diff --git a/internal/service/resiliencehub/resiliency_policy_test.go b/internal/service/resiliencehub/resiliency_policy_test.go new file mode 100644 index 000000000000..b0bc5fb6a617 --- /dev/null +++ b/internal/service/resiliencehub/resiliency_policy_test.go @@ -0,0 +1,1002 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resiliencehub_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/resiliencehub" + awstypes "github.com/aws/aws-sdk-go-v2/service/resiliencehub/types" + "github.com/hashicorp/terraform-plugin-testing/compare" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + tfresiliencehub "github.com/hashicorp/terraform-provider-aws/internal/service/resiliencehub" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccResilienceHubResiliencyPolicy_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, names.ResilienceHubServiceID, regexache.MustCompile(`resiliency-policy/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckNoResourceAttr(resourceName, names.AttrDescription), + resource.TestCheckResourceAttr(resourceName, "tier", "NotApplicable"), + resource.TestCheckResourceAttr(resourceName, "data_location_constraint", "AnyLocation"), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", "1h0m0s"), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", "1h0m0s"), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", "1h0m0s"), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", "1h0m0s"), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rpo"), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rto"), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", "1h0m0s"), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", "1h0m0s"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_dataLocationConstraint(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_dataLocationConstraint(rName, awstypes.DataLocationConstraintSameContinent), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, "data_location_constraint", string(awstypes.DataLocationConstraintSameContinent)), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_dataLocationConstraint(rName, awstypes.DataLocationConstraintSameCountry), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "data_location_constraint", string(awstypes.DataLocationConstraintSameCountry)), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_description(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + const ( + initial = "initial" + updated = "updated" + ) + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_description(rName, initial), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, initial), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_description(rName, updated), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, updated), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_name(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rNameUpdated := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_basic(rNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rNameUpdated), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_policy(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + const ( + initialDuration = "1h0m0s" + updatedDuration = "24h0m0s" + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_policy(rName, initialDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", initialDuration), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rpo"), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rto"), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", initialDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_policy(rName, updatedDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", updatedDuration), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rpo"), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rto"), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", updatedDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_policyWithRegion(rName, updatedDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", updatedDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_policyWithRegion(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + const ( + initialDuration = "1h0m0s" + updatedDuration = "24h0m0s" + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_policyWithRegion(rName, initialDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rto", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", initialDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", initialDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_policyWithRegion(rName, updatedDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.region.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", updatedDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_policy(rName, updatedDuration), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "policy.az.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.az.rto", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.hardware.rto", updatedDuration), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rpo"), + resource.TestCheckNoResourceAttr(resourceName, "policy.region.rto"), + resource.TestCheckResourceAttr(resourceName, "policy.software.rpo", updatedDuration), + resource.TestCheckResourceAttr(resourceName, "policy.software.rto", updatedDuration), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_tier(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + expectNoARNChange := statecheck.CompareValue(compare.ValuesSame()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_tier(rName, awstypes.ResiliencyPolicyTierMissionCritical), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, "tier", string(awstypes.ResiliencyPolicyTierMissionCritical)), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + { + Config: testAccResiliencyPolicyConfig_tier(rName, awstypes.ResiliencyPolicyTierCoreServices), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + resource.TestCheckResourceAttr(resourceName, "tier", string(awstypes.ResiliencyPolicyTierCoreServices)), + ), + ConfigStateChecks: []statecheck.StateCheck{ + expectNoARNChange.AddStateValue(resourceName, tfjsonpath.New(names.AttrARN)), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAttrImportStateIdFunc(resourceName, names.AttrARN), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrARN, + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_tags(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy1, policy2 resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy1), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, names.ResilienceHubServiceID, regexache.MustCompile(`resiliency-policy/.+`)), + ), + }, + { + Config: testAccResiliencyPolicyConfig_tag2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy2), + testAccCheckResiliencyPolicyNotRecreated(&policy1, &policy2), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, names.ResilienceHubServiceID, regexache.MustCompile(`resiliency-policy/.+`)), + ), + }, + }, + }) +} + +func TestAccResilienceHubResiliencyPolicy_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var policy resiliencehub.DescribeResiliencyPolicyOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_resiliencehub_resiliency_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ResilienceHubServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckResiliencyPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccResiliencyPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResiliencyPolicyExists(ctx, resourceName, &policy), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfresiliencehub.ResourceResiliencyPolicy, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckResiliencyPolicyDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ResilienceHubClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_resiliencehub_resiliency_policy" { + continue + } + + input := &resiliencehub.DescribeResiliencyPolicyInput{ + PolicyArn: aws.String(rs.Primary.Attributes[names.AttrARN]), + } + _, err := conn.DescribeResiliencyPolicy(ctx, input) + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil + } + if err != nil { + return create.Error(names.ResilienceHub, create.ErrActionCheckingDestroyed, tfresiliencehub.ResNameResiliencyPolicy, rs.Primary.ID, err) + } + + return create.Error(names.ResilienceHub, create.ErrActionCheckingDestroyed, tfresiliencehub.ResNameResiliencyPolicy, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckResiliencyPolicyExists(ctx context.Context, name string, policy *resiliencehub.DescribeResiliencyPolicyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.ResilienceHub, create.ErrActionCheckingExistence, tfresiliencehub.ResNameResiliencyPolicy, name, errors.New("not found")) + } + + if rs.Primary.Attributes[names.AttrARN] == "" { + return create.Error(names.ResilienceHub, create.ErrActionCheckingExistence, tfresiliencehub.ResNameResiliencyPolicy, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ResilienceHubClient(ctx) + resp, err := conn.DescribeResiliencyPolicy(ctx, &resiliencehub.DescribeResiliencyPolicyInput{ + PolicyArn: aws.String(rs.Primary.Attributes[names.AttrARN]), + }) + + if err != nil { + return create.Error(names.ResilienceHub, create.ErrActionCheckingExistence, tfresiliencehub.ResNameResiliencyPolicy, rs.Primary.ID, err) + } + + *policy = *resp + + return nil + } +} + +func testAccAttrImportStateIdFunc(resourceName, attrName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.Attributes[attrName], nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).ResilienceHubClient(ctx) + + input := &resiliencehub.ListResiliencyPoliciesInput{} + _, err := conn.ListResiliencyPolicies(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckResiliencyPolicyNotRecreated(before, after *resiliencehub.DescribeResiliencyPolicyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.Policy.PolicyArn), aws.ToString(after.Policy.PolicyArn); before != after { + return create.Error(names.ResilienceHub, create.ErrActionCheckingNotRecreated, tfresiliencehub.ResNameResiliencyPolicy, before, errors.New("recreated")) + } + + return nil + } +} + +func testAccResiliencyPolicyConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + tier = "NotApplicable" + + policy { + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } +} +`, rName) +} + +func testAccResiliencyPolicyConfig_dataLocationConstraint(rName string, dataLocationConstraint awstypes.DataLocationConstraint) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + tier = "NotApplicable" + + data_location_constraint = %[2]q + + policy { + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } +} +`, rName, dataLocationConstraint) +} + +func testAccResiliencyPolicyConfig_description(rName, resPolicyDescValue string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + description = %[2]q + + tier = "NotApplicable" + + policy { + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } +} +`, rName, resPolicyDescValue) +} + +func testAccResiliencyPolicyConfig_tier(rName string, tier awstypes.ResiliencyPolicyTier) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + tier = %[2]q + + data_location_constraint = "AnyLocation" + + policy { + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } +} +`, rName, tier) +} + +func testAccResiliencyPolicyConfig_policy(rName, duration string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + tier = "NotApplicable" + + policy { + az { + rpo = %[2]q + rto = %[2]q + } + hardware { + rpo = %[2]q + rto = %[2]q + } + software { + rpo = %[2]q + rto = %[2]q + } + } +} +`, rName, duration) +} + +func testAccResiliencyPolicyConfig_policyWithRegion(rName, duration string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + tier = "NotApplicable" + + policy { + az { + rpo = %[2]q + rto = %[2]q + } + hardware { + rpo = %[2]q + rto = %[2]q + } + region { + rpo = %[2]q + rto = %[2]q + } + software { + rpo = %[2]q + rto = %[2]q + } + } +} +`, rName, duration) +} + +func testAccResiliencyPolicyConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + description = %[1]q + + tier = "NotApplicable" + + data_location_constraint = "AnyLocation" + + policy { + region { + rpo = "1h0m0s" + rto = "1h0m0s" + } + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccResiliencyPolicyConfig_tag2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_resiliencehub_resiliency_policy" "test" { + name = %[1]q + + description = %[1]q + + tier = "NotApplicable" + + data_location_constraint = "AnyLocation" + + policy { + region { + rpo = "1h0m0s" + rto = "1h0m0s" + } + az { + rpo = "1h0m0s" + rto = "1h0m0s" + } + hardware { + rpo = "1h0m0s" + rto = "1h0m0s" + } + software { + rpo = "1h0m0s" + rto = "1h0m0s" + } + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/resiliencehub/service_package_gen.go b/internal/service/resiliencehub/service_package_gen.go index 85401587ecd9..6cb1f1d7d1cf 100644 --- a/internal/service/resiliencehub/service_package_gen.go +++ b/internal/service/resiliencehub/service_package_gen.go @@ -19,7 +19,15 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceResiliencyPolicy, + Name: "Resiliency Policy", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/internal/service/resiliencehub/sweep.go b/internal/service/resiliencehub/sweep.go new file mode 100644 index 000000000000..bebcc58f761b --- /dev/null +++ b/internal/service/resiliencehub/sweep.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resiliencehub + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/resiliencehub" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "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" +) + +func RegisterSweepers() { + awsv2.Register("aws_resiliencehub_resiliency_policy", sweepResiliencyPolicy) +} + +func sweepResiliencyPolicy(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + conn := client.ResilienceHubClient(ctx) + + var sweepResources []sweep.Sweepable + + pages := resiliencehub.NewListResiliencyPoliciesPaginator(conn, &resiliencehub.ListResiliencyPoliciesInput{}) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, policies := range page.ResiliencyPolicies { + sweepResources = append(sweepResources, framework.NewSweepResource(newResourceResiliencyPolicy, client, + framework.NewAttribute(names.AttrARN, aws.ToString(policies.PolicyArn)), + )) + } + } + + return sweepResources, nil +} diff --git a/internal/service/resiliencehub/tags_gen.go b/internal/service/resiliencehub/tags_gen.go new file mode 100644 index 000000000000..7bb3ab0bfa19 --- /dev/null +++ b/internal/service/resiliencehub/tags_gen.go @@ -0,0 +1,128 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package resiliencehub + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/resiliencehub" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/logging" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/types/option" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// listTags lists resiliencehub service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func listTags(ctx context.Context, conn *resiliencehub.Client, identifier string, optFns ...func(*resiliencehub.Options)) (tftags.KeyValueTags, error) { + input := &resiliencehub.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(ctx, input, optFns...) + + if err != nil { + return tftags.New(ctx, nil), err + } + + return KeyValueTags(ctx, output.Tags), nil +} + +// ListTags lists resiliencehub service tags and set them in Context. +// It is called from outside this package. +func (p *servicePackage) ListTags(ctx context.Context, meta any, identifier string) error { + tags, err := listTags(ctx, meta.(*conns.AWSClient).ResilienceHubClient(ctx), identifier) + + if err != nil { + return err + } + + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(tags) + } + + return nil +} + +// map[string]string handling + +// Tags returns resiliencehub service tags. +func Tags(tags tftags.KeyValueTags) map[string]string { + return tags.Map() +} + +// KeyValueTags creates tftags.KeyValueTags from resiliencehub service tags. +func KeyValueTags(ctx context.Context, tags map[string]string) tftags.KeyValueTags { + return tftags.New(ctx, tags) +} + +// getTagsIn returns resiliencehub service tags from Context. +// nil is returned if there are no input tags. +func getTagsIn(ctx context.Context) map[string]string { + if inContext, ok := tftags.FromContext(ctx); ok { + if tags := Tags(inContext.TagsIn.UnwrapOrDefault()); len(tags) > 0 { + return tags + } + } + + return nil +} + +// setTagsOut sets resiliencehub service tags in Context. +func setTagsOut(ctx context.Context, tags map[string]string) { + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(KeyValueTags(ctx, tags)) + } +} + +// updateTags updates resiliencehub service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func updateTags(ctx context.Context, conn *resiliencehub.Client, identifier string, oldTagsMap, newTagsMap any, optFns ...func(*resiliencehub.Options)) error { + oldTags := tftags.New(ctx, oldTagsMap) + newTags := tftags.New(ctx, newTagsMap) + + ctx = tflog.SetField(ctx, logging.KeyResourceId, identifier) + + removedTags := oldTags.Removed(newTags) + removedTags = removedTags.IgnoreSystem(names.ResilienceHub) + if len(removedTags) > 0 { + input := &resiliencehub.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: removedTags.Keys(), + } + + _, err := conn.UntagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + updatedTags := oldTags.Updated(newTags) + updatedTags = updatedTags.IgnoreSystem(names.ResilienceHub) + if len(updatedTags) > 0 { + input := &resiliencehub.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags), + } + + _, err := conn.TagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// UpdateTags updates resiliencehub service tags. +// It is called from outside this package. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + return updateTags(ctx, meta.(*conns.AWSClient).ResilienceHubClient(ctx), identifier, oldTags, newTags) +} diff --git a/internal/sweep/register_gen_test.go b/internal/sweep/register_gen_test.go index 918c6ebcd8ac..f0a66ba8bd73 100644 --- a/internal/sweep/register_gen_test.go +++ b/internal/sweep/register_gen_test.go @@ -124,6 +124,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/rds" "github.com/hashicorp/terraform-provider-aws/internal/service/redshift" "github.com/hashicorp/terraform-provider-aws/internal/service/redshiftserverless" + "github.com/hashicorp/terraform-provider-aws/internal/service/resiliencehub" "github.com/hashicorp/terraform-provider-aws/internal/service/resourceexplorer2" "github.com/hashicorp/terraform-provider-aws/internal/service/resourcegroups" "github.com/hashicorp/terraform-provider-aws/internal/service/route53" @@ -289,6 +290,7 @@ func registerSweepers() { rds.RegisterSweepers() redshift.RegisterSweepers() redshiftserverless.RegisterSweepers() + resiliencehub.RegisterSweepers() resourceexplorer2.RegisterSweepers() resourcegroups.RegisterSweepers() route53.RegisterSweepers() diff --git a/website/docs/r/resiliencehub_resiliency_policy.html.markdown b/website/docs/r/resiliencehub_resiliency_policy.html.markdown new file mode 100644 index 000000000000..3ddee6e8db0c --- /dev/null +++ b/website/docs/r/resiliencehub_resiliency_policy.html.markdown @@ -0,0 +1,142 @@ +--- +subcategory: "Resilience Hub" +layout: "aws" +page_title: "AWS: aws_resiliencehub_resiliency_policy" +description: |- + Terraform resource for managing an AWS Resilience Hub Resiliency Policy. +--- + +# Resource: aws_resiliencehub_resiliency_policy + +Terraform resource for managing an AWS Resilience Hub Resiliency Policy. + +## Example Usage + +```terraform +resource "aws_resiliencehub_resiliency_policy" "example" { + policy_name = "testexample" + policy_description = "testexample" + + tier = "NonCritical" + + data_location_constraint = "AnyLocation" + + policy { + region { + rpo = "24h" + rto = "24h" + } + az { + rpo = "24h" + rto = "24h" + } + hardware { + rpo = "24h" + rto = "24h" + } + software { + rpo = "24h" + rto = "24h" + } + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` (String) Name of Resiliency Policy. + Must be between 2 and 60 characters long. + Must start with an alphanumeric character and contain alphanumeric characters, underscores, or hyphens. +* `tier` (String) Resiliency Policy Tier. + Valid values are `MissionCritical`, `Critical`, `Important`, `CoreServices`, `NonCritical`, and `NotApplicable`. +* `policy` (Attributes) The type of resiliency policy to be created, including the recovery time objective (RTO) and recovery point objective (RPO) in seconds. See [`policy`](#policy). + +The following arguments are optional: + +* `description` (String) Description of Resiliency Policy. +* `data_location_constraint` (String) Data Location Constraint of the Policy. + Valid values are `AnyLocation`, `SameContinent`, and `SameCountry`. +* `tags` - (Map Of String) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### `policy` + +The following arguments are required: + +* `az` - (Attributes) Specifies Availability Zone failure policy. See [`policy.az`](#policyaz) +* `hardware` - (Attributes) Specifies Infrastructure failure policy. See [`policy.hardware`](#policyhardware) +* `software` - (Attributes) Specifies Application failure policy. See [`policy.software`](#policysoftware) + +The following arguments are optional: + +* `region` - (Attributes) Specifies Region failure policy. [`policy.region`](#policyregion) + +### `policy.az` + +The following arguments are required: + +* `rpo` - (Number) Recovery Point Objective (RPO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. +* `rto` - (Number) Recovery Time Objective (RTO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. + +### `policy.hardware` + +The following arguments are required: + +* `rpo` - (Number) Recovery Point Objective (RPO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. +* `rto` - (Number) Recovery Time Objective (RTO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. + +### `policy.software` + +The following arguments are required: + +* `rpo` - (Number) Recovery Point Objective (RPO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. +* `rto` - (Number) Recovery Time Objective (RTO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. + +### `policy.region` + +The following arguments are required: + +* `rpo` - (Number) Recovery Point Objective (RPO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. +* `rto` - (Number) Recovery Time Objective (RTO) as a Go duration. + Represented by a string such as `1h`, `2h45m`, or `30m15s`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Resiliency Policy. +* `estimated_cost_tier` - Estimated Cost Tier of the Resiliency Policy. +* `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). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Resilience Hub Resiliency Policy using the `arn`. For example: + +```terraform +import { + to = aws_resiliencehub_resiliency_policy.example + id = "arn:aws:resiliencehub:us-east-1:123456789012:resiliency-policy/8c1cfa29-d1dd-4421-aa68-c9f64cced4c2" +} +``` + +Using `terraform import`, import Resilience Hub Resiliency Policy using the `arn`. For example: + +```console +% terraform import aws_resiliencehub_resiliency_policy.example arn:aws:resiliencehub:us-east-1:123456789012:resiliency-policy/8c1cfa29-d1dd-4421-aa68-c9f64cced4c2 +```