diff --git a/.changelog/38662.txt b/.changelog/38662.txt new file mode 100644 index 00000000000..3cfd1caa714 --- /dev/null +++ b/.changelog/38662.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_service: Add `volume_configuration.managed_ebs_volume.tag_specifications` attribute +``` \ No newline at end of file diff --git a/internal/service/ecs/service.go b/internal/service/ecs/service.go index efc7a9ce3d9..43a817364fe 100644 --- a/internal/service/ecs/service.go +++ b/internal/service/ecs/service.go @@ -1050,6 +1050,31 @@ func resourceService() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "tag_specifications": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrResourceType: { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: enum.Validate[awstypes.EBSResourceType](), + }, + names.AttrPropagateTags: { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if awstypes.PropagateTags(old) == awstypes.PropagateTagsNone && new == "" { + return true + } + return false + }, + ValidateDiagFunc: enum.Validate[awstypes.PropagateTags](), + }, + names.AttrTags: tftags.TagsSchema(), + }, + }, + }, }, }, }, @@ -1202,7 +1227,7 @@ func resourceServiceCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.GetOk("volume_configuration"); ok && len(v.([]interface{})) > 0 { - input.VolumeConfigurations = expandVolumeConfigurations(v.([]interface{})) + input.VolumeConfigurations = expandVolumeConfigurations(ctx, v.([]interface{})) } output, err := retryServiceCreate(ctx, conn, input) @@ -1486,7 +1511,7 @@ func resourceServiceUpdate(ctx context.Context, d *schema.ResourceData, meta int } if d.HasChange("volume_configuration") { - input.VolumeConfigurations = expandVolumeConfigurations(d.Get("volume_configuration").([]interface{})) + input.VolumeConfigurations = expandVolumeConfigurations(ctx, d.Get("volume_configuration").([]interface{})) } // Retry due to IAM eventual consistency. @@ -2258,7 +2283,7 @@ func expandSecretOptions(sop []interface{}) []awstypes.Secret { return out } -func expandVolumeConfigurations(vc []interface{}) []awstypes.ServiceVolumeConfiguration { +func expandVolumeConfigurations(ctx context.Context, vc []interface{}) []awstypes.ServiceVolumeConfiguration { if len(vc) == 0 { return nil } @@ -2273,7 +2298,7 @@ func expandVolumeConfigurations(vc []interface{}) []awstypes.ServiceVolumeConfig } if v, ok := p["managed_ebs_volume"].([]interface{}); ok && len(v) > 0 { - config.ManagedEBSVolume = expandManagedEBSVolume(v) + config.ManagedEBSVolume = expandManagedEBSVolume(ctx, v) } vcs = append(vcs, config) } @@ -2281,7 +2306,7 @@ func expandVolumeConfigurations(vc []interface{}) []awstypes.ServiceVolumeConfig return vcs } -func expandManagedEBSVolume(ebs []interface{}) *awstypes.ServiceManagedEBSVolumeConfiguration { +func expandManagedEBSVolume(ctx context.Context, ebs []interface{}) *awstypes.ServiceManagedEBSVolumeConfiguration { if len(ebs) == 0 { return &awstypes.ServiceManagedEBSVolumeConfiguration{} } @@ -2315,10 +2340,43 @@ func expandManagedEBSVolume(ebs []interface{}) *awstypes.ServiceManagedEBSVolume if v, ok := raw[names.AttrVolumeType].(string); ok && v != "" { config.VolumeType = aws.String(v) } + if v, ok := raw["tag_specifications"].([]interface{}); ok && len(v) > 0 { + config.TagSpecifications = expandTagSpecifications(ctx, v) + } return config } +func expandTagSpecifications(ctx context.Context, ts []interface{}) []awstypes.EBSTagSpecification { + if len(ts) == 0 { + return []awstypes.EBSTagSpecification{} + } + + var s []awstypes.EBSTagSpecification + for _, item := range ts { + raw, ok := item.(map[string]interface{}) + if !ok { + continue + } + + var config awstypes.EBSTagSpecification + if v, ok := raw[names.AttrResourceType].(string); ok && v != "" { + config.ResourceType = awstypes.EBSResourceType(v) + } + if v, ok := raw[names.AttrPropagateTags].(string); ok && v != "" { + config.PropagateTags = awstypes.PropagateTags(v) + } + if v, ok := raw[names.AttrTags].(map[string]any); ok && len(v) > 0 { + if v := tftags.New(ctx, v).IgnoreAWS(); len(v) > 0 { + config.Tags = Tags(v) + } + } + s = append(s, config) + } + + return s +} + func expandServices(srv []interface{}) []awstypes.ServiceConnectService { if len(srv) == 0 { return nil diff --git a/internal/service/ecs/service_test.go b/internal/service/ecs/service_test.go index 3dc620a8f38..0365521f973 100644 --- a/internal/service/ecs/service_test.go +++ b/internal/service/ecs/service_test.go @@ -325,6 +325,28 @@ func TestAccECSService_VolumeConfiguration_basic(t *testing.T) { }) } +func TestAccECSService_VolumeConfiguration_tagSpecifications(t *testing.T) { + ctx := acctest.Context(t) + var service awstypes.Service + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ecs_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckServiceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccServiceConfig_volumeConfiguration_tagSpecifications(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceExists(ctx, resourceName, &service), + ), + }, + }, + }) +} + func TestAccECSService_VolumeConfiguration_update(t *testing.T) { ctx := acctest.Context(t) var service awstypes.Service @@ -2298,6 +2320,34 @@ resource "aws_ecs_service" "test" { `, rName)) } +func testAccServiceConfig_volumeConfiguration_tagSpecifications(rName string) string { + return acctest.ConfigCompose(testAccServiceConfig_baseVolumeConfiguration(rName), fmt.Sprintf(` +resource "aws_ecs_service" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.id + task_definition = aws_ecs_task_definition.test.arn + desired_count = 1 + + volume_configuration { + name = "vol1" + managed_ebs_volume { + role_arn = aws_iam_role.ecs_service.arn + size_in_gb = "8" + tag_specifications { + resource_type = "volume" + propagate_tags = "SERVICE" + tags = { + Name = %[1]q + } + } + } + } + + depends_on = [aws_iam_role_policy.ecs_service] +} +`, rName)) +} + func testAccServiceConfig_volumeConfiguration_update(rName, volumeType string, size int) string { return acctest.ConfigCompose(testAccServiceConfig_baseVolumeConfiguration(rName), fmt.Sprintf(` resource "aws_ecs_service" "test" { diff --git a/website/docs/r/ecs_service.html.markdown b/website/docs/r/ecs_service.html.markdown index 0b8715c5eb3..8be0add8d43 100644 --- a/website/docs/r/ecs_service.html.markdown +++ b/website/docs/r/ecs_service.html.markdown @@ -184,6 +184,7 @@ The `managed_ebs_volume` configuration block supports the following: * `snapshot_id` - (Optional) Snapshot that Amazon ECS uses to create the volume. You must specify either a `size_in_gb` or a `snapshot_id`. * `throughput` - (Optional) Throughput to provision for a volume, in MiB/s, with a maximum of 1,000 MiB/s. * `volume_type` - (Optional) Volume type. +* `tag_specifications` - (Optional) The tags to apply to the volume. [See below](#tag_specifications). ### capacity_provider_strategy @@ -318,6 +319,14 @@ For more information, see [Task Networking](https://docs.aws.amazon.com/AmazonEC * `dns_name` - (Optional) Name that you use in the applications of client tasks to connect to this service. * `port` - (Required) Listening port number for the Service Connect proxy. This port is available inside of all of the tasks within the same namespace. +### tag_specifications + +`tag_specifications` supports the following: + +* `resource_type` - (Required) The type of volume resource. Valid values, `volume`. +* `propagate_tags` - (Optional) Determines whether to propagate the tags from the task definition to the Amazon EBS volume. +* `tags` - (Optional) The tags applied to this Amazon EBS volume. `AmazonECSCreated` and `AmazonECSManaged` are reserved tags that can't be used. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: