diff --git a/packages/testcontainers/src/container-runtime/image-name.test.ts b/packages/testcontainers/src/container-runtime/image-name.test.ts index 249ad7d39..10697798c 100644 --- a/packages/testcontainers/src/container-runtime/image-name.test.ts +++ b/packages/testcontainers/src/container-runtime/image-name.test.ts @@ -8,6 +8,9 @@ describe("ContainerImage", { concurrent: false }, () => { expect(imageName.equals(new ImageName("registry", "image", "anotherTag"))).toBe(false); expect(imageName.equals(new ImageName("registry", "anotherImage", "tag"))).toBe(false); expect(imageName.equals(new ImageName("anotherRegistry", "image", "tag"))).toBe(false); + expect(imageName.equals(new ImageName("registry", "image", "tag", "sha256:1234abcd1234abcd1234abcd1234abcd"))).toBe( + false + ); }); describe("string", { concurrent: false }, () => { @@ -31,6 +34,16 @@ describe("ContainerImage", { concurrent: false }, () => { expect(imageName.string).toBe("registry/image@sha256:1234abcd1234abcd1234abcd1234abcd"); }); + it("should work with tag and digest", () => { + const imageName = new ImageName(undefined, "image", "tag", "sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.string).toBe("image:tag@sha256:1234abcd1234abcd1234abcd1234abcd"); + }); + + it("should work with registry, tag and digest", () => { + const imageName = new ImageName("registry", "image", "tag", "sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.string).toBe("registry/image:tag@sha256:1234abcd1234abcd1234abcd1234abcd"); + }); + it("should not append the `latest` tag to image IDs", () => { const imageName = new ImageName( undefined, @@ -74,6 +87,7 @@ describe("ContainerImage", { concurrent: false }, () => { expect(imageName.registry).toBeUndefined(); expect(imageName.image).toBe("image"); expect(imageName.tag).toBe("latest"); + expect(imageName.digest).toBeUndefined(); }); it("should work without tag", () => { @@ -130,6 +144,30 @@ describe("ContainerImage", { concurrent: false }, () => { expect(imageName.registry).toBe(undefined); expect(imageName.image).toBe("image"); expect(imageName.tag).toBe("sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.digest).toBe("sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.string).toBe("image@sha256:1234abcd1234abcd1234abcd1234abcd"); + }); + + it("should work with tag and digest", () => { + const imageName = ImageName.fromString("image:tag@sha256:1234abcd1234abcd1234abcd1234abcd"); + + expect(imageName.registry).toBe(undefined); + expect(imageName.image).toBe("image"); + expect(imageName.tag).toBe("tag"); + expect(imageName.digest).toBe("sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.string).toBe("image:tag@sha256:1234abcd1234abcd1234abcd1234abcd"); + }); + + it("should work with registry with port, nested image, tag and digest", () => { + const imageName = ImageName.fromString( + "domain.com:5000/parent/child:tag@sha256:1234abcd1234abcd1234abcd1234abcd" + ); + + expect(imageName.registry).toBe("domain.com:5000"); + expect(imageName.image).toBe("parent/child"); + expect(imageName.tag).toBe("tag"); + expect(imageName.digest).toBe("sha256:1234abcd1234abcd1234abcd1234abcd"); + expect(imageName.string).toBe("domain.com:5000/parent/child:tag@sha256:1234abcd1234abcd1234abcd1234abcd"); }); it("should work with image being an image ID", () => { diff --git a/packages/testcontainers/src/container-runtime/image-name.ts b/packages/testcontainers/src/container-runtime/image-name.ts index 8e8e85427..863b1c33f 100644 --- a/packages/testcontainers/src/container-runtime/image-name.ts +++ b/packages/testcontainers/src/container-runtime/image-name.ts @@ -8,8 +8,13 @@ export class ImageName { constructor( public readonly registry: string | undefined, public readonly image: string, - public readonly tag: string + public readonly tag: string, + public readonly digest?: string ) { + if (!this.digest && this.tag.startsWith("sha256:")) { + // A digest-only reference (image@sha256:...) keeps the digest as its tag. + this.digest = this.tag; + } if (!this.registry && process.env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX) { const prefix = process.env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX; @@ -31,11 +36,12 @@ export class ImageName { } log.info(message); } + const tagAndDigest = this.digest ? `${this.tag}@${this.digest}` : this.tag; if (this.registry) { if (this.tag.startsWith("sha256:")) { this.string = `${this.registry}/${this.image}@${this.tag}`; } else { - this.string = `${this.registry}/${this.image}:${this.tag}`; + this.string = `${this.registry}/${this.image}:${tagAndDigest}`; } } else if (this.tag === "latest" && ImageName.hexRE.test(this.image)) { // 64 byte hex string. This refers to an image sha256 directly. @@ -48,26 +54,32 @@ export class ImageName { } else if (this.tag.startsWith("sha256:")) { this.string = `${this.image}@${this.tag}`; } else { - this.string = `${this.image}:${this.tag}`; + this.string = `${this.image}:${tagAndDigest}`; } } public equals(other: ImageName): boolean { - return this.registry === other.registry && this.image === other.image && this.tag === other.tag; + return ( + this.registry === other.registry && + this.image === other.image && + this.tag === other.tag && + this.digest === other.digest + ); } public static fromString(string: string): ImageName { const registry = this.getRegistry(string); const stringWithoutRegistry = registry ? string.split("/").slice(1).join("/") : string; - if (stringWithoutRegistry.includes("@")) { - const [image, tag] = stringWithoutRegistry.split("@"); - return new ImageName(registry, image, tag); - } else if (stringWithoutRegistry.includes(":")) { - const [image, tag] = stringWithoutRegistry.split(":"); - return new ImageName(registry, image, tag); + const [imageAndTag, digest] = stringWithoutRegistry.split("@"); + + if (imageAndTag.includes(":")) { + const [image, tag] = imageAndTag.split(":"); + return new ImageName(registry, image, tag, digest); + } else if (digest) { + return new ImageName(registry, imageAndTag, digest); } else { - return new ImageName(registry, stringWithoutRegistry, "latest"); + return new ImageName(registry, imageAndTag, "latest"); } }