diff --git a/manifest/fixtures/ociv1.image.index.json b/manifest/fixtures/ociv1.image.index.json new file mode 100644 index 0000000000..066f058db1 --- /dev/null +++ b/manifest/fixtures/ociv1.image.index.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/manifest/fixtures/ociv1.manifest.json b/manifest/fixtures/ociv1.manifest.json index ce098423cc..1e1047ca7f 100644 --- a/manifest/fixtures/ociv1.manifest.json +++ b/manifest/fixtures/ociv1.manifest.json @@ -1,26 +1,29 @@ { - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.serialization.config.v1+json", - "size": 7023, - "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip", - "size": 32654, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" - }, - { - "mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip", - "size": 16724, - "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" - }, - { - "mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip", - "size": 73109, - "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" - } - ] -} \ No newline at end of file + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/manifest/fixtures/ociv1list.manifest.json b/manifest/fixtures/ociv1list.manifest.json deleted file mode 100644 index 2c334b6ee0..0000000000 --- a/manifest/fixtures/ociv1list.manifest.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.list.v1+json", - "manifests": [ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 2094, - "digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", - "platform": { - "architecture": "ppc64le", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 1922, - "digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd", - "platform": { - "architecture": "amd64", - "os": "linux", - "features": [ - "sse" - ] - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 2084, - "digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", - "platform": { - "architecture": "s390x", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 2084, - "digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40", - "platform": { - "architecture": "arm", - "os": "linux", - "variant": "armv7" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 2090, - "digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a", - "platform": { - "architecture": "arm64", - "os": "linux", - "variant": "armv8" - } - } - ] -} diff --git a/manifest/manifest.go b/manifest/manifest.go index 430331f0a3..605bab1db7 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -54,7 +54,7 @@ func GuessMIMEType(manifest []byte) string { } switch meta.MediaType { - case DockerV2Schema2MediaType, DockerV2ListMediaType, imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageManifestList: // A recognized type. + case DockerV2Schema2MediaType, DockerV2ListMediaType: // A recognized type. return meta.MediaType } // this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest. @@ -64,7 +64,31 @@ func GuessMIMEType(manifest []byte) string { return DockerV2Schema1SignedMediaType } return DockerV2Schema1MediaType - case 2: // Really should not happen, meta.MediaType should have been set. But given the data, this is our best guess. + case 2: + // best effort to understand if this is an OCI image since mediaType + // isn't in the manifest for OCI anymore + // for docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess. + ociMan := struct { + Config struct { + MediaType string `json:"mediaType"` + } `json:"config"` + Layers []imgspecv1.Descriptor `json:"layers"` + }{} + if err := json.Unmarshal(manifest, &ociMan); err != nil { + return "" + } + if ociMan.Config.MediaType == imgspecv1.MediaTypeImageConfig && len(ociMan.Layers) != 0 { + return imgspecv1.MediaTypeImageManifest + } + ociIndex := struct { + Manifests []imgspecv1.Descriptor `json:"manifests"` + }{} + if err := json.Unmarshal(manifest, &ociIndex); err != nil { + return "" + } + if len(ociIndex.Manifests) != 0 && ociIndex.Manifests[0].MediaType == imgspecv1.MediaTypeImageManifest { + return imgspecv1.MediaTypeImageIndex + } return DockerV2Schema2MediaType } return "" diff --git a/manifest/manifest_test.go b/manifest/manifest_test.go index 78da71a292..97febcc2d8 100644 --- a/manifest/manifest_test.go +++ b/manifest/manifest_test.go @@ -21,8 +21,6 @@ func TestGuessMIMEType(t *testing.T) { path string mimeType string }{ - {"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest}, - {"ociv1list.manifest.json", imgspecv1.MediaTypeImageManifestList}, {"v2s2.manifest.json", DockerV2Schema2MediaType}, {"v2list.manifest.json", DockerV2ListMediaType}, {"v2s1.manifest.json", DockerV2Schema1SignedMediaType}, @@ -31,6 +29,8 @@ func TestGuessMIMEType(t *testing.T) { {"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. {"unknown-version.manifest.json", ""}, {"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all + {"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, } for _, c := range cases { diff --git a/oci/layout/oci_dest.go b/oci/layout/oci_dest.go index 5c0d0e0f46..d2a44b22fc 100644 --- a/oci/layout/oci_dest.go +++ b/oci/layout/oci_dest.go @@ -6,22 +6,30 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "github.com/pkg/errors" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/opencontainers/go-digest" + imgspec "github.com/opencontainers/image-spec/specs-go" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) type ociImageDestination struct { - ref ociReference + ref ociReference + index imgspecv1.ImageIndex } // newImageDestination returns an ImageDestination for writing to an existing directory. func newImageDestination(ref ociReference) types.ImageDestination { - return &ociImageDestination{ref: ref} + index := imgspecv1.ImageIndex{ + Versioned: imgspec.Versioned{ + SchemaVersion: 2, + }, + } + return &ociImageDestination{ref: ref, index: index} } // Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, @@ -152,10 +160,6 @@ func (d *ociImageDestination) PutManifest(m []byte) error { // TODO(runcom): beaware and add support for OCI manifest list desc.MediaType = imgspecv1.MediaTypeImageManifest desc.Size = int64(len(m)) - data, err := json.Marshal(desc) - if err != nil { - return err - } blobPath, err := d.ref.blobPath(digest) if err != nil { @@ -167,15 +171,19 @@ func (d *ociImageDestination) PutManifest(m []byte) error { if err := ioutil.WriteFile(blobPath, m, 0644); err != nil { return err } - // TODO(runcom): ugly here? - if err := ioutil.WriteFile(d.ref.ociLayoutPath(), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil { - return err - } - descriptorPath := d.ref.descriptorPath(d.ref.tag) - if err := ensureParentDirectoryExists(descriptorPath); err != nil { - return err - } - return ioutil.WriteFile(descriptorPath, data, 0644) + + annotations := make(map[string]string) + annotations["org.opencontainers.ref.name"] = d.ref.tag + desc.Annotations = annotations + d.index.Manifests = append(d.index.Manifests, imgspecv1.ManifestDescriptor{ + Descriptor: desc, + Platform: imgspecv1.Platform{ + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + }, + }) + + return nil } func ensureDirectoryExists(path string) error { @@ -204,5 +212,12 @@ func (d *ociImageDestination) PutSignatures(signatures [][]byte) error { // - Uploaded data MAY be visible to others before Commit() is called // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) func (d *ociImageDestination) Commit() error { - return nil + if err := ioutil.WriteFile(d.ref.ociLayoutPath(), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil { + return err + } + indexJSON, err := json.Marshal(d.index) + if err != nil { + return err + } + return ioutil.WriteFile(d.ref.indexPath(), indexJSON, 0644) } diff --git a/oci/layout/oci_src.go b/oci/layout/oci_src.go index 6ae47c26b4..04eca3f092 100644 --- a/oci/layout/oci_src.go +++ b/oci/layout/oci_src.go @@ -1,7 +1,6 @@ package layout import ( - "encoding/json" "io" "io/ioutil" "os" @@ -12,12 +11,17 @@ import ( ) type ociImageSource struct { - ref ociReference + ref ociReference + descriptor imgspecv1.ManifestDescriptor } // newImageSource returns an ImageSource for reading from an existing directory. -func newImageSource(ref ociReference) types.ImageSource { - return &ociImageSource{ref: ref} +func newImageSource(ref ociReference) (types.ImageSource, error) { + descriptor, err := ref.getManifestDescriptor() + if err != nil { + return nil, err + } + return &ociImageSource{ref: ref, descriptor: descriptor}, nil } // Reference returns the reference used to set up this source. @@ -33,19 +37,7 @@ func (s *ociImageSource) Close() error { // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). // It may use a remote (= slow) service. func (s *ociImageSource) GetManifest() ([]byte, string, error) { - descriptorPath := s.ref.descriptorPath(s.ref.tag) - data, err := ioutil.ReadFile(descriptorPath) - if err != nil { - return nil, "", err - } - - desc := imgspecv1.Descriptor{} - err = json.Unmarshal(data, &desc) - if err != nil { - return nil, "", err - } - - manifestPath, err := s.ref.blobPath(digest.Digest(desc.Digest)) + manifestPath, err := s.ref.blobPath(digest.Digest(s.descriptor.Digest)) if err != nil { return nil, "", err } @@ -54,7 +46,7 @@ func (s *ociImageSource) GetManifest() ([]byte, string, error) { return nil, "", err } - return m, desc.MediaType, nil + return m, s.descriptor.MediaType, nil } func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) { diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go index 007798b4cd..b020aebc6f 100644 --- a/oci/layout/oci_transport.go +++ b/oci/layout/oci_transport.go @@ -1,7 +1,9 @@ package layout import ( + "encoding/json" "fmt" + "os" "path/filepath" "regexp" "strings" @@ -12,6 +14,7 @@ import ( "github.com/containers/image/transports" "github.com/containers/image/types" "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -176,16 +179,49 @@ func (ref ociReference) PolicyConfigurationNamespaces() []string { // NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource, // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage. func (ref ociReference) NewImage(ctx *types.SystemContext) (types.Image, error) { - src := newImageSource(ref) + src, err := newImageSource(ref) + if err != nil { + return nil, err + } return image.FromSource(src) } +func (ref ociReference) getManifestDescriptor() (imgspecv1.ManifestDescriptor, error) { + indexJSON, err := os.Open(ref.indexPath()) + if err != nil { + return imgspecv1.ManifestDescriptor{}, err + } + defer indexJSON.Close() + index := imgspecv1.ImageIndex{} + if err := json.NewDecoder(indexJSON).Decode(&index); err != nil { + return imgspecv1.ManifestDescriptor{}, err + } + var d *imgspecv1.ManifestDescriptor + for _, md := range index.Manifests { + if md.MediaType != imgspecv1.MediaTypeImageManifest { + continue + } + refName, ok := md.Annotations["org.opencontainers.ref.name"] + if !ok { + continue + } + if refName == ref.tag { + d = &md + break + } + } + if d == nil { + return imgspecv1.ManifestDescriptor{}, fmt.Errorf("no descriptor found for reference %q", ref.tag) + } + return *d, nil +} + // NewImageSource returns a types.ImageSource for this reference, // asking the backend to use a manifest from requestedManifestMIMETypes if possible. // nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes. // The caller must call .Close() on the returned ImageSource. func (ref ociReference) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) { - return newImageSource(ref), nil + return newImageSource(ref) } // NewImageDestination returns a types.ImageDestination for this reference. @@ -199,11 +235,16 @@ func (ref ociReference) DeleteImage(ctx *types.SystemContext) error { return errors.Errorf("Deleting images not implemented for oci: images") } -// ociLayoutPathPath returns a path for the oci-layout within a directory using OCI conventions. +// ociLayoutPath returns a path for the oci-layout within a directory using OCI conventions. func (ref ociReference) ociLayoutPath() string { return filepath.Join(ref.dir, "oci-layout") } +// indexPath returns a path for the index.json within a directory using OCI conventions. +func (ref ociReference) indexPath() string { + return filepath.Join(ref.dir, "index.json") +} + // blobPath returns a path for a blob within a directory using OCI image-layout conventions. func (ref ociReference) blobPath(digest digest.Digest) (string, error) { if err := digest.Validate(); err != nil { @@ -211,8 +252,3 @@ func (ref ociReference) blobPath(digest digest.Digest) (string, error) { } return filepath.Join(ref.dir, "blobs", digest.Algorithm().String(), digest.Hex()), nil } - -// descriptorPath returns a path for the manifest within a directory using OCI conventions. -func (ref ociReference) descriptorPath(digest string) string { - return filepath.Join(ref.dir, "refs", digest) -} diff --git a/oci/layout/oci_transport_test.go b/oci/layout/oci_transport_test.go index eeefd2cef5..bdd8a30a19 100644 --- a/oci/layout/oci_transport_test.go +++ b/oci/layout/oci_transport_test.go @@ -115,6 +115,25 @@ func TestNewReference(t *testing.T) { func refToTempOCI(t *testing.T) (ref types.ImageReference, tmpDir string) { tmpDir, err := ioutil.TempDir("", "oci-transport-test") require.NoError(t, err) + m := `{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.ref.name": "tagValue" + } + } + ] + } +` + ioutil.WriteFile(filepath.Join(tmpDir, "index.json"), []byte(m), 0644) ref, err = NewReference(tmpDir, "tagValue") require.NoError(t, err) return ref, tmpDir @@ -239,6 +258,14 @@ func TestReferenceOCILayoutPath(t *testing.T) { assert.Equal(t, tmpDir+"/oci-layout", ociRef.ociLayoutPath()) } +func TestReferenceIndexPath(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + defer os.RemoveAll(tmpDir) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/index.json", ociRef.indexPath()) +} + func TestReferenceBlobPath(t *testing.T) { const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" @@ -262,11 +289,3 @@ func TestReferenceBlobPathInvalid(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "unexpected digest reference "+hex) } - -func TestReferenceDescriptorPath(t *testing.T) { - ref, tmpDir := refToTempOCI(t) - defer os.RemoveAll(tmpDir) - ociRef, ok := ref.(ociReference) - require.True(t, ok) - assert.Equal(t, tmpDir+"/refs/notlatest", ociRef.descriptorPath("notlatest")) -} diff --git a/vendor.conf b/vendor.conf index 454aabbfca..1685422100 100644 --- a/vendor.conf +++ b/vendor.conf @@ -15,7 +15,7 @@ github.com/mattn/go-shellwords 005a0944d84452842197c2108bd9168ced206f78 github.com/mistifyio/go-zfs c0224de804d438efd11ea6e52ada8014537d6062 github.com/mtrmac/gpgme b2432428689ca58c2b8e8dea9449d3295cf96fc9 github.com/opencontainers/go-digest aa2ec055abd10d26d539eb630a92241b781ce4bc -github.com/opencontainers/image-spec v1.0.0-rc4 +github.com/opencontainers/image-spec v1.0.0-rc5 github.com/opencontainers/runc 6b1d0e76f239ffb435445e5ae316d2676c07c6e3 github.com/pborman/uuid 1b00554d822231195d1babd97ff4a781231955c9 github.com/pkg/errors 248dadf4e9068a0b3e79f02ed0a610d935de5302