@@ -2,14 +2,37 @@ import { RepositoryNotFoundException } from "@aws-sdk/client-ecr";
22import { describe , expect , it } from "vitest" ;
33import {
44 ecrImageExists ,
5+ inspectManifest ,
56 interpretBatchGetImageResponse ,
67 parseEcrImageReference ,
8+ treeHasUnpullableLayer ,
79} from "~/v3/services/verifyDeploymentImage.server" ;
810import { type RegistryConfig } from "~/v3/registryConfig.server" ;
911
1012const ECR_HOST = "123456789012.dkr.ecr.us-east-1.amazonaws.com" ;
1113const ecrConfig : RegistryConfig = { host : ECR_HOST , namespace : "deployments-test" } ;
1214
15+ const DIGEST_A = `sha256:${ "a" . repeat ( 64 ) } ` ;
16+ const DIGEST_B = `sha256:${ "b" . repeat ( 64 ) } ` ;
17+ const ZSTD_DOCKER = "application/vnd.docker.image.rootfs.diff.tar.zstd" ;
18+
19+ const imageManifest = ( layerMediaTypes : string [ ] ) =>
20+ JSON . stringify ( {
21+ schemaVersion : 2 ,
22+ mediaType : "application/vnd.docker.distribution.manifest.v2+json" ,
23+ layers : layerMediaTypes . map ( ( mediaType ) => ( { mediaType, digest : DIGEST_A } ) ) ,
24+ } ) ;
25+
26+ const indexManifest = ( childDigests : string [ ] ) =>
27+ JSON . stringify ( {
28+ schemaVersion : 2 ,
29+ mediaType : "application/vnd.oci.image.index.v1+json" ,
30+ manifests : childDigests . map ( ( digest ) => ( {
31+ digest,
32+ mediaType : "application/vnd.oci.image.manifest.v1+json" ,
33+ } ) ) ,
34+ } ) ;
35+
1336describe ( "parseEcrImageReference" , ( ) => {
1437 it ( "splits repository and tag for a ref under the configured host" , ( ) => {
1538 const ref = `${ ECR_HOST } /deployments-test/proj_abc:20240101.1.prod.a1b2c3d4` ;
@@ -59,41 +82,72 @@ describe("interpretBatchGetImageResponse", () => {
5982 ) ;
6083 expect ( interpretBatchGetImageResponse ( { } as any ) ) . toBe ( "unknown" ) ;
6184 } ) ;
85+ } ) ;
6286
63- const manifestWith = ( layerMediaTypes : string [ ] ) =>
64- JSON . stringify ( {
65- schemaVersion : 2 ,
66- mediaType : "application/vnd.docker.distribution.manifest.v2+json" ,
67- layers : layerMediaTypes . map ( ( mediaType ) => ( { mediaType, digest : `sha256:${ "a" . repeat ( 64 ) } ` } ) ) ,
68- } ) ;
87+ describe ( "inspectManifest" , ( ) => {
88+ it ( "flags an unpullable zstd layer in an image manifest" , ( ) => {
89+ const result = inspectManifest (
90+ imageManifest ( [ "application/vnd.docker.image.rootfs.diff.tar.gzip" , ZSTD_DOCKER ] )
91+ ) ;
92+ expect ( result . hasUnpullableLayer ) . toBe ( true ) ;
93+ expect ( result . childDigests ) . toEqual ( [ ] ) ;
94+ } ) ;
6995
70- it ( "returns nonconformant when any layer is a zstd layer in a Docker manifest" , ( ) => {
71- const imageManifest = manifestWith ( [
72- "application/vnd.docker.image.rootfs.diff.tar.gzip" ,
73- "application/vnd.docker.image.rootfs.diff.tar.zstd" ,
74- ] ) ;
75- expect ( interpretBatchGetImageResponse ( { images : [ { imageManifest } ] } as any ) ) . toBe (
76- "nonconformant"
96+ it ( "passes OCI zstd and gzip layers (runtime-supported media types)" , ( ) => {
97+ const result = inspectManifest (
98+ imageManifest ( [
99+ "application/vnd.oci.image.layer.v1.tar+gzip" ,
100+ "application/vnd.oci.image.layer.v1.tar+zstd" ,
101+ ] )
77102 ) ;
103+ expect ( result . hasUnpullableLayer ) . toBe ( false ) ;
78104 } ) ;
79105
80- it ( "returns found for OCI zstd layers (the runtime-supported media type)" , ( ) => {
81- const imageManifest = manifestWith ( [
82- "application/vnd.oci.image.layer.v1.tar+gzip" ,
83- "application/vnd.oci.image.layer.v1.tar+zstd" ,
84- ] ) ;
85- expect ( interpretBatchGetImageResponse ( { images : [ { imageManifest } ] } as any ) ) . toBe ( "found" ) ;
106+ it ( "returns child digests for an index and does not flag it directly" , ( ) => {
107+ const result = inspectManifest ( indexManifest ( [ DIGEST_A , DIGEST_B ] ) ) ;
108+ expect ( result . hasUnpullableLayer ) . toBe ( false ) ;
109+ expect ( result . childDigests ) . toEqual ( [ DIGEST_A , DIGEST_B ] ) ;
86110 } ) ;
87111
88- it ( "returns found (does not block) when the manifest is absent, unparseable, or an index" , ( ) => {
89- expect ( interpretBatchGetImageResponse ( { images : [ { } ] } as any ) ) . toBe ( "found" ) ;
90- expect ( interpretBatchGetImageResponse ( { images : [ { imageManifest : "not json" } ] } as any ) ) . toBe (
91- "found"
112+ it ( "fails open (empty) when the manifest is absent or unparseable" , ( ) => {
113+ expect ( inspectManifest ( undefined ) ) . toEqual ( { hasUnpullableLayer : false , childDigests : [ ] } ) ;
114+ expect ( inspectManifest ( "not json" ) ) . toEqual ( { hasUnpullableLayer : false , childDigests : [ ] } ) ;
115+ } ) ;
116+ } ) ;
117+
118+ describe ( "treeHasUnpullableLayer" , ( ) => {
119+ const neverFetch = async ( ) => undefined ;
120+
121+ it ( "detects an unpullable layer in a flat image manifest" , async ( ) => {
122+ expect ( await treeHasUnpullableLayer ( imageManifest ( [ ZSTD_DOCKER ] ) , neverFetch ) ) . toBe ( true ) ;
123+ } ) ;
124+
125+ it ( "follows an index and detects an unpullable layer in a child" , async ( ) => {
126+ const children : Record < string , string > = {
127+ [ DIGEST_A ] : imageManifest ( [ "application/vnd.oci.image.layer.v1.tar+gzip" ] ) ,
128+ [ DIGEST_B ] : imageManifest ( [ ZSTD_DOCKER ] ) ,
129+ } ;
130+ const result = await treeHasUnpullableLayer (
131+ indexManifest ( [ DIGEST_A , DIGEST_B ] ) ,
132+ async ( digest ) => children [ digest ]
92133 ) ;
93- const index = JSON . stringify ( { schemaVersion : 2 , manifests : [ { digest : "sha256:x" } ] } ) ;
94- expect ( interpretBatchGetImageResponse ( { images : [ { imageManifest : index } ] } as any ) ) . toBe (
95- "found"
134+ expect ( result ) . toBe ( true ) ;
135+ } ) ;
136+
137+ it ( "returns false for an index whose children are all conformant" , async ( ) => {
138+ const children : Record < string , string > = {
139+ [ DIGEST_A ] : imageManifest ( [ "application/vnd.oci.image.layer.v1.tar+zstd" ] ) ,
140+ [ DIGEST_B ] : imageManifest ( [ "application/vnd.docker.image.rootfs.diff.tar.gzip" ] ) ,
141+ } ;
142+ const result = await treeHasUnpullableLayer (
143+ indexManifest ( [ DIGEST_A , DIGEST_B ] ) ,
144+ async ( digest ) => children [ digest ]
96145 ) ;
146+ expect ( result ) . toBe ( false ) ;
147+ } ) ;
148+
149+ it ( "fails open when a child manifest can't be fetched" , async ( ) => {
150+ expect ( await treeHasUnpullableLayer ( indexManifest ( [ DIGEST_A ] ) , neverFetch ) ) . toBe ( false ) ;
97151 } ) ;
98152} ) ;
99153
@@ -210,4 +264,54 @@ describe("ecrImageExists", () => {
210264 ) ;
211265 expect ( seen . imageIds ) . toEqual ( [ { imageTag : "v1.prod.a1b2c3d4" } ] ) ;
212266 } ) ;
267+
268+ // Resolve a manifest by tag or digest against a fixture map, mimicking BatchGetImage.
269+ const sendFrom =
270+ ( byRef : Record < string , string > ) =>
271+ async ( input : any ) : Promise < any > => {
272+ const id = input . imageIds [ 0 ] ;
273+ const manifest = byRef [ id . imageDigest ?? id . imageTag ] ;
274+ return manifest ? { images : [ { imageManifest : manifest } ] } : { images : [ { } ] } ;
275+ } ;
276+
277+ it ( "returns nonconformant for a single-arch image with an unpullable zstd layer" , async ( ) => {
278+ const result = await ecrImageExists (
279+ {
280+ imageReference : `${ ECR_HOST } /deployments-test/proj_abc:v1.prod.a1b2c3d4` ,
281+ registryConfig : ecrConfig ,
282+ } ,
283+ sendFrom ( { "v1.prod.a1b2c3d4" : imageManifest ( [ ZSTD_DOCKER ] ) } )
284+ ) ;
285+ expect ( result ) . toBe ( "nonconformant" ) ;
286+ } ) ;
287+
288+ it ( "returns nonconformant when a multi-arch index has an unpullable child" , async ( ) => {
289+ const result = await ecrImageExists (
290+ {
291+ imageReference : `${ ECR_HOST } /deployments-test/proj_abc:v1.prod.a1b2c3d4` ,
292+ registryConfig : ecrConfig ,
293+ } ,
294+ sendFrom ( {
295+ "v1.prod.a1b2c3d4" : indexManifest ( [ DIGEST_A , DIGEST_B ] ) ,
296+ [ DIGEST_A ] : imageManifest ( [ "application/vnd.oci.image.layer.v1.tar+gzip" ] ) ,
297+ [ DIGEST_B ] : imageManifest ( [ ZSTD_DOCKER ] ) ,
298+ } )
299+ ) ;
300+ expect ( result ) . toBe ( "nonconformant" ) ;
301+ } ) ;
302+
303+ it ( "returns found when a multi-arch index's children are all conformant" , async ( ) => {
304+ const result = await ecrImageExists (
305+ {
306+ imageReference : `${ ECR_HOST } /deployments-test/proj_abc:v1.prod.a1b2c3d4` ,
307+ registryConfig : ecrConfig ,
308+ } ,
309+ sendFrom ( {
310+ "v1.prod.a1b2c3d4" : indexManifest ( [ DIGEST_A , DIGEST_B ] ) ,
311+ [ DIGEST_A ] : imageManifest ( [ "application/vnd.oci.image.layer.v1.tar+zstd" ] ) ,
312+ [ DIGEST_B ] : imageManifest ( [ "application/vnd.docker.image.rootfs.diff.tar.gzip" ] ) ,
313+ } )
314+ ) ;
315+ expect ( result ) . toBe ( "found" ) ;
316+ } ) ;
213317} ) ;
0 commit comments