diff --git a/.changelog/22529.txt b/.changelog/22529.txt new file mode 100644 index 000000000000..075160c1845c --- /dev/null +++ b/.changelog/22529.txt @@ -0,0 +1,19 @@ +```release-note:enhancement +resource/aws_ecs_capacity_provider: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +resource/aws_ecs_cluster: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +resource/aws_ecs_service: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +resource/aws_ecs_task_definition: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +resource/aws_ecs_task_set: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` \ No newline at end of file diff --git a/internal/service/ecs/capacity_provider.go b/internal/service/ecs/capacity_provider.go index a1d1d2f1a9d7..c813c666658e 100644 --- a/internal/service/ecs/capacity_provider.go +++ b/internal/service/ecs/capacity_provider.go @@ -125,10 +125,32 @@ func resourceCapacityProviderCreate(d *schema.ResourceData, meta interface{}) er log.Printf("[DEBUG] Creating ECS Capacity Provider: %s", input) output, err := conn.CreateCapacityProvider(&input) + // Some partitions (i.e., ISO) may not support tag-on-create + if input.Tags != nil && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] ECS Capacity Provider (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + input.Tags = nil + output, err = conn.CreateCapacityProvider(&input) + } + if err != nil { return fmt.Errorf("error creating ECS Capacity Provider (%s): %w", name, err) } + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if input.Tags == nil && len(tags) > 0 { + err := UpdateTags(conn, d.Id(), nil, tags) + + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + // If default tags only, log and continue. Otherwise, error. + log.Printf("[WARN] error adding tags after create for ECS Capacity Provider (%s): %s", d.Id(), err) + return resourceCapacityProviderRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating ECS Capacity Provider (%s) tags: %w", name, err) + } + } + d.SetId(aws.StringValue(output.CapacityProvider.CapacityProviderArn)) return resourceCapacityProviderRead(d, meta) @@ -161,6 +183,12 @@ func resourceCapacityProviderRead(d *schema.ResourceData, meta interface{}) erro tags := KeyValueTags(output.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to list tags for ECS Capacity Provider %s: %s", d.Id(), err) + return nil + } + //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) @@ -212,7 +240,16 @@ func resourceCapacityProviderUpdate(d *schema.ResourceData, meta interface{}) er if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Id(), o, n); err != nil { + + err := UpdateTags(conn, d.Id(), o, n) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to update tags for ECS Capacity Provider %s: %s", d.Id(), err) + return resourceCapacityProviderRead(d, meta) + } + + if err != nil { return fmt.Errorf("error updating ECS Capacity Provider (%s) tags: %w", d.Id(), err) } } diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index e49581daa68a..ff88d06c9ca9 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -181,8 +181,7 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error { input := &ecs.CreateClusterInput{ ClusterName: aws.String(clusterName), - DefaultCapacityProviderStrategy: expandEcsCapacityProviderStrategy(d.Get("default_capacity_provider_strategy").(*schema.Set)), - Tags: Tags(tags.IgnoreAWS()), + DefaultCapacityProviderStrategy: expandCapacityProviderStrategy(d.Get("default_capacity_provider_strategy").(*schema.Set)), } if v, ok := d.GetOk("capacity_providers"); ok { @@ -190,33 +189,27 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error { } if v, ok := d.GetOk("setting"); ok { - input.Settings = expandEcsSettings(v.(*schema.Set)) + input.Settings = expandClusterSettings(v.(*schema.Set)) } if v, ok := d.GetOk("configuration"); ok && len(v.([]interface{})) > 0 { - input.Configuration = expandECSClusterConfiguration(v.([]interface{})) + input.Configuration = expandClusterConfiguration(v.([]interface{})) + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) } // CreateCluster will create the ECS IAM Service Linked Role on first ECS provision // This process does not complete before the initial API call finishes. - var out *ecs.CreateClusterOutput - err := resource.Retry(tfiam.PropagationTimeout, func() *resource.RetryError { - var err error - out, err = conn.CreateCluster(input) - - if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "Unable to assume the service linked role") { - return resource.RetryableError(err) - } + out, err := retryClusterCreate(conn, input) - if err != nil { - return resource.NonRetryableError(err) - } - - return nil - }) + // Some partitions (i.e., ISO) may not support tag-on-create + if input.Tags != nil && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] ECS Cluster (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + input.Tags = nil - if tfresource.TimedOut(err) { - out, err = conn.CreateCluster(input) + out, err = retryClusterCreate(conn, input) } if err != nil { @@ -231,6 +224,21 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error waiting for ECS Cluster (%s) to become Available: %w", d.Id(), err) } + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if input.Tags == nil && len(tags) > 0 { + err := UpdateTags(conn, d.Id(), nil, tags) + + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + // If default tags only, log and continue. Otherwise, error. + log.Printf("[WARN] error adding tags after create for ECS Cluster (%s): %s", d.Id(), err) + return resourceClusterRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating ECS Cluster (%s) tags: %w", clusterName, err) + } + } + return resourceClusterRead(d, meta) } @@ -298,22 +306,28 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error { if err := d.Set("capacity_providers", aws.StringValueSlice(cluster.CapacityProviders)); err != nil { return fmt.Errorf("error setting capacity_providers: %w", err) } - if err := d.Set("default_capacity_provider_strategy", flattenEcsCapacityProviderStrategy(cluster.DefaultCapacityProviderStrategy)); err != nil { + if err := d.Set("default_capacity_provider_strategy", flattenCapacityProviderStrategy(cluster.DefaultCapacityProviderStrategy)); err != nil { return fmt.Errorf("error setting default_capacity_provider_strategy: %w", err) } - if err := d.Set("setting", flattenEcsSettings(cluster.Settings)); err != nil { + if err := d.Set("setting", flattenClusterSettings(cluster.Settings)); err != nil { return fmt.Errorf("error setting setting: %w", err) } if cluster.Configuration != nil { - if err := d.Set("configuration", flattenECSClusterConfiguration(cluster.Configuration)); err != nil { + if err := d.Set("configuration", flattenClusterConfiguration(cluster.Configuration)); err != nil { return fmt.Errorf("error setting configuration: %w", err) } } tags := KeyValueTags(cluster.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to list tags for ECS Cluster %s: %s", d.Id(), err) + return nil + } + //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) @@ -335,11 +349,11 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { } if v, ok := d.GetOk("setting"); ok { - input.Settings = expandEcsSettings(v.(*schema.Set)) + input.Settings = expandClusterSettings(v.(*schema.Set)) } if v, ok := d.GetOk("configuration"); ok && len(v.([]interface{})) > 0 { - input.Configuration = expandECSClusterConfiguration(v.([]interface{})) + input.Configuration = expandClusterConfiguration(v.([]interface{})) } _, err := conn.UpdateCluster(&input) @@ -352,19 +366,11 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { } } - if d.HasChange("tags_all") { - o, n := d.GetChange("tags_all") - - if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating ECS Cluster (%s) tags: %w", d.Id(), err) - } - } - if d.HasChanges("capacity_providers", "default_capacity_provider_strategy") { input := ecs.PutClusterCapacityProvidersInput{ Cluster: aws.String(d.Id()), CapacityProviders: flex.ExpandStringSet(d.Get("capacity_providers").(*schema.Set)), - DefaultCapacityProviderStrategy: expandEcsCapacityProviderStrategy(d.Get("default_capacity_provider_strategy").(*schema.Set)), + DefaultCapacityProviderStrategy: expandCapacityProviderStrategy(d.Get("default_capacity_provider_strategy").(*schema.Set)), } err := resource.Retry(ecsClusterTimeoutUpdate, func() *resource.RetryError { @@ -395,6 +401,22 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + err := UpdateTags(conn, d.Id(), o, n) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to update tags for ECS Cluster %s: %s", d.Id(), err) + return nil + } + + if err != nil { + return fmt.Errorf("error updating ECS Cluster (%s) tags: %w", d.Id(), err) + } + } + return nil } @@ -446,7 +468,31 @@ func resourceClusterDelete(d *schema.ResourceData, meta interface{}) error { return nil } -func expandEcsSettings(configured *schema.Set) []*ecs.ClusterSetting { +func retryClusterCreate(conn *ecs.ECS, input *ecs.CreateClusterInput) (*ecs.CreateClusterOutput, error) { + var output *ecs.CreateClusterOutput + err := resource.Retry(tfiam.PropagationTimeout, func() *resource.RetryError { + var err error + output, err = conn.CreateCluster(input) + + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "Unable to assume the service linked role") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + output, err = conn.CreateCluster(input) + } + + return output, err +} + +func expandClusterSettings(configured *schema.Set) []*ecs.ClusterSetting { list := configured.List() if len(list) == 0 { return nil @@ -468,7 +514,7 @@ func expandEcsSettings(configured *schema.Set) []*ecs.ClusterSetting { return settings } -func flattenEcsSettings(list []*ecs.ClusterSetting) []map[string]interface{} { +func flattenClusterSettings(list []*ecs.ClusterSetting) []map[string]interface{} { if len(list) == 0 { return nil } @@ -485,7 +531,7 @@ func flattenEcsSettings(list []*ecs.ClusterSetting) []map[string]interface{} { return result } -func flattenECSClusterConfiguration(apiObject *ecs.ClusterConfiguration) []interface{} { +func flattenClusterConfiguration(apiObject *ecs.ClusterConfiguration) []interface{} { if apiObject == nil { return nil } @@ -493,12 +539,12 @@ func flattenECSClusterConfiguration(apiObject *ecs.ClusterConfiguration) []inter tfMap := map[string]interface{}{} if apiObject.ExecuteCommandConfiguration != nil { - tfMap["execute_command_configuration"] = flattenECSClusterConfigurationExecuteCommandConfiguration(apiObject.ExecuteCommandConfiguration) + tfMap["execute_command_configuration"] = flattenClusterConfigurationExecuteCommandConfiguration(apiObject.ExecuteCommandConfiguration) } return []interface{}{tfMap} } -func flattenECSClusterConfigurationExecuteCommandConfiguration(apiObject *ecs.ExecuteCommandConfiguration) []interface{} { +func flattenClusterConfigurationExecuteCommandConfiguration(apiObject *ecs.ExecuteCommandConfiguration) []interface{} { if apiObject == nil { return nil } @@ -510,7 +556,7 @@ func flattenECSClusterConfigurationExecuteCommandConfiguration(apiObject *ecs.Ex } if apiObject.LogConfiguration != nil { - tfMap["log_configuration"] = flattenECSClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiObject.LogConfiguration) + tfMap["log_configuration"] = flattenClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiObject.LogConfiguration) } if apiObject.Logging != nil { @@ -520,7 +566,7 @@ func flattenECSClusterConfigurationExecuteCommandConfiguration(apiObject *ecs.Ex return []interface{}{tfMap} } -func flattenECSClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiObject *ecs.ExecuteCommandLogConfiguration) []interface{} { +func flattenClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiObject *ecs.ExecuteCommandLogConfiguration) []interface{} { if apiObject == nil { return nil } @@ -545,7 +591,7 @@ func flattenECSClusterConfigurationExecuteCommandConfigurationLogConfiguration(a return []interface{}{tfMap} } -func expandECSClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { +func expandClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { if len(nc) == 0 { return &ecs.ClusterConfiguration{} } @@ -553,13 +599,13 @@ func expandECSClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { config := &ecs.ClusterConfiguration{} if v, ok := raw["execute_command_configuration"].([]interface{}); ok && len(v) > 0 { - config.ExecuteCommandConfiguration = expandECSClusterConfigurationExecuteCommandConfiguration(v) + config.ExecuteCommandConfiguration = expandClusterConfigurationExecuteCommandConfiguration(v) } return config } -func expandECSClusterConfigurationExecuteCommandConfiguration(nc []interface{}) *ecs.ExecuteCommandConfiguration { +func expandClusterConfigurationExecuteCommandConfiguration(nc []interface{}) *ecs.ExecuteCommandConfiguration { if len(nc) == 0 { return &ecs.ExecuteCommandConfiguration{} } @@ -567,7 +613,7 @@ func expandECSClusterConfigurationExecuteCommandConfiguration(nc []interface{}) config := &ecs.ExecuteCommandConfiguration{} if v, ok := raw["log_configuration"].([]interface{}); ok && len(v) > 0 { - config.LogConfiguration = expandECSClusterConfigurationExecuteCommandLogConfiguration(v) + config.LogConfiguration = expandClusterConfigurationExecuteCommandLogConfiguration(v) } if v, ok := raw["kms_key_id"].(string); ok && v != "" { @@ -581,7 +627,7 @@ func expandECSClusterConfigurationExecuteCommandConfiguration(nc []interface{}) return config } -func expandECSClusterConfigurationExecuteCommandLogConfiguration(nc []interface{}) *ecs.ExecuteCommandLogConfiguration { +func expandClusterConfigurationExecuteCommandLogConfiguration(nc []interface{}) *ecs.ExecuteCommandLogConfiguration { if len(nc) == 0 { return &ecs.ExecuteCommandLogConfiguration{} } diff --git a/internal/service/ecs/cluster_data_source.go b/internal/service/ecs/cluster_data_source.go index 7fb12dfc4a3c..2aa010825f96 100644 --- a/internal/service/ecs/cluster_data_source.go +++ b/internal/service/ecs/cluster_data_source.go @@ -89,7 +89,7 @@ func dataSourceClusterRead(d *schema.ResourceData, meta interface{}) error { d.Set("running_tasks_count", cluster.RunningTasksCount) d.Set("registered_container_instances_count", cluster.RegisteredContainerInstancesCount) - if err := d.Set("setting", flattenEcsSettings(cluster.Settings)); err != nil { + if err := d.Set("setting", flattenClusterSettings(cluster.Settings)); err != nil { return fmt.Errorf("error setting setting: %w", err) } diff --git a/internal/service/ecs/flex.go b/internal/service/ecs/flex.go index 1bec50290005..35911df90001 100644 --- a/internal/service/ecs/flex.go +++ b/internal/service/ecs/flex.go @@ -34,7 +34,7 @@ func expandLoadBalancers(configured []interface{}) []*ecs.LoadBalancer { } // Flattens an array of ECS LoadBalancers into a []map[string]interface{} -func flattenECSLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{} { +func flattenLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) for _, loadBalancer := range list { l := map[string]interface{}{ diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index b583c63b6fda..4afd657df949 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -443,14 +443,13 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { deploymentMinimumHealthyPercent := d.Get("deployment_minimum_healthy_percent").(int) schedulingStrategy := d.Get("scheduling_strategy").(string) - deploymentController := expandEcsDeploymentController(d.Get("deployment_controller").([]interface{})) + deploymentController := expandDeploymentController(d.Get("deployment_controller").([]interface{})) input := ecs.CreateServiceInput{ ClientToken: aws.String(resource.UniqueId()), DeploymentController: deploymentController, SchedulingStrategy: aws.String(schedulingStrategy), ServiceName: aws.String(d.Get("name").(string)), - Tags: Tags(tags.IgnoreAWS()), TaskDefinition: aws.String(d.Get("task_definition").(string)), EnableECSManagedTags: aws.Bool(d.Get("enable_ecs_managed_tags").(bool)), EnableExecuteCommand: aws.Bool(d.Get("enable_execute_command").(bool)), @@ -471,7 +470,7 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { if v, ok := d.GetOk("deployment_circuit_breaker"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { input.DeploymentConfiguration = &ecs.DeploymentConfiguration{} - input.DeploymentConfiguration.DeploymentCircuitBreaker = expandECSDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) + input.DeploymentConfiguration.DeploymentCircuitBreaker = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) } if v, ok := d.GetOk("cluster"); ok { @@ -501,7 +500,7 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { input.PlatformVersion = aws.String(v.(string)) } - input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) + input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) loadBalancers := expandLoadBalancers(d.Get("load_balancer").(*schema.Set).List()) if len(loadBalancers) > 0 { @@ -512,7 +511,7 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { input.Role = aws.String(v.(string)) } - input.NetworkConfiguration = expandEcsNetworkConfiguration(d.Get("network_configuration").([]interface{})) + input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{})) if v, ok := d.GetOk("ordered_placement_strategy"); ok { ps, err := expandPlacementStrategy(v.([]interface{})) @@ -557,57 +556,33 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { input.ServiceRegistries = srs } - log.Printf("[DEBUG] Creating ECS service: %s", input) - - // Retry due to AWS IAM & ECS eventual consistency - err := resource.Retry(tfiam.PropagationTimeout+serviceCreateTimeout, func() *resource.RetryError { - output, err := conn.CreateService(&input) - - if err != nil { - if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) { - return resource.RetryableError(err) - } - - if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "verify that the ECS service role being passed has the proper permissions") { - return resource.RetryableError(err) - } - - if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { - return resource.RetryableError(err) - } - - if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "Unable to assume the service linked role") { - return resource.RetryableError(err) - } - - return resource.NonRetryableError(fmt.Errorf("error waiting for ECS service (%s) creation: %w", d.Get("name").(string), err)) - } - - log.Printf("[DEBUG] ECS service created: %s", aws.StringValue(output.Service.ServiceArn)) - d.SetId(aws.StringValue(output.Service.ServiceArn)) - - return nil - }) + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) // tags field doesn't exist in all partitions + } - if tfresource.TimedOut(err) { - output, err := conn.CreateService(&input) + log.Printf("[DEBUG] Creating ECS Service: %s", input) - if err != nil { - return fmt.Errorf("error creating ECS service: %w", err) - } + output, err := retryServiceCreate(conn, input) - if output == nil || output.Service == nil { - return fmt.Errorf("error creating ECS service: empty response") - } + // Some partitions (i.e., ISO) may not support tag-on-create + if input.Tags != nil && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] ECS Service (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + input.Tags = nil - log.Printf("[DEBUG] ECS service created: %s", aws.StringValue(output.Service.ServiceArn)) - d.SetId(aws.StringValue(output.Service.ServiceArn)) + output, err = retryServiceCreate(conn, input) } if err != nil { - return fmt.Errorf("error creating %s service: %w", d.Get("name").(string), err) + return fmt.Errorf("error creating ECS service (%s): %w", d.Get("name").(string), err) } + if output == nil || output.Service == nil { + return fmt.Errorf("error creating ECS service: empty response") + } + + log.Printf("[DEBUG] ECS service created: %s", aws.StringValue(output.Service.ServiceArn)) + d.SetId(aws.StringValue(output.Service.ServiceArn)) + if d.Get("wait_for_steady_state").(bool) { cluster := "" if v, ok := d.GetOk("cluster"); ok { @@ -619,6 +594,21 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { } } + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if input.Tags == nil && len(tags) > 0 { + err := UpdateTags(conn, d.Id(), nil, tags) + + // If default tags only, log and continue. Otherwise, error. + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] error adding tags after create for ECS Service (%s): %s", d.Id(), err) + return resourceServiceRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating ECS Service (%s) tags: %w", d.Id(), err) + } + } + return resourceServiceRead(d, meta) } @@ -724,7 +714,7 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("deployment_minimum_healthy_percent", service.DeploymentConfiguration.MinimumHealthyPercent) if service.DeploymentConfiguration.DeploymentCircuitBreaker != nil { - if err := d.Set("deployment_circuit_breaker", []interface{}{flattenECSDeploymentCircuitBreaker(service.DeploymentConfiguration.DeploymentCircuitBreaker)}); err != nil { + if err := d.Set("deployment_circuit_breaker", []interface{}{flattenDeploymentCircuitBreaker(service.DeploymentConfiguration.DeploymentCircuitBreaker)}); err != nil { return fmt.Errorf("error setting deployment_circuit_break: %w", err) } } else { @@ -732,15 +722,15 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { } } - if err := d.Set("deployment_controller", flattenEcsDeploymentController(service.DeploymentController)); err != nil { + if err := d.Set("deployment_controller", flattenDeploymentController(service.DeploymentController)); err != nil { return fmt.Errorf("error setting deployment_controller for (%s): %w", d.Id(), err) } if service.LoadBalancers != nil { - d.Set("load_balancer", flattenECSLoadBalancers(service.LoadBalancers)) + d.Set("load_balancer", flattenLoadBalancers(service.LoadBalancers)) } - if err := d.Set("capacity_provider_strategy", flattenEcsCapacityProviderStrategy(service.CapacityProviderStrategy)); err != nil { + if err := d.Set("capacity_provider_strategy", flattenCapacityProviderStrategy(service.CapacityProviderStrategy)); err != nil { return fmt.Errorf("error setting capacity_provider_strategy: %w", err) } @@ -752,7 +742,7 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err) } - if err := d.Set("network_configuration", flattenEcsNetworkConfiguration(service.NetworkConfiguration)); err != nil { + if err := d.Set("network_configuration", flattenNetworkConfiguration(service.NetworkConfiguration)); err != nil { return fmt.Errorf("error setting network_configuration for (%s): %w", d.Id(), err) } @@ -762,6 +752,12 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { tags := KeyValueTags(service.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to list tags for ECS Service %s: %s", d.Id(), err) + return nil + } + //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) @@ -774,7 +770,7 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { return nil } -func expandEcsDeploymentController(l []interface{}) *ecs.DeploymentController { +func expandDeploymentController(l []interface{}) *ecs.DeploymentController { if len(l) == 0 || l[0] == nil { return nil } @@ -788,7 +784,7 @@ func expandEcsDeploymentController(l []interface{}) *ecs.DeploymentController { return deploymentController } -func flattenEcsDeploymentController(deploymentController *ecs.DeploymentController) []interface{} { +func flattenDeploymentController(deploymentController *ecs.DeploymentController) []interface{} { m := map[string]interface{}{ "type": ecs.DeploymentControllerTypeEcs, } @@ -802,7 +798,7 @@ func flattenEcsDeploymentController(deploymentController *ecs.DeploymentControll return []interface{}{m} } -func expandECSDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.DeploymentCircuitBreaker { +func expandDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.DeploymentCircuitBreaker { if tfMap == nil { return nil } @@ -815,7 +811,7 @@ func expandECSDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.Deploy return apiObject } -func flattenECSDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) map[string]interface{} { +func flattenDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) map[string]interface{} { if apiObject == nil { return nil } @@ -828,7 +824,7 @@ func flattenECSDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) return tfMap } -func flattenEcsNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} { +func flattenNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} { if nc == nil { return nil } @@ -844,7 +840,7 @@ func flattenEcsNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} return []interface{}{result} } -func expandEcsNetworkConfiguration(nc []interface{}) *ecs.NetworkConfiguration { +func expandNetworkConfiguration(nc []interface{}) *ecs.NetworkConfiguration { if len(nc) == 0 { return nil } @@ -864,7 +860,7 @@ func expandEcsNetworkConfiguration(nc []interface{}) *ecs.NetworkConfiguration { return &ecs.NetworkConfiguration{AwsvpcConfiguration: awsVpcConfig} } -func expandEcsCapacityProviderStrategy(cps *schema.Set) []*ecs.CapacityProviderStrategyItem { +func expandCapacityProviderStrategy(cps *schema.Set) []*ecs.CapacityProviderStrategyItem { list := cps.List() results := make([]*ecs.CapacityProviderStrategyItem, 0) for _, raw := range list { @@ -885,7 +881,7 @@ func expandEcsCapacityProviderStrategy(cps *schema.Set) []*ecs.CapacityProviderS return results } -func flattenEcsCapacityProviderStrategy(cps []*ecs.CapacityProviderStrategyItem) []map[string]interface{} { +func flattenCapacityProviderStrategy(cps []*ecs.CapacityProviderStrategyItem) []map[string]interface{} { if cps == nil { return nil } @@ -1085,7 +1081,7 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { input.DeploymentConfiguration.DeploymentCircuitBreaker = &ecs.DeploymentCircuitBreaker{} if v, ok := d.GetOk("deployment_circuit_breaker"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.DeploymentConfiguration.DeploymentCircuitBreaker = expandECSDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) + input.DeploymentConfiguration.DeploymentCircuitBreaker = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) } } @@ -1140,12 +1136,12 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { if d.HasChange("network_configuration") { updateService = true - input.NetworkConfiguration = expandEcsNetworkConfiguration(d.Get("network_configuration").([]interface{})) + input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{})) } if d.HasChange("capacity_provider_strategy") { updateService = true - input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) + input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) } if d.HasChange("enable_execute_command") { @@ -1196,7 +1192,15 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Id(), o, n); err != nil { + err := UpdateTags(conn, d.Id(), o, n) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to update tags for ECS Service %s: %s", d.Id(), err) + return resourceServiceRead(d, meta) + } + + if err != nil { return fmt.Errorf("error updating ECS Service (%s) tags: %w", d.Id(), err) } } @@ -1299,6 +1303,42 @@ func resourceLoadBalancerHash(v interface{}) int { return create.StringHashcode(buf.String()) } +func retryServiceCreate(conn *ecs.ECS, input ecs.CreateServiceInput) (*ecs.CreateServiceOutput, error) { + var output *ecs.CreateServiceOutput + err := resource.Retry(tfiam.PropagationTimeout+serviceCreateTimeout, func() *resource.RetryError { + var err error + output, err = conn.CreateService(&input) + + if err != nil { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException) { + return resource.RetryableError(err) + } + + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "verify that the ECS service role being passed has the proper permissions") { + return resource.RetryableError(err) + } + + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { + return resource.RetryableError(err) + } + + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "Unable to assume the service linked role") { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + output, err = conn.CreateService(&input) + } + + return output, err +} + func buildFamilyAndRevisionFromARN(arn string) string { return strings.Split(arn, "/")[1] } diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index 400e6f4c0179..6ae098c277c5 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -315,6 +315,10 @@ func TestAccECSService_healthCheckGracePeriodSeconds(t *testing.T) { rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_service.test" + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), @@ -412,6 +416,10 @@ func TestAccECSService_DeploymentControllerType_codeDeployUpdateDesiredCountAndH rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_service.test" + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), @@ -1098,6 +1106,10 @@ func TestAccECSService_ServiceRegistries_changes(t *testing.T) { updatedServiceDiscoveryName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_service.test" + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(servicediscovery.EndpointsID, t) }, ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), diff --git a/internal/service/ecs/tag_test.go b/internal/service/ecs/tag_test.go index 0bb45c56104f..ef33498b12f4 100644 --- a/internal/service/ecs/tag_test.go +++ b/internal/service/ecs/tag_test.go @@ -24,7 +24,7 @@ func TestAccECSTag_basic(t *testing.T) { CheckDestroy: testAccCheckTagDestroy, Steps: []resource.TestStep{ { - Config: testAccEcsTagConfig(rName, "key1", "value1"), + Config: testAccTagConfig(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckTagExists(resourceName), resource.TestCheckResourceAttr(resourceName, "key", "key1"), @@ -51,7 +51,7 @@ func TestAccECSTag_disappears(t *testing.T) { CheckDestroy: testAccCheckTagDestroy, Steps: []resource.TestStep{ { - Config: testAccEcsTagConfig(rName, "key1", "value1"), + Config: testAccTagConfig(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckTagExists(resourceName), acctest.CheckResourceDisappears(acctest.Provider, tfecs.ResourceTag(), resourceName), @@ -74,7 +74,7 @@ func TestAccECSTag_ResourceARN_batchComputeEnvironment(t *testing.T) { CheckDestroy: testAccCheckTagDestroy, Steps: []resource.TestStep{ { - Config: testAccEcsTagConfigResourceArnBatchComputeEnvironment(rName), + Config: testAccTagConfigResourceArnBatchComputeEnvironment(rName), Check: resource.ComposeTestCheckFunc( testAccCheckTagExists(resourceName), ), @@ -99,7 +99,7 @@ func TestAccECSTag_value(t *testing.T) { CheckDestroy: testAccCheckTagDestroy, Steps: []resource.TestStep{ { - Config: testAccEcsTagConfig(rName, "key1", "value1"), + Config: testAccTagConfig(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckTagExists(resourceName), resource.TestCheckResourceAttr(resourceName, "key", "key1"), @@ -112,7 +112,7 @@ func TestAccECSTag_value(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccEcsTagConfig(rName, "key1", "value1updated"), + Config: testAccTagConfig(rName, "key1", "value1updated"), Check: resource.ComposeTestCheckFunc( testAccCheckTagExists(resourceName), resource.TestCheckResourceAttr(resourceName, "key", "key1"), @@ -123,7 +123,7 @@ func TestAccECSTag_value(t *testing.T) { }) } -func testAccEcsTagConfig(rName string, key string, value string) string { +func testAccTagConfig(rName string, key string, value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { name = %[1]q @@ -141,7 +141,7 @@ resource "aws_ecs_tag" "test" { `, rName, key, value) } -func testAccEcsTagConfigResourceArnBatchComputeEnvironment(rName string) string { +func testAccTagConfigResourceArnBatchComputeEnvironment(rName string) string { return fmt.Sprintf(` data "aws_partition" "current" {} diff --git a/internal/service/ecs/task_definition.go b/internal/service/ecs/task_definition.go index b619670d6d09..a592cf81dcc7 100644 --- a/internal/service/ecs/task_definition.go +++ b/internal/service/ecs/task_definition.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "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" @@ -67,9 +68,9 @@ func ResourceTaskDefinition() *schema.Resource { // Sort the lists of environment variables as they are serialized to state, so we won't get // spurious reorderings in plans (diff is suppressed if the environment variables haven't changed, // but they still show in the plan if some other property changes). - orderedCDs, _ := expandEcsContainerDefinitions(v.(string)) + orderedCDs, _ := expandContainerDefinitions(v.(string)) containerDefinitions(orderedCDs).OrderEnvironmentVariables() - unnormalizedJson, _ := flattenEcsContainerDefinitions(orderedCDs) + unnormalizedJson, _ := flattenContainerDefinitions(orderedCDs) json, _ := structure.NormalizeJsonString(unnormalizedJson) return json }, @@ -422,7 +423,7 @@ func ResourceTaskDefinition() *schema.Resource { func ValidTaskDefinitionContainerDefinitions(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - _, err := expandEcsContainerDefinitions(value) + _, err := expandContainerDefinitions(value) if err != nil { errors = append(errors, fmt.Errorf("ECS Task Definition container_definitions is invalid: %s", err)) } @@ -435,7 +436,7 @@ func resourceTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) erro tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) rawDefinitions := d.Get("container_definitions").(string) - definitions, err := expandEcsContainerDefinitions(rawDefinitions) + definitions, err := expandContainerDefinitions(rawDefinitions) if err != nil { return err } @@ -479,17 +480,17 @@ func resourceTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) erro } if v, ok := d.GetOk("volume"); ok { - volumes := expandEcsVolumes(v.(*schema.Set).List()) + volumes := expandVolumes(v.(*schema.Set).List()) input.Volumes = volumes } if v, ok := d.GetOk("inference_accelerator"); ok { - input.InferenceAccelerators = expandEcsInferenceAccelerators(v.(*schema.Set).List()) + input.InferenceAccelerators = expandInferenceAccelerators(v.(*schema.Set).List()) } constraints := d.Get("placement_constraints").(*schema.Set).List() if len(constraints) > 0 { - cons, err := expandEcsTaskDefinitionPlacementConstraints(constraints) + cons, err := expandTaskDefinitionPlacementConstraints(constraints) if err != nil { return err } @@ -502,22 +503,31 @@ func resourceTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) erro runtimePlatformConfigs := d.Get("runtime_platform").([]interface{}) if len(runtimePlatformConfigs) > 0 && runtimePlatformConfigs[0] != nil { - input.RuntimePlatform = expandEcsTaskDefinitionRuntimePlatformConfiguration(runtimePlatformConfigs) + input.RuntimePlatform = expandTaskDefinitionRuntimePlatformConfiguration(runtimePlatformConfigs) } proxyConfigs := d.Get("proxy_configuration").([]interface{}) if len(proxyConfigs) > 0 { - input.ProxyConfiguration = expandEcsTaskDefinitionProxyConfiguration(proxyConfigs) + input.ProxyConfiguration = expandTaskDefinitionProxyConfiguration(proxyConfigs) } if v, ok := d.GetOk("ephemeral_storage"); ok && len(v.([]interface{})) > 0 { - input.EphemeralStorage = expandEcsTaskDefinitionEphemeralStorage(v.([]interface{})) + input.EphemeralStorage = expandTaskDefinitionEphemeralStorage(v.([]interface{})) } log.Printf("[DEBUG] Registering ECS task definition: %s", input) out, err := conn.RegisterTaskDefinition(&input) + + // Some partitions (i.e., ISO) may not support tag-on-create + if input.Tags != nil && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] ECS Task Definition (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + input.Tags = nil + + out, err = conn.RegisterTaskDefinition(&input) + } + if err != nil { - return err + return fmt.Errorf("error creating ECS Task Definition (%s): %w", d.Get("family").(string), err) } taskDefinition := *out.TaskDefinition // nosemgrep: prefer-aws-go-sdk-pointer-conversion-assignment // false positive @@ -528,6 +538,21 @@ func resourceTaskDefinitionCreate(d *schema.ResourceData, meta interface{}) erro d.SetId(aws.StringValue(taskDefinition.Family)) d.Set("arn", taskDefinition.TaskDefinitionArn) + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if input.Tags == nil && len(tags) > 0 { + err := UpdateTags(conn, d.Id(), nil, tags) + + // If default tags only, log and continue. Otherwise, error. + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] error adding tags after create for ECS Task Definition (%s): %s", d.Id(), err) + return resourceTaskDefinitionRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating ECS Task Definition (%s) tags: %w", d.Id(), err) + } + } + return resourceTaskDefinitionRead(d, meta) } @@ -565,7 +590,7 @@ func resourceTaskDefinitionRead(d *schema.ResourceData, meta interface{}) error // some other property changes). containerDefinitions(taskDefinition.ContainerDefinitions).OrderEnvironmentVariables() - defs, err := flattenEcsContainerDefinitions(taskDefinition.ContainerDefinitions) + defs, err := flattenContainerDefinitions(taskDefinition.ContainerDefinitions) if err != nil { return err } @@ -582,22 +607,11 @@ func resourceTaskDefinitionRead(d *schema.ResourceData, meta interface{}) error d.Set("ipc_mode", taskDefinition.IpcMode) d.Set("pid_mode", taskDefinition.PidMode) - tags := KeyValueTags(out.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - - if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %w", err) - } - - if err := d.Set("volume", flattenEcsVolumes(taskDefinition.Volumes)); err != nil { + if err := d.Set("volume", flattenVolumes(taskDefinition.Volumes)); err != nil { return fmt.Errorf("error setting volume: %w", err) } - if err := d.Set("inference_accelerator", flattenEcsInferenceAccelerators(taskDefinition.InferenceAccelerators)); err != nil { + if err := d.Set("inference_accelerator", flattenInferenceAccelerators(taskDefinition.InferenceAccelerators)); err != nil { return fmt.Errorf("error setting inference accelerators: %w", err) } @@ -617,9 +631,27 @@ func resourceTaskDefinitionRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting proxy_configuration: %w", err) } - if err := d.Set("ephemeral_storage", flattenEcsTaskDefinitionEphemeralStorage(taskDefinition.EphemeralStorage)); err != nil { + if err := d.Set("ephemeral_storage", flattenTaskDefinitionEphemeralStorage(taskDefinition.EphemeralStorage)); err != nil { return fmt.Errorf("error setting ephemeral_storage: %w", err) } + + tags := KeyValueTags(out.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to list tags for ECS Task Definition %s: %s", d.Id(), err) + return nil + } + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } @@ -691,8 +723,16 @@ func resourceTaskDefinitionUpdate(d *schema.ResourceData, meta interface{}) erro if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating ECS Task Definition (%s) tags: %s", d.Id(), err) + err := UpdateTags(conn, d.Get("arn").(string), o, n) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to update tags for ECS Task Definition %s: %s", d.Id(), err) + return nil + } + + if err != nil { + return fmt.Errorf("error updating ECS Task Definition (%s) tags: %w", d.Id(), err) } } @@ -778,7 +818,7 @@ func resourceTaskDefinitionVolumeHash(v interface{}) int { return create.StringHashcode(buf.String()) } -func flattenEcsInferenceAccelerators(list []*ecs.InferenceAccelerator) []map[string]interface{} { +func flattenInferenceAccelerators(list []*ecs.InferenceAccelerator) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) for _, iAcc := range list { l := map[string]interface{}{ @@ -791,7 +831,7 @@ func flattenEcsInferenceAccelerators(list []*ecs.InferenceAccelerator) []map[str return result } -func expandEcsInferenceAccelerators(configured []interface{}) []*ecs.InferenceAccelerator { +func expandInferenceAccelerators(configured []interface{}) []*ecs.InferenceAccelerator { iAccs := make([]*ecs.InferenceAccelerator, 0, len(configured)) for _, lRaw := range configured { data := lRaw.(map[string]interface{}) @@ -805,7 +845,7 @@ func expandEcsInferenceAccelerators(configured []interface{}) []*ecs.InferenceAc return iAccs } -func expandEcsTaskDefinitionPlacementConstraints(constraints []interface{}) ([]*ecs.TaskDefinitionPlacementConstraint, error) { +func expandTaskDefinitionPlacementConstraints(constraints []interface{}) ([]*ecs.TaskDefinitionPlacementConstraint, error) { var pc []*ecs.TaskDefinitionPlacementConstraint for _, raw := range constraints { p := raw.(map[string]interface{}) @@ -823,7 +863,7 @@ func expandEcsTaskDefinitionPlacementConstraints(constraints []interface{}) ([]* return pc, nil } -func expandEcsTaskDefinitionRuntimePlatformConfiguration(runtimePlatformConfig []interface{}) *ecs.RuntimePlatform { +func expandTaskDefinitionRuntimePlatformConfiguration(runtimePlatformConfig []interface{}) *ecs.RuntimePlatform { config := runtimePlatformConfig[0] configMap := config.(map[string]interface{}) @@ -842,7 +882,7 @@ func expandEcsTaskDefinitionRuntimePlatformConfiguration(runtimePlatformConfig [ return ecsProxyConfig } -func expandEcsTaskDefinitionProxyConfiguration(proxyConfigs []interface{}) *ecs.ProxyConfiguration { +func expandTaskDefinitionProxyConfiguration(proxyConfigs []interface{}) *ecs.ProxyConfiguration { proxyConfig := proxyConfigs[0] configMap := proxyConfig.(map[string]interface{}) @@ -867,7 +907,7 @@ func expandEcsTaskDefinitionProxyConfiguration(proxyConfigs []interface{}) *ecs. return ecsProxyConfig } -func expandEcsVolumes(configured []interface{}) []*ecs.Volume { +func expandVolumes(configured []interface{}) []*ecs.Volume { volumes := make([]*ecs.Volume, 0, len(configured)) // Loop over our configured volumes and create @@ -887,15 +927,15 @@ func expandEcsVolumes(configured []interface{}) []*ecs.Volume { } if v, ok := data["docker_volume_configuration"].([]interface{}); ok && len(v) > 0 { - l.DockerVolumeConfiguration = expandEcsVolumesDockerVolume(v) + l.DockerVolumeConfiguration = expandVolumesDockerVolume(v) } if v, ok := data["efs_volume_configuration"].([]interface{}); ok && len(v) > 0 { - l.EfsVolumeConfiguration = expandEcsVolumesEFSVolume(v) + l.EfsVolumeConfiguration = expandVolumesEFSVolume(v) } if v, ok := data["fsx_windows_file_server_volume_configuration"].([]interface{}); ok && len(v) > 0 { - l.FsxWindowsFileServerVolumeConfiguration = expandEcsVolumesFsxWinVolume(v) + l.FsxWindowsFileServerVolumeConfiguration = expandVolumesFsxWinVolume(v) } volumes = append(volumes, l) @@ -904,7 +944,7 @@ func expandEcsVolumes(configured []interface{}) []*ecs.Volume { return volumes } -func expandEcsVolumesDockerVolume(configList []interface{}) *ecs.DockerVolumeConfiguration { +func expandVolumesDockerVolume(configList []interface{}) *ecs.DockerVolumeConfiguration { config := configList[0].(map[string]interface{}) dockerVol := &ecs.DockerVolumeConfiguration{} @@ -934,7 +974,7 @@ func expandEcsVolumesDockerVolume(configList []interface{}) *ecs.DockerVolumeCon return dockerVol } -func expandEcsVolumesEFSVolume(efsConfig []interface{}) *ecs.EFSVolumeConfiguration { +func expandVolumesEFSVolume(efsConfig []interface{}) *ecs.EFSVolumeConfiguration { config := efsConfig[0].(map[string]interface{}) efsVol := &ecs.EFSVolumeConfiguration{} @@ -954,13 +994,13 @@ func expandEcsVolumesEFSVolume(efsConfig []interface{}) *ecs.EFSVolumeConfigurat } if v, ok := config["authorization_config"].([]interface{}); ok && len(v) > 0 { efsVol.RootDirectory = nil - efsVol.AuthorizationConfig = expandEcsVolumesEFSVolumeAuthorizationConfig(v) + efsVol.AuthorizationConfig = expandVolumesEFSVolumeAuthorizationConfig(v) } return efsVol } -func expandEcsVolumesEFSVolumeAuthorizationConfig(efsConfig []interface{}) *ecs.EFSAuthorizationConfig { +func expandVolumesEFSVolumeAuthorizationConfig(efsConfig []interface{}) *ecs.EFSAuthorizationConfig { authconfig := efsConfig[0].(map[string]interface{}) auth := &ecs.EFSAuthorizationConfig{} @@ -975,7 +1015,7 @@ func expandEcsVolumesEFSVolumeAuthorizationConfig(efsConfig []interface{}) *ecs. return auth } -func expandEcsVolumesFsxWinVolume(fsxWinConfig []interface{}) *ecs.FSxWindowsFileServerVolumeConfiguration { +func expandVolumesFsxWinVolume(fsxWinConfig []interface{}) *ecs.FSxWindowsFileServerVolumeConfiguration { config := fsxWinConfig[0].(map[string]interface{}) fsxVol := &ecs.FSxWindowsFileServerVolumeConfiguration{} @@ -988,13 +1028,13 @@ func expandEcsVolumesFsxWinVolume(fsxWinConfig []interface{}) *ecs.FSxWindowsFil } if v, ok := config["authorization_config"].([]interface{}); ok && len(v) > 0 { - fsxVol.AuthorizationConfig = expandEcsVolumesFsxWinVolumeAuthorizationConfig(v) + fsxVol.AuthorizationConfig = expandVolumesFsxWinVolumeAuthorizationConfig(v) } return fsxVol } -func expandEcsVolumesFsxWinVolumeAuthorizationConfig(config []interface{}) *ecs.FSxWindowsFileServerAuthorizationConfig { +func expandVolumesFsxWinVolumeAuthorizationConfig(config []interface{}) *ecs.FSxWindowsFileServerAuthorizationConfig { authconfig := config[0].(map[string]interface{}) auth := &ecs.FSxWindowsFileServerAuthorizationConfig{} @@ -1009,7 +1049,7 @@ func expandEcsVolumesFsxWinVolumeAuthorizationConfig(config []interface{}) *ecs. return auth } -func flattenEcsVolumes(list []*ecs.Volume) []map[string]interface{} { +func flattenVolumes(list []*ecs.Volume) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) for _, volume := range list { l := map[string]interface{}{ @@ -1146,7 +1186,7 @@ func flattenFsxWinVolumeAuthorizationConfig(config *ecs.FSxWindowsFileServerAuth return items } -func flattenEcsContainerDefinitions(definitions []*ecs.ContainerDefinition) (string, error) { +func flattenContainerDefinitions(definitions []*ecs.ContainerDefinition) (string, error) { b, err := jsonutil.BuildJSON(definitions) if err != nil { return "", err @@ -1155,7 +1195,7 @@ func flattenEcsContainerDefinitions(definitions []*ecs.ContainerDefinition) (str return string(b), nil } -func expandEcsContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefinition, error) { +func expandContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefinition, error) { var definitions []*ecs.ContainerDefinition err := json.Unmarshal([]byte(rawDefinitions), &definitions) @@ -1166,7 +1206,7 @@ func expandEcsContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefin return definitions, nil } -func expandEcsTaskDefinitionEphemeralStorage(config []interface{}) *ecs.EphemeralStorage { +func expandTaskDefinitionEphemeralStorage(config []interface{}) *ecs.EphemeralStorage { configMap := config[0].(map[string]interface{}) es := &ecs.EphemeralStorage{ @@ -1176,7 +1216,7 @@ func expandEcsTaskDefinitionEphemeralStorage(config []interface{}) *ecs.Ephemera return es } -func flattenEcsTaskDefinitionEphemeralStorage(pc *ecs.EphemeralStorage) []map[string]interface{} { +func flattenTaskDefinitionEphemeralStorage(pc *ecs.EphemeralStorage) []map[string]interface{} { if pc == nil { return nil } diff --git a/internal/service/ecs/task_definition_test.go b/internal/service/ecs/task_definition_test.go index 21bb2f06a615..cbc80cf29f7b 100644 --- a/internal/service/ecs/task_definition_test.go +++ b/internal/service/ecs/task_definition_test.go @@ -17,10 +17,10 @@ import ( ) func init() { - acctest.RegisterServiceErrorCheckFunc(ecs.EndpointsID, testAccErrorCheckSkipECS) + acctest.RegisterServiceErrorCheckFunc(ecs.EndpointsID, testAccErrorCheckSkip) } -func testAccErrorCheckSkipECS(t *testing.T) resource.ErrorCheckFunc { +func testAccErrorCheckSkip(t *testing.T) resource.ErrorCheckFunc { return acctest.ErrorCheckSkipMessagesContaining(t, "Unsupported field 'inferenceAccelerators'", ) @@ -430,7 +430,7 @@ func TestAccECSTaskDefinition_fsxWinFileSystem(t *testing.T) { domainName := acctest.RandomDomainName() if testing.Short() { - t.Skip("skipping ~3400s test in short mode") + t.Skip("skipping long-running test in short mode") } if acctest.Partition() == "aws-us-gov" { @@ -713,7 +713,7 @@ func TestAccECSTaskDefinition_changeVolumesForcesNewResource(t *testing.T) { Config: testAccTaskDefinitionUpdatedVolume(rName), Check: resource.ComposeTestCheckFunc( testAccCheckTaskDefinitionExists(resourceName, &after), - testAccCheckEcsTaskDefinitionRecreated(t, &before, &after), + testAccCheckTaskDefinitionRecreated(t, &before, &after), ), }, { @@ -1099,7 +1099,7 @@ func testAccCheckTaskDefinitionProxyConfiguration(after *ecs.TaskDefinition, con } } -func testAccCheckEcsTaskDefinitionRecreated(t *testing.T, +func testAccCheckTaskDefinitionRecreated(t *testing.T, before, after *ecs.TaskDefinition) resource.TestCheckFunc { return func(s *terraform.State) error { if *before.Revision == *after.Revision { diff --git a/internal/service/ecs/task_set.go b/internal/service/ecs/task_set.go index 1165eb56fceb..61e75b6f8cc0 100644 --- a/internal/service/ecs/task_set.go +++ b/internal/service/ecs/task_set.go @@ -301,7 +301,7 @@ func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { } if v, ok := d.GetOk("capacity_provider_strategy"); ok && v.(*schema.Set).Len() > 0 { - input.CapacityProviderStrategy = expandEcsCapacityProviderStrategy(v.(*schema.Set)) + input.CapacityProviderStrategy = expandCapacityProviderStrategy(v.(*schema.Set)) } if v, ok := d.GetOk("external_id"); ok { @@ -317,7 +317,7 @@ func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { } if v, ok := d.GetOk("network_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.NetworkConfiguration = expandEcsNetworkConfiguration(v.([]interface{})) + input.NetworkConfiguration = expandNetworkConfiguration(v.([]interface{})) } if v, ok := d.GetOk("platform_version"); ok { @@ -332,31 +332,21 @@ func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { input.ServiceRegistries = expandServiceRegistries(v.([]interface{})) } - // Retry due to AWS IAM & ECS eventual consistency - output, err := tfresource.RetryWhen( - tfiam.PropagationTimeout+taskSetCreateTimeout, - func() (interface{}, error) { - return conn.CreateTaskSet(input) - }, - func(err error) (bool, error) { - if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException, ecs.ErrCodeServiceNotFoundException, ecs.ErrCodeTaskSetNotFoundException) || - tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { - return true, err - } - return false, err - }, - ) + output, err := retryTaskSetCreate(conn, input) - if err != nil { - return fmt.Errorf("error creating ECS TaskSet: %w", err) + // Some partitions (i.e., ISO) may not support tag-on-create + if input.Tags != nil && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + log.Printf("[WARN] ECS Task Set (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + input.Tags = nil + + output, err = retryTaskSetCreate(conn, input) } - result, ok := output.(*ecs.CreateTaskSetOutput) - if !ok || result == nil || result.TaskSet == nil { - return fmt.Errorf("error creating ECS TaskSet: empty output") + if err != nil { + return fmt.Errorf("error creating ECS TaskSet: %w", err) } - taskSetId := aws.StringValue(result.TaskSet.Id) + taskSetId := aws.StringValue(output.TaskSet.Id) d.SetId(fmt.Sprintf("%s,%s,%s", taskSetId, service, cluster)) @@ -367,6 +357,21 @@ func resourceTaskSetCreate(d *schema.ResourceData, meta interface{}) error { } } + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if input.Tags == nil && len(tags) > 0 { + err := UpdateTags(conn, d.Id(), nil, tags) + + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException)) { + // If default tags only, log and continue. Otherwise, error. + log.Printf("[WARN] error adding tags after create for ECS Task Set (%s): %s", d.Id(), err) + return resourceTaskSetRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating ECS Task Set (%s) tags: %w", d.Id(), err) + } + } + return resourceTaskSetRead(d, meta) } @@ -422,7 +427,7 @@ func resourceTaskSetRead(d *schema.ResourceData, meta interface{}) error { d.Set("task_definition", taskSet.TaskDefinition) d.Set("task_set_id", taskSet.Id) - if err := d.Set("capacity_provider_strategy", flattenEcsCapacityProviderStrategy(taskSet.CapacityProviderStrategy)); err != nil { + if err := d.Set("capacity_provider_strategy", flattenCapacityProviderStrategy(taskSet.CapacityProviderStrategy)); err != nil { return fmt.Errorf("error setting capacity_provider_strategy: %w", err) } @@ -430,7 +435,7 @@ func resourceTaskSetRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting load_balancer: %w", err) } - if err := d.Set("network_configuration", flattenEcsNetworkConfiguration(taskSet.NetworkConfiguration)); err != nil { + if err := d.Set("network_configuration", flattenNetworkConfiguration(taskSet.NetworkConfiguration)); err != nil { return fmt.Errorf("error setting network_configuration: %w", err) } @@ -444,6 +449,12 @@ func resourceTaskSetRead(d *schema.ResourceData, meta interface{}) error { tags := KeyValueTags(taskSet.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to list tags for ECS Task Set %s: %s", d.Id(), err) + return nil + } + //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) @@ -490,8 +501,16 @@ func resourceTaskSetUpdate(d *schema.ResourceData, meta interface{}) error { if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating ECS TaskSet (%s) tags: %w", d.Id(), err) + err := UpdateTags(conn, d.Get("arn").(string), o, n) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if tfawserr.ErrCodeContains(err, ecs.ErrCodeAccessDeniedException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeInvalidParameterException) || tfawserr.ErrCodeContains(err, ecs.ErrCodeUnsupportedFeatureException) { + log.Printf("[WARN] Unable to update tags for ECS Task Set %s: %s", d.Id(), err) + return resourceTaskSetRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error updating ECS Task Set (%s) tags: %w", d.Id(), err) } } @@ -543,3 +562,26 @@ func TaskSetParseID(id string) (string, string, string, error) { return parts[0], parts[1], parts[2], nil } + +func retryTaskSetCreate(conn *ecs.ECS, input *ecs.CreateTaskSetInput) (*ecs.CreateTaskSetOutput, error) { + outputRaw, err := tfresource.RetryWhen( + tfiam.PropagationTimeout+taskSetCreateTimeout, + func() (interface{}, error) { + return conn.CreateTaskSet(input) + }, + func(err error) (bool, error) { + if tfawserr.ErrCodeEquals(err, ecs.ErrCodeClusterNotFoundException, ecs.ErrCodeServiceNotFoundException, ecs.ErrCodeTaskSetNotFoundException) || + tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "does not have an associated load balancer") { + return true, err + } + return false, err + }, + ) + + output, ok := outputRaw.(*ecs.CreateTaskSetOutput) + if !ok || output == nil || output.TaskSet == nil { + return nil, fmt.Errorf("error creating ECS TaskSet: empty output") + } + + return output, err +}