Repository: pulumi/pulumi-awsx
Pull request #1467 (2025-01-08 11:57:26)

Fix breaking change in `imageUri` output of `ecr.Image`
I noticed that https://github.com/pulumi/pulumi-awsx/pull/1278 introduced an (unreleased) breaking change in the `imageUri` output of `ecr.Image`.

Before upgrading to docker-build, the component returned URIs in the form of `ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPOSITORY_NAME@sha256:1234567890123456789012345678901234567890123456789012345678901234`.

After the update it returns them in the following form `ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPOSITORY_NAME:TAG_NAME@sha256:1234567890123456789012345678901234567890123456789012345678901234`. (NOTE: they include the tag name now)

While it seems like an innocent change, it breaks the integration with lambda functions. AWS Lambda expects an image URI without the TAG.

The change proposed in this PR adds post processing for the image ref in order to strip the tag from the URI. Additionally it extends the upgrade test to assert on this behavior.

Relates to https://github.com/pulumi/pulumi-awsx/issues/1349


Patch:
diff --git a/awsx/ecr/image.ts b/awsx/ecr/image.ts
index b9eaf4de..b8a192ff 100644
--- a/awsx/ecr/image.ts
+++ b/awsx/ecr/image.ts
@@ -107,7 +107,9 @@ export function computeImageFromAsset(
     pulumi.log.debug(`    build complete: ${ref}`, parent);
   });
 
-  return image.ref;
+  // Return the image reference without the tag. This is necessary for backwards compatibility with earlier versions of the awsx provider
+  // that used pulumi-docker and in order to allow passing this output to Lambda functions (they expect an image URI without a tag).
+  return image.ref.apply(removeTagFromRef);
 }
 
 function createUniqueImageName(inputs: pulumi.Unwrap<schema.DockerBuildInputs>): string {
@@ -128,3 +130,18 @@ function createUniqueImageName(inputs: pulumi.Unwrap<schema.DockerBuildInputs>):
   buildSig += pulumi.getStack();
   return `${utils.sha1hash(buildSig)}-container`;
 }
+
+/**
+ * Removes the tag from the image reference.
+ * @param ref The image reference to remove the tag from.
+ * @returns The image reference without the tag.
+ */
+export function removeTagFromRef(ref: string): string {
+  // Match pattern: everything up to the tag, the tag itself, and the digest
+  // The image ref looks like this: ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPOSITORY_NAME:TAG_NAME@sha256:1234567890123456789012345678901234567890123456789012345678901234
+  // Neither repository name nor tag name can contain a colon, so we can safely split on the colon and take the first part.
+  const pattern = /^(.*):([^@]+)(@sha256:.+)$/;
+
+  // If the pattern is found, return the image without the tag. I.e. only the image name and digest.
+  return ref.replace(pattern, "$1$3");
+}


Test patch:
diff --git a/awsx/ecr/image.test.ts b/awsx/ecr/image.test.ts
new file mode 100644
index 00000000..07a5bbda
--- /dev/null
+++ b/awsx/ecr/image.test.ts
@@ -0,0 +1,55 @@
+// Copyright 2016-2024, Pulumi Corporation.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { removeTagFromRef } from "./image";
+
+describe("removeTagFromRef", () => {
+  it("should remove tag from standard ECR image reference", () => {
+    const input =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/repository-29552ef:7a3c38f0-container@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    const expected =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/repository-29552ef@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    expect(removeTagFromRef(input)).toBe(expected);
+  });
+
+  it("should handle ECR references with nested repository paths", () => {
+    const input =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/team/project/service:v1.0.0@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    const expected =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/team/project/service@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    expect(removeTagFromRef(input)).toBe(expected);
+  });
+
+  it("should handle complex but valid tag names", () => {
+    const input =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo:1.0.0-alpha.1_build-123@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    const expected =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    expect(removeTagFromRef(input)).toBe(expected);
+  });
+
+  it("should return original string if no tag is present", () => {
+    const input =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    expect(removeTagFromRef(input)).toBe(input);
+  });
+
+  it("should handle repository names with underscores and hyphens", () => {
+    const input =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/my_repo-name:latest@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    const expected =
+      "123456789012.dkr.ecr.us-west-2.amazonaws.com/my_repo-name@sha256:ccf572fa3e9a9b9316761f749c3020c5748e1c47052e5781eec04e0a53954428";
+    expect(removeTagFromRef(input)).toBe(expected);
+  });
+});
diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go
index 2b2923a2..0c494329 100644
--- a/examples/examples_nodejs_test.go
+++ b/examples/examples_nodejs_test.go
@@ -400,6 +400,10 @@ func TestDockerUpgrade(t *testing.T) {
 	updatedImageUri := result.Outputs["image"].Value.(string)
 	require.NotEmpty(t, updatedImageUri, "imageUri should not be empty")
 
+	originalImage := strings.Split(originalImageUri, "@sha256:")[0]
+	updatedImage := strings.Split(updatedImageUri, "@sha256:")[0]
+	require.Equal(t, originalImage, updatedImage, "original and updated image should be the same")
+
 	require.Contains(t, result.Outputs, "repositoryName", "repositoryName should be in the outputs")
 	repoName := result.Outputs["repositoryName"].Value.(string)
 	require.NotEmpty(t, repoName, "repositoryName should not be empty")