diff --git a/CHANGELOG.md b/CHANGELOG.md index a226c8dd..e1d7abd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,14 @@ formae agent. the association is removed. Exposing the resolvable removes the hand-edit; it does not change that ordering. +- `AWS::ECS::TaskDefinition`: `ContainerDefinition.image` now accepts a + resolvable (`String|formae.Resolvable`) instead of a bare `String`, so a + container image can be wired through the resource graph: an + `AWS::CodeBuild::ImageBuild` digest, an `AWS::ECR::Repository` URI, or any + other resolvable image reference. It was the only reference-shaped field on + the resource family still typed as a plain string, which forced the image to + be pinned by hand and re-pinned on every rebuild. + ### Changed - **Breaking.** `AWS::CodeBuild::ImageBuild` is now a pure build-and-push diff --git a/schema/pkl/ecs/taskdefinition.pkl b/schema/pkl/ecs/taskdefinition.pkl index 7ebb3593..76c88da9 100644 --- a/schema/pkl/ecs/taskdefinition.pkl +++ b/schema/pkl/ecs/taskdefinition.pkl @@ -45,7 +45,7 @@ open class ContainerDefinition extends formae.SubResource { firelensConfiguration: FirelensConfiguration? healthCheck: HealthCheck? hostname: String? - image: String + image: String|formae.Resolvable @aws.FieldHint{hasProviderDefault = true} interactive: Boolean? links: Listing? diff --git a/testdata/ecs-taskdefinition-image-ref.pkl b/testdata/ecs-taskdefinition-image-ref.pkl new file mode 100644 index 00000000..fbb70973 --- /dev/null +++ b/testdata/ecs-taskdefinition-image-ref.pkl @@ -0,0 +1,122 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" +import "@formae/formae.pkl" + +import "@aws/aws.pkl" + +import "@aws/ecr/repository.pkl" +import "@aws/iam/role.pkl" +import "@aws/ecs/taskdefinition.pkl" + +local testRunID = read("env:FORMAE_TEST_RUN_ID") +local stackName = "plugin-sdk-test-ecs-taskdefinition-image-ref-\(testRunID)" + +// From-scratch ECR repository whose URI the task definition references via +// `testRepo.res.repositoryUri`. The URI is unknown until the repository is +// created, so it resolves at apply time. ECS validates the image reference's +// syntax (not its existence) at RegisterTaskDefinition, so the repository can +// stay empty. +local testRepo = new repository.Repository { + label = "test-repo-for-ecs-taskdef-image" + repositoryName = "formae-plugin-sdk-test-ecs-td-image-\(testRunID)" +} + +// Fargate rejects a task definition whose image is an ECR reference unless an +// execution role is set, since the agent needs one to pull the image. The role +// must trust `ecs-tasks.amazonaws.com`; its ARN is referenced via +// `executionRole.res.arn`. +local executionRole = new role.Role { + label = "test-execution-role-for-ecs-taskdef-image" + roleName = "formae-plugin-sdk-test-ecs-td-img-role-\(testRunID)" + assumeRolePolicyDocument { + ["Version"] = "2012-10-17" + ["Statement"] { + new { + ["Effect"] = "Allow" + ["Principal"] { + ["Service"] = "ecs-tasks.amazonaws.com" + } + ["Action"] = "sts:AssumeRole" + } + } + } +} + +local testTaskDef = new taskdefinition.TaskDefinition { + label = "plugin-sdk-test-ecs-taskdefinition-image-ref" + family = "formae-sdk-test-td-image-ref-\(testRunID)" + requiresCompatibilities { + "FARGATE" + } + networkMode = "awsvpc" + cpu = "256" + memory = "512" + executionRoleArn = executionRole.res.arn + // AWS returns Tags as [] when unset. The conformance harness's reverse loop + // flags "extra" actual fields not in expected and not marked as + // hasProviderDefault. Set explicitly so the harness sees Tags as "in + // expected" and skips the check. Tags is user-canonical so we don't want + // hasProviderDefault on the schema (would silently drop user changes). + tags = new Listing {} + containerDefinitions { + new { + name = "test-container" + // The container image is the resolved repository URI; the apply only + // succeeds if `image` accepts a Resolvable and substitutes it before + // the RegisterTaskDefinition call. + image = testRepo.res.repositoryUri + essential = true + // AWS returns these container fields as [] / {} when unset. Same harness + // reason as Tags above. Schema deliberately omits hasProviderDefault on + // them because they are user-canonical and the strip would silently drop + // user changes. + command = new Listing {} + credentialSpecs = new Listing {} + dependsOn = new Listing {} + dnsSearchDomains = new Listing {} + dnsServers = new Listing {} + dockerLabels = new Mapping {} + dockerSecurityOptions = new Listing {} + entryPoint = new Listing {} + environment = new Listing {} + environmentFiles = new Listing {} + extraHosts = new Listing {} + links = new Listing {} + mountPoints = new Listing {} + portMappings = new Listing {} + resourceRequirements = new Listing {} + secrets = new Listing {} + systemControls = new Listing {} + ulimits = new Listing {} + volumesFrom = new Listing {} + } + } +} + +forma { + new formae.Stack { + label = stackName + description = "Plugin SDK test for resolving an ECR repository URI (testRepo.res.repositoryUri) in an ECS TaskDefinition container's image" + } + + new formae.Target { + label = "aws-target" + config = new aws.Config { + region = "us-east-1" + } + } + + testRepo + + executionRole + + // TaskDefinition under test, declared last: the conformance harness's + // findTargetResource falls back to the last resource. It is a singleton of + // its type and a deletable leaf (nothing references it). + testTaskDef +}