From 131212736580b6b8d95db8857423560e8d875ce0 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Mon, 28 Nov 2022 18:51:12 +0000 Subject: [PATCH 01/26] initial Commit --- .changelog/28043.txt | 11 + internal/service/ecs/cluster.go | 53 +++ internal/service/ecs/cluster_data_source.go | 6 + internal/service/ecs/cluster_test.go | 43 +++ internal/service/ecs/service.go | 396 ++++++++++++++++++-- internal/service/ecs/service_test.go | 152 ++++---- website/docs/d/ecs_cluster.html.markdown | 3 +- 7 files changed, 550 insertions(+), 114 deletions(-) create mode 100644 .changelog/28043.txt diff --git a/.changelog/28043.txt b/.changelog/28043.txt new file mode 100644 index 000000000000..660c0910df72 --- /dev/null +++ b/.changelog/28043.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_ecs_cluster: Add support for 'service_connect +``` + +```release-note:enhancement +resource/aws_ecs_service: Add support for `service_connect' +``` + +```release-note:enhancement +data/aws_ecs_cluster: Add support for `service_connect' +``` \ No newline at end of file diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index d1ff1474800c..1fa7baeb5e48 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -133,6 +133,20 @@ func ResourceCluster() *schema.Resource { }, }, }, + "service_connection_defaults": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "namespace": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, "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_connection_defaults"); ok { + input.ServiceConnectionDefaults = expandServiceConnectionDefaults(v.([]interface{})) + } + if v, ok := d.GetOk("setting"); ok { input.Settings = expandClusterSettings(v.(*schema.Set)) } @@ -292,6 +310,10 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting default_capacity_provider_strategy: %w", err) } + if err := d.Set("service_connection_defaults", flattenServiceConnection(cluster.ServiceConnectionDefaults)); err != nil { + return fmt.Errorf("error setting service connection: %w", err) + } + if err := d.Set("setting", flattenClusterSettings(cluster.Settings)); err != nil { return fmt.Errorf("error setting setting: %w", err) } @@ -324,6 +346,10 @@ func resourceClusterUpdate(d *schema.ResourceData, meta interface{}) error { Cluster: aws.String(d.Id()), } + if v, ok := d.GetOk("service_connection_defaults"); ok { + input.ServiceConnectionDefaults = expandServiceConnectionDefaults(v.([]interface{})) + } + if v, ok := d.GetOk("setting"); ok { input.Settings = expandClusterSettings(v.(*schema.Set)) } @@ -473,6 +499,21 @@ func expandClusterSettings(configured *schema.Set) []*ecs.ClusterSetting { return settings } +func expandServiceConnectionDefaults(list []interface{}) *ecs.ServiceConnectionDefaults { + if len(list) == 0 { + return nil + } + + m := list[0].(map[string]interface{}) + + var sc ecs.ServiceConnectionDefaults + if v, ok := m["namespace"].(string); ok && v != "" { + sc.Namespace = aws.String(v) + } + + return &sc +} + func flattenClusterSettings(list []*ecs.ClusterSetting) []map[string]interface{} { if len(list) == 0 { return nil @@ -550,6 +591,18 @@ func flattenClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiO return []interface{}{tfMap} } +func flattenServiceConnectionDefaults(apiObject *ecs.ServiceConnectionDefaults) []interface{} { + if apiObject == nil { + return nil + } + + result := map[string]interface{}{ + "namespace": aws.StringValue(apiObject.Namespace), + } + + return []interface{}{result} +} + func expandClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { if len(nc) == 0 { return &ecs.ClusterConfiguration{} diff --git a/internal/service/ecs/cluster_data_source.go b/internal/service/ecs/cluster_data_source.go index e05360e4cd7d..5d071403494a 100644 --- a/internal/service/ecs/cluster_data_source.go +++ b/internal/service/ecs/cluster_data_source.go @@ -24,6 +24,11 @@ func DataSourceCluster() *schema.Resource { Computed: true, }, + "namespace": { + Type: schema.TypeString, + Computed: true, + }, + "status": { Type: schema.TypeString, Computed: true, @@ -76,6 +81,7 @@ func dataSourceClusterRead(d *schema.ResourceData, meta interface{}) error { d.SetId(aws.StringValue(cluster.ClusterArn)) d.Set("arn", cluster.ClusterArn) + d.Set("namespace", cluster.Namespace) d.Set("status", cluster.Status) d.Set("pending_tasks_count", cluster.PendingTasksCount) d.Set("running_tasks_count", cluster.RunningTasksCount) diff --git a/internal/service/ecs/cluster_test.go b/internal/service/ecs/cluster_test.go index 7114bc0f81eb..02f6e10edc1b 100644 --- a/internal/service/ecs/cluster_test.go +++ b/internal/service/ecs/cluster_test.go @@ -45,6 +45,37 @@ func TestAccECSCluster_basic(t *testing.T) { }) } +func TestAccECSCluster_serviceConnectionDefaults(t *testing.T) { + var cluster1 ecs.Cluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ecs_cluster.test" + + 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_serviceConnectionDefaults(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(resourceName, &cluster1), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "ecs", fmt.Sprintf("cluster/%s", rName)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "namespace", "testnam1"), + ), + }, + { + ResourceName: resourceName, + ImportStateId: rName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccECSCluster_disappears(t *testing.T) { var cluster1 ecs.Cluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -391,6 +422,18 @@ resource "aws_ecs_cluster" "test" { `, rName) } +func testAccClusterConfig_serviceConnectionDefaults(rName string) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "test" { + name = %q + + service_connection_defaults = { + namespace = "testnam1" + } +} +`, rName) +} + func testAccClusterConfig_tags1(rName, tag1Key, tag1Value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 6bf22595cee8..fcfbfe0e81f6 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -306,6 +306,120 @@ 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, + Default: false, + }, + "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.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "awslogs_group": { + Type: schema.TypeString, + Required: true, + }, + "awslogs_region": { + Type: schema.TypeString, + Required: true, + }, + "awslogs_create_group": { + Type: schema.TypeString, + Optional: true, + }, + "awslogs_stream_prefix": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "secretoptions": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "valuefrom": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + }, + "services": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_aliases": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + 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, @@ -340,14 +454,6 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, - // modeled after null_resource & aws_api_gateway_deployment - // only for _updates in-place_ rather than replacements - "triggers": { - Type: schema.TypeMap, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -358,34 +464,10 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, - triggersCustomizeDiff, ), } } -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 @@ -537,6 +619,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 = expandServieConnectConfiguration(v.([]interface{})) + } + serviceRegistries := d.Get("service_registries").([]interface{}) if len(serviceRegistries) > 0 { srs := make([]*ecs.ServiceRegistry, 0, len(serviceRegistries)) @@ -674,8 +760,6 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) - d.Set("triggers", d.Get("triggers")) - // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) @@ -731,6 +815,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) } @@ -952,6 +1040,242 @@ func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { return results } +func expandServieConnectConfiguration(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["services"].([]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"].([]interface{}); ok && len(v) > 0 { + config.Options = expandOptions(v) + } + if v, ok := raw["secret_options"].([]interface{}); ok && len(v) > 0 { + config.Options = expandSecretOptions(v) + } + + return config +} + +func expandOptions(op []interface{}) *ecs.LogConfigurationOptions { + if len(op) == 0 { + return &ecs.LogConfigurationOptions{} + } + raw := op[0].(map[string]interface{}) + + config := &ecs.LogConfigurationOptions{} + if v, ok := raw["awslogs_group"].(string); ok && v != "" { + config.LogsGroup = aws.String(v) + } + if v, ok := raw["awslogs_region"].(string); ok && v != "" { + config.LogsRegion = aws.String(v) + } + if v, ok := raw["awslogs_create_group"].(string); ok && v != "" { + config.LogsCreateGroup = aws.String(v) + } + if v, ok := raw["awslogs_stream_prefix"].(string); ok && v != "" { + config.LogsStreamPrefix = aws.String(v) + } + return config +} + +func expandSecretOptions(sop []interface{}) *ecs.LogConfigurationSecretOptions { + if len(sop) == 0 { + return &ecs.LogConfigurationSecretOptions{} + } + raw := sop[0].(map[string]interface{}) + + config := &ecs.LogConfigurationSecretOptions{} + if v, ok := raw["name"].(string); ok && v != "" { + config.Name = aws.String(v) + } + if v, ok := raw["valuefrom"].(string); ok && v != "" { + config.ValueFrom = aws.String(v) + } + + return config +} + +func expandServices(srv []interface{}) *ecs.ServiceConnectConfiguration { + if len(srv) == 0 { + return &ecs.ServiceConnectConfiguration{} + } + raw := srv[0].(map[string]interface{}) + + config := &ecs.ServiceConnectConfiguration{} + if v, ok := raw["client_aliases"].([]interface{}); ok && len(v) > 0 { + config.ClientAliases = expandAliases(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.DiscoveryName = aws.Int(v) + } + if v, ok := raw["port_name"].(string); ok && v != "" { + config.PortName = aws.String(v) + } + + return config +} + +func expandAliases(srv []interface{}) *ecs.ServiceConnectConfiguration { + if len(srv) == 0 { + return &ecs.ServiceConnectConfiguration{} + } + raw := srv[0].(map[string]interface{}) + + config := &ecs.ServiceConnectConfiguration{} + + if v, ok := raw["port"].(int); ok { + config.Port = aws.Int(v) + } + if v, ok := raw["dns_name"].(string); ok && v != "" { + config.DnsName = aws.String(v) + } + + return config +} + +func flattenServiceConnectConfiguration(sc *ecs.ServiceConnectConfiguration) []interface{} { + if sc == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if sc.ServiceConnectConfiguration != nil { + tfMap["enabled"] = aws.Bool(sc.Enabled) + tfMap["log_configuration"] = flattenLogConfiguration(sc.LogConfiguration) + if sc.Namespace != nil { + tfMap["namespace"] = aws.StringValue(sc.Namespace) + } + tfMap["services"] = flattenServiceConfiguration(sc.Services) + } + return []interface{}{tfMap} +} + +func flattenLogConfiguration(sc *ecs.LogConfiguration) []interface{} { + if sc == nil { + return nil + } + + tfMap := map[string]interface{}{} + if sc.LogDriver != nil { + tfMap["log_driver"] = aws.StringValue(sc.LogDriver) + } + if sc.Options != nil { + tfMap["options"] = flattenOptions(sc.Options) + } + if sc.SecretOptions != nil { + tfMap["secretoptions"] = flattenSecretOptions(sc.SecretOptions) + } + return []interface{}{tfMap} +} + +func flattenOptions(sc *ecs.LogConfigurationOptions) []interface{} { + if sc == nil { + return nil + } + + tfMap := map[string]interface{}{} + if sc.AWSLogsGroup != nil { + tfMap["awslogs_group"] = aws.StringValue(sc.AWSLogsGroup) + } + if sc.AWSLogsRegion != nil { + tfMap["awslogs_region"] = aws.StringValue(sc.AWSLogsRegion) + } + if sc.AWSLogsCreateGroup != nil { + tfMap["awslogs_create_group"] = aws.StringValue(sc.AWSLogsCreateGroup) + } + if sc.AWSLogsStreamPrefix != nil { + tfMap["awslogs_stream_prefix"] = aws.StringValue(sc.AWSLogsStreamPrefix) + } + return []interface{}{tfMap} +} + +func flattenSecretOptions(sc []*ecs.Secret) []interface{} { + if len(sc) == 0 { + return nil + } + + var out []interface{} + for _, v := range sc { + m := map[string]interface{}{ + "name": aws.StringValue(v.Name), + "value_from": aws.StringValue(v.ValueFrom), + } + + out = append(out, m) + } + + return out +} + +func flattenServiceConfiguration(sc []*ecs.ServiceConnectService) []interface{} { + if len(sc) == 0 { + return nil + } + + var out []interface{} + for _, v := range sc { + tfMap := map[string]interface{}{} + tfMap["client_aliases"] = flattenClientAliases(v.ClientAliases) + tfMap["discovery_name"] = aws.StringValue(v.DiscoveryName) + tfMap["ingress_port_override"] = aws.Int64(v.IngressPortOverride) + tfMap["port_name"] = aws.StringValue(v.PortName) + + out = append(out, tfMap) + } + + return out +} + +func flattenClientAliases(sc []*ecs.ServiceConnectClientAlias) []interface{} { + if len(sc) == 0 { + return nil + } + + var out []interface{} + for _, v := range sc { + tfMap := map[string]interface{}{} + tfMap["dns_name"] = aws.StringValue(v.DnsName) + tfMap["port"] = aws.Int64(v.Port) + } + + return out +} + func flattenServiceRegistries(srs []*ecs.ServiceRegistry) []map[string]interface{} { if len(srs) == 0 { return nil @@ -1089,6 +1413,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 = expandServieConnectConfiguration(d.Get("service_connect_configuration").([]interface{})) + } + if d.HasChange("service_registries") { input.ServiceRegistries = expandServiceRegistries(d.Get("service_registries").([]interface{})) } diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index 820cb8d8066f..af4b9a443ea0 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -685,42 +685,6 @@ func TestAccECSService_forceNewDeployment(t *testing.T) { }) } -func TestAccECSService_forceNewDeploymentTriggers(t *testing.T) { - var service1, service2 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_forceNewDeployment(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceExists(resourceName, &service1), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"), - ), - }, - { - Config: testAccServiceConfig_forceNewDeploymentTriggers(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckServiceExists(resourceName, &service2), - testAccCheckServiceNotRecreated(&service1, &service2), - resource.TestCheckResourceAttr(resourceName, "force_new_deployment", "true"), - resource.TestCheckResourceAttrSet(resourceName, "triggers.update"), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"), - resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"), - ), - }, - }, - }) -} - func TestAccECSService_PlacementStrategy_basic(t *testing.T) { var service1, service2, service3, service4 ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1116,6 +1080,28 @@ func TestAccECSService_ServiceRegistries_basic(t *testing.T) { }) } +func TestAccECSService_ServiceConnect_configuration(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: testAccServiceConnect_configuration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service), + resource.TestCheckResourceAttr(resourceName, "service_connect_configuration.#", "1"), + ), + }, + }, + }) +} + func TestAccECSService_ServiceRegistries_container(t *testing.T) { var service ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1596,7 +1582,7 @@ resource "aws_route_table" "test" { resource "aws_route_table_association" "test" { count = 2 - subnet_id = element(aws_subnet.test[*].id, count.index) + subnet_id = element(aws_subnet.test.*.id, count.index) route_table_id = aws_route_table.test.id } @@ -1977,47 +1963,6 @@ resource "aws_ecs_service" "test" { `, rName) } -func testAccServiceConfig_forceNewDeploymentTriggers(rName string) string { - return fmt.Sprintf(` -resource "aws_ecs_cluster" "default" { - name = %[1]q -} - -resource "aws_ecs_task_definition" "test" { - family = %[1]q - - container_definitions = < Date: Mon, 28 Nov 2022 16:28:22 -0500 Subject: [PATCH 02/26] Correct CHANGELOG file name. --- .changelog/{28043.txt => 28052.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{28043.txt => 28052.txt} (100%) diff --git a/.changelog/28043.txt b/.changelog/28052.txt similarity index 100% rename from .changelog/28043.txt rename to .changelog/28052.txt From 4ac8987db784f64f88fdc01ac0497d1e848d0865 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 28 Nov 2022 16:32:47 -0500 Subject: [PATCH 03/26] Tweak CHANGELOG entries. --- .changelog/28052.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changelog/28052.txt b/.changelog/28052.txt index 660c0910df72..ddc19fea72ee 100644 --- a/.changelog/28052.txt +++ b/.changelog/28052.txt @@ -1,11 +1,11 @@ ```release-note:enhancement -resource/aws_ecs_cluster: Add support for 'service_connect +resource/aws_ecs_cluster: Add `service_connect_defaults` argument ``` ```release-note:enhancement -resource/aws_ecs_service: Add support for `service_connect' +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/aws_ecs_cluster: Add support for `service_connect' +data-source/aws_ecs_cluster: Add `namespace` attribute ``` \ No newline at end of file From c9635fb73bfcda45066169fb939214829b14255e Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 28 Nov 2022 17:01:05 -0500 Subject: [PATCH 04/26] d/aws_ecs_cluster: 'namespace' => 'service_connect_defaults'. --- .changelog/28052.txt | 2 +- internal/service/ecs/cluster_data_source.go | 60 ++++++++++++--------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/.changelog/28052.txt b/.changelog/28052.txt index ddc19fea72ee..a88ead375b46 100644 --- a/.changelog/28052.txt +++ b/.changelog/28052.txt @@ -7,5 +7,5 @@ resource/aws_ecs_service: Add `service_connect_configuration` argument in suppor ``` ```release-note:enhancement -data-source/aws_ecs_cluster: Add `namespace` attribute +data-source/aws_ecs_cluster: Add `service_connect_defaults` attribute ``` \ No newline at end of file diff --git a/internal/service/ecs/cluster_data_source.go b/internal/service/ecs/cluster_data_source.go index 5d071403494a..677a02b48dd8 100644 --- a/internal/service/ecs/cluster_data_source.go +++ b/internal/service/ecs/cluster_data_source.go @@ -2,53 +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, }, - - "namespace": { + "cluster_name": { Type: schema.TypeString, - Computed: true, + Required: true, }, - - "status": { - Type: schema.TypeString, + "pending_tasks_count": { + Type: schema.TypeInt, Computed: true, }, - - "pending_tasks_count": { + "registered_container_instances_count": { Type: schema.TypeInt, Computed: true, }, - "running_tasks_count": { Type: schema.TypeInt, Computed: true, }, - - "registered_container_instances_count": { - Type: schema.TypeInt, + "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, @@ -65,30 +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("namespace", cluster.Namespace) - 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: %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) + return diag.Errorf("setting setting: %s", err) } return nil From 8f6744154019902109011ca117de6082e0b2c37a Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 28 Nov 2022 17:01:42 -0500 Subject: [PATCH 05/26] r/aws_ecs_cluster: 'service_connection_defaults' => 'service_connect_defaults'. --- internal/service/ecs/cluster.go | 98 ++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index 1fa7baeb5e48..234ef1201616 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, }, @@ -133,7 +135,7 @@ func ResourceCluster() *schema.Resource { }, }, }, - "service_connection_defaults": { + "service_connect_defaults": { Type: schema.TypeList, Optional: true, Computed: true, @@ -200,8 +202,8 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error { input.CapacityProviders = flex.ExpandStringSet(v.(*schema.Set)) } - if v, ok := d.GetOk("service_connection_defaults"); ok { - input.ServiceConnectionDefaults = expandServiceConnectionDefaults(v.([]interface{})) + 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 { @@ -310,8 +312,12 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting default_capacity_provider_strategy: %w", err) } - if err := d.Set("service_connection_defaults", flattenServiceConnection(cluster.ServiceConnectionDefaults)); err != nil { - return fmt.Errorf("error setting service connection: %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 { @@ -338,18 +344,14 @@ 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()), } - if v, ok := d.GetOk("service_connection_defaults"); ok { - input.ServiceConnectionDefaults = expandServiceConnectionDefaults(v.([]interface{})) - } - if v, ok := d.GetOk("setting"); ok { input.Settings = expandClusterSettings(v.(*schema.Set)) } @@ -358,13 +360,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) } } @@ -375,21 +382,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) { @@ -398,7 +405,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) } } @@ -499,19 +506,32 @@ func expandClusterSettings(configured *schema.Set) []*ecs.ClusterSetting { return settings } -func expandServiceConnectionDefaults(list []interface{}) *ecs.ServiceConnectionDefaults { - if len(list) == 0 { +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 } - m := list[0].(map[string]interface{}) + tfMap := map[string]interface{}{} - var sc ecs.ServiceConnectionDefaults - if v, ok := m["namespace"].(string); ok && v != "" { - sc.Namespace = aws.String(v) + if v := apiObject.Namespace; v != nil { + tfMap["namespace"] = aws.StringValue(v) } - return &sc + return tfMap } func flattenClusterSettings(list []*ecs.ClusterSetting) []map[string]interface{} { @@ -591,18 +611,6 @@ func flattenClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiO return []interface{}{tfMap} } -func flattenServiceConnectionDefaults(apiObject *ecs.ServiceConnectionDefaults) []interface{} { - if apiObject == nil { - return nil - } - - result := map[string]interface{}{ - "namespace": aws.StringValue(apiObject.Namespace), - } - - return []interface{}{result} -} - func expandClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { if len(nc) == 0 { return &ecs.ClusterConfiguration{} From 6a2d9b73a7d1ec38ce880adc37aa26843e038c6a Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 28 Nov 2022 22:49:48 -0800 Subject: [PATCH 06/26] r/aws_ecs_service: service connect configuration --- internal/service/ecs/service.go | 269 ++++++++++++++++++++++++++++---- 1 file changed, 235 insertions(+), 34 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 6bf22595cee8..5d6240a2af61 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -306,6 +306,99 @@ 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, + Default: false, + }, + "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_options": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "value_from": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "namespace": { + Type: schema.TypeString, + Optional: true, + }, + "services": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_aliases": { + 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, @@ -340,14 +433,6 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, - // modeled after null_resource & aws_api_gateway_deployment - // only for _updates in-place_ rather than replacements - "triggers": { - Type: schema.TypeMap, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -358,34 +443,10 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, - triggersCustomizeDiff, ), } } -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 @@ -537,6 +598,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)) @@ -674,8 +739,6 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) - d.Set("triggers", d.Get("triggers")) - // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) @@ -731,6 +794,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) } @@ -952,6 +1019,136 @@ func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { 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["services"].([]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_options"].([]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_aliases"].([]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 @@ -1089,6 +1286,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{})) } From e5495ccd8dc900110753cb26678e187cb05679b5 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 28 Nov 2022 22:52:03 -0800 Subject: [PATCH 07/26] r/aws_ecs_service: service connect configuration --- internal/service/ecs/service.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 5d6240a2af61..4d52f9bba24f 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -433,6 +433,14 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, + // modeled after null_resource & aws_api_gateway_deployment + // only for _updates in-place_ rather than replacements + "triggers": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -443,10 +451,34 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, + triggersCustomizeDiff, ), } } +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 From ebcbabc7638f9bd6ef472a13ed240b602d1bfb5c Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 28 Nov 2022 22:53:23 -0800 Subject: [PATCH 08/26] r/aws_ecs_service: triggers --- internal/service/ecs/service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 4d52f9bba24f..436c61b0d312 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -771,6 +771,8 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) + d.Set("triggers", d.Get("triggers")) + // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) From d23aabf0f6cbd955747c639c08ebcd228715656f Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 08:37:45 -0500 Subject: [PATCH 09/26] r/aws_ecs_cluster: Alphabetize attributes. --- internal/service/ecs/cluster.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index 234ef1201616..eb3fa9d86bab 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -34,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, @@ -83,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, @@ -121,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, @@ -135,6 +127,12 @@ func ResourceCluster() *schema.Resource { }, }, }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateClusterName, + }, "service_connect_defaults": { Type: schema.TypeList, Optional: true, From eea5c95a0244501d27eb1a0d4182a5e6d72f0e52 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 09:11:29 -0500 Subject: [PATCH 10/26] r/aws_ecs_service: service connect configuration --- internal/service/ecs/service.go | 315 ++++++++++---------------------- 1 file changed, 100 insertions(+), 215 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index fcfbfe0e81f6..7580ffe02b43 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, }, }, }, @@ -329,41 +316,21 @@ func ResourceService() *schema.Resource { ValidateFunc: validation.StringInSlice(ecs.LogDriver_Values(), false), }, "options": { - Type: schema.TypeList, + Type: schema.TypeMap, Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "awslogs_group": { - Type: schema.TypeString, - Required: true, - }, - "awslogs_region": { - Type: schema.TypeString, - Required: true, - }, - "awslogs_create_group": { - Type: schema.TypeString, - Optional: true, - }, - "awslogs_stream_prefix": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "secretoptions": { + "secret_options": { Type: schema.TypeList, Optional: true, - MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, - "valuefrom": { + "value_from": { Type: schema.TypeString, Optional: true, }, @@ -386,7 +353,6 @@ func ResourceService() *schema.Resource { "client_aliases": { Type: schema.TypeList, Required: true, - MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "dns_name": { @@ -468,6 +434,29 @@ 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 @@ -620,7 +609,7 @@ func resourceServiceCreate(d *schema.ResourceData, meta interface{}) error { } if v, ok := d.GetOk("service_connect_configuration"); ok && len(v.([]interface{})) > 0 { - input.ServiceConnectConfiguration = expandServieConnectConfiguration(v.([]interface{})) + input.ServiceConnectConfiguration = expandServiceConnectConfiguration(v.([]interface{})) } serviceRegistries := d.Get("service_registries").([]interface{}) @@ -760,6 +749,8 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) + d.Set("triggers", d.Get("triggers")) + // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) @@ -815,9 +806,9 @@ 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_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) @@ -1040,7 +1031,7 @@ func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { return results } -func expandServieConnectConfiguration(sc []interface{}) *ecs.ServiceConnectConfiguration { +func expandServiceConnectConfiguration(sc []interface{}) *ecs.ServiceConnectConfiguration { if len(sc) == 0 { return &ecs.ServiceConnectConfiguration{} } @@ -1076,201 +1067,95 @@ func expandLogConfiguration(lc []interface{}) *ecs.LogConfiguration { if v, ok := raw["log_driver"].(string); ok && v != "" { config.LogDriver = aws.String(v) } - if v, ok := raw["options"].([]interface{}); ok && len(v) > 0 { - config.Options = expandOptions(v) + if v, ok := raw["options"].(map[string]interface{}); ok && len(v) > 0 { + config.Options = flex.ExpandStringMap(v) } if v, ok := raw["secret_options"].([]interface{}); ok && len(v) > 0 { - config.Options = expandSecretOptions(v) + config.SecretOptions = expandSecretOptions(v) } return config } -func expandOptions(op []interface{}) *ecs.LogConfigurationOptions { - if len(op) == 0 { - return &ecs.LogConfigurationOptions{} - } - raw := op[0].(map[string]interface{}) - - config := &ecs.LogConfigurationOptions{} - if v, ok := raw["awslogs_group"].(string); ok && v != "" { - config.LogsGroup = aws.String(v) - } - if v, ok := raw["awslogs_region"].(string); ok && v != "" { - config.LogsRegion = aws.String(v) - } - if v, ok := raw["awslogs_create_group"].(string); ok && v != "" { - config.LogsCreateGroup = aws.String(v) - } - if v, ok := raw["awslogs_stream_prefix"].(string); ok && v != "" { - config.LogsStreamPrefix = aws.String(v) - } - return config -} - -func expandSecretOptions(sop []interface{}) *ecs.LogConfigurationSecretOptions { +func expandSecretOptions(sop []interface{}) []*ecs.Secret { if len(sop) == 0 { - return &ecs.LogConfigurationSecretOptions{} - } - raw := sop[0].(map[string]interface{}) - - config := &ecs.LogConfigurationSecretOptions{} - if v, ok := raw["name"].(string); ok && v != "" { - config.Name = aws.String(v) - } - if v, ok := raw["valuefrom"].(string); ok && v != "" { - config.ValueFrom = aws.String(v) - } - - return config -} - -func expandServices(srv []interface{}) *ecs.ServiceConnectConfiguration { - if len(srv) == 0 { - return &ecs.ServiceConnectConfiguration{} - } - raw := srv[0].(map[string]interface{}) - - config := &ecs.ServiceConnectConfiguration{} - if v, ok := raw["client_aliases"].([]interface{}); ok && len(v) > 0 { - config.ClientAliases = expandAliases(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.DiscoveryName = aws.Int(v) - } - if v, ok := raw["port_name"].(string); ok && v != "" { - config.PortName = aws.String(v) - } - - return config -} - -func expandAliases(srv []interface{}) *ecs.ServiceConnectConfiguration { - if len(srv) == 0 { - return &ecs.ServiceConnectConfiguration{} - } - raw := srv[0].(map[string]interface{}) - - config := &ecs.ServiceConnectConfiguration{} - - if v, ok := raw["port"].(int); ok { - config.Port = aws.Int(v) - } - if v, ok := raw["dns_name"].(string); ok && v != "" { - config.DnsName = aws.String(v) - } - - return config -} - -func flattenServiceConnectConfiguration(sc *ecs.ServiceConnectConfiguration) []interface{} { - if sc == nil { return nil } - tfMap := map[string]interface{}{} + var out []*ecs.Secret + for _, item := range sop { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } - if sc.ServiceConnectConfiguration != nil { - tfMap["enabled"] = aws.Bool(sc.Enabled) - tfMap["log_configuration"] = flattenLogConfiguration(sc.LogConfiguration) - if sc.Namespace != nil { - tfMap["namespace"] = aws.StringValue(sc.Namespace) + 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) } - tfMap["services"] = flattenServiceConfiguration(sc.Services) - } - return []interface{}{tfMap} -} -func flattenLogConfiguration(sc *ecs.LogConfiguration) []interface{} { - if sc == nil { - return nil + out = append(out, &config) } - tfMap := map[string]interface{}{} - if sc.LogDriver != nil { - tfMap["log_driver"] = aws.StringValue(sc.LogDriver) - } - if sc.Options != nil { - tfMap["options"] = flattenOptions(sc.Options) - } - if sc.SecretOptions != nil { - tfMap["secretoptions"] = flattenSecretOptions(sc.SecretOptions) - } - return []interface{}{tfMap} + return out } -func flattenOptions(sc *ecs.LogConfigurationOptions) []interface{} { - if sc == nil { +func expandServices(srv []interface{}) []*ecs.ServiceConnectService { + if len(srv) == 0 { return nil } - tfMap := map[string]interface{}{} - if sc.AWSLogsGroup != nil { - tfMap["awslogs_group"] = aws.StringValue(sc.AWSLogsGroup) - } - if sc.AWSLogsRegion != nil { - tfMap["awslogs_region"] = aws.StringValue(sc.AWSLogsRegion) - } - if sc.AWSLogsCreateGroup != nil { - tfMap["awslogs_create_group"] = aws.StringValue(sc.AWSLogsCreateGroup) - } - if sc.AWSLogsStreamPrefix != nil { - tfMap["awslogs_stream_prefix"] = aws.StringValue(sc.AWSLogsStreamPrefix) - } - return []interface{}{tfMap} -} - -func flattenSecretOptions(sc []*ecs.Secret) []interface{} { - if len(sc) == 0 { - return nil - } + var out []*ecs.ServiceConnectService + for _, item := range srv { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } - var out []interface{} - for _, v := range sc { - m := map[string]interface{}{ - "name": aws.StringValue(v.Name), - "value_from": aws.StringValue(v.ValueFrom), + var config ecs.ServiceConnectService + if v, ok := raw["client_aliases"].([]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, m) + out = append(out, &config) } return out } -func flattenServiceConfiguration(sc []*ecs.ServiceConnectService) []interface{} { - if len(sc) == 0 { +func expandClientAliases(srv []interface{}) []*ecs.ServiceConnectClientAlias { + if len(srv) == 0 { return nil } - var out []interface{} - for _, v := range sc { - tfMap := map[string]interface{}{} - tfMap["client_aliases"] = flattenClientAliases(v.ClientAliases) - tfMap["discovery_name"] = aws.StringValue(v.DiscoveryName) - tfMap["ingress_port_override"] = aws.Int64(v.IngressPortOverride) - tfMap["port_name"] = aws.StringValue(v.PortName) - - out = append(out, tfMap) - } - - return out -} + var out []*ecs.ServiceConnectClientAlias + for _, item := range srv { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } -func flattenClientAliases(sc []*ecs.ServiceConnectClientAlias) []interface{} { - if len(sc) == 0 { - return nil - } + 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) + } - var out []interface{} - for _, v := range sc { - tfMap := map[string]interface{}{} - tfMap["dns_name"] = aws.StringValue(v.DnsName) - tfMap["port"] = aws.Int64(v.Port) + out = append(out, &config) } return out @@ -1414,7 +1299,7 @@ func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { } if d.HasChange("service_connect_configuration") { - input.ServiceConnectConfiguration = expandServieConnectConfiguration(d.Get("service_connect_configuration").([]interface{})) + input.ServiceConnectConfiguration = expandServiceConnectConfiguration(d.Get("service_connect_configuration").([]interface{})) } if d.HasChange("service_registries") { From a886504955449457461e74171ef856eb21a18eeb Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 09:13:59 -0500 Subject: [PATCH 11/26] Revert "r/aws_ecs_service: triggers" This reverts commit ebcbabc7638f9bd6ef472a13ed240b602d1bfb5c. --- internal/service/ecs/service.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 436c61b0d312..4d52f9bba24f 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -771,8 +771,6 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) - d.Set("triggers", d.Get("triggers")) - // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) From d553e54caed827e1bf693c4ad023d6f581bb7d94 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 09:14:06 -0500 Subject: [PATCH 12/26] Revert "r/aws_ecs_service: service connect configuration" This reverts commit e5495ccd8dc900110753cb26678e187cb05679b5. --- internal/service/ecs/service.go | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 4d52f9bba24f..5d6240a2af61 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -433,14 +433,6 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, - // modeled after null_resource & aws_api_gateway_deployment - // only for _updates in-place_ rather than replacements - "triggers": { - Type: schema.TypeMap, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -451,34 +443,10 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, - triggersCustomizeDiff, ), } } -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 From 0b6d009c09f0039b9ef27ba49080900980980bdf Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 09:14:20 -0500 Subject: [PATCH 13/26] Revert "r/aws_ecs_service: service connect configuration" This reverts commit 6a2d9b73a7d1ec38ce880adc37aa26843e038c6a. --- internal/service/ecs/service.go | 269 ++++---------------------------- 1 file changed, 34 insertions(+), 235 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 5d6240a2af61..6bf22595cee8 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -306,99 +306,6 @@ 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, - Default: false, - }, - "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_options": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Optional: true, - }, - "value_from": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - }, - }, - "namespace": { - Type: schema.TypeString, - Optional: true, - }, - "services": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "client_aliases": { - 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, @@ -433,6 +340,14 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, + // modeled after null_resource & aws_api_gateway_deployment + // only for _updates in-place_ rather than replacements + "triggers": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -443,10 +358,34 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, + triggersCustomizeDiff, ), } } +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 @@ -598,10 +537,6 @@ 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)) @@ -739,6 +674,8 @@ func resourceServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("platform_version", service.PlatformVersion) d.Set("enable_execute_command", service.EnableExecuteCommand) + d.Set("triggers", d.Get("triggers")) + // Save cluster in the same format if strings.HasPrefix(d.Get("cluster").(string), "arn:"+meta.(*conns.AWSClient).Partition+":ecs:") { d.Set("cluster", service.ClusterArn) @@ -794,10 +731,6 @@ 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) } @@ -1019,136 +952,6 @@ func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []interface{} { 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["services"].([]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_options"].([]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_aliases"].([]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 @@ -1286,10 +1089,6 @@ 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{})) } From 3f7d42451fc7fb436227c62c57420fc899d56cf5 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 09:29:16 -0500 Subject: [PATCH 14/26] r/aws_ecs_service: Correct order of CRUD handlers. --- internal/service/ecs/service.go | 833 ++++++++++++++++---------------- 1 file changed, 421 insertions(+), 412 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 7580ffe02b43..fdbebb17f1d7 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -420,6 +420,14 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, + // modeled after null_resource & aws_api_gateway_deployment + // only for _updates in-place_ rather than replacements + "triggers": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "wait_for_steady_state": { Type: schema.TypeBool, Optional: true, @@ -430,84 +438,11 @@ func ResourceService() *schema.Resource { CustomizeDiff: customdiff.Sequence( verify.SetTagsDiff, capacityProviderStrategyCustomizeDiff, + triggersCustomizeDiff, ), } } -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 @@ -806,9 +741,9 @@ 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_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) @@ -828,126 +763,446 @@ 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{}) - - deploymentController := &ecs.DeploymentController{ - Type: aws.String(m["type"].(string)), - } - - return deploymentController -} - -func flattenDeploymentController(deploymentController *ecs.DeploymentController) []interface{} { - m := map[string]interface{}{ - "type": ecs.DeploymentControllerTypeEcs, - } +func resourceServiceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ECSConn - if deploymentController == nil { - return []interface{}{m} - } + 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()), + } - m["type"] = aws.StringValue(deploymentController.Type) + schedulingStrategy := d.Get("scheduling_strategy").(string) - return []interface{}{m} -} + 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))) + } -func expandDeploymentCircuitBreaker(tfMap map[string]interface{}) *ecs.DeploymentCircuitBreaker { - if tfMap == nil { - return nil - } + 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))), + } + } + } - apiObject := &ecs.DeploymentCircuitBreaker{} + if d.HasChange("deployment_circuit_breaker") { + if input.DeploymentConfiguration == nil { + input.DeploymentConfiguration = &ecs.DeploymentConfiguration{} + } - apiObject.Enable = aws.Bool(tfMap["enable"].(bool)) - apiObject.Rollback = aws.Bool(tfMap["rollback"].(bool)) + // To remove an existing deployment circuit breaker, specify an empty object. + input.DeploymentConfiguration.DeploymentCircuitBreaker = &ecs.DeploymentCircuitBreaker{} - return apiObject -} + 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{})) + } + } -func flattenDeploymentCircuitBreaker(apiObject *ecs.DeploymentCircuitBreaker) map[string]interface{} { - if apiObject == nil { - return nil - } + 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{} - tfMap := map[string]interface{}{} + if v, ok := d.GetOk("ordered_placement_strategy"); ok && len(v.([]interface{})) > 0 { + ps, err := expandPlacementStrategy(v.([]interface{})) - tfMap["enable"] = aws.BoolValue(apiObject.Enable) - tfMap["rollback"] = aws.BoolValue(apiObject.Rollback) + if err != nil { + return err + } - return tfMap -} + input.PlacementStrategy = ps + } + } -func flattenNetworkConfiguration(nc *ecs.NetworkConfiguration) []interface{} { - if nc == nil { - return nil - } + 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{} - result := make(map[string]interface{}) - result["security_groups"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.SecurityGroups) - result["subnets"] = flex.FlattenStringSet(nc.AwsvpcConfiguration.Subnets) + if v, ok := d.Get("placement_constraints").(*schema.Set); ok && v.Len() > 0 { + pc, err := expandPlacementConstraints(v.List()) - if nc.AwsvpcConfiguration.AssignPublicIp != nil { - result["assign_public_ip"] = aws.StringValue(nc.AwsvpcConfiguration.AssignPublicIp) == ecs.AssignPublicIpEnabled - } + if err != nil { + return err + } - return []interface{}{result} -} + input.PlacementConstraints = pc + } + } -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("platform_version") { + input.PlatformVersion = aws.String(d.Get("platform_version").(string)) } - } - return &ecs.NetworkConfiguration{AwsvpcConfiguration: awsVpcConfig} -} + if d.HasChange("health_check_grace_period_seconds") { + input.HealthCheckGracePeriodSeconds = aws.Int64(int64(d.Get("health_check_grace_period_seconds").(int))) + } -func expandPlacementConstraints(tfList []interface{}) ([]*ecs.PlacementConstraint, error) { - if len(tfList) == 0 { - return nil, nil - } + if d.HasChange("task_definition") { + input.TaskDefinition = aws.String(d.Get("task_definition").(string)) + } - var result []*ecs.PlacementConstraint + if d.HasChange("network_configuration") { + input.NetworkConfiguration = expandNetworkConfiguration(d.Get("network_configuration").([]interface{})) + } - for _, tfMapRaw := range tfList { - if tfMapRaw == nil { - continue + if d.HasChange("capacity_provider_strategy") { + input.CapacityProviderStrategy = expandCapacityProviderStrategy(d.Get("capacity_provider_strategy").(*schema.Set)) } - tfMap := tfMapRaw.(map[string]interface{}) + if d.HasChange("enable_execute_command") { + input.EnableExecuteCommand = aws.Bool(d.Get("enable_execute_command").(bool)) + } - apiObject := &ecs.PlacementConstraint{} + if d.HasChange("enable_ecs_managed_tags") { + input.EnableECSManagedTags = aws.Bool(d.Get("enable_ecs_managed_tags").(bool)) + } - if v, ok := tfMap["expression"].(string); ok && v != "" { - apiObject.Expression = aws.String(v) + if d.HasChange("load_balancer") { + if v, ok := d.Get("load_balancer").(*schema.Set); ok && v != nil { + input.LoadBalancers = expandLoadBalancers(v.List()) + } } - if v, ok := tfMap["type"].(string); ok && v != "" { - apiObject.Type = aws.String(v) + if d.HasChange("propagate_tags") { + input.PropagateTags = aws.String(d.Get("propagate_tags").(string)) } - if err := validPlacementConstraint(aws.StringValue(apiObject.Type), aws.StringValue(apiObject.Expression)); err != nil { - return result, err + if d.HasChange("service_connect_configuration") { + input.ServiceConnectConfiguration = expandServiceConnectConfiguration(d.Get("service_connect_configuration").([]interface{})) } - result = append(result, apiObject) - } + if d.HasChange("service_registries") { + input.ServiceRegistries = expandServiceRegistries(d.Get("service_registries").([]interface{})) + } + + log.Printf("[DEBUG] Updating ECS Service (%s): %s", d.Id(), input) + // Retry due to IAM eventual consistency + err := resource.Retry(propagationTimeout+serviceUpdateTimeout, func() *resource.RetryError { + _, err := conn.UpdateService(input) + + if err != nil { + 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) + } + + return resource.NonRetryableError(err) + } + return nil + }) + + if tfresource.TimedOut(err) { + _, err = conn.UpdateService(input) + } + + if err != nil { + return fmt.Errorf("error updating ECS Service (%s): %w", d.Id(), err) + } + + cluster := d.Get("cluster").(string) + if d.Get("wait_for_steady_state").(bool) { + if _, err := waitServiceStable(conn, d.Id(), cluster, d.Timeout(schema.TimeoutUpdate)); err != nil { + return fmt.Errorf("error waiting for ECS service (%s) to reach steady state after update: %w", d.Id(), err) + } + } else { + if _, err := waitServiceActive(conn, d.Id(), cluster, d.Timeout(schema.TimeoutUpdate)); err != nil { + return fmt.Errorf("error waiting for ECS service (%s) to become active after update: %w", d.Id(), err) + } + } + } + + 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 verify.ErrorISOUnsupported(conn.PartitionID, err) { + log.Printf("[WARN] failed updating tags for ECS Service (%s): %s", d.Id(), err) + return resourceServiceRead(d, meta) + } + + if err != nil { + return fmt.Errorf("failed updating tags for ECS Service (%s): %w", d.Id(), err) + } + } + + return resourceServiceRead(d, meta) +} + +func resourceServiceDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).ECSConn + + service, err := FindServiceNoTagsByID(context.TODO(), conn, d.Id(), d.Get("cluster").(string)) + if tfresource.NotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("error retrieving ECS Service (%s) for deletion: %w", d.Id(), err) + } + + if aws.StringValue(service.Status) == serviceStatusInactive { + return nil + } + + // Drain the ECS service + if aws.StringValue(service.Status) != serviceStatusDraining && aws.StringValue(service.SchedulingStrategy) != ecs.SchedulingStrategyDaemon { + log.Printf("[DEBUG] Draining ECS Service (%s)", d.Id()) + _, err = conn.UpdateService(&ecs.UpdateServiceInput{ + Service: aws.String(d.Id()), + Cluster: aws.String(d.Get("cluster").(string)), + DesiredCount: aws.Int64(0), + }) + if err != nil { + return err + } + } + + input := ecs.DeleteServiceInput{ + Service: aws.String(d.Id()), + Cluster: aws.String(d.Get("cluster").(string)), + } + // Wait until the ECS service is drained + err = resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + _, err := conn.DeleteService(&input) + + if err != nil { + if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "The service cannot be stopped while deployments are active.") { + return resource.RetryableError(err) + } + + if tfawserr.ErrMessageContains(err, "DependencyViolation", "has a dependent object") { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + _, err = conn.DeleteService(&input) + } + + if err != nil { + return fmt.Errorf("error deleting ECS Service (%s): %w", d.Id(), err) + } + + if err := waitServiceInactive(conn, d.Id(), d.Get("cluster").(string), d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("error waiting for ECS Service (%s) to be deleted: %w", d.Id(), 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 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 } @@ -1184,252 +1439,6 @@ func flattenServiceRegistries(srs []*ecs.ServiceRegistry) []map[string]interface 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("enable_execute_command") { - input.EnableExecuteCommand = aws.Bool(d.Get("enable_execute_command").(bool)) - } - - if d.HasChange("enable_ecs_managed_tags") { - input.EnableECSManagedTags = aws.Bool(d.Get("enable_ecs_managed_tags").(bool)) - } - - if d.HasChange("load_balancer") { - if v, ok := d.Get("load_balancer").(*schema.Set); ok && v != nil { - input.LoadBalancers = expandLoadBalancers(v.List()) - } - } - - if d.HasChange("propagate_tags") { - 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{})) - } - - log.Printf("[DEBUG] Updating ECS Service (%s): %s", d.Id(), input) - // Retry due to IAM eventual consistency - err := resource.Retry(propagationTimeout+serviceUpdateTimeout, func() *resource.RetryError { - _, err := conn.UpdateService(input) - - if err != nil { - 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) - } - - return resource.NonRetryableError(err) - } - return nil - }) - - if tfresource.TimedOut(err) { - _, err = conn.UpdateService(input) - } - - if err != nil { - return fmt.Errorf("error updating ECS Service (%s): %w", d.Id(), err) - } - - cluster := d.Get("cluster").(string) - if d.Get("wait_for_steady_state").(bool) { - if _, err := waitServiceStable(conn, d.Id(), cluster, d.Timeout(schema.TimeoutUpdate)); err != nil { - return fmt.Errorf("error waiting for ECS service (%s) to reach steady state after update: %w", d.Id(), err) - } - } else { - if _, err := waitServiceActive(conn, d.Id(), cluster, d.Timeout(schema.TimeoutUpdate)); err != nil { - return fmt.Errorf("error waiting for ECS service (%s) to become active after update: %w", d.Id(), err) - } - } - } - - 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 verify.ErrorISOUnsupported(conn.PartitionID, err) { - log.Printf("[WARN] failed updating tags for ECS Service (%s): %s", d.Id(), err) - return resourceServiceRead(d, meta) - } - - if err != nil { - return fmt.Errorf("failed updating tags for ECS Service (%s): %w", d.Id(), err) - } - } - - return resourceServiceRead(d, meta) -} - -func resourceServiceDelete(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).ECSConn - - service, err := FindServiceNoTagsByID(context.TODO(), conn, d.Id(), d.Get("cluster").(string)) - if tfresource.NotFound(err) { - return nil - } - if err != nil { - return fmt.Errorf("error retrieving ECS Service (%s) for deletion: %w", d.Id(), err) - } - - if aws.StringValue(service.Status) == serviceStatusInactive { - return nil - } - - // Drain the ECS service - if aws.StringValue(service.Status) != serviceStatusDraining && aws.StringValue(service.SchedulingStrategy) != ecs.SchedulingStrategyDaemon { - log.Printf("[DEBUG] Draining ECS Service (%s)", d.Id()) - _, err = conn.UpdateService(&ecs.UpdateServiceInput{ - Service: aws.String(d.Id()), - Cluster: aws.String(d.Get("cluster").(string)), - DesiredCount: aws.Int64(0), - }) - if err != nil { - return err - } - } - - input := ecs.DeleteServiceInput{ - Service: aws.String(d.Id()), - Cluster: aws.String(d.Get("cluster").(string)), - } - // Wait until the ECS service is drained - err = resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { - _, err := conn.DeleteService(&input) - - if err != nil { - if tfawserr.ErrMessageContains(err, ecs.ErrCodeInvalidParameterException, "The service cannot be stopped while deployments are active.") { - return resource.RetryableError(err) - } - - if tfawserr.ErrMessageContains(err, "DependencyViolation", "has a dependent object") { - return resource.RetryableError(err) - } - - return resource.NonRetryableError(err) - } - - return nil - }) - - if tfresource.TimedOut(err) { - _, err = conn.DeleteService(&input) - } - - if err != nil { - return fmt.Errorf("error deleting ECS Service (%s): %w", d.Id(), err) - } - - if err := waitServiceInactive(conn, d.Id(), d.Get("cluster").(string), d.Timeout(schema.TimeoutDelete)); err != nil { - return fmt.Errorf("error waiting for ECS Service (%s) to be deleted: %w", d.Id(), err) - } - - return nil -} - func resourceLoadBalancerHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) From af3c42c2c8828ba85babfbbfcba1e7b87f6a9908 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 10:59:34 -0500 Subject: [PATCH 15/26] r/aws_ecs_service: Fix 'resource aws_ecs_service: enabled: Default cannot be set with Required'. --- internal/service/ecs/service.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index fdbebb17f1d7..747c3e8d1d24 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -302,7 +302,6 @@ func ResourceService() *schema.Resource { "enabled": { Type: schema.TypeBool, Required: true, - Default: false, }, "log_configuration": { Type: schema.TypeList, From 184dd2d100c5bfaa73c54cb5c9ac1252682315c0 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 11:03:15 -0500 Subject: [PATCH 16/26] r/aws_ecs_cluster: 'service_connect_defaults' is not Computed. --- internal/service/ecs/cluster.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index eb3fa9d86bab..0ad40003571f 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -136,7 +136,6 @@ func ResourceCluster() *schema.Resource { "service_connect_defaults": { Type: schema.TypeList, Optional: true, - Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ From faafff35b8827a7bd1999bd00451461ab262622f Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 12:08:11 -0500 Subject: [PATCH 17/26] r/aws_ecs_cluster: Correct acceptance test configurations. Acceptance test output: % make testacc TESTARGS='-run=TestAccECSCluster_\|TestAccECSClusterDataSource_' PKG=ecs ACCTEST_PARALLELISM=3 ==> Checking that code complies with gofmt requirements... TF_ACC=1 go test ./internal/service/ecs/... -v -count 1 -parallel 3 -run=TestAccECSCluster_\|TestAccECSClusterDataSource_ -timeout 180m === RUN TestAccECSClusterDataSource_ecsCluster === PAUSE TestAccECSClusterDataSource_ecsCluster === RUN TestAccECSClusterDataSource_ecsClusterContainerInsights === PAUSE TestAccECSClusterDataSource_ecsClusterContainerInsights === RUN TestAccECSCluster_basic === PAUSE TestAccECSCluster_basic === RUN TestAccECSCluster_disappears === PAUSE TestAccECSCluster_disappears === RUN TestAccECSCluster_tags === PAUSE TestAccECSCluster_tags === RUN TestAccECSCluster_serviceConnectDefaults === PAUSE TestAccECSCluster_serviceConnectDefaults === RUN TestAccECSCluster_singleCapacityProvider === PAUSE TestAccECSCluster_singleCapacityProvider === RUN TestAccECSCluster_capacityProviders === PAUSE TestAccECSCluster_capacityProviders === RUN TestAccECSCluster_capacityProvidersUpdate === PAUSE TestAccECSCluster_capacityProvidersUpdate === RUN TestAccECSCluster_capacityProvidersNoStrategy === PAUSE TestAccECSCluster_capacityProvidersNoStrategy === RUN TestAccECSCluster_containerInsights === PAUSE TestAccECSCluster_containerInsights === RUN TestAccECSCluster_configuration === PAUSE TestAccECSCluster_configuration === CONT TestAccECSClusterDataSource_ecsCluster === CONT TestAccECSCluster_configuration === CONT TestAccECSCluster_containerInsights --- PASS: TestAccECSClusterDataSource_ecsCluster (31.39s) === CONT TestAccECSCluster_capacityProvidersNoStrategy --- PASS: TestAccECSCluster_configuration (63.01s) === CONT TestAccECSCluster_capacityProvidersUpdate --- PASS: TestAccECSCluster_containerInsights (81.57s) === CONT TestAccECSCluster_capacityProviders --- PASS: TestAccECSCluster_capacityProvidersNoStrategy (58.52s) === CONT TestAccECSCluster_singleCapacityProvider --- PASS: TestAccECSCluster_capacityProviders (42.00s) === CONT TestAccECSCluster_serviceConnectDefaults --- PASS: TestAccECSCluster_capacityProvidersUpdate (82.57s) === CONT TestAccECSCluster_tags --- PASS: TestAccECSCluster_singleCapacityProvider (78.18s) === CONT TestAccECSCluster_disappears --- PASS: TestAccECSCluster_serviceConnectDefaults (142.47s) === CONT TestAccECSCluster_basic --- PASS: TestAccECSCluster_disappears (24.96s) === CONT TestAccECSClusterDataSource_ecsClusterContainerInsights --- PASS: TestAccECSCluster_tags (55.91s) --- PASS: TestAccECSCluster_basic (29.97s) --- PASS: TestAccECSClusterDataSource_ecsClusterContainerInsights (28.05s) PASS ok github.com/hashicorp/terraform-provider-aws/internal/service/ecs 229.778s --- internal/service/ecs/cluster.go | 5 +- internal/service/ecs/cluster_data_source.go | 2 +- .../service/ecs/cluster_data_source_test.go | 1 + internal/service/ecs/cluster_test.go | 170 ++++++++++-------- 4 files changed, 102 insertions(+), 76 deletions(-) diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index 0ad40003571f..4f67c24b2339 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -140,8 +140,9 @@ func ResourceCluster() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "namespace": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, }, }, }, diff --git a/internal/service/ecs/cluster_data_source.go b/internal/service/ecs/cluster_data_source.go index 677a02b48dd8..c258ad8bc4c2 100644 --- a/internal/service/ecs/cluster_data_source.go +++ b/internal/service/ecs/cluster_data_source.go @@ -89,7 +89,7 @@ func dataSourceClusterRead(ctx context.Context, d *schema.ResourceData, meta int if cluster.ServiceConnectDefaults != nil { if err := d.Set("service_connect_defaults", []interface{}{flattenClusterServiceConnectDefaults(cluster.ServiceConnectDefaults)}); err != nil { - return diag.Errorf("setting service_connect_defaults: %w", err) + return diag.Errorf("setting service_connect_defaults: %s", err) } } else { d.Set("service_connect_defaults", 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 02f6e10edc1b..8b6ebe4a5817 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,42 +28,20 @@ func TestAccECSCluster_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccClusterConfig_basic(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckClusterExists(resourceName, &cluster1), - acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "ecs", fmt.Sprintf("cluster/%s", rName)), - resource.TestCheckResourceAttr(resourceName, "name", rName), - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), - ), - }, - { - ResourceName: resourceName, - ImportStateId: rName, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func TestAccECSCluster_serviceConnectionDefaults(t *testing.T) { - var cluster1 ecs.Cluster - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceName := "aws_ecs_cluster.test" - - 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_serviceConnectionDefaults(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"), - resource.TestCheckResourceAttr(resourceName, "namespace", "testnam1"), ), }, { @@ -77,7 +55,7 @@ func TestAccECSCluster_serviceConnectionDefaults(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" @@ -90,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, @@ -100,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" @@ -113,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"), ), @@ -127,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"), @@ -136,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"), ), @@ -145,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{ @@ -158,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), ), @@ -417,49 +434,68 @@ 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) } -func testAccClusterConfig_serviceConnectionDefaults(rName string) string { +func testAccClusterConfig_tags1(rName, tag1Key, tag1Value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { - name = %q - - service_connection_defaults = { - namespace = "testnam1" + name = %[1]q + + tags = { + %[2]q = %[3]q } } -`, rName) +`, rName, tag1Key, tag1Value) } -func testAccClusterConfig_tags1(rName, tag1Key, tag1Value string) string { +func testAccClusterConfig_tags2(rName, tag1Key, tag1Value, tag2Key, tag2Value string) string { return fmt.Sprintf(` resource "aws_ecs_cluster" "test" { - name = %q + name = %[1]q tags = { - %q = %q + %[2]q = %[3]q + %[4]q = %[5]q } } -`, rName, tag1Key, tag1Value) +`, rName, tag1Key, tag1Value, tag2Key, tag2Value) } -func testAccClusterCapacityProviderConfig(rName string) string { - return testAccCapacityProviderBaseConfig(rName) + fmt.Sprintf(` +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 @@ -471,7 +507,7 @@ resource "aws_ecs_cluster" "test" { weight = 1 } } -`, rName) +`, rName)) } func testAccClusterConfig_capacityProviders(rName string) string { @@ -584,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 From ffac43d8ae6bb86264d5960d933062ea2e2c7474 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 12:15:42 -0500 Subject: [PATCH 18/26] ECS Cluster: Update documentation. --- website/docs/d/ecs_cluster.html.markdown | 4 ++-- website/docs/r/ecs_cluster.html.markdown | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/website/docs/d/ecs_cluster.html.markdown b/website/docs/d/ecs_cluster.html.markdown index 2170f0c96f08..722c8312c96e 100644 --- a/website/docs/d/ecs_cluster.html.markdown +++ b/website/docs/d/ecs_cluster.html.markdown @@ -34,5 +34,5 @@ In addition to all arguments above, the following attributes are exported: * `pending_tasks_count` - Number of pending tasks for the ECS Cluster * `running_tasks_count` - Number of running tasks for the ECS Cluster * `registered_container_instances_count` - The number of registered container instances for the ECS Cluster -* `service_connection_defaults` - The namespace of the service connection service created in the cluster -* `setting` - Settings associated with the ECS Cluster. \ No newline at end of file +* `service_connect_defaults` - The default Service Connect namespace +* `setting` - Settings associated with the ECS Cluster \ No newline at end of file diff --git a/website/docs/r/ecs_cluster.html.markdown b/website/docs/r/ecs_cluster.html.markdown index 9eb8ffec640c..53fc46666054 100644 --- a/website/docs/r/ecs_cluster.html.markdown +++ b/website/docs/r/ecs_cluster.html.markdown @@ -90,6 +90,7 @@ The following arguments are supported: * `configuration` - (Optional) The execute command configuration for the cluster. Detailed below. * `default_capacity_provider_strategy` - (Optional, **Deprecated** use the `aws_ecs_cluster_capacity_providers` resource instead) Configuration block for capacity provider strategy to use by default for the cluster. Can be one or more. Detailed below. * `name` - (Required) Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) +* `service_connect_defaults` - (Optional) Configures a default Service Connect namespace. Detailed below. * `setting` - (Optional) Configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster. Detailed below. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. @@ -122,6 +123,10 @@ The following arguments are supported: * `name` - (Required) Name of the setting to manage. Valid values: `containerInsights`. * `value` - (Required) The value to assign to the setting. Valid values are `enabled` and `disabled`. +### `service_connect_defaults` + +* `namespace` - (Required) The ARN of the [`aws_service_discovery_http_namespace`](/docs/providers/aws/r/service_discovery_http_namespace.html) that's used when you create a service and don't specify a Service Connect configuration. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From 48a54d21f5af3959b74ef3763ce00e1e3821d0c5 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:32:19 -0500 Subject: [PATCH 19/26] r/aws_service_discovery_http_namespace: Retry delete on ResourceInUse errors (ECS Service Connect). --- internal/service/servicediscovery/http_namespace.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/service/servicediscovery/http_namespace.go b/internal/service/servicediscovery/http_namespace.go index 08e3ffb85225..6939850d55d2 100644 --- a/internal/service/servicediscovery/http_namespace.go +++ b/internal/service/servicediscovery/http_namespace.go @@ -3,6 +3,7 @@ package servicediscovery import ( "context" "log" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -163,9 +164,11 @@ func resourceHTTPNamespaceDelete(ctx context.Context, d *schema.ResourceData, me conn := meta.(*conns.AWSClient).ServiceDiscoveryConn log.Printf("[INFO] Deleting Service Discovery HTTP Namespace: %s", d.Id()) - output, err := conn.DeleteNamespaceWithContext(ctx, &servicediscovery.DeleteNamespaceInput{ - Id: aws.String(d.Id()), - }) + outputRaw, err := tfresource.RetryWhenAWSErrCodeEqualsContext(ctx, 2*time.Minute, func() (interface{}, error) { + return conn.DeleteNamespaceWithContext(ctx, &servicediscovery.DeleteNamespaceInput{ + Id: aws.String(d.Id()), + }) + }, servicediscovery.ErrCodeResourceInUse) if tfawserr.ErrCodeEquals(err, servicediscovery.ErrCodeNamespaceNotFound) { return nil @@ -175,7 +178,7 @@ func resourceHTTPNamespaceDelete(ctx context.Context, d *schema.ResourceData, me return diag.Errorf("deleting Service Discovery HTTP Namespace (%s): %s", d.Id(), err) } - if output != nil && output.OperationId != nil { + if output := outputRaw.(*servicediscovery.DeleteNamespaceOutput); output != nil && output.OperationId != nil { if _, err := WaitOperationSuccess(ctx, conn, aws.StringValue(output.OperationId)); err != nil { return diag.Errorf("waiting for Service Discovery HTTP Namespace (%s) delete: %s", d.Id(), err) } From b7343fd048395cffef8b035f2bfb3b7500b619e8 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:33:16 -0500 Subject: [PATCH 20/26] r/aws_ecs_service: 'client_aliases' -> 'client_alias'. --- internal/service/ecs/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 747c3e8d1d24..933a671484c3 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -349,7 +349,7 @@ func ResourceService() *schema.Resource { MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "client_aliases": { + "client_alias": { Type: schema.TypeList, Required: true, Elem: &schema.Resource{ @@ -1370,7 +1370,7 @@ func expandServices(srv []interface{}) []*ecs.ServiceConnectService { } var config ecs.ServiceConnectService - if v, ok := raw["client_aliases"].([]interface{}); ok && len(v) > 0 { + if v, ok := raw["client_alias"].([]interface{}); ok && len(v) > 0 { config.ClientAliases = expandClientAliases(v) } if v, ok := raw["discovery_name"].(string); ok && v != "" { From 507b397e25ddacdfc1761db2e9578d95d9924fbf Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:41:51 -0500 Subject: [PATCH 21/26] r/aws_ecs_service: 'services' -> 'service'. --- internal/service/ecs/service.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 933a671484c3..9dec59c62c82 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -327,11 +327,11 @@ func ResourceService() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, - Optional: true, + Required: true, }, "value_from": { Type: schema.TypeString, - Optional: true, + Required: true, }, }, }, @@ -343,7 +343,7 @@ func ResourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "services": { + "service": { Type: schema.TypeList, Optional: true, MaxItems: 1, @@ -1304,7 +1304,7 @@ func expandServiceConnectConfiguration(sc []interface{}) *ecs.ServiceConnectConf config.Namespace = aws.String(v) } - if v, ok := raw["services"].([]interface{}); ok && len(v) > 0 { + if v, ok := raw["service"].([]interface{}); ok && len(v) > 0 { config.Services = expandServices(v) } From 7da83a8e5caff40a66bfe26abc198fb496c3f183 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:48:04 -0500 Subject: [PATCH 22/26] ECS Service: Update documentation. --- website/docs/r/ecs_service.html.markdown | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/website/docs/r/ecs_service.html.markdown b/website/docs/r/ecs_service.html.markdown index ca649b10a908..1a7260e67488 100644 --- a/website/docs/r/ecs_service.html.markdown +++ b/website/docs/r/ecs_service.html.markdown @@ -129,6 +129,7 @@ The following arguments are optional: * `platform_version` - (Optional) Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST`. More information about Fargate platform versions can be found in the [AWS ECS User Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html). * `propagate_tags` - (Optional) Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION`. * `scheduling_strategy` - (Optional) Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA`. Note that [*Tasks using the Fargate launch type or the `CODE_DEPLOY` or `EXTERNAL` deployment controller types don't support the `DAEMON` scheduling strategy*](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_CreateService.html). +* `service_connect_configuration` - (Optional) The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace. See below. * `service_registries` - (Optional) Service discovery registries for the service. The maximum number of `service_registries` blocks is `1`. See below. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `task_definition` - (Optional) Family and revision (`family:revision`) or full ARN of the task definition that you want to run in your service. Required unless using the `EXTERNAL` deployment controller. If a revision is not specified, the latest `ACTIVE` revision is used. @@ -205,6 +206,46 @@ For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonEC * `container_port` - (Optional) Port value, already specified in the task definition, to be used for your service discovery service. * `container_name` - (Optional) Container name value, already specified in the task definition, to be used for your service discovery service. +### service_connect_configuration + +`service_connect_configuration` supports the following: + +* `enabled` - (Required) Specifies whether to use Service Connect with this service. +* `log_configuration` - (Optional) The log configuration for the container. See below. +* `namespace` - (Optional) The namespace name or ARN of the [`aws_service_discovery_http_namespace`](/docs/providers/aws/r/service_discovery_http_namespace.html) for use with Service Connect. +* `service` - (Optional) The list of Service Connect service objects. See below. + +### log_configuration + +`log_configuration` supports the following: + +* `log_driver` - (Optional) The log driver to use for the container. +* `options` - (Optional) The configuration options to send to the log driver. +* `secret_option` - (Optional) The secrets to pass to the log configuration. See below. + +### secret_option + +`secret_option` supports the following: + +* `name` - (Optional) The name of the secret. +* `value_from` - (Optional) The secret to expose to the container. The supported values are either the full ARN of the AWS Secrets Manager secret or the full ARN of the parameter in the SSM Parameter Store. + +### service + +`service` supports the following: + +* `client_alias` - (Optional) The list of client aliases for this Service Connect service. You use these to assign names that can be used by client applications. The maximum number of client aliases that you can have in this list is 1. See below. +* `discovery_name` - (Optional) The name of the new AWS Cloud Map service that Amazon ECS creates for this Amazon ECS service. +* `ingress_port_override` - (Optional) The port number for the Service Connect proxy to listen on. +* `port_name` - (Required) The name of one of the `portMappings` from all the containers in the task definition of this Amazon ECS service. + +### client_alias + +`client_alias` supports the following: + +* `dns_name` - (Optional) The name that you use in the applications of client tasks to connect to this service. +* `port` - (Required) The listening port number for the Service Connect proxy. This port is available inside of all of the tasks within the same namespace. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From ae7ff9ecd6481680dd488ebf044bfcff904859de Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:48:32 -0500 Subject: [PATCH 23/26] r/aws_ecs_service: 'secret_options' -> 'secret_option'. --- internal/service/ecs/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index 9dec59c62c82..dadfbb4033fc 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -320,7 +320,7 @@ func ResourceService() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "secret_options": { + "secret_option": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ @@ -1324,7 +1324,7 @@ func expandLogConfiguration(lc []interface{}) *ecs.LogConfiguration { if v, ok := raw["options"].(map[string]interface{}); ok && len(v) > 0 { config.Options = flex.ExpandStringMap(v) } - if v, ok := raw["secret_options"].([]interface{}); ok && len(v) > 0 { + if v, ok := raw["secret_option"].([]interface{}); ok && len(v) > 0 { config.SecretOptions = expandSecretOptions(v) } From 2e36648edacc31dedf4ce3dffd674ec2f9c2a053 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:49:34 -0500 Subject: [PATCH 24/26] r/aws_ecs_service: Correct acceptance test configurations. --- internal/service/ecs/service_test.go | 307 +++++++++++++++++++++------ 1 file changed, 238 insertions(+), 69 deletions(-) diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index af4b9a443ea0..5b4a3dbf9147 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -685,6 +685,42 @@ func TestAccECSService_forceNewDeployment(t *testing.T) { }) } +func TestAccECSService_forceNewDeploymentTriggers(t *testing.T) { + var service1, service2 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_forceNewDeployment(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service1), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"), + ), + }, + { + Config: testAccServiceConfig_forceNewDeploymentTriggers(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service2), + testAccCheckServiceNotRecreated(&service1, &service2), + resource.TestCheckResourceAttr(resourceName, "force_new_deployment", "true"), + resource.TestCheckResourceAttrSet(resourceName, "triggers.update"), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.type", "binpack"), + resource.TestCheckResourceAttr(resourceName, "ordered_placement_strategy.0.field", "memory"), + ), + }, + }, + }) +} + func TestAccECSService_PlacementStrategy_basic(t *testing.T) { var service1, service2, service3, service4 ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1080,33 +1116,39 @@ func TestAccECSService_ServiceRegistries_basic(t *testing.T) { }) } -func TestAccECSService_ServiceConnect_configuration(t *testing.T) { +func TestAccECSService_ServiceRegistries_container(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) }, + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(servicediscovery.EndpointsID, t) }, ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccServiceConnect_configuration(rName), + Config: testAccServiceConfig_registriesContainer(rName), Check: resource.ComposeTestCheckFunc( testAccCheckServiceExists(resourceName, &service), - resource.TestCheckResourceAttr(resourceName, "service_connect_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), ), }, }, }) } -func TestAccECSService_ServiceRegistries_container(t *testing.T) { +func TestAccECSService_ServiceRegistries_changes(t *testing.T) { var service ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + serviceDiscoveryName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + 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), @@ -1114,7 +1156,14 @@ func TestAccECSService_ServiceRegistries_container(t *testing.T) { CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccServiceConfig_registriesContainer(rName), + Config: testAccServiceConfig_registriesChanges(rName, serviceDiscoveryName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(resourceName, &service), + resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), + ), + }, + { + Config: testAccServiceConfig_registriesChanges(rName, updatedServiceDiscoveryName), Check: resource.ComposeTestCheckFunc( testAccCheckServiceExists(resourceName, &service), resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), @@ -1124,35 +1173,44 @@ func TestAccECSService_ServiceRegistries_container(t *testing.T) { }) } -func TestAccECSService_ServiceRegistries_changes(t *testing.T) { +func TestAccECSService_ServiceConnect_basic(t *testing.T) { var service ecs.Service rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - serviceDiscoveryName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - 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) }, + PreCheck: func() { acctest.PreCheck(t) }, ErrorCheck: acctest.ErrorCheck(t, ecs.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckServiceDestroy, Steps: []resource.TestStep{ { - Config: testAccServiceConfig_registriesChanges(rName, serviceDiscoveryName), + Config: testAccServiceConfig_serviceConnectBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckServiceExists(resourceName, &service), - resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), + 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_registriesChanges(rName, updatedServiceDiscoveryName), + Config: testAccServiceConfig_serviceConnectAllAttributes(rName), Check: resource.ComposeTestCheckFunc( testAccCheckServiceExists(resourceName, &service), - resource.TestCheckResourceAttr(resourceName, "service_registries.#", "1"), + resource.TestCheckResourceAttr(resourceName, "service_connect_configuration.#", "1"), ), }, }, @@ -1582,7 +1640,7 @@ resource "aws_route_table" "test" { resource "aws_route_table_association" "test" { count = 2 - subnet_id = element(aws_subnet.test.*.id, count.index) + subnet_id = element(aws_subnet.test[*].id, count.index) route_table_id = aws_route_table.test.id } @@ -1963,6 +2021,47 @@ resource "aws_ecs_service" "test" { `, rName) } +func testAccServiceConfig_forceNewDeploymentTriggers(rName string) string { + return fmt.Sprintf(` +resource "aws_ecs_cluster" "default" { + name = %[1]q +} + +resource "aws_ecs_task_definition" "test" { + family = %[1]q + + container_definitions = < Date: Tue, 29 Nov 2022 14:53:56 -0500 Subject: [PATCH 25/26] Fix acceptance test configuration terrafmt errors. --- internal/service/ecs/cluster_test.go | 4 ++-- internal/service/ecs/service_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/ecs/cluster_test.go b/internal/service/ecs/cluster_test.go index 8b6ebe4a5817..0597c8767cbf 100644 --- a/internal/service/ecs/cluster_test.go +++ b/internal/service/ecs/cluster_test.go @@ -474,9 +474,9 @@ resource "aws_service_discovery_http_namespace" "test" { resource "aws_ecs_cluster" "test" { name = %[1]q - + service_connect_defaults { - namespace = aws_service_discovery_http_namespace.test[%[3]d].arn + namespace = aws_service_discovery_http_namespace.test[%[3]d].arn } } `, rName, ns, idx) diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index 5b4a3dbf9147..5bb1fc707eee 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -4409,7 +4409,7 @@ resource "aws_ecs_cluster" "test" { service_connect_defaults { namespace = aws_service_discovery_http_namespace.test.arn - } + } } resource "aws_ecs_task_definition" "test" { From ea909a6485f319cf452b3965d2f63c7f4bee3cd1 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 29 Nov 2022 14:54:44 -0500 Subject: [PATCH 26/26] Fix markdown-lint 'MD047/single-trailing-newline Files should end with a single newline character'. --- website/docs/d/ecs_cluster.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/d/ecs_cluster.html.markdown b/website/docs/d/ecs_cluster.html.markdown index 722c8312c96e..dec2728fd822 100644 --- a/website/docs/d/ecs_cluster.html.markdown +++ b/website/docs/d/ecs_cluster.html.markdown @@ -35,4 +35,4 @@ In addition to all arguments above, the following attributes are exported: * `running_tasks_count` - Number of running tasks for the ECS Cluster * `registered_container_instances_count` - The number of registered container instances for the ECS Cluster * `service_connect_defaults` - The default Service Connect namespace -* `setting` - Settings associated with the ECS Cluster \ No newline at end of file +* `setting` - Settings associated with the ECS Cluster