diff --git a/.changelog/28052.txt b/.changelog/28052.txt new file mode 100644 index 000000000000..a88ead375b46 --- /dev/null +++ b/.changelog/28052.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_ecs_cluster: Add `service_connect_defaults` argument +``` + +```release-note:enhancement +resource/aws_ecs_service: Add `service_connect_configuration` argument in support of [ECS Service Connect](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-connect.html) +``` + +```release-note:enhancement +data-source/aws_ecs_cluster: Add `service_connect_defaults` attribute +``` \ No newline at end of file diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index d1ff1474800c..4f67c24b2339 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -21,10 +22,11 @@ import ( func ResourceCluster() *schema.Resource { return &schema.Resource{ - Create: resourceClusterCreate, - Read: resourceClusterRead, - Update: resourceClusterUpdate, - Delete: resourceClusterDelete, + Create: resourceClusterCreate, + Read: resourceClusterRead, + UpdateWithoutTimeout: resourceClusterUpdate, + Delete: resourceClusterDelete, + Importer: &schema.ResourceImporter{ State: resourceClusterImport, }, @@ -32,12 +34,6 @@ func ResourceCluster() *schema.Resource { CustomizeDiff: verify.SetTagsDiff, Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateClusterName, - }, "arn": { Type: schema.TypeString, Computed: true, @@ -81,14 +77,14 @@ func ResourceCluster() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "s3_bucket_name": { - Type: schema.TypeString, - Optional: true, - }, "s3_bucket_encryption_enabled": { Type: schema.TypeBool, Optional: true, }, + "s3_bucket_name": { + Type: schema.TypeString, + Optional: true, + }, "s3_key_prefix": { Type: schema.TypeString, Optional: true, @@ -119,12 +115,10 @@ func ResourceCluster() *schema.Resource { Optional: true, ValidateFunc: validation.IntBetween(0, 100000), }, - "capacity_provider": { Type: schema.TypeString, Required: true, }, - "weight": { Type: schema.TypeInt, Optional: true, @@ -133,6 +127,26 @@ func ResourceCluster() *schema.Resource { }, }, }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateClusterName, + }, + "service_connect_defaults": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "namespace": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, "setting": { Type: schema.TypeSet, Optional: true, @@ -186,6 +200,10 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error { input.CapacityProviders = flex.ExpandStringSet(v.(*schema.Set)) } + if v, ok := d.GetOk("service_connect_defaults"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.ServiceConnectDefaults = expandClusterServiceConnectDefaultsRequest(v.([]interface{})[0].(map[string]interface{})) + } + if v, ok := d.GetOk("setting"); ok { input.Settings = expandClusterSettings(v.(*schema.Set)) } @@ -292,6 +310,14 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting default_capacity_provider_strategy: %w", err) } + if cluster.ServiceConnectDefaults != nil { + if err := d.Set("service_connect_defaults", []interface{}{flattenClusterServiceConnectDefaults(cluster.ServiceConnectDefaults)}); err != nil { + return fmt.Errorf("error setting service_connect_defaults: %w", err) + } + } else { + d.Set("service_connect_defaults", nil) + } + if err := d.Set("setting", flattenClusterSettings(cluster.Settings)); err != nil { return fmt.Errorf("error setting setting: %w", err) } @@ -316,11 +342,11 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error { return nil } -func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).ECSConn - if d.HasChanges("setting", "configuration") { - input := ecs.UpdateClusterInput{ + if d.HasChanges("setting", "configuration", "service_connect_defaults") { + input := &ecs.UpdateClusterInput{ Cluster: aws.String(d.Id()), } @@ -332,13 +358,18 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { input.Configuration = expandClusterConfiguration(v.([]interface{})) } - _, err := conn.UpdateCluster(&input) + if v, ok := d.GetOk("service_connect_defaults"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.ServiceConnectDefaults = expandClusterServiceConnectDefaultsRequest(v.([]interface{})[0].(map[string]interface{})) + } + + _, err := conn.UpdateClusterWithContext(ctx, input) + if err != nil { - return fmt.Errorf("error changing ECS cluster (%s): %w", d.Id(), err) + return diag.Errorf("updating ECS Cluster (%s): %s", d.Id(), err) } - if _, err := waitClusterAvailable(context.Background(), conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for ECS Cluster (%s) to become Available while updating setting and configuration: %w", d.Id(), err) + if _, err := waitClusterAvailable(ctx, conn, d.Id()); err != nil { + return diag.Errorf("waiting for ECS Cluster (%s) update: %s", d.Id(), err) } } @@ -349,21 +380,21 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { DefaultCapacityProviderStrategy: expandCapacityProviderStrategy(d.Get("default_capacity_provider_strategy").(*schema.Set)), } - err := retryClusterCapacityProvidersPut(context.Background(), conn, &input) + err := retryClusterCapacityProvidersPut(ctx, conn, &input) if err != nil { - return fmt.Errorf("error changing ECS cluster capacity provider settings (%s): %w", d.Id(), err) + return diag.Errorf("updating ECS Cluster (%s) capacity providers: %s", d.Id(), err) } - if _, err := waitClusterAvailable(context.Background(), conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for ECS Cluster (%s) to become Available while updating capacity_providers, default_capacity_provider_strategy: %w", d.Id(), err) + if _, err := waitClusterAvailable(ctx, conn, d.Id()); err != nil { + return diag.Errorf("waiting for ECS Cluster (%s) capacity providers update: %s", d.Id(), err) } } if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - err := UpdateTags(conn, d.Id(), o, n) + err := UpdateTagsWithContext(ctx, conn, d.Id(), o, n) // Some partitions (i.e., ISO) may not support tagging, giving error if verify.ErrorISOUnsupported(conn.PartitionID, err) { @@ -372,7 +403,7 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { } if err != nil { - return fmt.Errorf("ECS tagging failed updating tags for Cluster (%s): %w", d.Id(), err) + return diag.Errorf("updating ECS Cluster (%s) tags: %s", d.Id(), err) } } @@ -473,6 +504,34 @@ func expandClusterSettings(configured *schema.Set) []*ecs.ClusterSetting { return settings } +func expandClusterServiceConnectDefaultsRequest(tfMap map[string]interface{}) *ecs.ClusterServiceConnectDefaultsRequest { + if tfMap == nil { + return nil + } + + apiObject := &ecs.ClusterServiceConnectDefaultsRequest{} + + if v, ok := tfMap["namespace"].(string); ok && v != "" { + apiObject.Namespace = aws.String(v) + } + + return apiObject +} + +func flattenClusterServiceConnectDefaults(apiObject *ecs.ClusterServiceConnectDefaults) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Namespace; v != nil { + tfMap["namespace"] = aws.StringValue(v) + } + + return tfMap +} + func flattenClusterSettings(list []*ecs.ClusterSetting) []map[string]interface{} { if len(list) == 0 { return nil diff --git a/internal/service/ecs/cluster_data_source.go b/internal/service/ecs/cluster_data_source.go index e05360e4cd7d..c258ad8bc4c2 100644 --- a/internal/service/ecs/cluster_data_source.go +++ b/internal/service/ecs/cluster_data_source.go @@ -2,48 +2,50 @@ package ecs import ( "context" - "fmt" "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" ) func DataSourceCluster() *schema.Resource { return &schema.Resource{ - Read: dataSourceClusterRead, + ReadWithoutTimeout: dataSourceClusterRead, Schema: map[string]*schema.Schema{ - "cluster_name": { - Type: schema.TypeString, - Required: true, - }, - "arn": { Type: schema.TypeString, Computed: true, }, - - "status": { + "cluster_name": { Type: schema.TypeString, - Computed: true, + Required: true, }, - "pending_tasks_count": { Type: schema.TypeInt, Computed: true, }, - - "running_tasks_count": { + "registered_container_instances_count": { Type: schema.TypeInt, Computed: true, }, - - "registered_container_instances_count": { + "running_tasks_count": { Type: schema.TypeInt, Computed: true, }, - + "service_connect_defaults": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "namespace": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "setting": { Type: schema.TypeSet, Computed: true, @@ -60,29 +62,41 @@ func DataSourceCluster() *schema.Resource { }, }, }, + "status": { + Type: schema.TypeString, + Computed: true, + }, }, } } -func dataSourceClusterRead(d *schema.ResourceData, meta interface{}) error { +func dataSourceClusterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).ECSConn clusterName := d.Get("cluster_name").(string) - cluster, err := FindClusterByNameOrARN(context.Background(), conn, d.Get("cluster_name").(string)) + cluster, err := FindClusterByNameOrARN(ctx, conn, d.Get("cluster_name").(string)) if err != nil { - return fmt.Errorf("error reading ECS Cluster (%s): %w", clusterName, err) + return diag.Errorf("reading ECS Cluster (%s): %s", clusterName, err) } d.SetId(aws.StringValue(cluster.ClusterArn)) d.Set("arn", cluster.ClusterArn) - d.Set("status", cluster.Status) d.Set("pending_tasks_count", cluster.PendingTasksCount) d.Set("running_tasks_count", cluster.RunningTasksCount) d.Set("registered_container_instances_count", cluster.RegisteredContainerInstancesCount) + d.Set("status", cluster.Status) + + if cluster.ServiceConnectDefaults != nil { + if err := d.Set("service_connect_defaults", []interface{}{flattenClusterServiceConnectDefaults(cluster.ServiceConnectDefaults)}); err != nil { + return diag.Errorf("setting service_connect_defaults: %s", err) + } + } else { + d.Set("service_connect_defaults", nil) + } if err := d.Set("setting", flattenClusterSettings(cluster.Settings)); err != nil { - return fmt.Errorf("error setting setting: %w", err) + return diag.Errorf("setting setting: %s", err) } return nil diff --git a/internal/service/ecs/cluster_data_source_test.go b/internal/service/ecs/cluster_data_source_test.go index 080b20990790..5e9f365690bf 100644 --- a/internal/service/ecs/cluster_data_source_test.go +++ b/internal/service/ecs/cluster_data_source_test.go @@ -27,6 +27,7 @@ func TestAccECSClusterDataSource_ecsCluster(t *testing.T) { resource.TestCheckResourceAttr(dataSourceName, "pending_tasks_count", "0"), resource.TestCheckResourceAttr(dataSourceName, "registered_container_instances_count", "0"), resource.TestCheckResourceAttr(dataSourceName, "running_tasks_count", "0"), + resource.TestCheckResourceAttrPair(dataSourceName, "service_connect_defaults.#", resourceName, "service_connect_defaults.#"), resource.TestCheckResourceAttr(dataSourceName, "status", "ACTIVE"), ), }, diff --git a/internal/service/ecs/cluster_test.go b/internal/service/ecs/cluster_test.go index 7114bc0f81eb..0597c8767cbf 100644 --- a/internal/service/ecs/cluster_test.go +++ b/internal/service/ecs/cluster_test.go @@ -16,7 +16,7 @@ import ( ) func TestAccECSCluster_basic(t *testing.T) { - var cluster1 ecs.Cluster + var v ecs.Cluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_cluster.test" @@ -28,10 +28,19 @@ func TestAccECSCluster_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccClusterConfig_basic(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckClusterExists(resourceName, &v), acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "ecs", fmt.Sprintf("cluster/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "capacity_providers.#", "0"), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + resource.TestCheckResourceAttr(resourceName, "default_capacity_provider_strategy.#", "0"), resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "service_connect_defaults.#", "0"), + resource.TestCheckResourceAttr(resourceName, "setting.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "setting.*", map[string]string{ + "name": "containerInsights", + "value": "disabled", + }), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, @@ -46,7 +55,7 @@ func TestAccECSCluster_basic(t *testing.T) { } func TestAccECSCluster_disappears(t *testing.T) { - var cluster1 ecs.Cluster + var v ecs.Cluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_cluster.test" @@ -59,7 +68,7 @@ func TestAccECSCluster_disappears(t *testing.T) { { Config: testAccClusterConfig_basic(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), + testAccCheckClusterExists(resourceName, &v), acctest.CheckResourceDisappears(acctest.Provider, tfecs.ResourceCluster(), resourceName), ), ExpectNonEmptyPlan: true, @@ -69,7 +78,7 @@ func TestAccECSCluster_disappears(t *testing.T) { } func TestAccECSCluster_tags(t *testing.T) { - var cluster1 ecs.Cluster + var v ecs.Cluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_cluster.test" @@ -82,7 +91,7 @@ func TestAccECSCluster_tags(t *testing.T) { { Config: testAccClusterConfig_tags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), + testAccCheckClusterExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), @@ -96,7 +105,7 @@ func TestAccECSCluster_tags(t *testing.T) { { Config: testAccClusterConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), + testAccCheckClusterExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), @@ -105,7 +114,7 @@ func TestAccECSCluster_tags(t *testing.T) { { Config: testAccClusterConfig_tags1(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), + testAccCheckClusterExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), @@ -114,10 +123,49 @@ func TestAccECSCluster_tags(t *testing.T) { }) } +func TestAccECSCluster_serviceConnectDefaults(t *testing.T) { + var v ecs.Cluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + ns := fmt.Sprintf("%s-%s", acctest.ResourcePrefix, sdkacctest.RandStringFromCharSet(8, sdkacctest.CharSetAlpha)) + resourceName := "aws_ecs_cluster.test" + namespace1ResourceName := "aws_service_discovery_http_namespace.test.0" + namespace2ResourceName := "aws_service_discovery_http_namespace.test.1" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_serviceConnectDefaults(rName, ns, 0), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "service_connect_defaults.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "service_connect_defaults.0.namespace", namespace1ResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportStateId: rName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccClusterConfig_serviceConnectDefaults(rName, ns, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "service_connect_defaults.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "service_connect_defaults.0.namespace", namespace2ResourceName, "arn"), + ), + }, + }, + }) +} + func TestAccECSCluster_singleCapacityProvider(t *testing.T) { var cluster1 ecs.Cluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - providerName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ecs_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -127,7 +175,7 @@ func TestAccECSCluster_singleCapacityProvider(t *testing.T) { CheckDestroy: testAccCheckClusterDestroy, Steps: []resource.TestStep{ { - Config: testAccClusterConfig_singleCapacityProvider(rName, providerName), + Config: testAccClusterConfig_singleCapacityProvider(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClusterExists(resourceName, &cluster1), ), @@ -386,7 +434,7 @@ func testAccCheckClusterExists(resourceName string, cluster *ecs.Cluster) resour func testAccClusterConfig_basic(rName string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { - name = %q + name = %[1]q } `, rName) } @@ -394,29 +442,60 @@ resource "aws_ecs_cluster" "test" { func testAccClusterConfig_tags1(rName, tag1Key, tag1Value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { - name = %q + name = %[1]q tags = { - %q = %q + %[2]q = %[3]q } } `, rName, tag1Key, tag1Value) } -func testAccClusterCapacityProviderConfig(rName string) string { - return testAccCapacityProviderBaseConfig(rName) + fmt.Sprintf(` +func testAccClusterConfig_tags2(rName, tag1Key, tag1Value, tag2Key, tag2Value string) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tag1Key, tag1Value, tag2Key, tag2Value) +} + +func testAccClusterConfig_serviceConnectDefaults(rName, ns string, idx int) string { + return fmt.Sprintf(` +resource "aws_service_discovery_http_namespace" "test" { + count = 2 + + name = "%[2]s-${count.index}" +} + +resource "aws_ecs_cluster" "test" { + name = %[1]q + + service_connect_defaults { + namespace = aws_service_discovery_http_namespace.test[%[3]d].arn + } +} +`, rName, ns, idx) +} + +func testAccClusterCapacityProviderConfig_base(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderBaseConfig(rName), fmt.Sprintf(` resource "aws_ecs_capacity_provider" "test" { - name = %q + name = %[1]q auto_scaling_group_provider { auto_scaling_group_arn = aws_autoscaling_group.test.arn } } -`, rName) +`, rName)) } -func testAccClusterConfig_singleCapacityProvider(rName, providerName string) string { - return testAccClusterCapacityProviderConfig(providerName) + fmt.Sprintf(` +func testAccClusterConfig_singleCapacityProvider(rName string) string { + return acctest.ConfigCompose(testAccClusterCapacityProviderConfig_base(rName), fmt.Sprintf(` resource "aws_ecs_cluster" "test" { name = %[1]q @@ -428,7 +507,7 @@ resource "aws_ecs_cluster" "test" { weight = 1 } } -`, rName) +`, rName)) } func testAccClusterConfig_capacityProviders(rName string) string { @@ -541,23 +620,11 @@ resource "aws_ecs_cluster" "test" { `, rName) } -func testAccClusterConfig_tags2(rName, tag1Key, tag1Value, tag2Key, tag2Value string) string { - return fmt.Sprintf(` -resource "aws_ecs_cluster" "test" { - name = %q - - tags = { - %q = %q - %q = %q - } -} -`, rName, tag1Key, tag1Value, tag2Key, tag2Value) -} - func testAccClusterConfig_containerInsights(rName, value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { name = %[1]q + setting { name = "containerInsights" value = %[2]q diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 6bf22595cee8..dadfbb4033fc 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -32,6 +32,7 @@ func ResourceService() *schema.Resource { Read: resourceServiceRead, Update: resourceServiceUpdate, Delete: resourceServiceDelete, + Importer: &schema.ResourceImporter{ State: resourceServiceImport, }, @@ -72,16 +73,10 @@ func ResourceService() *schema.Resource { ForceNew: true, }, "deployment_circuit_breaker": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - // Ignore missing configuration block - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if old == "1" && new == "0" { - return true - } - return false - }, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "enable": { @@ -96,16 +91,10 @@ func ResourceService() *schema.Resource { }, }, "deployment_controller": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - // Ignore missing configuration block - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if old == "1" && new == "0" { - return true - } - return false - }, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { @@ -226,13 +215,11 @@ func ResourceService() *schema.Resource { Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "subnets": { Type: schema.TypeSet, Required: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, }, }, @@ -306,6 +293,98 @@ func ResourceService() *schema.Resource { Default: ecs.SchedulingStrategyReplica, ValidateFunc: validation.StringInSlice(ecs.SchedulingStrategy_Values(), false), }, + "service_connect_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + "log_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_driver": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(ecs.LogDriver_Values(), false), + }, + "options": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "secret_option": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value_from": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + }, + "service": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_alias": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dns_name": { + Type: schema.TypeString, + Optional: true, + }, + "port": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), + }, + }, + }, + }, + "discovery_name": { + Type: schema.TypeString, + Optional: true, + }, + "ingress_port_override": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 65535), + }, + "port_name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, "service_registries": { Type: schema.TypeList, Optional: true, @@ -363,80 +442,6 @@ func ResourceService() *schema.Resource { } } -func triggersCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { - // clears diff to avoid extraneous diffs but lets it pass for triggering update - fnd := false - if v, ok := d.GetOk("force_new_deployment"); ok { - fnd = v.(bool) - } - - if d.HasChange("triggers") && !fnd { - return d.Clear("triggers") - } - - if d.HasChange("triggers") && fnd { - o, n := d.GetChange("triggers") - if len(o.(map[string]interface{})) > 0 && len(n.(map[string]interface{})) == 0 { - return d.Clear("triggers") - } - - return nil - } - - return nil -} - -func capacityProviderStrategyCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { - // to be backward compatible, should ForceNew almost always (previous behavior), unless: - // force_new_deployment is true and - // neither the old set nor new set is 0 length - if v := d.Get("force_new_deployment").(bool); !v { - return capacityProviderStrategyForceNew(d) - } - - old, new := d.GetChange("capacity_provider_strategy") - - ol := old.(*schema.Set).Len() - nl := new.(*schema.Set).Len() - - if (ol == 0 && nl > 0) || (ol > 0 && nl == 0) { - return capacityProviderStrategyForceNew(d) - } - - return nil -} - -func capacityProviderStrategyForceNew(d *schema.ResourceDiff) error { - for _, key := range d.GetChangedKeysPrefix("capacity_provider_strategy") { - if d.HasChange(key) { - if err := d.ForceNew(key); err != nil { - return fmt.Errorf("while attempting to force a new ECS service for capacity_provider_strategy: %w", err) - } - } - } - return nil -} - -func resourceServiceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - if len(strings.Split(d.Id(), "/")) != 2 { - return []*schema.ResourceData{}, fmt.Errorf("wrong format of resource: %s, expecting 'cluster-name/service-name'", d.Id()) - } - cluster := strings.Split(d.Id(), "/")[0] - name := strings.Split(d.Id(), "/")[1] - log.Printf("[DEBUG] Importing ECS service %s from cluster %s", name, cluster) - - d.SetId(name) - clusterArn := arn.ARN{ - Partition: meta.(*conns.AWSClient).Partition, - Region: meta.(*conns.AWSClient).Region, - Service: "ecs", - AccountID: meta.(*conns.AWSClient).AccountID, - Resource: fmt.Sprintf("cluster/%s", cluster), - }.String() - d.Set("cluster", clusterArn) - return []*schema.ResourceData{d}, nil -} - func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ECSConn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig @@ -537,6 +542,10 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { input.PlacementConstraints = pc } + if v, ok := d.GetOk("service_connect_configuration"); ok && len(v.([]interface{})) > 0 { + input.ServiceConnectConfiguration = expandServiceConnectConfiguration(v.([]interface{})) + } + serviceRegistries := d.Get("service_registries").([]interface{}) if len(serviceRegistries) > 0 { srs := make([]*ecs.ServiceRegistry, 0, len(serviceRegistries)) @@ -731,6 +740,10 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting network_configuration for (%s): %w", d.Id(), err) } + // if err := d.Set("service_connect_configuration", flattenServiceConnectConfiguration(service.ServiceConnectConfiguration)); err != nil { + // return fmt.Errorf("error setting service_connect_configuration for (%s): %w", d.Id(), err) + // } + if err := d.Set("service_registries", flattenServiceRegistries(service.ServiceRegistries)); err != nil { return fmt.Errorf("error setting service_registries for (%s): %w", d.Id(), err) } @@ -749,327 +762,101 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { return nil } -func expandDeploymentController(l []interface{}) *ecs.DeploymentController { - if len(l) == 0 || l[0] == nil { - return nil - } - - m := l[0].(map[string]interface{}) +func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ECSConn - deploymentController := &ecs.DeploymentController{ - Type: aws.String(m["type"].(string)), - } + if d.HasChangesExcept("tags", "tags_all") { + input := &ecs.UpdateServiceInput{ + Cluster: aws.String(d.Get("cluster").(string)), + ForceNewDeployment: aws.Bool(d.Get("force_new_deployment").(bool)), + Service: aws.String(d.Id()), + } - return deploymentController -} + schedulingStrategy := d.Get("scheduling_strategy").(string) -func flattenDeploymentController(deploymentController *ecs.DeploymentController) []interface{} { - m := map[string]interface{}{ - "type": ecs.DeploymentControllerTypeEcs, - } + if schedulingStrategy == ecs.SchedulingStrategyDaemon { + if d.HasChange("deployment_minimum_healthy_percent") { + input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ + MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), + } + } + } else if schedulingStrategy == ecs.SchedulingStrategyReplica { + if d.HasChange("desired_count") { + input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int))) + } - if deploymentController == nil { - return []interface{}{m} - } + if d.HasChanges("deployment_maximum_percent", "deployment_minimum_healthy_percent") { + input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ + MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))), + MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), + } + } + } - m["type"] = aws.StringValue(deploymentController.Type) + if d.HasChange("deployment_circuit_breaker") { + if input.DeploymentConfiguration == nil { + input.DeploymentConfiguration = &ecs.DeploymentConfiguration{} + } - return []interface{}{m} -} + // To remove an existing deployment circuit breaker, specify an empty object. + input.DeploymentConfiguration.DeploymentCircuitBreaker = &ecs.DeploymentCircuitBreaker{} -func expandDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.DeploymentCircuitBreaker { - if tfMap == nil { - return nil - } + if v, ok := d.GetOk("deployment_circuit_breaker"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentConfiguration.DeploymentCircuitBreaker = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) + } + } - apiObject := &ecs.DeploymentCircuitBreaker{} + if d.HasChange("ordered_placement_strategy") { + // Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementStrategy + // To remove an existing placement strategy, specify an empty object. + input.PlacementStrategy = []*ecs.PlacementStrategy{} - apiObject.Enable = aws.Bool(tfMap["enable"].(bool)) - apiObject.Rollback = aws.Bool(tfMap["rollback"].(bool)) + if v, ok := d.GetOk("ordered_placement_strategy"); ok && len(v.([]interface{})) > 0 { + ps, err := expandPlacementStrategy(v.([]interface{})) - return apiObject -} + if err != nil { + return err + } -func flattenDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) map[string]interface{} { - if apiObject == nil { - return nil - } + input.PlacementStrategy = ps + } + } - tfMap := map[string]interface{}{} + if d.HasChange("placement_constraints") { + // Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementConstraints + // To remove all existing placement constraints, specify an empty array. + input.PlacementConstraints = []*ecs.PlacementConstraint{} - tfMap["enable"] = aws.BoolValue(apiObject.Enable) - tfMap["rollback"] = aws.BoolValue(apiObject.Rollback) + if v, ok := d.Get("placement_constraints").(*schema.Set); ok && v.Len() > 0 { + pc, err := expandPlacementConstraints(v.List()) - return tfMap -} + if err != nil { + return err + } -func flattenNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} { - if nc == nil { - return nil - } + input.PlacementConstraints = pc + } + } - result := make(map[string]interface{}) - result["security_groups"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.SecurityGroups) - result["subnets"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.Subnets) + if d.HasChange("platform_version") { + input.PlatformVersion = aws.String(d.Get("platform_version").(string)) + } - if nc.AwsvpcConfiguration.AssignPublicIp != nil { - result["assign_public_ip"] = aws.StringValue(nc.AwsvpcConfiguration.AssignPublicIp) == ecs.AssignPublicIpEnabled - } + if d.HasChange("health_check_grace_period_seconds") { + input.HealthCheckGracePeriodSeconds = aws.Int64(int64(d.Get("health_check_grace_period_seconds").(int))) + } - return []interface{}{result} -} + if d.HasChange("task_definition") { + input.TaskDefinition = aws.String(d.Get("task_definition").(string)) + } -func expandNetworkConfiguration(nc []interface{}) *ecs.NetworkConfiguration { - if len(nc) == 0 { - return nil - } - awsVpcConfig := &ecs.AwsVpcConfiguration{} - raw := nc[0].(map[string]interface{}) - if val, ok := raw["security_groups"]; ok { - awsVpcConfig.SecurityGroups = flex.ExpandStringSet(val.(*schema.Set)) - } - awsVpcConfig.Subnets = flex.ExpandStringSet(raw["subnets"].(*schema.Set)) - if val, ok := raw["assign_public_ip"].(bool); ok { - awsVpcConfig.AssignPublicIp = aws.String(ecs.AssignPublicIpDisabled) - if val { - awsVpcConfig.AssignPublicIp = aws.String(ecs.AssignPublicIpEnabled) + if d.HasChange("network_configuration") { + input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{})) } - } - return &ecs.NetworkConfiguration{AwsvpcConfiguration: awsVpcConfig} -} - -func expandPlacementConstraints(tfList []interface{}) ([]*ecs.PlacementConstraint, error) { - if len(tfList) == 0 { - return nil, nil - } - - var result []*ecs.PlacementConstraint - - for _, tfMapRaw := range tfList { - if tfMapRaw == nil { - continue - } - - tfMap := tfMapRaw.(map[string]interface{}) - - apiObject := &ecs.PlacementConstraint{} - - if v, ok := tfMap["expression"].(string); ok && v != "" { - apiObject.Expression = aws.String(v) - } - - if v, ok := tfMap["type"].(string); ok && v != "" { - apiObject.Type = aws.String(v) - } - - if err := validPlacementConstraint(aws.StringValue(apiObject.Type), aws.StringValue(apiObject.Expression)); err != nil { - return result, err - } - - result = append(result, apiObject) - } - - return result, nil -} - -func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} { - if len(pcs) == 0 { - return nil - } - results := make([]map[string]interface{}, 0) - for _, pc := range pcs { - c := make(map[string]interface{}) - c["type"] = aws.StringValue(pc.Type) - if pc.Expression != nil { - c["expression"] = aws.StringValue(pc.Expression) - } - - results = append(results, c) - } - return results -} - -func expandPlacementStrategy(s []interface{}) ([]*ecs.PlacementStrategy, error) { - if len(s) == 0 { - return nil, nil - } - pss := make([]*ecs.PlacementStrategy, 0) - for _, raw := range s { - p, ok := raw.(map[string]interface{}) - - if !ok { - continue - } - - t, ok := p["type"].(string) - - if !ok { - return nil, fmt.Errorf("missing type attribute in placement strategy configuration block") - } - - f, ok := p["field"].(string) - - if !ok { - return nil, fmt.Errorf("missing field attribute in placement strategy configuration block") - } - - if err := validPlacementStrategy(t, f); err != nil { - return nil, err - } - ps := &ecs.PlacementStrategy{ - Type: aws.String(t), - } - if f != "" { - // Field must be omitted (i.e. not empty string) for random strategy - ps.Field = aws.String(f) - } - pss = append(pss, ps) - } - return pss, nil -} - -func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { - if len(pss) == 0 { - return nil - } - results := make([]interface{}, 0, len(pss)) - for _, ps := range pss { - c := make(map[string]interface{}) - c["type"] = aws.StringValue(ps.Type) - - if ps.Field != nil { - c["field"] = aws.StringValue(ps.Field) - - // for some fields the API requires lowercase for creation but will return uppercase on query - if aws.StringValue(ps.Field) == "MEMORY" || aws.StringValue(ps.Field) == "CPU" { - c["field"] = strings.ToLower(aws.StringValue(ps.Field)) - } - } - - results = append(results, c) - } - return results -} - -func flattenServiceRegistries(srs []*ecs.ServiceRegistry) []map[string]interface{} { - if len(srs) == 0 { - return nil - } - results := make([]map[string]interface{}, 0) - for _, sr := range srs { - c := map[string]interface{}{ - "registry_arn": aws.StringValue(sr.RegistryArn), - } - if sr.Port != nil { - c["port"] = int(aws.Int64Value(sr.Port)) - } - if sr.ContainerPort != nil { - c["container_port"] = int(aws.Int64Value(sr.ContainerPort)) - } - if sr.ContainerName != nil { - c["container_name"] = aws.StringValue(sr.ContainerName) - } - results = append(results, c) - } - return results -} - -func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).ECSConn - - if d.HasChangesExcept("tags", "tags_all") { - input := &ecs.UpdateServiceInput{ - Cluster: aws.String(d.Get("cluster").(string)), - ForceNewDeployment: aws.Bool(d.Get("force_new_deployment").(bool)), - Service: aws.String(d.Id()), - } - - schedulingStrategy := d.Get("scheduling_strategy").(string) - - if schedulingStrategy == ecs.SchedulingStrategyDaemon { - if d.HasChange("deployment_minimum_healthy_percent") { - input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ - MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), - } - } - } else if schedulingStrategy == ecs.SchedulingStrategyReplica { - if d.HasChange("desired_count") { - input.DesiredCount = aws.Int64(int64(d.Get("desired_count").(int))) - } - - if d.HasChanges("deployment_maximum_percent", "deployment_minimum_healthy_percent") { - input.DeploymentConfiguration = &ecs.DeploymentConfiguration{ - MaximumPercent: aws.Int64(int64(d.Get("deployment_maximum_percent").(int))), - MinimumHealthyPercent: aws.Int64(int64(d.Get("deployment_minimum_healthy_percent").(int))), - } - } - } - - if d.HasChange("deployment_circuit_breaker") { - if input.DeploymentConfiguration == nil { - input.DeploymentConfiguration = &ecs.DeploymentConfiguration{} - } - - // To remove an existing deployment circuit breaker, specify an empty object. - 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 = expandDeploymentCircuitBreaker(v.([]interface{})[0].(map[string]interface{})) - } - } - - if d.HasChange("ordered_placement_strategy") { - // Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementStrategy - // To remove an existing placement strategy, specify an empty object. - input.PlacementStrategy = []*ecs.PlacementStrategy{} - - if v, ok := d.GetOk("ordered_placement_strategy"); ok && len(v.([]interface{})) > 0 { - ps, err := expandPlacementStrategy(v.([]interface{})) - - if err != nil { - return err - } - - input.PlacementStrategy = ps - } - } - - if d.HasChange("placement_constraints") { - // Reference: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html#ECS-UpdateService-request-placementConstraints - // To remove all existing placement constraints, specify an empty array. - input.PlacementConstraints = []*ecs.PlacementConstraint{} - - if v, ok := d.Get("placement_constraints").(*schema.Set); ok && v.Len() > 0 { - pc, err := expandPlacementConstraints(v.List()) - - if err != nil { - return err - } - - input.PlacementConstraints = pc - } - } - - if d.HasChange("platform_version") { - input.PlatformVersion = aws.String(d.Get("platform_version").(string)) - } - - if d.HasChange("health_check_grace_period_seconds") { - input.HealthCheckGracePeriodSeconds = aws.Int64(int64(d.Get("health_check_grace_period_seconds").(int))) - } - - if d.HasChange("task_definition") { - input.TaskDefinition = aws.String(d.Get("task_definition").(string)) - } - - if d.HasChange("network_configuration") { - input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{})) - } - - if d.HasChange("capacity_provider_strategy") { - input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) - } + if d.HasChange("capacity_provider_strategy") { + input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) + } if d.HasChange("enable_execute_command") { input.EnableExecuteCommand = aws.Bool(d.Get("enable_execute_command").(bool)) @@ -1089,6 +876,10 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { input.PropagateTags = aws.String(d.Get("propagate_tags").(string)) } + if d.HasChange("service_connect_configuration") { + input.ServiceConnectConfiguration = expandServiceConnectConfiguration(d.Get("service_connect_configuration").([]interface{})) + } + if d.HasChange("service_registries") { input.ServiceRegistries = expandServiceRegistries(d.Get("service_registries").([]interface{})) } @@ -1217,6 +1008,436 @@ func resourceServiceDelete(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceServiceImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if len(strings.Split(d.Id(), "/")) != 2 { + return []*schema.ResourceData{}, fmt.Errorf("wrong format of resource: %s, expecting 'cluster-name/service-name'", d.Id()) + } + cluster := strings.Split(d.Id(), "/")[0] + name := strings.Split(d.Id(), "/")[1] + log.Printf("[DEBUG] Importing ECS service %s from cluster %s", name, cluster) + + d.SetId(name) + clusterArn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Region: meta.(*conns.AWSClient).Region, + Service: "ecs", + AccountID: meta.(*conns.AWSClient).AccountID, + Resource: fmt.Sprintf("cluster/%s", cluster), + }.String() + d.Set("cluster", clusterArn) + return []*schema.ResourceData{d}, nil +} + +func triggersCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { + // clears diff to avoid extraneous diffs but lets it pass for triggering update + fnd := false + if v, ok := d.GetOk("force_new_deployment"); ok { + fnd = v.(bool) + } + + if d.HasChange("triggers") && !fnd { + return d.Clear("triggers") + } + + if d.HasChange("triggers") && fnd { + o, n := d.GetChange("triggers") + if len(o.(map[string]interface{})) > 0 && len(n.(map[string]interface{})) == 0 { + return d.Clear("triggers") + } + + return nil + } + + return nil +} + +func capacityProviderStrategyCustomizeDiff(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { + // to be backward compatible, should ForceNew almost always (previous behavior), unless: + // force_new_deployment is true and + // neither the old set nor new set is 0 length + if v := d.Get("force_new_deployment").(bool); !v { + return capacityProviderStrategyForceNew(d) + } + + old, new := d.GetChange("capacity_provider_strategy") + + ol := old.(*schema.Set).Len() + nl := new.(*schema.Set).Len() + + if (ol == 0 && nl > 0) || (ol > 0 && nl == 0) { + return capacityProviderStrategyForceNew(d) + } + + return nil +} + +func capacityProviderStrategyForceNew(d *schema.ResourceDiff) error { + for _, key := range d.GetChangedKeysPrefix("capacity_provider_strategy") { + if d.HasChange(key) { + if err := d.ForceNew(key); err != nil { + return fmt.Errorf("while attempting to force a new ECS service for capacity_provider_strategy: %w", err) + } + } + } + return nil +} + +func expandDeploymentController(l []interface{}) *ecs.DeploymentController { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + deploymentController := &ecs.DeploymentController{ + Type: aws.String(m["type"].(string)), + } + + return deploymentController +} + +func flattenDeploymentController(deploymentController *ecs.DeploymentController) []interface{} { + m := map[string]interface{}{ + "type": ecs.DeploymentControllerTypeEcs, + } + + if deploymentController == nil { + return []interface{}{m} + } + + m["type"] = aws.StringValue(deploymentController.Type) + + return []interface{}{m} +} + +func expandDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.DeploymentCircuitBreaker { + if tfMap == nil { + return nil + } + + apiObject := &ecs.DeploymentCircuitBreaker{} + + apiObject.Enable = aws.Bool(tfMap["enable"].(bool)) + apiObject.Rollback = aws.Bool(tfMap["rollback"].(bool)) + + return apiObject +} + +func flattenDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + tfMap["enable"] = aws.BoolValue(apiObject.Enable) + tfMap["rollback"] = aws.BoolValue(apiObject.Rollback) + + return tfMap +} + +func flattenNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} { + if nc == nil { + return nil + } + + result := make(map[string]interface{}) + result["security_groups"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.SecurityGroups) + result["subnets"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.Subnets) + + if nc.AwsvpcConfiguration.AssignPublicIp != nil { + result["assign_public_ip"] = aws.StringValue(nc.AwsvpcConfiguration.AssignPublicIp) == ecs.AssignPublicIpEnabled + } + + return []interface{}{result} +} + +func expandNetworkConfiguration(nc []interface{}) *ecs.NetworkConfiguration { + if len(nc) == 0 { + return nil + } + awsVpcConfig := &ecs.AwsVpcConfiguration{} + raw := nc[0].(map[string]interface{}) + if val, ok := raw["security_groups"]; ok { + awsVpcConfig.SecurityGroups = flex.ExpandStringSet(val.(*schema.Set)) + } + awsVpcConfig.Subnets = flex.ExpandStringSet(raw["subnets"].(*schema.Set)) + if val, ok := raw["assign_public_ip"].(bool); ok { + awsVpcConfig.AssignPublicIp = aws.String(ecs.AssignPublicIpDisabled) + if val { + awsVpcConfig.AssignPublicIp = aws.String(ecs.AssignPublicIpEnabled) + } + } + + return &ecs.NetworkConfiguration{AwsvpcConfiguration: awsVpcConfig} +} + +func expandPlacementConstraints(tfList []interface{}) ([]*ecs.PlacementConstraint, error) { + if len(tfList) == 0 { + return nil, nil + } + + var result []*ecs.PlacementConstraint + + for _, tfMapRaw := range tfList { + if tfMapRaw == nil { + continue + } + + tfMap := tfMapRaw.(map[string]interface{}) + + apiObject := &ecs.PlacementConstraint{} + + if v, ok := tfMap["expression"].(string); ok && v != "" { + apiObject.Expression = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + apiObject.Type = aws.String(v) + } + + if err := validPlacementConstraint(aws.StringValue(apiObject.Type), aws.StringValue(apiObject.Expression)); err != nil { + return result, err + } + + result = append(result, apiObject) + } + + return result, nil +} + +func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} { + if len(pcs) == 0 { + return nil + } + results := make([]map[string]interface{}, 0) + for _, pc := range pcs { + c := make(map[string]interface{}) + c["type"] = aws.StringValue(pc.Type) + if pc.Expression != nil { + c["expression"] = aws.StringValue(pc.Expression) + } + + results = append(results, c) + } + return results +} + +func expandPlacementStrategy(s []interface{}) ([]*ecs.PlacementStrategy, error) { + if len(s) == 0 { + return nil, nil + } + pss := make([]*ecs.PlacementStrategy, 0) + for _, raw := range s { + p, ok := raw.(map[string]interface{}) + + if !ok { + continue + } + + t, ok := p["type"].(string) + + if !ok { + return nil, fmt.Errorf("missing type attribute in placement strategy configuration block") + } + + f, ok := p["field"].(string) + + if !ok { + return nil, fmt.Errorf("missing field attribute in placement strategy configuration block") + } + + if err := validPlacementStrategy(t, f); err != nil { + return nil, err + } + ps := &ecs.PlacementStrategy{ + Type: aws.String(t), + } + if f != "" { + // Field must be omitted (i.e. not empty string) for random strategy + ps.Field = aws.String(f) + } + pss = append(pss, ps) + } + return pss, nil +} + +func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { + if len(pss) == 0 { + return nil + } + results := make([]interface{}, 0, len(pss)) + for _, ps := range pss { + c := make(map[string]interface{}) + c["type"] = aws.StringValue(ps.Type) + + if ps.Field != nil { + c["field"] = aws.StringValue(ps.Field) + + // for some fields the API requires lowercase for creation but will return uppercase on query + if aws.StringValue(ps.Field) == "MEMORY" || aws.StringValue(ps.Field) == "CPU" { + c["field"] = strings.ToLower(aws.StringValue(ps.Field)) + } + } + + results = append(results, c) + } + return results +} + +func expandServiceConnectConfiguration(sc []interface{}) *ecs.ServiceConnectConfiguration { + if len(sc) == 0 { + return &ecs.ServiceConnectConfiguration{} + } + raw := sc[0].(map[string]interface{}) + + config := &ecs.ServiceConnectConfiguration{} + if v, ok := raw["enabled"].(bool); ok { + config.Enabled = aws.Bool(v) + } + + if v, ok := raw["log_configuration"].([]interface{}); ok && len(v) > 0 { + config.LogConfiguration = expandLogConfiguration(v) + } + + if v, ok := raw["namespace"].(string); ok && v != "" { + config.Namespace = aws.String(v) + } + + if v, ok := raw["service"].([]interface{}); ok && len(v) > 0 { + config.Services = expandServices(v) + } + + return config +} + +func expandLogConfiguration(lc []interface{}) *ecs.LogConfiguration { + if len(lc) == 0 { + return &ecs.LogConfiguration{} + } + raw := lc[0].(map[string]interface{}) + + config := &ecs.LogConfiguration{} + if v, ok := raw["log_driver"].(string); ok && v != "" { + config.LogDriver = aws.String(v) + } + if v, ok := raw["options"].(map[string]interface{}); ok && len(v) > 0 { + config.Options = flex.ExpandStringMap(v) + } + if v, ok := raw["secret_option"].([]interface{}); ok && len(v) > 0 { + config.SecretOptions = expandSecretOptions(v) + } + + return config +} + +func expandSecretOptions(sop []interface{}) []*ecs.Secret { + if len(sop) == 0 { + return nil + } + + var out []*ecs.Secret + for _, item := range sop { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + + var config ecs.Secret + if v, ok := raw["name"].(string); ok && v != "" { + config.Name = aws.String(v) + } + if v, ok := raw["value_from"].(string); ok && v != "" { + config.ValueFrom = aws.String(v) + } + + out = append(out, &config) + } + + return out +} + +func expandServices(srv []interface{}) []*ecs.ServiceConnectService { + if len(srv) == 0 { + return nil + } + + var out []*ecs.ServiceConnectService + for _, item := range srv { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + + var config ecs.ServiceConnectService + if v, ok := raw["client_alias"].([]interface{}); ok && len(v) > 0 { + config.ClientAliases = expandClientAliases(v) + } + if v, ok := raw["discovery_name"].(string); ok && v != "" { + config.DiscoveryName = aws.String(v) + } + if v, ok := raw["ingress_port_override"].(int); ok { + config.IngressPortOverride = aws.Int64(int64(v)) + } + if v, ok := raw["port_name"].(string); ok && v != "" { + config.PortName = aws.String(v) + } + + out = append(out, &config) + } + + return out +} + +func expandClientAliases(srv []interface{}) []*ecs.ServiceConnectClientAlias { + if len(srv) == 0 { + return nil + } + + var out []*ecs.ServiceConnectClientAlias + for _, item := range srv { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + + var config ecs.ServiceConnectClientAlias + if v, ok := raw["port"].(int); ok { + config.Port = aws.Int64(int64(v)) + } + if v, ok := raw["dns_name"].(string); ok && v != "" { + config.DnsName = aws.String(v) + } + + out = append(out, &config) + } + + return out +} + +func flattenServiceRegistries(srs []*ecs.ServiceRegistry) []map[string]interface{} { + if len(srs) == 0 { + return nil + } + results := make([]map[string]interface{}, 0) + for _, sr := range srs { + c := map[string]interface{}{ + "registry_arn": aws.StringValue(sr.RegistryArn), + } + if sr.Port != nil { + c["port"] = int(aws.Int64Value(sr.Port)) + } + if sr.ContainerPort != nil { + c["container_port"] = int(aws.Int64Value(sr.ContainerPort)) + } + if sr.ContainerName != nil { + c["container_name"] = aws.StringValue(sr.ContainerName) + } + results = append(results, c) + } + return results +} + func resourceLoadBalancerHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index 820cb8d8066f..5bb1fc707eee 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -1173,6 +1173,50 @@ func TestAccECSService_ServiceRegistries_changes(t *testing.T) { }) } +func TestAccECSService_ServiceConnect_basic(t *testing.T) { + var service ecs.Service + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ecs_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServiceConfig_serviceConnectBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service), + resource.TestCheckResourceAttr(resourceName, "service_connect_configuration.#", "1"), + ), + }, + }, + }) +} + +func TestAccECSService_ServiceConnect_full(t *testing.T) { + var service ecs.Service + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ecs_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccServiceConfig_serviceConnectAllAttributes(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service), + resource.TestCheckResourceAttr(resourceName, "service_connect_configuration.#", "1"), + ), + }, + }, + }) +} + func TestAccECSService_Tags_basic(t *testing.T) { var service ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -4353,3 +4397,122 @@ resource "aws_ecs_service" "test" { } `, rName, enable) } + +func testAccServiceConfig_serviceConnectBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_service_discovery_http_namespace" "test" { + name = %[1]q +} + +resource "aws_ecs_cluster" "test" { + name = %[1]q + + service_connect_defaults { + namespace = aws_service_discovery_http_namespace.test.arn + } +} + +resource "aws_ecs_task_definition" "test" { + family = %[1]q + network_mode = "bridge" + + container_definitions = <