Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_ecs_task_definition: Restore container_definitions serialization fidelity #38804

Merged
3 changes: 3 additions & 0 deletions .changelog/38804.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_ecs_task_definition: Prevent lowercasing of the first character of JSON keys in `container_definitions.dockerLabels`
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
package ecs

import (
"fmt"
"sort"
_ "unsafe"

Check failure on line 9 in internal/service/ecs/container_definitions.go

View workflow job for this annotation

GitHub Actions / 2 of 2

blank-imports: a blank import should be only in a main or test package, or have a comment justifying it (revive)

"github.com/aws/aws-sdk-go-v2/aws"
_ "github.com/aws/aws-sdk-go-v2/service/ecs"

Check failure on line 12 in internal/service/ecs/container_definitions.go

View workflow job for this annotation

GitHub Actions / 2 of 2

blank-imports: a blank import should be only in a main or test package, or have a comment justifying it (revive)
awstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
smithyjson "github.com/aws/smithy-go/encoding/json"
tfjson "github.com/hashicorp/terraform-provider-aws/internal/json"
itypes "github.com/hashicorp/terraform-provider-aws/internal/types"
)

func containerDefinitionsAreEquivalent(def1, def2 string, isAWSVPC bool) (bool, error) {
Expand Down Expand Up @@ -143,3 +148,36 @@
return aws.ToString(cd[i].Name) < aws.ToString(cd[j].Name)
})
}

// Dirty hack to avoid any backwards compatibility issues with the AWS SDK for Go v2 migration.
// Reach down into the SDK and use the same serialization function that the SDK uses.
//
//go:linkname serializeContainerDefinitions github.com/aws/aws-sdk-go-v2/service/ecs.awsAwsjson11_serializeDocumentContainerDefinitions
func serializeContainerDefinitions(v []awstypes.ContainerDefinition, value smithyjson.Value) error

func flattenContainerDefinitions(apiObjects []awstypes.ContainerDefinition) (string, error) {
jsonEncoder := smithyjson.NewEncoder()
err := serializeContainerDefinitions(apiObjects, jsonEncoder.Value)

if err != nil {
return "", err
}

return jsonEncoder.String(), nil
}

func expandContainerDefinitions(tfString string) ([]awstypes.ContainerDefinition, error) {
var apiObjects []awstypes.ContainerDefinition

if err := tfjson.DecodeFromString(tfString, &apiObjects); err != nil {
return nil, err
}

for i, apiObject := range apiObjects {
if itypes.IsZero(&apiObject) {
return nil, fmt.Errorf("invalid container definition supplied at index (%d)", i)
}
}

return apiObjects, nil
}
31 changes: 0 additions & 31 deletions internal/service/ecs/task_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/errs"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
"github.com/hashicorp/terraform-provider-aws/internal/flex"
tfjson "github.com/hashicorp/terraform-provider-aws/internal/json"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
itypes "github.com/hashicorp/terraform-provider-aws/internal/types"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)
Expand Down Expand Up @@ -1211,35 +1209,6 @@ func flattenFSxWinVolumeAuthorizationConfig(config *awstypes.FSxWindowsFileServe
return items
}

func flattenContainerDefinitions(apiObjects []awstypes.ContainerDefinition) (string, error) {
json, err := tfjson.EncodeToBytes(apiObjects)
if err != nil {
return "", err
}

// Remove empty fields and convert first character of keys to lowercase.
json = tfjson.RemoveEmptyFields(json)
json = tfjson.KeyFirstLower(json)

return string(json), nil
}

func expandContainerDefinitions(tfString string) ([]awstypes.ContainerDefinition, error) {
var apiObjects []awstypes.ContainerDefinition

if err := tfjson.DecodeFromString(tfString, &apiObjects); err != nil {
return nil, err
}

for i, apiObject := range apiObjects {
if itypes.IsZero(&apiObject) {
return nil, fmt.Errorf("invalid container definition supplied at index (%d)", i)
}
}

return apiObjects, nil
}

func expandTaskDefinitionEphemeralStorage(config []interface{}) *awstypes.EphemeralStorage {
configMap := config[0].(map[string]interface{})

Expand Down
176 changes: 176 additions & 0 deletions internal/service/ecs/task_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,133 @@ func TestAccECSTaskDefinition_v5590ContainerDefinitionsRegression(t *testing.T)
})
}

// https://github.com/hashicorp/terraform-provider-aws/issues/38779.
func TestAccECSTaskDefinition_containerDefinitionEmptyPortMappings(t *testing.T) {
ctx := acctest.Context(t)
var def awstypes.TaskDefinition
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_ecs_task_definition.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID),
CheckDestroy: testAccCheckTaskDefinitionDestroy(ctx),
Steps: []resource.TestStep{
{
ExternalProviders: map[string]resource.ExternalProvider{
"aws": {
Source: "hashicorp/aws",
VersionConstraint: "5.58.0",
},
},
Config: testAccTaskDefinitionConfig_containerDefinitionEmptyPortMappings(rName, "alpine"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTaskDefinitionExists(ctx, resourceName, &def),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length(@)", acctest.Ct1),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].cpu", acctest.Ct10),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].essential", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].image", "alpine"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].memory", "512"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].name", "first"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length([0].portMappings)", acctest.Ct0),
),
},
// At v5.59.0 and v5.60.0, JSON keys were returned with leading capital letters.
{
ExternalProviders: map[string]resource.ExternalProvider{
"aws": {
Source: "hashicorp/aws",
VersionConstraint: "5.60.0",
},
},
Config: testAccTaskDefinitionConfig_containerDefinitionEmptyPortMappings(rName, "jenkins"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTaskDefinitionExists(ctx, resourceName, &def),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length(@)", acctest.Ct1),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].Cpu", acctest.Ct10),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].Essential", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].Image", "jenkins"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].Memory", "512"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].Name", "first"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length([0].PortMappings)", acctest.Ct0),
),
},
// At v5.61.0, all empty values were removed.
{
ExternalProviders: map[string]resource.ExternalProvider{
"aws": {
Source: "hashicorp/aws",
VersionConstraint: "5.61.0",
},
},
Config: testAccTaskDefinitionConfig_containerDefinitionEmptyPortMappings(rName, "nginx"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTaskDefinitionExists(ctx, resourceName, &def),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length(@)", acctest.Ct1),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].cpu", acctest.Ct10),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].essential", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].image", "nginx"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].memory", "512"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].name", "first"),
acctest.CheckResourceAttrJMESNotExists(resourceName, "container_definitions", "[0].portMappings"),
),
},
// At v5.62.0, fidelity with v5.58 was restored.
{
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Config: testAccTaskDefinitionConfig_containerDefinitionEmptyPortMappings(rName, "alpine"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTaskDefinitionExists(ctx, resourceName, &def),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length(@)", acctest.Ct1),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].cpu", acctest.Ct10),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].essential", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].image", "alpine"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].memory", "512"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].name", "first"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length([0].portMappings)", acctest.Ct0),
),
},
},
})
}

// https://github.com/hashicorp/terraform-provider-aws/issues/38782.
func TestAccECSTaskDefinition_containerDefinitionDockerLabels(t *testing.T) {
ctx := acctest.Context(t)
var def awstypes.TaskDefinition
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_ecs_task_definition.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckTaskDefinitionDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccTaskDefinitionConfig_containerDefinitionDockerLabels(rName, "alpine"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTaskDefinitionExists(ctx, resourceName, &def),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "length(@)", acctest.Ct1),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'PROMETHEUS_EXPORTER_JOB_NAME')", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].dockerLabels.PROMETHEUS_EXPORTER_JOB_NAME", "my-job"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'PROMETHEUS_EXPORTER_PATH')", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].dockerLabels.PROMETHEUS_EXPORTER_PATH", "/prometheus"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'PROMETHEUS_EXPORTER_PORT')", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].dockerLabels.PROMETHEUS_EXPORTER_PORT", "12345"),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'PROMETHEUS_TARGET')", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].dockerLabels.PROMETHEUS_TARGET", acctest.CtTrue),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'pROMETHEUS_EXPORTER_JOB_NAME')", acctest.CtFalse),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'pROMETHEUS_EXPORTER_PATH')", acctest.CtFalse),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'pROMETHEUS_EXPORTER_PORT')", acctest.CtFalse),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "keys([0].dockerLabels) | contains(@, 'pROMETHEUS_TARGET')", acctest.CtFalse),
acctest.CheckResourceAttrJMES(resourceName, "container_definitions", "[0].image", "alpine"),
),
},
},
})
}

func testAccCheckTaskDefinitionProxyConfiguration(after *awstypes.TaskDefinition, containerName string, proxyType string,
ignoredUid string, ignoredGid string, appPorts string, proxyIngressPort string, proxyEgressPort string,
egressIgnoredPorts string, egressIgnoredIPs string) resource.TestCheckFunc {
Expand Down Expand Up @@ -3261,3 +3388,52 @@ resource "aws_ecs_task_definition" "test" {
}
`, rName, image)
}

func testAccTaskDefinitionConfig_containerDefinitionEmptyPortMappings(rName, image string) string {
return fmt.Sprintf(`
resource "aws_ecs_task_definition" "test" {
family = %[1]q
container_definitions = jsonencode([
{
name = "first"
image = %[2]q
cpu = 10
memory = 512
essential = true
portMappings = []
}
])
}
`, rName, image)
}

func testAccTaskDefinitionConfig_containerDefinitionDockerLabels(rName, image string) string {
return fmt.Sprintf(`
resource "aws_ecs_task_definition" "test" {
family = %[1]q
container_definitions = jsonencode([
{
name = "first"
image = %[2]q
cpu = 10
memory = 512
essential = true

portMappings = [
{
containerPort = 80
hostPort = 80
}
]

dockerLabels = {
"PROMETHEUS_TARGET" = "true"
"PROMETHEUS_EXPORTER_PORT" = "12345"
"PROMETHEUS_EXPORTER_PATH" = "/prometheus"
"PROMETHEUS_EXPORTER_JOB_NAME" = "my-job"
}
}
])
}
`, rName, image)
}
Loading