media type checks
When copying an image, record the compression in the BlobInfo and use
the information when updating the manifest's layer infos to set the
layers' media types correctly.
Also check for supported media types when parsing a v2s2/OCI1 manifest.
Note that consumers of the containers/image library need to update
opencontainers/image-spec to commit 775207bd45b6cb8153ce218cc59351799217451f.
Fixes: github.com/containers/libpod/issues/2013
Fixes: github.com/containers/buildah/issues/1589
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Valentin Rothberg
4 years ago
910 | 910 | return types.BlobInfo{}, errors.Wrap(err, "Error writing blob") |
911 | 911 | } |
912 | 912 | |
913 | uploadedInfo.CompressionOperation = compressionOperation | |
914 | // If we can modify the layer's blob, set the desired algorithm for it to be set in the manifest. | |
915 | if canModifyBlob && !isConfig { | |
916 | uploadedInfo.CompressionAlgorithm = &desiredCompressionFormat | |
917 | } | |
918 | ||
913 | 919 | // This is fairly horrible: the writer from getOriginalLayerCopyWriter wants to consumer |
914 | 920 | // all of the input (to compute DiffIDs), even if dest.PutBlob does not need it. |
915 | 921 | // So, read everything from originalLayerReader, which will cause the rest to be |
26 | 26 | github.com/mattn/go-isatty v0.0.4 // indirect |
27 | 27 | github.com/mtrmac/gpgme v0.0.0-20170102180018-b2432428689c |
28 | 28 | github.com/opencontainers/go-digest v1.0.0-rc1 |
29 | github.com/opencontainers/image-spec v1.0.0 | |
29 | github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 | |
30 | 30 | github.com/opencontainers/selinux v1.2.2 |
31 | 31 | github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913 |
32 | 32 | github.com/pkg/errors v0.8.1 |
74 | 74 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= |
75 | 75 | github.com/opencontainers/image-spec v1.0.0 h1:jcw3cCH887bLKETGYpv8afogdYchbShR0eH6oD9d5PQ= |
76 | 76 | github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= |
77 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= | |
78 | github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 h1:yN8BPXVwMBAm3Cuvh1L5XE8XpvYRMdsVLd82ILprhUU= | |
79 | github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= | |
77 | 80 | github.com/opencontainers/runc v1.0.0-rc8 h1:dDCFes8Hj1r/i5qnypONo5jdOme/8HWZC/aNDyhECt0= |
78 | 81 | github.com/opencontainers/runc v1.0.0-rc8/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= |
79 | 82 | github.com/opencontainers/selinux v1.2.2 h1:Kx9J6eDG5/24A6DtUquGSpJQ+m2MUTahn4FtGEe8bFg= |
97 | 100 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
98 | 101 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= |
99 | 102 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
103 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | |
100 | 104 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
101 | 105 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8= |
102 | 106 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= |
141 | 145 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= |
142 | 146 | gopkg.in/yaml.v2 v2.0.0-20170208141851-a3f3340b5840 h1:BftvRMCaj0KX6UeD7gnNJv0W8b4HAYTEWes978CoWlY= |
143 | 147 | gopkg.in/yaml.v2 v2.0.0-20170208141851-a3f3340b5840/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= |
148 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | |
144 | 149 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
145 | 150 | gotest.tools v0.0.0-20190624233834-05ebafbffc79 h1:C+K4iPg1rIvmCf4JjelkbWv2jeWevEwp05Lz8XfTYgE= |
146 | 151 | gotest.tools v0.0.0-20190624233834-05ebafbffc79/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90= |
5 | 5 | "crypto/sha256" |
6 | 6 | "encoding/hex" |
7 | 7 | "encoding/json" |
8 | "fmt" | |
8 | 9 | "io/ioutil" |
9 | 10 | "strings" |
10 | 11 | |
206 | 207 | layers := make([]imgspecv1.Descriptor, len(m.m.LayersDescriptors)) |
207 | 208 | for idx := range layers { |
208 | 209 | layers[idx] = oci1DescriptorFromSchema2Descriptor(m.m.LayersDescriptors[idx]) |
209 | if m.m.LayersDescriptors[idx].MediaType == manifest.DockerV2Schema2ForeignLayerMediaType { | |
210 | switch m.m.LayersDescriptors[idx].MediaType { | |
211 | case manifest.DockerV2Schema2ForeignLayerMediaType: | |
210 | 212 | layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable |
211 | } else { | |
212 | // we assume layers are gzip'ed because docker v2s2 only deals with | |
213 | // gzip'ed layers. However, OCI has non-gzip'ed layers as well. | |
213 | case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip: | |
214 | layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributableGzip | |
215 | case manifest.DockerV2SchemaLayerMediaTypeUncompressed: | |
216 | layers[idx].MediaType = imgspecv1.MediaTypeImageLayer | |
217 | case manifest.DockerV2Schema2LayerMediaType: | |
214 | 218 | layers[idx].MediaType = imgspecv1.MediaTypeImageLayerGzip |
219 | default: | |
220 | return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", m.m.LayersDescriptors[idx].MediaType) | |
215 | 221 | } |
216 | 222 | } |
217 | 223 |
44 | 44 | panic("Unexpected call to a mock function") |
45 | 45 | } |
46 | 46 | |
47 | func manifestSchema2FromFixture(t *testing.T, src types.ImageSource, fixture string) genericManifest { | |
47 | func manifestSchema2FromFixture(t *testing.T, src types.ImageSource, fixture string, mustFail bool) genericManifest { | |
48 | 48 | manifest, err := ioutil.ReadFile(filepath.Join("fixtures", fixture)) |
49 | 49 | require.NoError(t, err) |
50 | 50 | |
51 | 51 | m, err := manifestSchema2FromManifest(src, manifest) |
52 | require.NoError(t, err) | |
52 | if mustFail { | |
53 | require.Error(t, err) | |
54 | } else { | |
55 | require.NoError(t, err) | |
56 | } | |
53 | 57 | return m |
54 | 58 | } |
55 | 59 | |
90 | 94 | func TestManifestSchema2FromManifest(t *testing.T) { |
91 | 95 | // This just tests that the JSON can be loaded; we test that the parsed |
92 | 96 | // values are correctly returned in tests for the individual getter methods. |
93 | _ = manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json") | |
97 | _ = manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false) | |
94 | 98 | |
95 | 99 | _, err := manifestSchema2FromManifest(nil, []byte{}) |
96 | 100 | assert.Error(t, err) |
104 | 108 | |
105 | 109 | func TestManifestSchema2Serialize(t *testing.T) { |
106 | 110 | for _, m := range []genericManifest{ |
107 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
111 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
108 | 112 | manifestSchema2FromComponentsLikeFixture(nil), |
109 | 113 | } { |
110 | 114 | serialized, err := m.serialize() |
128 | 132 | |
129 | 133 | func TestManifestSchema2ManifestMIMEType(t *testing.T) { |
130 | 134 | for _, m := range []genericManifest{ |
131 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
135 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
132 | 136 | manifestSchema2FromComponentsLikeFixture(nil), |
133 | 137 | } { |
134 | 138 | assert.Equal(t, manifest.DockerV2Schema2MediaType, m.manifestMIMEType()) |
137 | 141 | |
138 | 142 | func TestManifestSchema2ConfigInfo(t *testing.T) { |
139 | 143 | for _, m := range []genericManifest{ |
140 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
144 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
141 | 145 | manifestSchema2FromComponentsLikeFixture(nil), |
142 | 146 | } { |
143 | 147 | assert.Equal(t, types.BlobInfo{ |
194 | 198 | } else { |
195 | 199 | src = nil |
196 | 200 | } |
197 | m := manifestSchema2FromFixture(t, src, "schema2.json") | |
201 | m := manifestSchema2FromFixture(t, src, "schema2.json", false) | |
198 | 202 | blob, err := m.ConfigBlob(context.Background()) |
199 | 203 | if c.blob != nil { |
200 | 204 | assert.NoError(t, err) |
218 | 222 | |
219 | 223 | func TestManifestSchema2LayerInfo(t *testing.T) { |
220 | 224 | for _, m := range []genericManifest{ |
221 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
225 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
222 | 226 | manifestSchema2FromComponentsLikeFixture(nil), |
223 | 227 | } { |
224 | 228 | assert.Equal(t, []types.BlobInfo{ |
253 | 257 | |
254 | 258 | func TestManifestSchema2EmbeddedDockerReferenceConflicts(t *testing.T) { |
255 | 259 | for _, m := range []genericManifest{ |
256 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
260 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
257 | 261 | manifestSchema2FromComponentsLikeFixture(nil), |
258 | 262 | } { |
259 | 263 | for _, name := range []string{"busybox", "example.com:5555/ns/repo:tag"} { |
309 | 313 | |
310 | 314 | func TestManifestSchema2UpdatedImageNeedsLayerDiffIDs(t *testing.T) { |
311 | 315 | for _, m := range []genericManifest{ |
312 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json"), | |
316 | manifestSchema2FromFixture(t, unusedImageSource{}, "schema2.json", false), | |
313 | 317 | manifestSchema2FromComponentsLikeFixture(nil), |
314 | 318 | } { |
315 | 319 | assert.False(t, m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{ |
437 | 441 | |
438 | 442 | func TestManifestSchema2UpdatedImage(t *testing.T) { |
439 | 443 | originalSrc := newSchema2ImageSource(t, "httpd:latest") |
440 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json") | |
444 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) | |
441 | 445 | |
442 | 446 | // LayerInfos: |
443 | 447 | layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0]) |
489 | 493 | } |
490 | 494 | |
491 | 495 | // m hasn’t been changed: |
492 | m2 := manifestSchema2FromFixture(t, originalSrc, "schema2.json") | |
496 | m2 := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) | |
493 | 497 | typedOriginal, ok := original.(*manifestSchema2) |
494 | 498 | require.True(t, ok) |
495 | 499 | typedM2, ok := m2.(*manifestSchema2) |
499 | 503 | |
500 | 504 | func TestConvertToManifestOCI(t *testing.T) { |
501 | 505 | originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") |
502 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json") | |
506 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) | |
503 | 507 | res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ |
504 | 508 | ManifestMIMEType: imgspecv1.MediaTypeImageManifest, |
505 | 509 | }) |
519 | 523 | assert.Equal(t, byHand, converted) |
520 | 524 | } |
521 | 525 | |
526 | func TestConvertToManifestOCIAllMediaTypes(t *testing.T) { | |
527 | originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") | |
528 | original := manifestSchema2FromFixture(t, originalSrc, "schema2-all-media-types.json", false) | |
529 | res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ | |
530 | ManifestMIMEType: imgspecv1.MediaTypeImageManifest, | |
531 | }) | |
532 | require.NoError(t, err) | |
533 | convertedJSON, mt, err := res.Manifest(context.Background()) | |
534 | require.NoError(t, err) | |
535 | assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt) | |
536 | ||
537 | byHandJSON, err := ioutil.ReadFile("fixtures/schema2-all-media-types-to-oci1.json") | |
538 | require.NoError(t, err) | |
539 | var converted, byHand map[string]interface{} | |
540 | err = json.Unmarshal(byHandJSON, &byHand) | |
541 | require.NoError(t, err) | |
542 | err = json.Unmarshal(convertedJSON, &converted) | |
543 | require.NoError(t, err) | |
544 | assert.Equal(t, byHand, converted) | |
545 | } | |
546 | ||
547 | func TestConvertToOCIWithInvalidMIMEType(t *testing.T) { | |
548 | originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") | |
549 | manifestSchema2FromFixture(t, originalSrc, "schema2-invalid-media-type.json", true) | |
550 | } | |
551 | ||
522 | 552 | func TestConvertToManifestSchema1(t *testing.T) { |
523 | 553 | originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") |
524 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json") | |
554 | original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) | |
525 | 555 | memoryDest := &memoryImageDest{ref: originalSrc.ref} |
526 | 556 | res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ |
527 | 557 | ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/vnd.docker.container.image.v1+json", | |
5 | "size": 4651, | |
6 | "digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar", | |
11 | "size": 51354364, | |
12 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
13 | }, | |
14 | { | |
15 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.zstd", | |
16 | "size": 150, | |
17 | "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
18 | }, | |
19 | { | |
20 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
21 | "size": 152, | |
22 | "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
23 | }, | |
24 | { | |
25 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", | |
26 | "size": 11739507, | |
27 | "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" | |
28 | }, | |
29 | { | |
30 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", | |
31 | "size": 8841833, | |
32 | "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" | |
33 | }, | |
34 | { | |
35 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", | |
36 | "size": 291, | |
37 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" | |
38 | } | |
39 | ] | |
40 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 4651, | |
5 | "digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar", | |
10 | "size": 51354364, | |
11 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
12 | }, | |
13 | { | |
14 | "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", | |
15 | "size": 150, | |
16 | "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
17 | }, | |
18 | { | |
19 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | |
20 | "size": 152, | |
21 | "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
22 | }, | |
23 | { | |
24 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", | |
25 | "size": 11739507, | |
26 | "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" | |
27 | }, | |
28 | { | |
29 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", | |
30 | "size": 8841833, | |
31 | "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" | |
32 | }, | |
33 | { | |
34 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", | |
35 | "size": 291, | |
36 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" | |
37 | } | |
38 | ] | |
39 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 5940, | |
5 | "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar+invalid-suffix", | |
10 | "size": 51354364, | |
11 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
12 | } | |
13 | ] | |
14 | }⏎ |
33 | 33 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" |
34 | 34 | } |
35 | 35 | ] |
36 | } | |
36 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 4651, | |
5 | "digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar", | |
10 | "size": 51354364, | |
11 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
12 | }, | |
13 | { | |
14 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | |
15 | "size": 152, | |
16 | "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
17 | }, | |
18 | { | |
19 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", | |
20 | "size": 11739507, | |
21 | "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" | |
22 | }, | |
23 | { | |
24 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", | |
25 | "size": 8841833, | |
26 | "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" | |
27 | }, | |
28 | { | |
29 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", | |
30 | "size": 291, | |
31 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" | |
32 | } | |
33 | ] | |
34 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/vnd.docker.container.image.v1+json", | |
5 | "size": 4651, | |
6 | "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar", | |
11 | "size": 51354364, | |
12 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
13 | }, | |
14 | { | |
15 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
16 | "size": 152, | |
17 | "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
18 | }, | |
19 | { | |
20 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", | |
21 | "size": 11739507, | |
22 | "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" | |
23 | }, | |
24 | { | |
25 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", | |
26 | "size": 8841833, | |
27 | "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" | |
28 | }, | |
29 | { | |
30 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", | |
31 | "size": 291, | |
32 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" | |
33 | } | |
34 | ] | |
35 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/octet-stream", | |
5 | "size": 5940, | |
6 | "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.zstd", | |
11 | "size": 51354364, | |
12 | "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" | |
13 | }, | |
14 | { | |
15 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
16 | "size": 150, | |
17 | "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" | |
18 | }, | |
19 | { | |
20 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
21 | "size": 11739507, | |
22 | "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" | |
23 | }, | |
24 | { | |
25 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
26 | "size": 8841833, | |
27 | "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" | |
28 | }, | |
29 | { | |
30 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | |
31 | "size": 291, | |
32 | "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" | |
33 | } | |
34 | ] | |
35 | }⏎ |
2 | 2 | import ( |
3 | 3 | "context" |
4 | 4 | "encoding/json" |
5 | "fmt" | |
5 | 6 | "io/ioutil" |
6 | 7 | |
7 | 8 | "github.com/containers/image/docker/reference" |
186 | 187 | layers := make([]manifest.Schema2Descriptor, len(m.m.Layers)) |
187 | 188 | for idx := range layers { |
188 | 189 | layers[idx] = schema2DescriptorFromOCI1Descriptor(m.m.Layers[idx]) |
189 | layers[idx].MediaType = manifest.DockerV2Schema2LayerMediaType | |
190 | switch layers[idx].MediaType { | |
191 | case imgspecv1.MediaTypeImageLayerNonDistributable: | |
192 | layers[idx].MediaType = manifest.DockerV2Schema2ForeignLayerMediaType | |
193 | case imgspecv1.MediaTypeImageLayerNonDistributableGzip: | |
194 | layers[idx].MediaType = manifest.DockerV2Schema2ForeignLayerMediaTypeGzip | |
195 | case imgspecv1.MediaTypeImageLayerNonDistributableZstd: | |
196 | return nil, fmt.Errorf("Error during manifest conversion: %q: zstd compression is not supported for docker images", layers[idx].MediaType) | |
197 | case imgspecv1.MediaTypeImageLayer: | |
198 | layers[idx].MediaType = manifest.DockerV2SchemaLayerMediaTypeUncompressed | |
199 | case imgspecv1.MediaTypeImageLayerGzip: | |
200 | layers[idx].MediaType = manifest.DockerV2Schema2LayerMediaType | |
201 | case imgspecv1.MediaTypeImageLayerZstd: | |
202 | return nil, fmt.Errorf("Error during manifest conversion: %q: zstd compression is not supported for docker images", layers[idx].MediaType) | |
203 | default: | |
204 | return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", layers[idx].MediaType) | |
205 | } | |
190 | 206 | } |
191 | 207 | |
192 | 208 | // Rather than copying the ConfigBlob now, we just pass m.src to the |
409 | 409 | |
410 | 410 | // FIXME? Test also the various failure cases, if only to see that we don't crash? |
411 | 411 | } |
412 | ||
413 | func TestConvertToManifestSchema2AllMediaTypes(t *testing.T) { | |
414 | originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") | |
415 | original := manifestOCI1FromFixture(t, originalSrc, "oci1-all-media-types.json") | |
416 | _, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ | |
417 | ManifestMIMEType: manifest.DockerV2Schema2MediaType, | |
418 | }) | |
419 | require.Error(t, err) // zstd compression is not supported for docker images | |
420 | } | |
421 | ||
422 | func TestConvertToV2S2WithInvalidMIMEType(t *testing.T) { | |
423 | originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") | |
424 | manifest, err := ioutil.ReadFile(filepath.Join("fixtures", "oci1-invalid-media-type.json")) | |
425 | require.NoError(t, err) | |
426 | ||
427 | _, err = manifestOCI1FromManifest(originalSrc, manifest) | |
428 | require.Error(t, err) | |
429 | } |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "encoding/json" |
4 | "fmt" | |
4 | 5 | "time" |
5 | 6 | |
7 | "github.com/containers/image/pkg/compression" | |
6 | 8 | "github.com/containers/image/pkg/strslice" |
7 | 9 | "github.com/containers/image/types" |
8 | 10 | "github.com/opencontainers/go-digest" |
9 | 11 | "github.com/pkg/errors" |
12 | "github.com/sirupsen/logrus" | |
10 | 13 | ) |
11 | 14 | |
12 | 15 | // Schema2Descriptor is a “descriptor” in docker/distribution schema 2. |
160 | 163 | if err := json.Unmarshal(manifest, &s2); err != nil { |
161 | 164 | return nil, err |
162 | 165 | } |
166 | // Check manifest's and layers' media types. | |
167 | if err := SupportedSchema2MediaType(s2.MediaType); err != nil { | |
168 | return nil, err | |
169 | } | |
170 | for _, layer := range s2.LayersDescriptors { | |
171 | if err := SupportedSchema2MediaType(layer.MediaType); err != nil { | |
172 | return nil, err | |
173 | } | |
174 | } | |
163 | 175 | return &s2, nil |
164 | 176 | } |
165 | 177 | |
206 | 218 | original := m.LayersDescriptors |
207 | 219 | m.LayersDescriptors = make([]Schema2Descriptor, len(layerInfos)) |
208 | 220 | for i, info := range layerInfos { |
209 | m.LayersDescriptors[i].MediaType = original[i].MediaType | |
221 | // First make sure we support the media type of the original layer. | |
222 | if err := SupportedSchema2MediaType(original[i].MediaType); err != nil { | |
223 | return fmt.Errorf("Error preparing updated manifest: unknown media type of original layer: %q", original[i].MediaType) | |
224 | } | |
225 | ||
226 | // Set the correct media types based on the specified compression | |
227 | // operation, the desired compression algorithm AND the original media | |
228 | // type. | |
229 | switch info.CompressionOperation { | |
230 | case types.PreserveOriginal: | |
231 | // Keep the original media type. | |
232 | m.LayersDescriptors[i].MediaType = original[i].MediaType | |
233 | ||
234 | case types.Decompress: | |
235 | // Decompress the original media type and check if it was | |
236 | // non-distributable one or not. | |
237 | switch original[i].MediaType { | |
238 | case DockerV2Schema2ForeignLayerMediaTypeGzip: | |
239 | m.LayersDescriptors[i].MediaType = DockerV2Schema2ForeignLayerMediaType | |
240 | case DockerV2Schema2LayerMediaType: | |
241 | m.LayersDescriptors[i].MediaType = DockerV2SchemaLayerMediaTypeUncompressed | |
242 | default: | |
243 | return fmt.Errorf("Error preparing updated manifest: unsupported media type for decompression: %q", original[i].MediaType) | |
244 | } | |
245 | ||
246 | case types.Compress: | |
247 | if info.CompressionAlgorithm == nil { | |
248 | logrus.Debugf("Preparing updated manifest: blob %q was compressed but does not specify by which algorithm: falling back to use the original blob", info.Digest) | |
249 | m.LayersDescriptors[i].MediaType = original[i].MediaType | |
250 | break | |
251 | } | |
252 | // Compress the original media type and set the new one based on | |
253 | // that type (distributable or not) and the specified compression | |
254 | // algorithm. Throw an error if the algorithm is not supported. | |
255 | switch info.CompressionAlgorithm.Name() { | |
256 | case compression.Gzip.Name(): | |
257 | switch original[i].MediaType { | |
258 | case DockerV2Schema2ForeignLayerMediaType: | |
259 | m.LayersDescriptors[i].MediaType = DockerV2Schema2ForeignLayerMediaTypeGzip | |
260 | case DockerV2SchemaLayerMediaTypeUncompressed: | |
261 | m.LayersDescriptors[i].MediaType = DockerV2Schema2LayerMediaType | |
262 | default: | |
263 | return fmt.Errorf("Error preparing updated manifest: unsupported media type for compression: %q", original[i].MediaType) | |
264 | } | |
265 | case compression.Zstd.Name(): | |
266 | return fmt.Errorf("Error preparing updated manifest: zstd compression is not supported for docker images") | |
267 | default: | |
268 | return fmt.Errorf("Error preparing updated manifest: unknown compression algorithm %q for layer %q", info.CompressionAlgorithm.Name(), info.Digest) | |
269 | } | |
270 | ||
271 | default: | |
272 | return fmt.Errorf("Error preparing updated manifest: unknown compression operation (%d) for layer %q", info.CompressionOperation, info.Digest) | |
273 | } | |
210 | 274 | m.LayersDescriptors[i].Digest = info.Digest |
211 | 275 | m.LayersDescriptors[i].Size = info.Size |
212 | 276 | m.LayersDescriptors[i].URLs = info.URLs |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "testing" | |
5 | ||
6 | "github.com/containers/image/pkg/compression" | |
7 | "github.com/containers/image/types" | |
8 | "github.com/stretchr/testify/assert" | |
9 | ) | |
10 | ||
11 | func TestSupportedSchema2MediaType(t *testing.T) { | |
12 | type testData struct { | |
13 | m string | |
14 | mustFail bool | |
15 | } | |
16 | data := []testData{ | |
17 | { | |
18 | DockerV2Schema2MediaType, | |
19 | false, | |
20 | }, | |
21 | { | |
22 | DockerV2Schema2ConfigMediaType, | |
23 | false, | |
24 | }, | |
25 | { | |
26 | DockerV2Schema2LayerMediaType, | |
27 | false, | |
28 | }, | |
29 | { | |
30 | DockerV2SchemaLayerMediaTypeUncompressed, | |
31 | false, | |
32 | }, | |
33 | { | |
34 | DockerV2ListMediaType, | |
35 | false, | |
36 | }, | |
37 | { | |
38 | DockerV2Schema2ForeignLayerMediaType, | |
39 | false, | |
40 | }, | |
41 | { | |
42 | DockerV2Schema2ForeignLayerMediaTypeGzip, | |
43 | false, | |
44 | }, | |
45 | { | |
46 | "application/vnd.docker.image.rootfs.foreign.diff.unknown", | |
47 | true, | |
48 | }, | |
49 | } | |
50 | for _, d := range data { | |
51 | err := SupportedSchema2MediaType(d.m) | |
52 | if d.mustFail { | |
53 | assert.NotNil(t, err) | |
54 | } else { | |
55 | assert.Nil(t, err) | |
56 | } | |
57 | } | |
58 | } | |
59 | ||
60 | func TestUpdateLayerInfosV2S2GzipToZstd(t *testing.T) { | |
61 | bytes, err := ioutil.ReadFile("fixtures/v2s2.manifest.json") | |
62 | assert.Nil(t, err) | |
63 | ||
64 | origManifest, err := Schema2FromManifest(bytes) | |
65 | assert.Nil(t, err) | |
66 | ||
67 | err = origManifest.UpdateLayerInfos([]types.BlobInfo{ | |
68 | { | |
69 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
70 | Size: 32654, | |
71 | MediaType: DockerV2Schema2LayerMediaType, | |
72 | CompressionOperation: types.Compress, | |
73 | CompressionAlgorithm: &compression.Zstd, | |
74 | }, | |
75 | { | |
76 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
77 | Size: 16724, | |
78 | MediaType: DockerV2Schema2LayerMediaType, | |
79 | CompressionOperation: types.Compress, | |
80 | CompressionAlgorithm: &compression.Zstd, | |
81 | }, | |
82 | { | |
83 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
84 | Size: 73109, | |
85 | MediaType: DockerV2Schema2LayerMediaType, | |
86 | CompressionOperation: types.Compress, | |
87 | CompressionAlgorithm: &compression.Zstd, | |
88 | }, | |
89 | }) | |
90 | assert.NotNil(t, err) // zstd is not supported for docker images | |
91 | } | |
92 | ||
93 | func TestUpdateLayerInfosV2S2InvalidCompressionOperation(t *testing.T) { | |
94 | bytes, err := ioutil.ReadFile("fixtures/v2s2.manifest.json") | |
95 | assert.Nil(t, err) | |
96 | ||
97 | origManifest, err := Schema2FromManifest(bytes) | |
98 | assert.Nil(t, err) | |
99 | ||
100 | err = origManifest.UpdateLayerInfos([]types.BlobInfo{ | |
101 | { | |
102 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
103 | Size: 32654, | |
104 | MediaType: DockerV2Schema2LayerMediaType, | |
105 | CompressionOperation: types.Decompress, | |
106 | }, | |
107 | { | |
108 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
109 | Size: 16724, | |
110 | MediaType: DockerV2Schema2LayerMediaType, | |
111 | CompressionOperation: types.Decompress, | |
112 | }, | |
113 | { | |
114 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
115 | Size: 73109, | |
116 | MediaType: DockerV2Schema2LayerMediaType, | |
117 | CompressionOperation: 42, // MUST fail here | |
118 | }, | |
119 | }) | |
120 | assert.NotNil(t, err) | |
121 | } | |
122 | ||
123 | func TestUpdateLayerInfosV2S2InvalidCompressionAlgorithm(t *testing.T) { | |
124 | bytes, err := ioutil.ReadFile("fixtures/v2s2.manifest.json") | |
125 | assert.Nil(t, err) | |
126 | ||
127 | origManifest, err := Schema2FromManifest(bytes) | |
128 | assert.Nil(t, err) | |
129 | ||
130 | err = origManifest.UpdateLayerInfos([]types.BlobInfo{ | |
131 | { | |
132 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
133 | Size: 32654, | |
134 | MediaType: DockerV2Schema2LayerMediaType, | |
135 | CompressionOperation: types.Compress, | |
136 | CompressionAlgorithm: &compression.Gzip, | |
137 | }, | |
138 | { | |
139 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
140 | Size: 16724, | |
141 | MediaType: DockerV2Schema2LayerMediaType, | |
142 | CompressionOperation: types.Compress, | |
143 | CompressionAlgorithm: &compression.Gzip, | |
144 | }, | |
145 | { | |
146 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
147 | Size: 73109, | |
148 | MediaType: DockerV2Schema2LayerMediaType, | |
149 | CompressionOperation: types.Compress, | |
150 | CompressionAlgorithm: &compression.Zstd, // MUST fail here | |
151 | }, | |
152 | }) | |
153 | assert.NotNil(t, err) | |
154 | } | |
155 | ||
156 | func TestUpdateLayerInfosV2S2NondistributableToGzip(t *testing.T) { | |
157 | bytes, err := ioutil.ReadFile("fixtures/v2s2.nondistributable.manifest.json") | |
158 | assert.Nil(t, err) | |
159 | ||
160 | origManifest, err := Schema2FromManifest(bytes) | |
161 | assert.Nil(t, err) | |
162 | ||
163 | err = origManifest.UpdateLayerInfos([]types.BlobInfo{ | |
164 | { | |
165 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
166 | Size: 32654, | |
167 | MediaType: DockerV2Schema2ForeignLayerMediaType, | |
168 | CompressionOperation: types.Compress, | |
169 | CompressionAlgorithm: &compression.Gzip, | |
170 | }, | |
171 | }) | |
172 | assert.Nil(t, err) | |
173 | ||
174 | updatedManifestBytes, err := origManifest.Serialize() | |
175 | assert.Nil(t, err) | |
176 | ||
177 | bytes, err = ioutil.ReadFile("fixtures/v2s2.nondistributable.gzip.manifest.json") | |
178 | assert.Nil(t, err) | |
179 | ||
180 | expectedManifest, err := Schema2FromManifest(bytes) | |
181 | assert.Nil(t, err) | |
182 | ||
183 | expectedManifestBytes, err := expectedManifest.Serialize() | |
184 | assert.Nil(t, err) | |
185 | ||
186 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
187 | } | |
188 | ||
189 | func TestUpdateLayerInfosV2S2NondistributableGzipToUncompressed(t *testing.T) { | |
190 | bytes, err := ioutil.ReadFile("fixtures/v2s2.nondistributable.gzip.manifest.json") | |
191 | assert.Nil(t, err) | |
192 | ||
193 | origManifest, err := Schema2FromManifest(bytes) | |
194 | assert.Nil(t, err) | |
195 | ||
196 | err = origManifest.UpdateLayerInfos([]types.BlobInfo{ | |
197 | { | |
198 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
199 | Size: 32654, | |
200 | MediaType: DockerV2Schema2ForeignLayerMediaType, | |
201 | CompressionOperation: types.Decompress, | |
202 | }, | |
203 | }) | |
204 | assert.Nil(t, err) | |
205 | ||
206 | updatedManifestBytes, err := origManifest.Serialize() | |
207 | assert.Nil(t, err) | |
208 | ||
209 | bytes, err = ioutil.ReadFile("fixtures/v2s2.nondistributable.manifest.json") | |
210 | assert.Nil(t, err) | |
211 | ||
212 | expectedManifest, err := Schema2FromManifest(bytes) | |
213 | assert.Nil(t, err) | |
214 | ||
215 | expectedManifestBytes, err := expectedManifest.Serialize() | |
216 | assert.Nil(t, err) | |
217 | ||
218 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
219 | } |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar+unknown", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | }, | |
13 | { | |
14 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | |
15 | "size": 16724, | |
16 | "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" | |
17 | }, | |
18 | { | |
19 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | |
20 | "size": 73109, | |
21 | "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" | |
22 | } | |
23 | ], | |
24 | "annotations": { | |
25 | "com.example.key1": "value1", | |
26 | "com.example.key2": "value2" | |
27 | } | |
28 | } |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | } | |
13 | ], | |
14 | "annotations": { | |
15 | "com.example.key1": "value1", | |
16 | "com.example.key2": "value2" | |
17 | } | |
18 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | } | |
13 | ], | |
14 | "annotations": { | |
15 | "com.example.key1": "value1", | |
16 | "com.example.key2": "value2" | |
17 | } | |
18 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | } | |
13 | ], | |
14 | "annotations": { | |
15 | "com.example.key1": "value1", | |
16 | "com.example.key2": "value2" | |
17 | } | |
18 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | }, | |
13 | { | |
14 | "mediaType": "application/vnd.oci.image.layer.v1.tar", | |
15 | "size": 16724, | |
16 | "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" | |
17 | }, | |
18 | { | |
19 | "mediaType": "application/vnd.oci.image.layer.v1.tar", | |
20 | "size": 73109, | |
21 | "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" | |
22 | } | |
23 | ], | |
24 | "annotations": { | |
25 | "com.example.key1": "value1", | |
26 | "com.example.key2": "value2" | |
27 | } | |
28 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "config": { | |
3 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
4 | "size": 7023, | |
5 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
6 | }, | |
7 | "layers": [ | |
8 | { | |
9 | "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", | |
10 | "size": 32654, | |
11 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
12 | }, | |
13 | { | |
14 | "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", | |
15 | "size": 16724, | |
16 | "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" | |
17 | }, | |
18 | { | |
19 | "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", | |
20 | "size": 73109, | |
21 | "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" | |
22 | } | |
23 | ], | |
24 | "annotations": { | |
25 | "com.example.key1": "value1", | |
26 | "com.example.key2": "value2" | |
27 | } | |
28 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
5 | "size": 7023, | |
6 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", | |
11 | "size": 32654, | |
12 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
13 | } | |
14 | ], | |
15 | "annotations": { | |
16 | "com.example.key1": "value1", | |
17 | "com.example.key2": "value2" | |
18 | } | |
19 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/vnd.oci.image.config.v1+json", | |
5 | "size": 7023, | |
6 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", | |
11 | "size": 32654, | |
12 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
13 | } | |
14 | ], | |
15 | "annotations": { | |
16 | "com.example.key1": "value1", | |
17 | "com.example.key2": "value2" | |
18 | } | |
19 | }⏎ |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | |
3 | "config": { | |
4 | "mediaType": "application/vnd.docker.container.image.v1+json", | |
5 | "size": 7023, | |
6 | "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | |
7 | }, | |
8 | "layers": [ | |
9 | { | |
10 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar", | |
11 | "size": 32654, | |
12 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | |
13 | }, | |
14 | { | |
15 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar", | |
16 | "size": 16724, | |
17 | "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" | |
18 | }, | |
19 | { | |
20 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar", | |
21 | "size": 73109, | |
22 | "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" | |
23 | } | |
24 | ] | |
25 | }⏎ |
11 | 11 | |
12 | 12 | // FIXME: Should we just use docker/distribution and docker/docker implementations directly? |
13 | 13 | |
14 | // FIXME(runcom, mitr): should we havea mediatype pkg?? | |
14 | // FIXME(runcom, mitr): should we have a mediatype pkg?? | |
15 | 15 | const ( |
16 | 16 | // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 |
17 | 17 | DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json" |
23 | 23 | DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json" |
24 | 24 | // DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers. |
25 | 25 | DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" |
26 | // DockerV2SchemaLayerMediaTypeUncompressed is the mediaType used for uncompressed layers. | |
27 | DockerV2SchemaLayerMediaTypeUncompressed = "application/vnd.docker.image.rootfs.diff.tar" | |
26 | 28 | // DockerV2ListMediaType MIME type represents Docker manifest schema 2 list |
27 | 29 | DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json" |
28 | 30 | // DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers. |
29 | DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" | |
31 | DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar" | |
32 | // DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzippped schema 2 foreign layers. | |
33 | DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" | |
30 | 34 | ) |
35 | ||
36 | // SupportedSchema2MediaType checks if the specified string is a supported Docker v2s2 media type. | |
37 | func SupportedSchema2MediaType(m string) error { | |
38 | switch m { | |
39 | case DockerV2ListMediaType, DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, DockerV2Schema2ConfigMediaType, DockerV2Schema2ForeignLayerMediaType, DockerV2Schema2ForeignLayerMediaTypeGzip, DockerV2Schema2LayerMediaType, DockerV2Schema2MediaType, DockerV2SchemaLayerMediaTypeUncompressed: | |
40 | return nil | |
41 | default: | |
42 | return fmt.Errorf("unsupported docker v2s2 media type: %q", m) | |
43 | } | |
44 | } | |
31 | 45 | |
32 | 46 | // DefaultRequestedManifestMIMETypes is a list of MIME types a types.ImageSource |
33 | 47 | // should request from the backend unless directed otherwise. |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "encoding/json" |
4 | ||
4 | "fmt" | |
5 | ||
6 | "github.com/containers/image/pkg/compression" | |
5 | 7 | "github.com/containers/image/types" |
6 | 8 | "github.com/opencontainers/go-digest" |
7 | 9 | "github.com/opencontainers/image-spec/specs-go" |
8 | 10 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" |
9 | 11 | "github.com/pkg/errors" |
12 | "github.com/sirupsen/logrus" | |
10 | 13 | ) |
11 | 14 | |
12 | 15 | // BlobInfoFromOCI1Descriptor returns a types.BlobInfo based on the input OCI1 descriptor. |
26 | 29 | imgspecv1.Manifest |
27 | 30 | } |
28 | 31 | |
32 | // SupportedOCI1MediaType checks if the specified string is a supported OCI1 media type. | |
33 | func SupportedOCI1MediaType(m string) error { | |
34 | switch m { | |
35 | case imgspecv1.MediaTypeDescriptor, imgspecv1.MediaTypeImageConfig, imgspecv1.MediaTypeImageLayer, imgspecv1.MediaTypeImageLayerGzip, imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributableZstd, imgspecv1.MediaTypeImageLayerZstd, imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeLayoutHeader: | |
36 | return nil | |
37 | default: | |
38 | return fmt.Errorf("unsupported OCIv1 media type: %q", m) | |
39 | } | |
40 | } | |
41 | ||
29 | 42 | // OCI1FromManifest creates an OCI1 manifest instance from a manifest blob. |
30 | 43 | func OCI1FromManifest(manifest []byte) (*OCI1, error) { |
31 | 44 | oci1 := OCI1{} |
32 | 45 | if err := json.Unmarshal(manifest, &oci1); err != nil { |
33 | 46 | return nil, err |
47 | } | |
48 | // Check manifest's and layers' media types. | |
49 | if err := SupportedOCI1MediaType(oci1.Config.MediaType); err != nil { | |
50 | return nil, err | |
51 | } | |
52 | for _, layer := range oci1.Layers { | |
53 | if err := SupportedOCI1MediaType(layer.MediaType); err != nil { | |
54 | return nil, err | |
55 | } | |
34 | 56 | } |
35 | 57 | return &oci1, nil |
36 | 58 | } |
80 | 102 | original := m.Layers |
81 | 103 | m.Layers = make([]imgspecv1.Descriptor, len(layerInfos)) |
82 | 104 | for i, info := range layerInfos { |
83 | m.Layers[i].MediaType = original[i].MediaType | |
105 | // First make sure we support the media type of the original layer. | |
106 | if err := SupportedOCI1MediaType(original[i].MediaType); err != nil { | |
107 | return fmt.Errorf("Error preparing updated manifest: unknown media type of original layer: %q", original[i].MediaType) | |
108 | } | |
109 | ||
110 | // Set the correct media types based on the specified compression | |
111 | // operation, the desired compression algorithm AND the original media | |
112 | // type. | |
113 | switch info.CompressionOperation { | |
114 | case types.PreserveOriginal: | |
115 | // Keep the original media type. | |
116 | m.Layers[i].MediaType = original[i].MediaType | |
117 | ||
118 | case types.Decompress: | |
119 | // Decompress the original media type and check if it was | |
120 | // non-distributable one or not. | |
121 | switch original[i].MediaType { | |
122 | case imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributableZstd: | |
123 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable | |
124 | default: | |
125 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayer | |
126 | } | |
127 | ||
128 | case types.Compress: | |
129 | if info.CompressionAlgorithm == nil { | |
130 | logrus.Debugf("Error preparing updated manifest: blob %q was compressed but does not specify by which algorithm: falling back to use the original blob", info.Digest) | |
131 | m.Layers[i].MediaType = original[i].MediaType | |
132 | break | |
133 | } | |
134 | // Compress the original media type and set the new one based on | |
135 | // that type (distributable or not) and the specified compression | |
136 | // algorithm. Throw an error if the algorithm is not supported. | |
137 | switch info.CompressionAlgorithm.Name() { | |
138 | case compression.Gzip.Name(): | |
139 | switch original[i].MediaType { | |
140 | case imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableZstd: | |
141 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerNonDistributableGzip | |
142 | ||
143 | default: | |
144 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerGzip | |
145 | } | |
146 | ||
147 | case compression.Zstd.Name(): | |
148 | switch original[i].MediaType { | |
149 | case imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableGzip: | |
150 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerNonDistributableZstd | |
151 | ||
152 | default: | |
153 | m.Layers[i].MediaType = imgspecv1.MediaTypeImageLayerZstd | |
154 | } | |
155 | ||
156 | default: | |
157 | return fmt.Errorf("Error preparing updated manifest: unknown compression algorithm %q for layer %q", info.CompressionAlgorithm.Name(), info.Digest) | |
158 | } | |
159 | ||
160 | default: | |
161 | return fmt.Errorf("Error preparing updated manifest: unknown compression operation (%d) for layer %q", info.CompressionOperation, info.Digest) | |
162 | } | |
84 | 163 | m.Layers[i].Digest = info.Digest |
85 | 164 | m.Layers[i].Size = info.Size |
86 | 165 | m.Layers[i].Annotations = info.Annotations |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "testing" | |
5 | ||
6 | "github.com/containers/image/pkg/compression" | |
7 | "github.com/containers/image/types" | |
8 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
9 | "github.com/stretchr/testify/assert" | |
10 | ) | |
11 | ||
12 | func TestSupportedOCI1MediaType(t *testing.T) { | |
13 | type testData struct { | |
14 | m string | |
15 | mustFail bool | |
16 | } | |
17 | data := []testData{ | |
18 | { | |
19 | imgspecv1.MediaTypeDescriptor, | |
20 | false, | |
21 | }, | |
22 | { | |
23 | imgspecv1.MediaTypeImageConfig, | |
24 | false, | |
25 | }, | |
26 | { | |
27 | imgspecv1.MediaTypeImageLayer, | |
28 | false, | |
29 | }, | |
30 | { | |
31 | imgspecv1.MediaTypeImageLayerGzip, | |
32 | false, | |
33 | }, | |
34 | { | |
35 | imgspecv1.MediaTypeImageLayerNonDistributable, | |
36 | false, | |
37 | }, | |
38 | { | |
39 | imgspecv1.MediaTypeImageLayerNonDistributableGzip, | |
40 | false, | |
41 | }, | |
42 | { | |
43 | imgspecv1.MediaTypeImageLayerNonDistributableZstd, | |
44 | false, | |
45 | }, | |
46 | { | |
47 | imgspecv1.MediaTypeImageLayerZstd, | |
48 | false, | |
49 | }, | |
50 | { | |
51 | imgspecv1.MediaTypeImageManifest, | |
52 | false, | |
53 | }, | |
54 | { | |
55 | imgspecv1.MediaTypeLayoutHeader, | |
56 | false, | |
57 | }, | |
58 | { | |
59 | "application/vnd.oci.image.layer.nondistributable.v1.tar+unknown", | |
60 | true, | |
61 | }, | |
62 | } | |
63 | for _, d := range data { | |
64 | err := SupportedOCI1MediaType(d.m) | |
65 | if d.mustFail { | |
66 | assert.NotNil(t, err) | |
67 | } else { | |
68 | assert.Nil(t, err) | |
69 | } | |
70 | } | |
71 | } | |
72 | ||
73 | func TestInvalidOCI1MediaType(t *testing.T) { | |
74 | bytes, err := ioutil.ReadFile("fixtures/ociv1.invalid.mediatype.manifest.json") | |
75 | assert.Nil(t, err) | |
76 | ||
77 | _, err = OCI1FromManifest(bytes) | |
78 | assert.NotNil(t, err) | |
79 | } | |
80 | ||
81 | func TestUpdateLayerInfosOCIGzipToZstd(t *testing.T) { | |
82 | bytes, err := ioutil.ReadFile("fixtures/ociv1.manifest.json") | |
83 | assert.Nil(t, err) | |
84 | ||
85 | manifest, err := OCI1FromManifest(bytes) | |
86 | assert.Nil(t, err) | |
87 | ||
88 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
89 | { | |
90 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
91 | Size: 32654, | |
92 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
93 | CompressionOperation: types.Compress, | |
94 | CompressionAlgorithm: &compression.Zstd, | |
95 | }, | |
96 | { | |
97 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
98 | Size: 16724, | |
99 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
100 | CompressionOperation: types.Compress, | |
101 | CompressionAlgorithm: &compression.Zstd, | |
102 | }, | |
103 | { | |
104 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
105 | Size: 73109, | |
106 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
107 | CompressionOperation: types.Compress, | |
108 | CompressionAlgorithm: &compression.Zstd, | |
109 | }, | |
110 | }) | |
111 | assert.Nil(t, err) | |
112 | ||
113 | updatedManifestBytes, err := manifest.Serialize() | |
114 | assert.Nil(t, err) | |
115 | ||
116 | bytes, err = ioutil.ReadFile("fixtures/ociv1.zstd.manifest.json") | |
117 | assert.Nil(t, err) | |
118 | ||
119 | expectedManifest, err := OCI1FromManifest(bytes) | |
120 | assert.Nil(t, err) | |
121 | ||
122 | expectedManifestBytes, err := expectedManifest.Serialize() | |
123 | assert.Nil(t, err) | |
124 | ||
125 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
126 | } | |
127 | ||
128 | func TestUpdateLayerInfosOCIZstdToGzip(t *testing.T) { | |
129 | bytes, err := ioutil.ReadFile("fixtures/ociv1.zstd.manifest.json") | |
130 | assert.Nil(t, err) | |
131 | ||
132 | manifest, err := OCI1FromManifest(bytes) | |
133 | assert.Nil(t, err) | |
134 | ||
135 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
136 | { | |
137 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
138 | Size: 32654, | |
139 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
140 | CompressionOperation: types.Compress, | |
141 | CompressionAlgorithm: &compression.Gzip, | |
142 | }, | |
143 | { | |
144 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
145 | Size: 16724, | |
146 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
147 | CompressionOperation: types.Compress, | |
148 | CompressionAlgorithm: &compression.Gzip, | |
149 | }, | |
150 | { | |
151 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
152 | Size: 73109, | |
153 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
154 | CompressionOperation: types.Compress, | |
155 | CompressionAlgorithm: &compression.Gzip, | |
156 | }, | |
157 | }) | |
158 | assert.Nil(t, err) | |
159 | ||
160 | updatedManifestBytes, err := manifest.Serialize() | |
161 | assert.Nil(t, err) | |
162 | ||
163 | bytes, err = ioutil.ReadFile("fixtures/ociv1.manifest.json") | |
164 | assert.Nil(t, err) | |
165 | ||
166 | expectedManifest, err := OCI1FromManifest(bytes) | |
167 | assert.Nil(t, err) | |
168 | ||
169 | expectedManifestBytes, err := expectedManifest.Serialize() | |
170 | assert.Nil(t, err) | |
171 | ||
172 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
173 | } | |
174 | ||
175 | func TestUpdateLayerInfosOCIZstdToUncompressed(t *testing.T) { | |
176 | bytes, err := ioutil.ReadFile("fixtures/ociv1.zstd.manifest.json") | |
177 | assert.Nil(t, err) | |
178 | ||
179 | manifest, err := OCI1FromManifest(bytes) | |
180 | assert.Nil(t, err) | |
181 | ||
182 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
183 | { | |
184 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
185 | Size: 32654, | |
186 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
187 | CompressionOperation: types.Decompress, | |
188 | }, | |
189 | { | |
190 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
191 | Size: 16724, | |
192 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
193 | CompressionOperation: types.Decompress, | |
194 | }, | |
195 | { | |
196 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
197 | Size: 73109, | |
198 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
199 | CompressionOperation: types.Decompress, | |
200 | }, | |
201 | }) | |
202 | assert.Nil(t, err) | |
203 | ||
204 | updatedManifestBytes, err := manifest.Serialize() | |
205 | assert.Nil(t, err) | |
206 | ||
207 | bytes, err = ioutil.ReadFile("fixtures/ociv1.uncompressed.manifest.json") | |
208 | assert.Nil(t, err) | |
209 | ||
210 | expectedManifest, err := OCI1FromManifest(bytes) | |
211 | assert.Nil(t, err) | |
212 | ||
213 | expectedManifestBytes, err := expectedManifest.Serialize() | |
214 | assert.Nil(t, err) | |
215 | ||
216 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
217 | } | |
218 | ||
219 | func TestUpdateLayerInfosInvalidCompressionOperation(t *testing.T) { | |
220 | bytes, err := ioutil.ReadFile("fixtures/ociv1.zstd.manifest.json") | |
221 | assert.Nil(t, err) | |
222 | ||
223 | manifest, err := OCI1FromManifest(bytes) | |
224 | assert.Nil(t, err) | |
225 | ||
226 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
227 | { | |
228 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
229 | Size: 32654, | |
230 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
231 | CompressionOperation: types.Compress, | |
232 | CompressionAlgorithm: &compression.Gzip, | |
233 | }, | |
234 | { | |
235 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
236 | Size: 16724, | |
237 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
238 | CompressionOperation: 42, // MUST fail here | |
239 | CompressionAlgorithm: &compression.Gzip, | |
240 | }, | |
241 | { | |
242 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
243 | Size: 73109, | |
244 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
245 | CompressionOperation: types.Compress, | |
246 | CompressionAlgorithm: &compression.Gzip, | |
247 | }, | |
248 | }) | |
249 | assert.NotNil(t, err) | |
250 | } | |
251 | ||
252 | func TestUpdateLayerInfosInvalidCompressionAlgorithm(t *testing.T) { | |
253 | bytes, err := ioutil.ReadFile("fixtures/ociv1.zstd.manifest.json") | |
254 | assert.Nil(t, err) | |
255 | ||
256 | manifest, err := OCI1FromManifest(bytes) | |
257 | assert.Nil(t, err) | |
258 | ||
259 | customCompression := compression.Algorithm{} | |
260 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
261 | { | |
262 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
263 | Size: 32654, | |
264 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
265 | CompressionOperation: types.Compress, | |
266 | CompressionAlgorithm: &compression.Gzip, | |
267 | }, | |
268 | { | |
269 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
270 | Size: 16724, | |
271 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
272 | CompressionOperation: 42, | |
273 | CompressionAlgorithm: &compression.Gzip, | |
274 | }, | |
275 | { | |
276 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
277 | Size: 73109, | |
278 | MediaType: imgspecv1.MediaTypeImageLayerZstd, | |
279 | CompressionOperation: types.Compress, | |
280 | CompressionAlgorithm: &customCompression, // MUST fail here | |
281 | }, | |
282 | }) | |
283 | assert.NotNil(t, err) | |
284 | } | |
285 | ||
286 | func TestUpdateLayerInfosOCIGzipToUncompressed(t *testing.T) { | |
287 | bytes, err := ioutil.ReadFile("fixtures/ociv1.manifest.json") | |
288 | assert.Nil(t, err) | |
289 | ||
290 | manifest, err := OCI1FromManifest(bytes) | |
291 | assert.Nil(t, err) | |
292 | ||
293 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
294 | { | |
295 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
296 | Size: 32654, | |
297 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
298 | CompressionOperation: types.Decompress, | |
299 | }, | |
300 | { | |
301 | Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", | |
302 | Size: 16724, | |
303 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
304 | CompressionOperation: types.Decompress, | |
305 | }, | |
306 | { | |
307 | Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", | |
308 | Size: 73109, | |
309 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
310 | CompressionOperation: types.Decompress, | |
311 | }, | |
312 | }) | |
313 | assert.Nil(t, err) | |
314 | ||
315 | updatedManifestBytes, err := manifest.Serialize() | |
316 | assert.Nil(t, err) | |
317 | ||
318 | bytes, err = ioutil.ReadFile("fixtures/ociv1.uncompressed.manifest.json") | |
319 | assert.Nil(t, err) | |
320 | ||
321 | expectedManifest, err := OCI1FromManifest(bytes) | |
322 | assert.Nil(t, err) | |
323 | ||
324 | expectedManifestBytes, err := expectedManifest.Serialize() | |
325 | assert.Nil(t, err) | |
326 | ||
327 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
328 | } | |
329 | ||
330 | func TestUpdateLayerInfosOCINondistributableToGzip(t *testing.T) { | |
331 | bytes, err := ioutil.ReadFile("fixtures/ociv1.nondistributable.manifest.json") | |
332 | assert.Nil(t, err) | |
333 | ||
334 | manifest, err := OCI1FromManifest(bytes) | |
335 | assert.Nil(t, err) | |
336 | ||
337 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
338 | { | |
339 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
340 | Size: 32654, | |
341 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
342 | CompressionOperation: types.Compress, | |
343 | CompressionAlgorithm: &compression.Gzip, | |
344 | }, | |
345 | }) | |
346 | assert.Nil(t, err) | |
347 | ||
348 | updatedManifestBytes, err := manifest.Serialize() | |
349 | assert.Nil(t, err) | |
350 | ||
351 | bytes, err = ioutil.ReadFile("fixtures/ociv1.nondistributable.gzip.manifest.json") | |
352 | assert.Nil(t, err) | |
353 | ||
354 | expectedManifest, err := OCI1FromManifest(bytes) | |
355 | assert.Nil(t, err) | |
356 | ||
357 | expectedManifestBytes, err := expectedManifest.Serialize() | |
358 | assert.Nil(t, err) | |
359 | ||
360 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
361 | } | |
362 | ||
363 | func TestUpdateLayerInfosOCINondistributableToZstd(t *testing.T) { | |
364 | bytes, err := ioutil.ReadFile("fixtures/ociv1.nondistributable.manifest.json") | |
365 | assert.Nil(t, err) | |
366 | ||
367 | manifest, err := OCI1FromManifest(bytes) | |
368 | assert.Nil(t, err) | |
369 | ||
370 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
371 | { | |
372 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
373 | Size: 32654, | |
374 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
375 | CompressionOperation: types.Compress, | |
376 | CompressionAlgorithm: &compression.Zstd, | |
377 | }, | |
378 | }) | |
379 | assert.Nil(t, err) | |
380 | ||
381 | updatedManifestBytes, err := manifest.Serialize() | |
382 | assert.Nil(t, err) | |
383 | ||
384 | bytes, err = ioutil.ReadFile("fixtures/ociv1.nondistributable.zstd.manifest.json") | |
385 | assert.Nil(t, err) | |
386 | ||
387 | expectedManifest, err := OCI1FromManifest(bytes) | |
388 | assert.Nil(t, err) | |
389 | ||
390 | expectedManifestBytes, err := expectedManifest.Serialize() | |
391 | assert.Nil(t, err) | |
392 | ||
393 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
394 | } | |
395 | ||
396 | func TestUpdateLayerInfosOCINondistributableGzipToUncompressed(t *testing.T) { | |
397 | bytes, err := ioutil.ReadFile("fixtures/ociv1.nondistributable.gzip.manifest.json") | |
398 | assert.Nil(t, err) | |
399 | ||
400 | manifest, err := OCI1FromManifest(bytes) | |
401 | assert.Nil(t, err) | |
402 | ||
403 | err = manifest.UpdateLayerInfos([]types.BlobInfo{ | |
404 | { | |
405 | Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
406 | Size: 32654, | |
407 | MediaType: imgspecv1.MediaTypeImageLayerGzip, | |
408 | CompressionOperation: types.Decompress, | |
409 | }, | |
410 | }) | |
411 | assert.Nil(t, err) | |
412 | ||
413 | updatedManifestBytes, err := manifest.Serialize() | |
414 | assert.Nil(t, err) | |
415 | ||
416 | bytes, err = ioutil.ReadFile("fixtures/ociv1.nondistributable.manifest.json") | |
417 | assert.Nil(t, err) | |
418 | ||
419 | expectedManifest, err := OCI1FromManifest(bytes) | |
420 | assert.Nil(t, err) | |
421 | ||
422 | expectedManifestBytes, err := expectedManifest.Serialize() | |
423 | assert.Nil(t, err) | |
424 | ||
425 | assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) | |
426 | } |
11 | 11 | "github.com/sirupsen/logrus" |
12 | 12 | "github.com/ulikunitz/xz" |
13 | 13 | ) |
14 | ||
15 | // Algorithm is a compression algorithm that can be used for CompressStream. | |
16 | type Algorithm struct { | |
17 | name string | |
18 | prefix []byte | |
19 | decompressor DecompressorFunc | |
20 | compressor compressorFunc | |
21 | } | |
22 | ||
23 | var ( | |
24 | // Gzip compression. | |
25 | Gzip = Algorithm{"gzip", []byte{0x1F, 0x8B, 0x08}, GzipDecompressor, gzipCompressor} | |
26 | // Bzip2 compression. | |
27 | Bzip2 = Algorithm{"bzip2", []byte{0x42, 0x5A, 0x68}, Bzip2Decompressor, bzip2Compressor} | |
28 | // Xz compression. | |
29 | Xz = Algorithm{"Xz", []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, XzDecompressor, xzCompressor} | |
30 | // Zstd compression. | |
31 | Zstd = Algorithm{"zstd", []byte{0x28, 0xb5, 0x2f, 0xfd}, ZstdDecompressor, zstdCompressor} | |
32 | ||
33 | compressionAlgorithms = map[string]Algorithm{ | |
34 | Gzip.name: Gzip, | |
35 | Bzip2.name: Bzip2, | |
36 | Xz.name: Xz, | |
37 | Zstd.name: Zstd, | |
38 | } | |
39 | ) | |
40 | ||
41 | // Name returns the name for the compression algorithm. | |
42 | func (c Algorithm) Name() string { | |
43 | return c.name | |
44 | } | |
45 | ||
46 | // AlgorithmByName returns the compressor by its name | |
47 | func AlgorithmByName(name string) (Algorithm, error) { | |
48 | algorithm, ok := compressionAlgorithms[name] | |
49 | if ok { | |
50 | return algorithm, nil | |
51 | } | |
52 | return Algorithm{}, fmt.Errorf("cannot find compressor for %q", name) | |
53 | } | |
14 | 54 | |
15 | 55 | // DecompressorFunc returns the decompressed stream, given a compressed stream. |
16 | 56 | // The caller must call Close() on the decompressed stream (even if the compressed input stream does not need closing!). |
57 | 97 | return xz.NewWriter(r) |
58 | 98 | } |
59 | 99 | |
60 | // Algorithm is a compression algorithm that can be used for CompressStream. | |
61 | type Algorithm struct { | |
62 | name string | |
63 | prefix []byte | |
64 | decompressor DecompressorFunc | |
65 | compressor compressorFunc | |
66 | } | |
67 | ||
68 | // Name returns the name for the compression algorithm. | |
69 | func (c Algorithm) Name() string { | |
70 | return c.name | |
71 | } | |
72 | ||
73 | // compressionAlgos is an internal implementation detail of DetectCompression | |
74 | var compressionAlgos = []Algorithm{ | |
75 | {"gzip", []byte{0x1F, 0x8B, 0x08}, GzipDecompressor, gzipCompressor}, // gzip (RFC 1952) | |
76 | {"bzip2", []byte{0x42, 0x5A, 0x68}, Bzip2Decompressor, bzip2Compressor}, // bzip2 (decompress.c:BZ2_decompress) | |
77 | {"xz", []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, XzDecompressor, xzCompressor}, // xz (/usr/share/doc/xz/xz-file-format.txt) | |
78 | {"zstd", []byte{0x28, 0xb5, 0x2f, 0xfd}, ZstdDecompressor, zstdCompressor}, // zstd (http://www.zstd.net) | |
79 | } | |
80 | ||
81 | // AlgorithmByName returns the compressor by its name | |
82 | func AlgorithmByName(name string) (Algorithm, error) { | |
83 | for _, c := range compressionAlgos { | |
84 | if c.name == name { | |
85 | return c, nil | |
86 | } | |
87 | } | |
88 | return Algorithm{}, fmt.Errorf("cannot find compressor for %q", name) | |
89 | } | |
90 | ||
91 | 100 | // CompressStream returns the compressor by its name |
92 | 101 | func CompressStream(dest io.Writer, algo Algorithm, level *int) (io.WriteCloser, error) { |
93 | 102 | return algo.compressor(dest, level) |
107 | 116 | |
108 | 117 | var retAlgo Algorithm |
109 | 118 | var decompressor DecompressorFunc |
110 | for _, algo := range compressionAlgos { | |
119 | for _, algo := range compressionAlgorithms { | |
111 | 120 | if bytes.HasPrefix(buffer[:n], algo.prefix) { |
112 | 121 | logrus.Debugf("Detected compression format %s", algo.name) |
113 | 122 | retAlgo = algo |
344 | 344 | } |
345 | 345 | |
346 | 346 | func (s *storageImageDestination) DesiredLayerCompression() types.LayerCompression { |
347 | // We ultimately have to decompress layers to populate trees on disk, | |
348 | // so callers shouldn't bother compressing them before handing them to | |
349 | // us, if they're not already compressed. | |
347 | // We ultimately have to decompress layers to populate trees on disk | |
348 | // and need to explicitly ask for it here, so that the layers' MIME | |
349 | // types can be set accordingly. | |
350 | 350 | return types.PreserveOriginal |
351 | 351 | } |
352 | 352 |
7 | 7 | "github.com/containers/image/docker/reference" |
8 | 8 | "github.com/containers/image/pkg/compression" |
9 | 9 | "github.com/opencontainers/go-digest" |
10 | "github.com/opencontainers/image-spec/specs-go/v1" | |
10 | v1 "github.com/opencontainers/image-spec/specs-go/v1" | |
11 | 11 | ) |
12 | 12 | |
13 | 13 | // ImageTransport is a top-level namespace for ways to to store/load an image. |
90 | 90 | DeleteImage(ctx context.Context, sys *SystemContext) error |
91 | 91 | } |
92 | 92 | |
93 | // LayerCompression indicates if layers must be compressed, decompressed or preserved | |
94 | type LayerCompression int | |
95 | ||
96 | const ( | |
97 | // PreserveOriginal indicates the layer must be preserved, ie | |
98 | // no compression or decompression. | |
99 | PreserveOriginal LayerCompression = iota | |
100 | // Decompress indicates the layer must be decompressed | |
101 | Decompress | |
102 | // Compress indicates the layer must be compressed | |
103 | Compress | |
104 | ) | |
105 | ||
93 | 106 | // BlobInfo collects known information about a blob (layer/config). |
94 | 107 | // In some situations, some fields may be unknown, in others they may be mandatory; documenting an “unknown” value here does not override that. |
95 | 108 | type BlobInfo struct { |
98 | 111 | URLs []string |
99 | 112 | Annotations map[string]string |
100 | 113 | MediaType string |
114 | // CompressionOperation is used in Image.UpdateLayerInfos to instruct | |
115 | // whether the original layer should be preserved or (de)compressed. The | |
116 | // field defaults to preserve the original layer. | |
117 | CompressionOperation LayerCompression | |
118 | // CompressionAlgorithm is used in Image.UpdateLayerInfos to set the correct | |
119 | // MIME type for compressed layers (e.g., gzip or zstd). This field MUST be | |
120 | // set when `CompressionOperation == Compress`. | |
121 | CompressionAlgorithm *compression.Algorithm | |
101 | 122 | } |
102 | 123 | |
103 | 124 | // BICTransportScope encapsulates transport-dependent representation of a “scope” where blobs are or are not present. |
210 | 231 | // WARNING: The list may contain duplicates, and they are semantically relevant. |
211 | 232 | LayerInfosForCopy(ctx context.Context) ([]BlobInfo, error) |
212 | 233 | } |
213 | ||
214 | // LayerCompression indicates if layers must be compressed, decompressed or preserved | |
215 | type LayerCompression int | |
216 | ||
217 | const ( | |
218 | // PreserveOriginal indicates the layer must be preserved, ie | |
219 | // no compression or decompression. | |
220 | PreserveOriginal LayerCompression = iota | |
221 | // Decompress indicates the layer must be decompressed | |
222 | Decompress | |
223 | // Compress indicates the layer must be compressed | |
224 | Compress | |
225 | ) | |
226 | 234 | |
227 | 235 | // ImageDestination is a service, possibly remote (= slow), to store components of a single image. |
228 | 236 | // |