diff --git a/.changelog/35349.txt b/.changelog/35349.txt new file mode 100644 index 000000000000..ac16340ec4da --- /dev/null +++ b/.changelog/35349.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_computeoptimizer_enrollment_status +``` + +```release-note:new-resource +aws_computeoptimizer_recommendation_preferences +``` \ No newline at end of file diff --git a/internal/service/computeoptimizer/computeoptimizer_test.go b/internal/service/computeoptimizer/computeoptimizer_test.go new file mode 100644 index 000000000000..c52f80540aa4 --- /dev/null +++ b/internal/service/computeoptimizer/computeoptimizer_test.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer_test + +import ( + "context" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/computeoptimizer/types" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfcomputeoptimizer "github.com/hashicorp/terraform-provider-aws/internal/service/computeoptimizer" +) + +func TestAccComputeOptimizer_serial(t *testing.T) { + t.Parallel() + + testCases := map[string]map[string]func(t *testing.T){ + "EnrollmentStatus": { + acctest.CtBasic: testAccEnrollmentStatus_basic, + "includeMemberAccounts": testAccEnrollmentStatus_includeMemberAccounts, + }, + "RecommendationPreferences": { + acctest.CtBasic: testAccRecommendationPreferences_basic, + acctest.CtDisappears: testAccRecommendationPreferences_disappears, + "preferredResources": testAccRecommendationPreferences_preferredResources, + "utilizationPreferences": testAccRecommendationPreferences_utilizationPreferences, + }, + } + + acctest.RunSerialTests2Levels(t, testCases, 0) +} + +func testAccPreCheckEnrollmentStatus(ctx context.Context, t *testing.T, want awstypes.Status) { + conn := acctest.Provider.Meta().(*conns.AWSClient).ComputeOptimizerClient(ctx) + + output, err := tfcomputeoptimizer.FindEnrollmentStatus(ctx, conn) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } + + if got := output.Status; got != want { + t.Fatalf("Compute Optimizer enrollment status: %s", got) + } +} diff --git a/internal/service/computeoptimizer/enrollment_status.go b/internal/service/computeoptimizer/enrollment_status.go new file mode 100644 index 000000000000..42d40eac25d3 --- /dev/null +++ b/internal/service/computeoptimizer/enrollment_status.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/computeoptimizer" + awstypes "github.com/aws/aws-sdk-go-v2/service/computeoptimizer/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Enrollment Status") +func newEnrollmentStatusResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &enrollmentStatusResource{} + + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultUpdateTimeout(5 * time.Minute) + + return r, nil +} + +type enrollmentStatusResource struct { + framework.ResourceWithConfigure + framework.WithTimeouts + framework.WithNoOpDelete + framework.WithImportByID +} + +func (*enrollmentStatusResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_computeoptimizer_enrollment_status" +} + +func (r *enrollmentStatusResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrID: framework.IDAttribute(), + "include_member_accounts": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "number_of_member_accounts_opted_in": schema.Int64Attribute{ + Computed: true, + }, + names.AttrStatus: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("Active", "Inactive"), + }, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + }), + }, + } +} + +func (r *enrollmentStatusResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data enrollmentStatusResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + input := &computeoptimizer.UpdateEnrollmentStatusInput{ + IncludeMemberAccounts: fwflex.BoolValueFromFramework(ctx, data.MemberAccountsEnrolled), + Status: awstypes.Status(fwflex.StringValueFromFramework(ctx, data.Status)), + } + + _, err := conn.UpdateEnrollmentStatus(ctx, input) + + if err != nil { + response.Diagnostics.AddError("creating Compute Optimizer Enrollment Status", err.Error()) + + return + } + + // Set values for unknowns. + data.ID = fwflex.StringValueToFramework(ctx, r.Meta().AccountID) + + output, err := waitEnrollmentStatusUpdated(ctx, conn, string(input.Status), r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Compute Optimizer Enrollment Status (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + data.NumberOfMemberAccountsOptedIn = fwflex.Int32ToFramework(ctx, output.NumberOfMemberAccountsOptedIn) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *enrollmentStatusResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data enrollmentStatusResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + output, err := findEnrollmentStatus(ctx, conn) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Compute Optimizer Enrollment Status (%s)", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *enrollmentStatusResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var new enrollmentStatusResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + input := &computeoptimizer.UpdateEnrollmentStatusInput{ + Status: awstypes.Status(fwflex.StringValueFromFramework(ctx, new.Status)), + } + + _, err := conn.UpdateEnrollmentStatus(ctx, input) + + if err != nil { + response.Diagnostics.AddError("updating Compute Optimizer Enrollment Status", err.Error()) + + return + } + + output, err := waitEnrollmentStatusUpdated(ctx, conn, string(input.Status), r.CreateTimeout(ctx, new.Timeouts)) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Compute Optimizer Enrollment Status (%s) update", new.ID.ValueString()), err.Error()) + + return + } + + new.NumberOfMemberAccountsOptedIn = fwflex.Int32ToFramework(ctx, output.NumberOfMemberAccountsOptedIn) + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func findEnrollmentStatus(ctx context.Context, conn *computeoptimizer.Client) (*computeoptimizer.GetEnrollmentStatusOutput, error) { + input := &computeoptimizer.GetEnrollmentStatusInput{} + + output, err := conn.GetEnrollmentStatus(ctx, input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func statusEnrollmentStatus(ctx context.Context, conn *computeoptimizer.Client) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findEnrollmentStatus(ctx, conn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} + +func waitEnrollmentStatusUpdated(ctx context.Context, conn *computeoptimizer.Client, targetStatus string, timeout time.Duration) (*computeoptimizer.GetEnrollmentStatusOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.StatusPending), + Target: []string{targetStatus}, + Refresh: statusEnrollmentStatus(ctx, conn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*computeoptimizer.GetEnrollmentStatusOutput); ok { + tfresource.SetLastError(err, errors.New(aws.ToString(output.StatusReason))) + + return output, err + } + + return nil, err +} + +type enrollmentStatusResourceModel struct { + ID types.String `tfsdk:"id"` + MemberAccountsEnrolled types.Bool `tfsdk:"include_member_accounts"` + NumberOfMemberAccountsOptedIn types.Int64 `tfsdk:"number_of_member_accounts_opted_in"` + Status types.String `tfsdk:"status"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/internal/service/computeoptimizer/enrollment_status_test.go b/internal/service/computeoptimizer/enrollment_status_test.go new file mode 100644 index 000000000000..6c3be196b6ed --- /dev/null +++ b/internal/service/computeoptimizer/enrollment_status_test.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/computeoptimizer" + "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" + tfcomputeoptimizer "github.com/hashicorp/terraform-provider-aws/internal/service/computeoptimizer" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccEnrollmentStatus_basic(t *testing.T) { + ctx := acctest.Context(t) + var v computeoptimizer.GetEnrollmentStatusOutput + resourceName := "aws_computeoptimizer_enrollment_status.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + acctest.PreCheckOrganizationMemberAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccEnrollmentStatusConfig_basic("Active"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnrollmentStatusExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "Active"), + resource.TestCheckResourceAttr(resourceName, "include_member_accounts", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "number_of_member_accounts_opted_in", acctest.Ct0), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEnrollmentStatusConfig_basic("Inactive"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnrollmentStatusExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "Inactive"), + resource.TestCheckResourceAttr(resourceName, "include_member_accounts", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "number_of_member_accounts_opted_in", acctest.Ct0), + ), + }, + }, + }) +} + +func testAccEnrollmentStatus_includeMemberAccounts(t *testing.T) { + ctx := acctest.Context(t) + var v computeoptimizer.GetEnrollmentStatusOutput + resourceName := "aws_computeoptimizer_enrollment_status.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccEnrollmentStatusConfig_includeMemberAccounts("Active", true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnrollmentStatusExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "Active"), + resource.TestCheckResourceAttr(resourceName, "include_member_accounts", acctest.CtTrue), + acctest.CheckResourceAttrGreaterThanOrEqualValue(resourceName, "number_of_member_accounts_opted_in", 0), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEnrollmentStatusConfig_includeMemberAccounts("Inactive", false), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckEnrollmentStatusExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "Inactive"), + resource.TestCheckResourceAttr(resourceName, "include_member_accounts", acctest.CtFalse), + acctest.CheckResourceAttrGreaterThanOrEqualValue(resourceName, "number_of_member_accounts_opted_in", 0), + ), + }, + }, + }) +} + +func testAccCheckEnrollmentStatusExists(ctx context.Context, n string, v *computeoptimizer.GetEnrollmentStatusOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ComputeOptimizerClient(ctx) + + output, err := tfcomputeoptimizer.FindEnrollmentStatus(ctx, conn) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccEnrollmentStatusConfig_basic(status string) string { + return fmt.Sprintf(` +resource "aws_computeoptimizer_enrollment_status" "test" { + status = %[1]q +} +`, status) +} + +func testAccEnrollmentStatusConfig_includeMemberAccounts(status string, includeMemberAccounts bool) string { + return fmt.Sprintf(` +resource "aws_computeoptimizer_enrollment_status" "test" { + status = %[1]q + + include_member_accounts = %[2]t +} +`, status, includeMemberAccounts) +} diff --git a/internal/service/computeoptimizer/exports_test.go b/internal/service/computeoptimizer/exports_test.go new file mode 100644 index 000000000000..173e9ceedf18 --- /dev/null +++ b/internal/service/computeoptimizer/exports_test.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer + +// Exports for use in tests only. +var ( + ResourceEnrollmentStatus = newEnrollmentStatusResource + ResourceRecommendationPreferences = newRecommendationPreferencesResource + + FindEnrollmentStatus = findEnrollmentStatus + FindRecommendationPreferencesByThreePartKey = findRecommendationPreferencesByThreePartKey +) diff --git a/internal/service/computeoptimizer/recommendation_preferences.go b/internal/service/computeoptimizer/recommendation_preferences.go new file mode 100644 index 000000000000..033a49c41492 --- /dev/null +++ b/internal/service/computeoptimizer/recommendation_preferences.go @@ -0,0 +1,470 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/computeoptimizer" + awstypes "github.com/aws/aws-sdk-go-v2/service/computeoptimizer/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "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/listplanmodifier" + "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/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Recommendation Preferences") +func newRecommendationPreferencesResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &recommendationPreferencesResource{} + + return r, nil +} + +type recommendationPreferencesResource struct { + framework.ResourceWithConfigure + framework.WithImportByID +} + +func (*recommendationPreferencesResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_computeoptimizer_recommendation_preferences" +} + +func (r *recommendationPreferencesResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + + Attributes: map[string]schema.Attribute{ + "enhanced_infrastructure_metrics": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.EnhancedInfrastructureMetrics](), + Optional: true, + }, + names.AttrID: framework.IDAttribute(), + "inferred_workload_types": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.InferredWorkloadTypesPreference](), + Optional: true, + }, + "look_back_period": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.LookBackPeriodPreference](), + Optional: true, + Computed: true, + }, + names.AttrResourceType: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(enum.Slice(awstypes.ResourceTypeAutoScalingGroup, awstypes.ResourceTypeEc2Instance, awstypes.ResourceTypeRdsDbInstance)...), + }, + }, + "savings_estimation_mode": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.SavingsEstimationMode](), + Optional: true, + }, + }, + Blocks: map[string]schema.Block{ + "external_metrics_preference": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[externalMetricsPreferenceModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrSource: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ExternalMetricsSource](), + Required: true, + }, + }, + }, + }, + "preferred_resource": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[preferredResourceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Validators: []validator.Object{ + objectvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("exclude_list"), + path.MatchRelative().AtParent().AtName("include_list"), + ), + }, + Attributes: map[string]schema.Attribute{ + "exclude_list": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("include_list")), + setvalidator.SizeAtMost(1000), + }, + }, + "include_list": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("exclude_list")), + setvalidator.SizeAtMost(1000), + }, + }, + names.AttrName: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.PreferredResourceName](), + Required: true, + }, + }, + }, + }, + names.AttrScope: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[scopeModel](ctx), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrName: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ScopeName](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "utilization_preference": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[utilizationPreferenceModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrMetricName: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CustomizableMetricName](), + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "metric_parameters": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[customizableMetricParametersModel](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "headroom": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CustomizableMetricHeadroom](), + Required: true, + }, + "threshold": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CustomizableMetricThreshold](), + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r *recommendationPreferencesResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data recommendationPreferencesResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + input := &computeoptimizer.PutRecommendationPreferencesInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.PutRecommendationPreferences(ctx, input) + + if err != nil { + response.Diagnostics.AddError("creating Compute Optimizer Recommendation Preferences", err.Error()) + + return + } + + // Set values for unknowns. + data.setID(ctx) + + // Read the resource to get other Computed attribute values. + scope := fwdiag.Must(data.Scope.ToPtr(ctx)) + output, err := findRecommendationPreferencesByThreePartKey(ctx, conn, data.ResourceType.ValueString(), scope.Name.ValueString(), scope.Value.ValueString()) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Compute Optimizer Recommendation Preferences (%s)", data.ID.ValueString()), err.Error()) + + return + } + + data.LookBackPeriod = fwtypes.StringEnumValue(output.LookBackPeriod) + + response.Diagnostics.Append(response.State.Set(ctx, data)...) +} + +func (r *recommendationPreferencesResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data recommendationPreferencesResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(ctx); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + scope := fwdiag.Must(data.Scope.ToPtr(ctx)) + output, err := findRecommendationPreferencesByThreePartKey(ctx, conn, data.ResourceType.ValueString(), scope.Name.ValueString(), scope.Value.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Compute Optimizer Recommendation Preferences (%s)", data.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *recommendationPreferencesResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var new recommendationPreferencesResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + input := &computeoptimizer.PutRecommendationPreferencesInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.PutRecommendationPreferences(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating Compute Optimizer Recommendation Preferences (%s)", new.ID.String()), err.Error()) + + return + } + + // Read the resource to get Computed attribute values. + scope := fwdiag.Must(new.Scope.ToPtr(ctx)) + output, err := findRecommendationPreferencesByThreePartKey(ctx, conn, new.ResourceType.ValueString(), scope.Name.ValueString(), scope.Value.ValueString()) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Compute Optimizer Recommendation Preferences (%s)", new.ID.ValueString()), err.Error()) + + return + } + + new.LookBackPeriod = fwtypes.StringEnumValue(output.LookBackPeriod) + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *recommendationPreferencesResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data recommendationPreferencesResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().ComputeOptimizerClient(ctx) + + input := &computeoptimizer.DeleteRecommendationPreferencesInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + input.RecommendationPreferenceNames = enum.EnumValues[awstypes.RecommendationPreferenceName]() + + _, err := conn.DeleteRecommendationPreferences(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Compute Optimizer Recommendation Preferences (%s)", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *recommendationPreferencesResource) ConfigValidators(context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.AtLeastOneOf( + path.MatchRoot("enhanced_infrastructure_metrics"), + path.MatchRoot("external_metrics_preference"), + path.MatchRoot("inferred_workload_types"), + path.MatchRoot("look_back_period"), + path.MatchRoot("preferred_resource"), + path.MatchRoot("savings_estimation_mode"), + path.MatchRoot("utilization_preference"), + ), + } +} + +func findRecommendationPreferencesByThreePartKey(ctx context.Context, conn *computeoptimizer.Client, resourceType, scopeName, scopeValue string) (*awstypes.RecommendationPreferencesDetail, error) { + input := &computeoptimizer.GetRecommendationPreferencesInput{ + ResourceType: awstypes.ResourceType(resourceType), + } + if scopeName != "" && scopeValue != "" { + input.Scope = &awstypes.Scope{ + Name: awstypes.ScopeName(scopeName), + Value: aws.String(scopeValue), + } + } + + return findRecommendationPreferences(ctx, conn, input) +} + +func findRecommendationPreferences(ctx context.Context, conn *computeoptimizer.Client, input *computeoptimizer.GetRecommendationPreferencesInput) (*awstypes.RecommendationPreferencesDetail, error) { + output, err := findRecommendationPreferenceses(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSingleValueResult(output) +} + +func findRecommendationPreferenceses(ctx context.Context, conn *computeoptimizer.Client, input *computeoptimizer.GetRecommendationPreferencesInput) ([]awstypes.RecommendationPreferencesDetail, error) { + var output []awstypes.RecommendationPreferencesDetail + + pages := computeoptimizer.NewGetRecommendationPreferencesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + output = append(output, page.RecommendationPreferencesDetails...) + } + + return output, nil +} + +type recommendationPreferencesResourceModel struct { + EnhancedInfrastructureMetrics fwtypes.StringEnum[awstypes.EnhancedInfrastructureMetrics] `tfsdk:"enhanced_infrastructure_metrics"` + ExternalMetricsPreference fwtypes.ListNestedObjectValueOf[externalMetricsPreferenceModel] `tfsdk:"external_metrics_preference"` + ID types.String `tfsdk:"id"` + InferredWorkloadTypes fwtypes.StringEnum[awstypes.InferredWorkloadTypesPreference] `tfsdk:"inferred_workload_types"` + LookBackPeriod fwtypes.StringEnum[awstypes.LookBackPeriodPreference] `tfsdk:"look_back_period"` + PreferredResources fwtypes.ListNestedObjectValueOf[preferredResourceModel] `tfsdk:"preferred_resource"` + ResourceType types.String `tfsdk:"resource_type"` + SavingsEstimationMode fwtypes.StringEnum[awstypes.SavingsEstimationMode] `tfsdk:"savings_estimation_mode"` + Scope fwtypes.ListNestedObjectValueOf[scopeModel] `tfsdk:"scope"` + UtilizationPreferences fwtypes.ListNestedObjectValueOf[utilizationPreferenceModel] `tfsdk:"utilization_preference"` +} + +const ( + recommendationPreferencesResourceIDPartCount = 3 +) + +func (m *recommendationPreferencesResourceModel) InitFromID(ctx context.Context) error { + parts, err := flex.ExpandResourceId(m.ID.ValueString(), recommendationPreferencesResourceIDPartCount, false) + if err != nil { + return err + } + + m.ResourceType = types.StringValue(parts[0]) + scope := &scopeModel{ + Name: fwtypes.StringEnumValue(awstypes.ScopeName(parts[1])), + Value: types.StringValue(parts[2]), + } + m.Scope = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, scope) + + return nil +} + +func (m *recommendationPreferencesResourceModel) setID(ctx context.Context) { + scope := fwdiag.Must(m.Scope.ToPtr(ctx)) + m.ID = types.StringValue(errs.Must(flex.FlattenResourceId([]string{m.ResourceType.ValueString(), scope.Name.ValueString(), scope.Value.ValueString()}, recommendationPreferencesResourceIDPartCount, false))) +} + +type externalMetricsPreferenceModel struct { + Source fwtypes.StringEnum[awstypes.ExternalMetricsSource] `tfsdk:"source"` +} + +type preferredResourceModel struct { + ExcludeList fwtypes.SetValueOf[types.String] `tfsdk:"exclude_list"` + IncludeList fwtypes.SetValueOf[types.String] `tfsdk:"include_list"` + Name fwtypes.StringEnum[awstypes.PreferredResourceName] `tfsdk:"name"` +} + +type scopeModel struct { + Name fwtypes.StringEnum[awstypes.ScopeName] `tfsdk:"name"` + Value types.String `tfsdk:"value"` +} + +type utilizationPreferenceModel struct { + MetricName fwtypes.StringEnum[awstypes.CustomizableMetricName] `tfsdk:"metric_name"` + MetricParameters fwtypes.ListNestedObjectValueOf[customizableMetricParametersModel] `tfsdk:"metric_parameters"` +} + +type customizableMetricParametersModel struct { + Headroom fwtypes.StringEnum[awstypes.CustomizableMetricHeadroom] `tfsdk:"headroom"` + Threshold fwtypes.StringEnum[awstypes.CustomizableMetricThreshold] `tfsdk:"threshold"` +} diff --git a/internal/service/computeoptimizer/recommendation_preferences_test.go b/internal/service/computeoptimizer/recommendation_preferences_test.go new file mode 100644 index 000000000000..50a0e60bb0af --- /dev/null +++ b/internal/service/computeoptimizer/recommendation_preferences_test.go @@ -0,0 +1,412 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package computeoptimizer_test + +import ( + "context" + "fmt" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/computeoptimizer/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "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" + tfcomputeoptimizer "github.com/hashicorp/terraform-provider-aws/internal/service/computeoptimizer" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccRecommendationPreferences_basic(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.RecommendationPreferencesDetail + resourceName := "aws_computeoptimizer_recommendation_preferences.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + testAccPreCheckEnrollmentStatus(ctx, t, "Active") + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRecommendationPreferencesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRecommendationPreferencesConfig_basic, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRecommendationPreferencesExists(ctx, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enhanced_infrastructure_metrics"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metrics_preference"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("inferred_workload_types"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("look_back_period"), knownvalue.StringExact("DAYS_32")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("preferred_resource"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrResourceType), knownvalue.StringExact("Ec2Instance")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("savings_estimation_mode"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + names.AttrName: knownvalue.StringExact("AccountId"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListSizeExact(0)), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccRecommendationPreferences_disappears(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.RecommendationPreferencesDetail + resourceName := "aws_computeoptimizer_recommendation_preferences.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + testAccPreCheckEnrollmentStatus(ctx, t, "Active") + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRecommendationPreferencesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRecommendationPreferencesConfig_basic, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRecommendationPreferencesExists(ctx, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfcomputeoptimizer.ResourceRecommendationPreferences, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccRecommendationPreferences_preferredResources(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.RecommendationPreferencesDetail + resourceName := "aws_computeoptimizer_recommendation_preferences.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + testAccPreCheckEnrollmentStatus(ctx, t, "Active") + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRecommendationPreferencesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRecommendationPreferencesConfig_preferredResources, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRecommendationPreferencesExists(ctx, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enhanced_infrastructure_metrics"), knownvalue.StringExact("Active")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metrics_preference"), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metrics_preference"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + names.AttrSource: knownvalue.StringExact("Datadog"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("inferred_workload_types"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("look_back_period"), knownvalue.StringExact("DAYS_93")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("preferred_resource"), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("preferred_resource"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "exclude_list": knownvalue.Null(), + "include_list": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("m5.xlarge"), + knownvalue.StringExact("r5"), + }), + names.AttrName: knownvalue.StringExact("Ec2InstanceTypes"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrResourceType), knownvalue.StringExact("Ec2Instance")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("savings_estimation_mode"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + names.AttrName: knownvalue.StringExact("AccountId"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListSizeExact(0)), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccRecommendationPreferences_utilizationPreferences(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.RecommendationPreferencesDetail + resourceName := "aws_computeoptimizer_recommendation_preferences.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.ComputeOptimizerEndpointID) + testAccPreCheckEnrollmentStatus(ctx, t, "Active") + }, + ErrorCheck: acctest.ErrorCheck(t, names.ComputeOptimizerServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRecommendationPreferencesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRecommendationPreferencesConfig_utilizationPreferences, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRecommendationPreferencesExists(ctx, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enhanced_infrastructure_metrics"), knownvalue.StringExact("Active")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metrics_preference"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("inferred_workload_types"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("look_back_period"), knownvalue.StringExact("DAYS_93")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("preferred_resource"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrResourceType), knownvalue.StringExact("Ec2Instance")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("savings_estimation_mode"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + names.AttrName: knownvalue.StringExact("AccountId"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListSizeExact(2)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + names.AttrMetricName: knownvalue.StringExact("CpuUtilization"), + "metric_parameters": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "headroom": knownvalue.StringExact("PERCENT_20"), + "threshold": knownvalue.StringExact("P95"), + }), + }, + ), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + names.AttrMetricName: knownvalue.StringExact("MemoryUtilization"), + "metric_parameters": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "headroom": knownvalue.StringExact("PERCENT_30"), + "threshold": knownvalue.Null(), + }), + }, + ), + }), + }), + ), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccRecommendationPreferencesConfig_utilizationPreferencesUpdated, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRecommendationPreferencesExists(ctx, resourceName, &v), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("enhanced_infrastructure_metrics"), knownvalue.StringExact("Inactive")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metrics_preference"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("inferred_workload_types"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("look_back_period"), knownvalue.StringExact("DAYS_14")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("preferred_resource"), knownvalue.ListSizeExact(0)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrResourceType), knownvalue.StringExact("Ec2Instance")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("savings_estimation_mode"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrScope), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + names.AttrName: knownvalue.StringExact("AccountId"), + }), + }), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListSizeExact(1)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("utilization_preference"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + names.AttrMetricName: knownvalue.StringExact("CpuUtilization"), + "metric_parameters": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "headroom": knownvalue.StringExact("PERCENT_0"), + "threshold": knownvalue.StringExact("P90"), + }), + }, + ), + }), + }), + ), + }, + }, + }, + }) +} + +func testAccCheckRecommendationPreferencesExists(ctx context.Context, n string, v *awstypes.RecommendationPreferencesDetail) 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).ComputeOptimizerClient(ctx) + + output, err := tfcomputeoptimizer.FindRecommendationPreferencesByThreePartKey(ctx, conn, rs.Primary.Attributes[names.AttrResourceType], rs.Primary.Attributes["scope.0.name"], rs.Primary.Attributes["scope.0.value"]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckRecommendationPreferencesDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ComputeOptimizerClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_computeoptimizer_recommendation_preferences" { + continue + } + + _, err := tfcomputeoptimizer.FindRecommendationPreferencesByThreePartKey(ctx, conn, rs.Primary.Attributes[names.AttrResourceType], rs.Primary.Attributes["scope.0.name"], rs.Primary.Attributes["scope.0.value"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Compute Optimizer Recommendation Preferences %s still exists", rs.Primary.ID) + } + + return nil + } +} + +const testAccRecommendationPreferencesConfig_basic = ` +data "aws_caller_identity" "current" {} + +resource "aws_computeoptimizer_recommendation_preferences" "test" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = data.aws_caller_identity.current.account_id + } + + look_back_period = "DAYS_32" +} +` + +const testAccRecommendationPreferencesConfig_preferredResources = ` +data "aws_caller_identity" "current" {} + +resource "aws_computeoptimizer_recommendation_preferences" "test" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = data.aws_caller_identity.current.account_id + } + + enhanced_infrastructure_metrics = "Active" + + external_metrics_preference { + source = "Datadog" + } + + preferred_resource { + include_list = ["m5.xlarge", "r5"] + name = "Ec2InstanceTypes" + } +} +` + +const testAccRecommendationPreferencesConfig_utilizationPreferences = ` +data "aws_caller_identity" "current" {} + +resource "aws_computeoptimizer_recommendation_preferences" "test" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = data.aws_caller_identity.current.account_id + } + + enhanced_infrastructure_metrics = "Active" + + utilization_preference { + metric_name = "CpuUtilization" + metric_parameters { + headroom = "PERCENT_20" + threshold = "P95" + } + } + + utilization_preference { + metric_name = "MemoryUtilization" + metric_parameters { + headroom = "PERCENT_30" + } + } +} +` + +const testAccRecommendationPreferencesConfig_utilizationPreferencesUpdated = ` +data "aws_caller_identity" "current" {} + +resource "aws_computeoptimizer_recommendation_preferences" "test" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = data.aws_caller_identity.current.account_id + } + + enhanced_infrastructure_metrics = "Inactive" + + utilization_preference { + metric_name = "CpuUtilization" + metric_parameters { + headroom = "PERCENT_0" + threshold = "P90" + } + } +} +` diff --git a/internal/service/computeoptimizer/service_package_gen.go b/internal/service/computeoptimizer/service_package_gen.go index 1fc4ab60df63..3f7ba40be62b 100644 --- a/internal/service/computeoptimizer/service_package_gen.go +++ b/internal/service/computeoptimizer/service_package_gen.go @@ -19,7 +19,16 @@ 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: newEnrollmentStatusResource, + Name: "Enrollment Status", + }, + { + Factory: newRecommendationPreferencesResource, + Name: "Recommendation Preferences", + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/names/names.go b/names/names.go index ffc74783da89..e85525cc1643 100644 --- a/names/names.go +++ b/names/names.go @@ -63,6 +63,7 @@ const ( CodeStarConnectionsEndpointID = "codestar-connections" CognitoIdentityEndpointID = "cognito-identity" ComprehendEndpointID = "comprehend" + ComputeOptimizerEndpointID = "compute-optimizer" ConfigServiceEndpointID = "config" ConnectEndpointID = "connect" DataExchangeEndpointID = "dataexchange" diff --git a/website/docs/r/computeoptimizer_enrollment_status.html.markdown b/website/docs/r/computeoptimizer_enrollment_status.html.markdown new file mode 100644 index 000000000000..2e5ebf0f7dd3 --- /dev/null +++ b/website/docs/r/computeoptimizer_enrollment_status.html.markdown @@ -0,0 +1,56 @@ +--- +subcategory: "Compute Optimizer" +layout: "aws" +page_title: "AWS: aws_computeoptimizer_enrollment_status" +description: |- + Manages AWS Compute Optimizer enrollment status. +--- + +# Resource: aws_computeoptimizer_enrollment_status + +Manages AWS Compute Optimizer enrollment status. + +## Example Usage + +```terraform +resource "aws_computeoptimizer_enrollment_status" "example" { + status = "Active" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `include_member_accounts` - (Optional) Whether to enroll member accounts of the organization if the account is the management account of an organization. Default is `false`. +* `status` - (Required) The enrollment status of the account. Valid values: `Active`, `Inactive`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `number_of_member_accounts_opted_in` - The count of organization member accounts that are opted in to the service, if your account is an organization management account. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `update` - (Default `5m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import enrollment status using the account ID. For example: + +```terraform +import { + to = aws_computeoptimizer_enrollment_status.example + id = "123456789012" +} +``` + +Using `terraform import`, import enrollment status using the account ID. For example: + +```console +% terraform import aws_computeoptimizer_enrollment_status.example 123456789012 +``` diff --git a/website/docs/r/computeoptimizer_recommendation_preferences.html.markdown b/website/docs/r/computeoptimizer_recommendation_preferences.html.markdown new file mode 100644 index 000000000000..33ed655d5f00 --- /dev/null +++ b/website/docs/r/computeoptimizer_recommendation_preferences.html.markdown @@ -0,0 +1,110 @@ +--- +subcategory: "Compute Optimizer" +layout: "aws" +page_title: "AWS: aws_computeoptimizer_recommendation_preferences" +description: |- + Manages AWS Compute Optimizer recommendation preferences. +--- + +# Resource: aws_computeoptimizer_recommendation_preferences + +Manages AWS Compute Optimizer recommendation preferences. + +## Example Usage + +### Lookback Period Preference + +```terraform +resource "aws_computeoptimizer_recommendation_preferences" "example" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = "123456789012" + } + + look_back_period = "DAYS_32" +} +``` + +### Multiple Preferences + +```terraform +resource "aws_computeoptimizer_recommendation_preferences" "example" { + resource_type = "Ec2Instance" + scope { + name = "AccountId" + value = "123456789012" + } + + enhanced_infrastructure_metrics = "Active" + + external_metrics_preference { + source = "Datadog" + } + + preferred_resource { + include_list = ["m5.xlarge", "r5"] + name = "Ec2InstanceTypes" + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `enhanced_infrastructure_metrics` - (Optional) The status of the enhanced infrastructure metrics recommendation preference. Valid values: `Active`, `Inactive`. +* `external_metrics_preference` - (Optional) The provider of the external metrics recommendation preference. See [External Metrics Preference](#external-metrics-preference) below. +* `inferred_workload_types` - (Optional) The status of the inferred workload types recommendation preference. Valid values: `Active`, `Inactive`. +* `look_back_period` - (Optional) The preference to control the number of days the utilization metrics of the AWS resource are analyzed. Valid values: `DAYS_14`, `DAYS_32`, `DAYS_93`. +* `preferred_resource` - (Optional) The preference to control which resource type values are considered when generating rightsizing recommendations. See [Preferred Resources](#preferred-resources) below. +* `resource_type` - (Required) The target resource type of the recommendation preferences. Valid values: `Ec2Instance`, `AutoScalingGroup`, `RdsDBInstance`. +* `savings_estimation_mode` - (Optional) The status of the savings estimation mode preference. Valid values: `AfterDiscounts`, `BeforeDiscounts`. +* `scope` - (Required) The scope of the recommendation preferences. See [Scope](#scope) below. +* `utilization_preference` - (Optional) The preference to control the resource’s CPU utilization threshold, CPU utilization headroom, and memory utilization headroom. See [Utilization Preferences](#utilization-preferences) below. + +### External Metrics Preference + +* `source` - (Required) The source options for external metrics preferences. Valid values: `Datadog`, `Dynatrace`, `NewRelic`, `Instana`. + +### Preferred Resources + +You can specify this preference as a combination of include and exclude lists. +You must specify either an `include_list` or `exclude_list`. + +* `exclude_list` - (Optional) The preferred resource type values to exclude from the recommendation candidates. If this isn’t specified, all supported resources are included by default. +* `include_list` - (Optional) The preferred resource type values to include in the recommendation candidates. You can specify the exact resource type value, such as `"m5.large"`, or use wild card expressions, such as `"m5"`. If this isn’t specified, all supported resources are included by default. +* `name` - (Required) The type of preferred resource to customize. Valid values: `Ec2InstanceTypes`. + +### Scope + +* `name` - (Required) The name of the scope. Valid values: `Organization`, `AccountId`, `ResourceArn`. +* `value` - (Required) The value of the scope. `ALL_ACCOUNTS` for `Organization` scopes, AWS account ID for `AccountId` scopes, ARN of an EC2 instance or an Auto Scaling group for `ResourceArn` scopes. + +### Utilization Preferences + +* `metric_name` - (Required) The name of the resource utilization metric name to customize. Valid values: `CpuUtilization`, `MemoryUtilization`. +* `metric_parameters` - (Required) The parameters to set when customizing the resource utilization thresholds. + * `headroom` - (Required) The headroom value in percentage used for the specified metric parameter. Valid values: `PERCENT_30`, `PERCENT_20`, `PERCENT_10`, `PERCENT_0`. + * `threshold` - (Optional) The threshold value used for the specified metric parameter. You can only specify the threshold value for CPU utilization. Valid values: `P90`, `P95`, `P99_5`. + +## 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 recommendation preferences using the resource type, scope name and scope value. For example: + +```terraform +import { + to = aws_computeoptimizer_recommendation_preferences.example + id = "Ec2Instance,AccountId,123456789012" +} +``` + +Using `terraform import`, import recommendation preferences using the resource type, scope name and scope value. For example: + +```console +% terraform import aws_computeoptimizer_recommendation_preferences.example Ec2Instance,AccountId,123456789012 +```