From 8f7d6858e41b434af5425a953ed9216a86f33c9b Mon Sep 17 00:00:00 2001 From: Hunter Tom Date: Wed, 27 Mar 2024 10:36:54 -0700 Subject: [PATCH] Created the recommendation preferences resource. --- .../recommendation_preferences.go | 834 +++++++----------- 1 file changed, 335 insertions(+), 499 deletions(-) diff --git a/internal/service/computeoptimizer/recommendation_preferences.go b/internal/service/computeoptimizer/recommendation_preferences.go index b6d487dccf0..c8b5abb9c4f 100644 --- a/internal/service/computeoptimizer/recommendation_preferences.go +++ b/internal/service/computeoptimizer/recommendation_preferences.go @@ -2,42 +2,13 @@ // SPDX-License-Identifier: MPL-2.0 package computeoptimizer -// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** -// -// TIP: ==== INTRODUCTION ==== -// Thank you for trying the skaff tool! -// -// You have opted to include these helpful comments. They all include "TIP:" -// to help you find and remove them when you're done with them. -// -// While some aspects of this file are customized to your input, the -// scaffold tool does *not* look at the AWS API and ensure it has correct -// function, structure, and variable names. It makes guesses based on -// commonalities. You will need to make significant adjustments. -// -// In other words, as generated, this is a rough outline of the work you will -// need to do. If something doesn't make sense for your situation, get rid of -// it. import ( - // TIP: ==== IMPORTS ==== - // This is a common set of imports but not customized to your code since - // your code hasn't been written yet. Make sure you, your IDE, or - // goimports -w fixes these imports. - // - // The provider linter wants your imports to be in two groups: first, - // standard library (i.e., "fmt" or "strings"), second, everything else. - // - // Also, AWS Go SDK v2 may handle nested structures differently than v1, - // using the services/computeoptimizer/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // types.. "context" "errors" "fmt" "log" - "reflect" - "regexp" + "strings" "time" @@ -47,126 +18,174 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" "github.com/hashicorp/terraform-provider-aws/internal/errs" - "github.com/hashicorp/terraform-provider-aws/internal/flex" + + // tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" - "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) -// TIP: ==== FILE STRUCTURE ==== -// All resources should follow this basic outline. Improve this resource's -// maintainability by sticking to it. -// -// 1. Package declaration -// 2. Imports -// 3. Main resource function with schema -// 4. Create, read, update, delete functions (in that order) -// 5. Other functions (flatteners, expanders, waiters, finders, etc.) - // Function annotations are used for resource registration to the Provider. DO NOT EDIT. // @SDKResource("aws_computeoptimizer_recommendation_preferences", name="Recommendation Preferences") func ResourceRecommendationPreferences() *schema.Resource { return &schema.Resource{ - // TIP: ==== ASSIGN CRUD FUNCTIONS ==== - // These 4 functions handle CRUD responsibilities below. CreateWithoutTimeout: resourceRecommendationPreferencesCreate, ReadWithoutTimeout: resourceRecommendationPreferencesRead, - UpdateWithoutTimeout: resourceRecommendationPreferencesUpdate, + // UpdateWithoutTimeout: resourceRecommendationPreferencesUpdate, DeleteWithoutTimeout: resourceRecommendationPreferencesDelete, - - // TIP: ==== TERRAFORM IMPORTING ==== - // If Read can get all the information it needs from the Identifier - // (i.e., d.Id()), you can use the Passthrough importer. Otherwise, - // you'll need a custom import function. - // - // See more: - // https://hashicorp.github.io/terraform-provider-aws/add-import-support/ - // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/#implicit-state-passthrough - // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/#virtual-attributes + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, - - // TIP: ==== CONFIGURABLE TIMEOUTS ==== - // Users can configure timeout lengths but you need to use the times they - // provide. Access the timeout they configure (or the defaults) using, - // e.g., d.Timeout(schema.TimeoutCreate) (see below). The times here are - // the defaults if they don't configure timeouts. + Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(30 * time.Minute), Update: schema.DefaultTimeout(30 * time.Minute), Delete: schema.DefaultTimeout(30 * time.Minute), }, - - // TIP: ==== SCHEMA ==== - // In the schema, add each of the attributes in snake case (e.g., - // delete_automated_backups). - // - // Formatting rules: - // * Alphabetize attributes to make them easier to find. - // * Do not add a blank line between attributes. - // - // Attribute basics: - // * If a user can provide a value ("configure a value") for an - // attribute (e.g., instances = 5), we call the attribute an - // "argument." - // * You change the way users interact with attributes using: - // - Required - // - Optional - // - Computed - // * There are only four valid combinations: - // - // 1. Required only - the user must provide a value - // Required: true, - // - // 2. Optional only - the user can configure or omit a value; do not - // use Default or DefaultFunc - // Optional: true, - // - // 3. Computed only - the provider can provide a value but the user - // cannot, i.e., read-only - // Computed: true, - // - // 4. Optional AND Computed - the provider or user can provide a value; - // use this combination if you are using Default or DefaultFunc - // Optional: true, - // Computed: true, - // - // You will typically find arguments in the input struct - // (e.g., CreateDBInstanceInput) for the create operation. Sometimes - // they are only in the input struct (e.g., ModifyDBInstanceInput) for - // the modify operation. - // - // For more about schema options, visit - // https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Schema + Schema: map[string]*schema.Schema{ - "arn": { // TIP: Many, but not all, resources have an `arn` attribute. + "enhanced_infrastructure_metrics": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The status of the enhanced infrastructure metrics recommendation preference to create or update.", + ValidateDiagFunc: enum.Validate[types.EnhancedInfrastructureMetrics](), + }, + "external_metrics_preference": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.ExternalMetricsSource](), + }, + }, + }, + }, + "id": { Type: schema.TypeString, Computed: true, }, - "replace_with_arguments": { // TIP: Add all your arguments and attributes. - Type: schema.TypeString, + "inferred_workload_types": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.InferredWorkloadType](), + }, + "look_back_period": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.LookBackPeriodPreference](), + }, + "preferred_resources": { + Type: schema.TypeList, Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exclude_list": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "include_list": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "resource_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.ResourceType](), + }, + "savings_estimation_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.SavingsEstimationMode](), }, - "complex_argument": { // TIP: See setting, getting, flattening, expanding examples below for this complex argument. + "scope": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "sub_field_one": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringLenBetween(1, 2048), + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.ScopeName](), + // Update Validation: https://docs.aws.amazon.com/compute-optimizer/latest/APIReference/API_Scope.html + // DMS Endpoint Case? + }, + "value": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + // Update Validation: https://docs.aws.amazon.com/compute-optimizer/latest/APIReference/API_Scope.html + // DMS Endpoint Case? }, - "sub_field_two": { + }, + }, + }, + // names.AttrTags: tftags.TagsSchema(), + // names.AttrTagsAll: tftags.TagsSchemaComputed(), + "utilization_preferences": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_name": { Type: schema.TypeString, Optional: true, + ForceNew: true, + }, + "metric_parameters": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "headroom": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.CustomizableMetricHeadroom](), + }, + "threshold": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.CustomizableMetricThreshold](), + }, + }, + }, }, }, }, @@ -181,93 +200,76 @@ const ( func resourceRecommendationPreferencesCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - // TIP: ==== RESOURCE CREATE ==== - // Generally, the Create function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Populate a create input structure - // 3. Call the AWS create/put function - // 4. Using the output from the create function, set the minimum arguments - // and attributes for the Read function to work. At a minimum, set the - // resource ID. E.g., d.SetId() - // 5. Use a waiter to wait for create to complete - // 6. Call the Read function in the Create return - - // TIP: -- 1. Get a client connection to the relevant service conn := meta.(*conns.AWSClient).ComputeOptimizerClient(ctx) - - // TIP: -- 2. Populate a create input structure - in := &computeoptimizer.CreateRecommendationPreferencesInput{ - // TIP: Mandatory or fields that will always be present can be set when - // you create the Input structure. (Replace these with real fields.) - RecommendationPreferencesName: aws.String(d.Get("name").(string)), - RecommendationPreferencesType: aws.String(d.Get("type").(string)), - - // TIP: Not all resources support tags and tags don't always make sense. If - // your resource doesn't need tags, you can remove the tags lines here and - // below. Many resources do include tags so this a reminder to include them - // where possible. - } - - if v, ok := d.GetOk("max_size"); ok { - // TIP: Optional fields should be set based on whether or not they are - // used. - in.MaxSize = aws.Int64(int64(v.(int))) - } - - if v, ok := d.GetOk("complex_argument"); ok && len(v.([]interface{})) > 0 { - // TIP: Use an expander to assign a complex argument. - in.ComplexArguments = expandComplexArguments(v.([]interface{})) - } - - - // TIP: -- 3. Call the AWS create function - out, err := conn.CreateRecommendationPreferences(ctx, in) + + in := &computeoptimizer.PutRecommendationPreferencesInput{ + ResourceType: types.ResourceType(*aws.String(d.Get("resource_type").(string))), + } + + recommendationPreferences := []string{} + recommendationPreferences = append(recommendationPreferences, d.Get("resource_type").(string)) + + if v, ok := d.GetOk("enhanced_infrastructure_metrics"); ok { + in.EnhancedInfrastructureMetrics = types.EnhancedInfrastructureMetrics(*aws.String(v.(string))) + recommendationPreferences = append(recommendationPreferences, "EnhancedInfrastructureMetrics") + } + + if v, ok := d.GetOk("external_metrics_preference"); ok && len(v.([]interface{})) > 0 { + in.ExternalMetricsPreference = expandExternalMetricsPreference(v.([]interface{})) + recommendationPreferences = append(recommendationPreferences, "ExternalMetricsPreference") + } + + if v, ok := d.GetOk("inferred_workload_types"); ok { + in.InferredWorkloadTypes = types.InferredWorkloadTypesPreference(*aws.String(v.(string))) + recommendationPreferences = append(recommendationPreferences, "InferredWorkloadTypes") + } + + if v, ok := d.GetOk("look_back_period"); ok { + in.LookBackPeriod = types.LookBackPeriodPreference(*aws.String(v.(string))) + recommendationPreferences = append(recommendationPreferences, "LookBackPeriodPreference") + } + + if v, ok := d.GetOk("preferred_resources"); ok && len(v.([]interface{})) > 0 { + in.PreferredResources = expandPreferredResources(v.([]interface{})) + recommendationPreferences = append(recommendationPreferences, "PreferredResources") + } + + if v, ok := d.GetOk("savings_estimation_mode"); ok { + in.SavingsEstimationMode = types.SavingsEstimationMode(*aws.String(v.(string))) + } + + if v, ok := d.GetOk("scope"); ok && len(v.([]interface{})) > 0 { + in.Scope = expandScope(v.([]interface{})) + } + + if v, ok := d.GetOk("utilization_preferences"); ok && len(v.([]interface{})) > 0 { + in.UtilizationPreferences = expandUtilizationPreferences(v.([]interface{})) + recommendationPreferences = append(recommendationPreferences, "UtilizationPreferences") + } + + out, err := conn.PutRecommendationPreferences(ctx, in) if err != nil { - // TIP: Since d.SetId() has not been called yet, you cannot use d.Id() - // in error messages at this point. return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionCreating, ResNameRecommendationPreferences, d.Get("name").(string), err) } - if out == nil || out.RecommendationPreferences == nil { + if out == nil { return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionCreating, ResNameRecommendationPreferences, d.Get("name").(string), errors.New("empty output")) } - - // TIP: -- 4. Set the minimum arguments and/or attributes for the Read function to - // work. - d.SetId(aws.ToString(out.RecommendationPreferences.RecommendationPreferencesID)) - - // TIP: -- 5. Use a waiter to wait for create to complete - if _, err := waitRecommendationPreferencesCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionWaitingForCreation, ResNameRecommendationPreferences, d.Id(), err) - } - - // TIP: -- 6. Call the Read function in the Create return + + id := preferencesCreateResourceID(recommendationPreferences) + + d.SetId(id) + return append(diags, resourceRecommendationPreferencesRead(ctx, d, meta)...) } func resourceRecommendationPreferencesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - // TIP: ==== RESOURCE READ ==== - // Generally, the Read function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Get the resource from AWS - // 3. Set ID to empty where resource is not new and not found - // 4. Set the arguments and attributes - // 5. Set the tags - // 6. Return diags - - // TIP: -- 1. Get a client connection to the relevant service + conn := meta.(*conns.AWSClient).ComputeOptimizerClient(ctx) - - // TIP: -- 2. Get the resource from AWS using an API Get, List, or Describe- - // type function, or, better yet, using a finder. - out, err := findRecommendationPreferencesByID(ctx, conn, d.Id()) - - // TIP: -- 3. Set ID to empty where resource is not new and not found + + out, err := findRecommendationPreferencesByResource(ctx, conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] ComputeOptimizer RecommendationPreferences (%s) not found, removing from state", d.Id()) d.SetId("") @@ -277,165 +279,45 @@ func resourceRecommendationPreferencesRead(ctx context.Context, d *schema.Resour if err != nil { return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionReading, ResNameRecommendationPreferences, d.Id(), err) } - - // TIP: -- 4. Set the arguments and attributes - // - // For simple data types (i.e., schema.TypeString, schema.TypeBool, - // schema.TypeInt, and schema.TypeFloat), a simple Set call (e.g., - // d.Set("arn", out.Arn) is sufficient. No error or nil checking is - // necessary. - // - // However, there are some situations where more handling is needed. - // a. Complex data types (e.g., schema.TypeList, schema.TypeSet) - // b. Where errorneous diffs occur. For example, a schema.TypeString may be - // a JSON. AWS may return the JSON in a slightly different order but it - // is equivalent to what is already set. In that case, you may check if - // it is equivalent before setting the different JSON. - d.Set("arn", out.Arn) - d.Set("name", out.Name) - - // TIP: Setting a complex type. - // For more information, see: - // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/#data-handling-and-conversion - // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/#flatten-functions-for-blocks - // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/#root-typeset-of-resource-and-aws-list-of-structure - if err := d.Set("complex_argument", flattenComplexArguments(out.ComplexArguments)); err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionSetting, ResNameRecommendationPreferences, d.Id(), err) - } - - // TIP: Setting a JSON string to avoid errorneous diffs. - p, err := verify.SecondJSONUnlessEquivalent(d.Get("policy").(string), aws.ToString(out.Policy)) - if err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionSetting, ResNameRecommendationPreferences, d.Id(), err) - } - - p, err = structure.NormalizeJsonString(p) - if err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionSetting, ResNameRecommendationPreferences, d.Id(), err) - } - d.Set("policy", p) + // TODO - How to set a list? + outputData := out.RecommendationPreferencesDetails + d.Set("resource_type", outputData[0].ResourceType) - - // TIP: -- 6. Return diags return diags } -func resourceRecommendationPreferencesUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceRecommendationPreferencesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - // TIP: ==== RESOURCE UPDATE ==== - // Not all resources have Update functions. There are a few reasons: - // a. The AWS API does not support changing a resource - // b. All arguments have ForceNew: true, set - // c. The AWS API uses a create call to modify an existing resource - // - // In the cases of a. and b., the main resource function will not have a - // UpdateWithoutTimeout defined. In the case of c., Update and Create are - // the same. - // - // The rest of the time, there should be an Update function and it should - // do the following things. Make sure there is a good reason if you don't - // do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Populate a modify input structure and check for changes - // 3. Call the AWS modify/update function - // 4. Use a waiter to wait for update to complete - // 5. Call the Read function in the Update return - - // TIP: -- 1. Get a client connection to the relevant service + conn := meta.(*conns.AWSClient).ComputeOptimizerClient(ctx) - - // TIP: -- 2. Populate a modify input structure and check for changes - // - // When creating the input structure, only include mandatory fields. Other - // fields are set as needed. You can use a flag, such as update below, to - // determine if a certain portion of arguments have been changed and - // whether to call the AWS update function. - update := false - in := &computeoptimizer.UpdateRecommendationPreferencesInput{ - Id: aws.String(d.Id()), - } + log.Printf("[INFO] Deleting ComputeOptimizer RecommendationPreferences %s", d.Id()) - if d.HasChanges("an_argument") { - in.AnArgument = aws.String(d.Get("an_argument").(string)) - update = true - } + var resourceType string + var recommendationPreferencesNames []types.RecommendationPreferenceName + var err error - if !update { - // TIP: If update doesn't do anything at all, which is rare, you can - // return diags. Otherwise, return a read call, as below. - return diags - } - - // TIP: -- 3. Call the AWS modify/update function - log.Printf("[DEBUG] Updating ComputeOptimizer RecommendationPreferences (%s): %#v", d.Id(), in) - out, err := conn.UpdateRecommendationPreferences(ctx, in) + resourceType, recommendationPreferencesNames, err = preferencesParseResourceID(d.Id()) if err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionUpdating, ResNameRecommendationPreferences, d.Id(), err) - } - - // TIP: -- 4. Use a waiter to wait for update to complete - if _, err := waitRecommendationPreferencesUpdated(ctx, conn, aws.ToString(out.OperationId), d.Timeout(schema.TimeoutUpdate)); err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionWaitingForUpdate, ResNameRecommendationPreferences, d.Id(), err) + return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionDeleting, ResNameRecommendationPreferences, d.Id(), err) } - - // TIP: -- 5. Call the Read function in the Update return - return append(diags, resourceRecommendationPreferencesRead(ctx, d, meta)...) -} -func resourceRecommendationPreferencesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - // TIP: ==== RESOURCE DELETE ==== - // Most resources have Delete functions. There are rare situations - // where you might not need a delete: - // a. The AWS API does not provide a way to delete the resource - // b. The point of your resource is to perform an action (e.g., reboot a - // server) and deleting serves no purpose. - // - // The Delete function should do the following things. Make sure there - // is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Populate a delete input structure - // 3. Call the AWS delete function - // 4. Use a waiter to wait for delete to complete - // 5. Return diags - - // TIP: -- 1. Get a client connection to the relevant service - conn := meta.(*conns.AWSClient).ComputeOptimizerClient(ctx) - - // TIP: -- 2. Populate a delete input structure - log.Printf("[INFO] Deleting ComputeOptimizer RecommendationPreferences %s", d.Id()) - - // TIP: -- 3. Call the AWS delete function - _, err := conn.DeleteRecommendationPreferences(ctx, &computeoptimizer.DeleteRecommendationPreferencesInput{ - Id: aws.String(d.Id()), + _, err = conn.DeleteRecommendationPreferences(ctx, &computeoptimizer.DeleteRecommendationPreferencesInput{ + ResourceType: types.ResourceType(resourceType), + RecommendationPreferenceNames: recommendationPreferencesNames, }) - - // TIP: On rare occassions, the API returns a not found error after deleting a - // resource. If that happens, we don't want it to show up as an error. - if errs.IsA[*types.ResourceNotFoundException](err){ + + if errs.IsA[*types.ResourceNotFoundException](err) { return diags } if err != nil { return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionDeleting, ResNameRecommendationPreferences, d.Id(), err) } - - // TIP: -- 4. Use a waiter to wait for delete to complete - if _, err := waitRecommendationPreferencesDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { - return create.AppendDiagError(diags, names.ComputeOptimizer, create.ErrActionWaitingForDeletion, ResNameRecommendationPreferences, d.Id(), err) - } - - // TIP: -- 5. Return diags + return diags } -// TIP: ==== STATUS CONSTANTS ==== -// Create constants for states and statuses if the service does not -// already have suitable constants. We prefer that you use the constants -// provided in the service if available (e.g., amp.WorkspaceStatusCodeActive). const ( statusChangePending = "Pending" statusDeleting = "Deleting" @@ -443,115 +325,31 @@ const ( statusUpdated = "Updated" ) -// TIP: ==== WAITERS ==== -// Some resources of some services have waiters provided by the AWS API. -// Unless they do not work properly, use them rather than defining new ones -// here. -// -// Sometimes we define the wait, status, and find functions in separate -// files, wait.go, status.go, and find.go. Follow the pattern set out in the -// service and define these where it makes the most sense. -// -// If these functions are used in the _test.go file, they will need to be -// exported (i.e., capitalized). -// -// You will need to adjust the parameters and names to fit the service. - -func waitRecommendationPreferencesCreated(ctx context.Context, conn *computeoptimizer.Client, id string, timeout time.Duration) (*computeoptimizer.RecommendationPreferences, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: []string{statusNormal}, - Refresh: statusRecommendationPreferences(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*computeoptimizer.RecommendationPreferences); ok { - return out, err - } - - return nil, err -} - -// TIP: It is easier to determine whether a resource is updated for some -// resources than others. The best case is a status flag that tells you when -// the update has been fully realized. Other times, you can check to see if a -// key resource argument is updated to a new value or not. - -func waitRecommendationPreferencesUpdated(ctx context.Context, conn *computeoptimizer.Client, id string, timeout time.Duration) (*computeoptimizer.RecommendationPreferences, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusChangePending}, - Target: []string{statusUpdated}, - Refresh: statusRecommendationPreferences(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*computeoptimizer.RecommendationPreferences); ok { - return out, err - } - - return nil, err -} - -// TIP: A deleted waiter is almost like a backwards created waiter. There may -// be additional pending states, however. - -func waitRecommendationPreferencesDeleted(ctx context.Context, conn *computeoptimizer.Client, id string, timeout time.Duration) (*computeoptimizer.RecommendationPreferences, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusDeleting, statusNormal}, - Target: []string{}, - Refresh: statusRecommendationPreferences(ctx, conn, id), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*computeoptimizer.RecommendationPreferences); ok { - return out, err - } - - return nil, err -} - -// TIP: ==== STATUS ==== -// The status function can return an actual status when that field is -// available from the API (e.g., out.Status). Otherwise, you can use custom -// statuses to communicate the states of the resource. -// -// Waiters consume the values returned by status functions. Design status so -// that it can be reused by a create, update, and delete waiter, if possible. - func statusRecommendationPreferences(ctx context.Context, conn *computeoptimizer.Client, id string) retry.StateRefreshFunc { return func() (interface{}, string, error) { - out, err := findRecommendationPreferencesByID(ctx, conn, id) - if tfresource.NotFound(err) { - return nil, "", nil - } - + out, err := findRecommendationPreferencesByResource(ctx, conn, id) if err != nil { return nil, "", err } - return out, aws.ToString(out.Status), nil + var recommendationStatus string + + if tfresource.NotFound(err) { + return nil, "", nil + } else { + recommendationStatus = "ACTIVE" + } + + return out, recommendationStatus, nil } } -// TIP: ==== FINDERS ==== -// The find function is not strictly necessary. You could do the API -// request from the status function. However, we have found that find often -// comes in handy in other places besides the status function. As a result, it -// is good practice to define it separately. - -func findRecommendationPreferencesByID(ctx context.Context, conn *computeoptimizer.Client, id string) (*computeoptimizer.RecommendationPreferences, error) { +func findRecommendationPreferencesByResource(ctx context.Context, conn *computeoptimizer.Client, resource string) (*computeoptimizer.GetRecommendationPreferencesOutput, error) { // doesn't returns subtype types.RecommendationPreferencesDetails with details in := &computeoptimizer.GetRecommendationPreferencesInput{ - Id: aws.String(id), + ResourceType: types.ResourceType(resource), } out, err := conn.GetRecommendationPreferences(ctx, in) - if errs.IsA[*types.ResourceNotFoundException](err){ + if errs.IsA[*types.ResourceNotFoundException](err) { return nil, &retry.NotFoundError{ LastError: err, LastRequest: in, @@ -561,11 +359,11 @@ func findRecommendationPreferencesByID(ctx context.Context, conn *computeoptimiz return nil, err } - if out == nil || out.RecommendationPreferences == nil { + if out == nil || out.RecommendationPreferencesDetails == nil { return nil, tfresource.NewEmptyResultError(in) } - return out.RecommendationPreferences, nil + return out, nil } // TIP: ==== FLEX ==== @@ -579,112 +377,150 @@ func findRecommendationPreferencesByID(ctx context.Context, conn *computeoptimiz // // See more: // https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ -func flattenComplexArgument(apiObject *computeoptimizer.ComplexArgument) map[string]interface{} { - if apiObject == nil { - return nil - } +// func flattenComplexArgument(apiObject *computeoptimizer.ComplexArgument) map[string]interface{} { +// if apiObject == nil { +// return nil +// } - m := map[string]interface{}{} +// m := map[string]interface{}{} - if v := apiObject.SubFieldOne; v != nil { - m["sub_field_one"] = aws.ToString(v) - } +// if v := apiObject.SubFieldOne; v != nil { +// m["sub_field_one"] = aws.ToString(v) +// } - if v := apiObject.SubFieldTwo; v != nil { - m["sub_field_two"] = aws.ToString(v) - } +// if v := apiObject.SubFieldTwo; v != nil { +// m["sub_field_two"] = aws.ToString(v) +// } - return m -} +// return m +// } // TIP: Often the AWS API will return a slice of structures in response to a // request for information. Sometimes you will have set criteria (e.g., the ID) // that means you'll get back a one-length slice. This plural function works // brilliantly for that situation too. -func flattenComplexArguments(apiObjects []*computeoptimizer.ComplexArgument) []interface{} { - if len(apiObjects) == 0 { +// func flattenComplexArguments(apiObjects []*computeoptimizer.ComplexArgument) []interface{} { +// if len(apiObjects) == 0 { +// return nil +// } + +// var l []interface{} + +// for _, apiObject := range apiObjects { +// if apiObject == nil { +// continue +// } + +// l = append(l, flattenComplexArgument(apiObject)) +// } + +// return l +// } + +func expandExternalMetricsPreference(tfList []interface{}) *types.ExternalMetricsPreference { + if len(tfList) == 0 || tfList[0] == nil { return nil } - var l []interface{} + p, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } - for _, apiObject := range apiObjects { - if apiObject == nil { - continue - } + externalMetricsPreference := &types.ExternalMetricsPreference{ + Source: types.ExternalMetricsSource(p["source"].(string)), + } - l = append(l, flattenComplexArgument(apiObject)) + return externalMetricsPreference +} + +func expandPreferredResources(tfList []interface{}) []types.PreferredResource { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + p, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil } - return l + var preferredResourceArray []types.PreferredResource + + preferredResourceConfig := &types.PreferredResource{ + ExcludeList: []string{p["exclude_list"].(string)}, + IncludeList: []string{p["include_list"].(string)}, + Name: types.PreferredResourceName(p["name"].(string)), + } + + preferredResourceArray = append(preferredResourceArray, *preferredResourceConfig) + + return preferredResourceArray } -// TIP: Remember, as mentioned above, expanders take a Terraform data structure -// and return something that you can send to the AWS API. In other words, -// expanders translate from Terraform -> AWS. -// -// See more: -// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ -func expandComplexArgument(tfMap map[string]interface{}) *computeoptimizer.ComplexArgument { - if tfMap == nil { +func expandScope(tfList []interface{}) *types.Scope { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + p, ok := tfList[0].(map[string]interface{}) + if !ok { return nil } - a := &computeoptimizer.ComplexArgument{} + scope := &types.Scope{ + Name: types.ScopeName(p["name"].(string)), + Value: aws.String(p["value"].(string)), + } - if v, ok := tfMap["sub_field_one"].(string); ok && v != "" { - a.SubFieldOne = aws.String(v) + return scope +} + +func expandUtilizationPreferences(tfList []interface{}) []types.UtilizationPreference { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + p, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil } - if v, ok := tfMap["sub_field_two"].(string); ok && v != "" { - a.SubFieldTwo = aws.String(v) + var utilizationPreferenceArray []types.UtilizationPreference + + utilizationPreference := &types.UtilizationPreference{ + MetricName: types.CustomizableMetricName(p["metric_name"].(string)), + MetricParameters: &types.CustomizableMetricParameters{ + Headroom: types.CustomizableMetricHeadroom(p["headroom"].(string)), + Threshold: types.CustomizableMetricThreshold(p["threshold"].(string)), + }, } - return a + utilizationPreferenceArray = append(utilizationPreferenceArray, *utilizationPreference) + + return utilizationPreferenceArray } -// TIP: Even when you have a list with max length of 1, this plural function -// works brilliantly. However, if the AWS API takes a structure rather than a -// slice of structures, you will not need it. -func expandComplexArguments(tfList []interface{}) []*computeoptimizer.ComplexArgument { - // TIP: The AWS API can be picky about whether you send a nil or zero- - // length for an argument that should be cleared. For example, in some - // cases, if you send a nil value, the AWS API interprets that as "make no - // changes" when what you want to say is "remove everything." Sometimes - // using a zero-length list will cause an error. - // - // As a result, here are two options. Usually, option 1, nil, will work as - // expected, clearing the field. But, test going from something to nothing - // to make sure it works. If not, try the second option. - - // TIP: Option 1: Returning nil for zero-length list - if len(tfList) == 0 { - return nil - } - - var s []*computeoptimizer.ComplexArgument - - // TIP: Option 2: Return zero-length list for zero-length list. If option 1 does - // not work, after testing going from something to nothing (if that is - // possible), uncomment out the next line and remove option 1. - // - // s := make([]*computeoptimizer.ComplexArgument, 0) - - for _, r := range tfList { - m, ok := r.(map[string]interface{}) - - if !ok { - continue - } +const preferencesIDSeparator = ":" - a := expandComplexArgument(m) +func preferencesCreateResourceID(recommendationPreferencesName []string) string { - if a == nil { - continue - } + id := strings.Join(recommendationPreferencesName, preferencesIDSeparator) + + return id +} + +func preferencesParseResourceID(id string) (string, []types.RecommendationPreferenceName, error) { + parts := strings.Split(id, preferencesIDSeparator) + + if len(parts) < 2 || parts[0] == "" || len(parts[1:]) == 0 { + return "", []types.RecommendationPreferenceName{}, fmt.Errorf("unexpected format of ID (%[1]s), expected resource_type:%[2]s<...recommendation_preference_names>", id, preferencesIDSeparator) + } + + var recommendationPreferenceNames []types.RecommendationPreferenceName - s = append(s, a) + for _, part := range parts[1:] { + recommendationPreferenceNames = append(recommendationPreferenceNames, types.RecommendationPreferenceName(part)) } - return s + return parts[0], recommendationPreferenceNames, nil }