1
0
mirror of https://github.com/distribution/distribution synced 2024-11-12 05:45:51 +01:00

Merge pull request #3869 from brackendawson/split-oci-index

Split OCI Image Index from Docker Manifest List
This commit is contained in:
Wang Yan 2023-07-19 12:02:15 +08:00 committed by GitHub
commit 46b3d62016
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 548 additions and 332 deletions

@ -23,13 +23,6 @@ var SchemaVersion = manifest.Versioned{
MediaType: MediaTypeManifestList, MediaType: MediaTypeManifestList,
} }
// OCISchemaVersion provides a pre-initialized version structure for this
// packages OCIschema version of the manifest.
var OCISchemaVersion = manifest.Versioned{
SchemaVersion: 2,
MediaType: v1.MediaTypeImageIndex,
}
func init() { func init() {
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifestList) m := new(DeserializedManifestList)
@ -52,31 +45,6 @@ func init() {
if err != nil { if err != nil {
panic(fmt.Sprintf("Unable to register manifest: %s", err)) panic(fmt.Sprintf("Unable to register manifest: %s", err))
} }
imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
if err := validateIndex(b); err != nil {
return nil, distribution.Descriptor{}, err
}
m := new(DeserializedManifestList)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex {
err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'",
v1.MediaTypeImageIndex, m.MediaType)
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err
}
err = distribution.RegisterManifestSchema(v1.MediaTypeImageIndex, imageIndexFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register OCI Image Index: %s", err))
}
} }
// PlatformSpec specifies a platform where a particular image manifest is // PlatformSpec specifies a platform where a particular image manifest is
@ -154,21 +122,14 @@ type DeserializedManifestList struct {
// DeserializedManifestList which contains the resulting manifest list // DeserializedManifestList which contains the resulting manifest list
// and its JSON representation. // and its JSON representation.
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
var mediaType string return fromDescriptorsWithMediaType(descriptors, MediaTypeManifestList)
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest {
mediaType = v1.MediaTypeImageIndex
} else {
mediaType = MediaTypeManifestList
}
return FromDescriptorsWithMediaType(descriptors, mediaType)
} }
// FromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly // fromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) { func fromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) {
m := ManifestList{ m := ManifestList{
Versioned: manifest.Versioned{ Versioned: manifest.Versioned{
SchemaVersion: 2, SchemaVersion: SchemaVersion.SchemaVersion,
MediaType: mediaType, MediaType: mediaType,
}, },
} }
@ -215,32 +176,21 @@ func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
// Payload returns the raw content of the manifest list. The contents can be // Payload returns the raw content of the manifest list. The contents can be
// used to calculate the content identifier. // used to calculate the content identifier.
func (m DeserializedManifestList) Payload() (string, []byte, error) { func (m DeserializedManifestList) Payload() (string, []byte, error) {
var mediaType string return m.MediaType, m.canonical, nil
if m.MediaType == "" {
mediaType = v1.MediaTypeImageIndex
} else {
mediaType = m.MediaType
}
return mediaType, m.canonical, nil
} }
// unknownDocument represents a manifest, manifest list, or index that has not // validateManifestList returns an error if the byte slice is invalid JSON or if it
// yet been validated
type unknownDocument struct {
Config interface{} `json:"config,omitempty"`
Layers interface{} `json:"layers,omitempty"`
}
// validateIndex returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a manifest // contains fields that belong to a manifest
func validateIndex(b []byte) error { func validateManifestList(b []byte) error {
var doc unknownDocument var doc struct {
Config interface{} `json:"config,omitempty"`
Layers interface{} `json:"layers,omitempty"`
}
if err := json.Unmarshal(b, &doc); err != nil { if err := json.Unmarshal(b, &doc); err != nil {
return err return err
} }
if doc.Config != nil || doc.Layers != nil { if doc.Config != nil || doc.Layers != nil {
return errors.New("index: expected index but found manifest") return errors.New("manifestlist: expected list but found manifest")
} }
return nil return nil
} }

@ -7,7 +7,7 @@ import (
"testing" "testing"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/manifest/schema2"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
) )
@ -67,7 +67,7 @@ func makeTestManifestList(t *testing.T, mediaType string) ([]ManifestDescriptor,
}, },
} }
deserialized, err := FromDescriptorsWithMediaType(manifestDescriptors, mediaType) deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, mediaType)
if err != nil { if err != nil {
t.Fatalf("error creating DeserializedManifestList: %v", err) t.Fatalf("error creating DeserializedManifestList: %v", err)
} }
@ -130,223 +130,69 @@ func TestManifestList(t *testing.T) {
} }
} }
// TODO (mikebrow): add annotations on the manifest list (index) and support for func mediaTypeTest(contentType string, mediaType string, shouldError bool) func(*testing.T) {
// empty platform structs (move to Platform *Platform `json:"platform,omitempty"` return func(t *testing.T) {
// from current Platform PlatformSpec `json:"platform"`) in the manifest descriptor. var m *DeserializedManifestList
// Requires changes to distribution/distribution/manifest/manifestlist.ManifestList and .ManifestDescriptor
// and associated serialization APIs in manifestlist.go. Or split the OCI index and
// docker manifest list implementations, which would require a lot of refactoring.
const expectedOCIImageIndexSerialization = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
"size": 985,
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse4"
]
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
"size": 985,
"annotations": {
"platform": "none"
},
"platform": {
"architecture": "",
"os": ""
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608",
"size": 2392,
"annotations": {
"what": "for"
},
"platform": {
"architecture": "sun4m",
"os": "sunos"
}
}
]
}`
func makeTestOCIImageIndex(t *testing.T, mediaType string) ([]ManifestDescriptor, *DeserializedManifestList) {
manifestDescriptors := []ManifestDescriptor{
{
Descriptor: distribution.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985,
},
Platform: PlatformSpec{
Architecture: "amd64",
OS: "linux",
Features: []string{"sse4"},
},
},
{
Descriptor: distribution.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985,
Annotations: map[string]string{"platform": "none"},
},
},
{
Descriptor: distribution.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:6346340964309634683409684360934680934608934608934608934068934608",
Size: 2392,
Annotations: map[string]string{"what": "for"},
},
Platform: PlatformSpec{
Architecture: "sun4m",
OS: "sunos",
},
},
}
deserialized, err := FromDescriptorsWithMediaType(manifestDescriptors, mediaType)
if err != nil {
t.Fatalf("error creating DeserializedManifestList: %v", err)
}
return manifestDescriptors, deserialized
}
func TestOCIImageIndex(t *testing.T) {
manifestDescriptors, deserialized := makeTestOCIImageIndex(t, v1.MediaTypeImageIndex)
mediaType, canonical, _ := deserialized.Payload()
if mediaType != v1.MediaTypeImageIndex {
t.Fatalf("unexpected media type: %s", mediaType)
}
// Check that the canonical field is the same as json.MarshalIndent
// with these parameters.
expected, err := json.MarshalIndent(&deserialized.ManifestList, "", " ")
if err != nil {
t.Fatalf("error marshaling manifest list: %v", err)
}
if !bytes.Equal(expected, canonical) {
t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", string(expected), string(canonical))
}
// Check that the canonical field has the expected value.
if !bytes.Equal([]byte(expectedOCIImageIndexSerialization), canonical) {
t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", expectedOCIImageIndexSerialization, string(canonical))
}
var unmarshalled DeserializedManifestList
if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil {
t.Fatalf("error unmarshaling manifest: %v", err)
}
if !reflect.DeepEqual(&unmarshalled, deserialized) {
t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized)
}
references := deserialized.References()
if len(references) != 3 {
t.Fatalf("unexpected number of references: %d", len(references))
}
for i := range references {
platform := manifestDescriptors[i].Platform
expectedPlatform := &v1.Platform{
Architecture: platform.Architecture,
OS: platform.OS,
OSFeatures: platform.OSFeatures,
OSVersion: platform.OSVersion,
Variant: platform.Variant,
}
if !reflect.DeepEqual(references[i].Platform, expectedPlatform) {
t.Fatalf("unexpected value %d returned by References: %v", i, references[i])
}
references[i].Platform = nil
if !reflect.DeepEqual(references[i], manifestDescriptors[i].Descriptor) {
t.Fatalf("unexpected value %d returned by References: %v", i, references[i])
}
}
}
func mediaTypeTest(t *testing.T, contentType string, mediaType string, shouldError bool) {
var m *DeserializedManifestList
if contentType == MediaTypeManifestList {
_, m = makeTestManifestList(t, mediaType) _, m = makeTestManifestList(t, mediaType)
} else {
_, m = makeTestOCIImageIndex(t, mediaType)
}
_, canonical, err := m.Payload() _, canonical, err := m.Payload()
if err != nil {
t.Fatalf("error getting payload, %v", err)
}
unmarshalled, descriptor, err := distribution.UnmarshalManifest(
contentType,
canonical)
if shouldError {
if err == nil {
t.Fatalf("bad content type should have produced error")
}
} else {
if err != nil { if err != nil {
t.Fatalf("error unmarshaling manifest, %v", err) t.Fatalf("error getting payload, %v", err)
} }
asManifest := unmarshalled.(*DeserializedManifestList) unmarshalled, descriptor, err := distribution.UnmarshalManifest(
if asManifest.MediaType != mediaType { contentType,
t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType) canonical)
}
if descriptor.MediaType != contentType { if shouldError {
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType) if err == nil {
} t.Fatalf("bad content type should have produced error")
}
} else {
if err != nil {
t.Fatalf("error unmarshaling manifest, %v", err)
}
unmarshalledMediaType, _, _ := unmarshalled.Payload() asManifest := unmarshalled.(*DeserializedManifestList)
if unmarshalledMediaType != contentType { if asManifest.MediaType != mediaType {
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType) t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType)
}
if descriptor.MediaType != contentType {
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType)
}
unmarshalledMediaType, _, _ := unmarshalled.Payload()
if unmarshalledMediaType != contentType {
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
}
} }
} }
} }
func TestMediaTypes(t *testing.T) { func TestMediaTypes(t *testing.T) {
mediaTypeTest(t, MediaTypeManifestList, "", true) t.Run("ManifestList_No_MediaType", mediaTypeTest(MediaTypeManifestList, "", true))
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList, false) t.Run("ManifestList", mediaTypeTest(MediaTypeManifestList, MediaTypeManifestList, false))
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList+"XXX", true) t.Run("ManifestList_Bad_MediaType", mediaTypeTest(MediaTypeManifestList, MediaTypeManifestList+"XXX", true))
mediaTypeTest(t, v1.MediaTypeImageIndex, "", false)
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex, false)
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex+"XXX", true)
} }
func TestValidateManifest(t *testing.T) { func TestValidateManifestList(t *testing.T) {
manifest := ocischema.Manifest{ manifest := schema2.Manifest{
Config: distribution.Descriptor{Size: 1}, Config: distribution.Descriptor{Size: 1},
Layers: []distribution.Descriptor{{Size: 2}}, Layers: []distribution.Descriptor{{Size: 2}},
} }
index := ManifestList{ manifestList := ManifestList{
Manifests: []ManifestDescriptor{ Manifests: []ManifestDescriptor{
{Descriptor: distribution.Descriptor{Size: 3}}, {Descriptor: distribution.Descriptor{Size: 3}},
}, },
} }
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
b, err := json.Marshal(index) b, err := json.Marshal(manifestList)
if err != nil { if err != nil {
t.Fatal("unexpected error marshaling index", err) t.Fatal("unexpected error marshaling manifest list", err)
} }
if err := validateIndex(b); err != nil { if err := validateManifestList(b); err != nil {
t.Error("index should be valid", err) t.Error("list should be valid", err)
} }
}) })
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
@ -354,7 +200,7 @@ func TestValidateManifest(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("unexpected error marshaling manifest", err) t.Fatal("unexpected error marshaling manifest", err)
} }
if err := validateIndex(b); err == nil { if err := validateManifestList(b); err == nil {
t.Error("manifest should not be valid") t.Error("manifest should not be valid")
} }
}) })

156
manifest/ocischema/index.go Normal file

@ -0,0 +1,156 @@
package ocischema
import (
"encoding/json"
"errors"
"fmt"
"github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/manifest"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// IndexSchemaVersion provides a pre-initialized version structure for OCI Image
// Indices.
var IndexSchemaVersion = manifest.Versioned{
SchemaVersion: 2,
MediaType: v1.MediaTypeImageIndex,
}
func init() {
imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
if err := validateIndex(b); err != nil {
return nil, distribution.Descriptor{}, err
}
m := new(DeserializedImageIndex)
err := m.UnmarshalJSON(b)
if err != nil {
return nil, distribution.Descriptor{}, err
}
if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex {
err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'",
v1.MediaTypeImageIndex, m.MediaType)
return nil, distribution.Descriptor{}, err
}
dgst := digest.FromBytes(b)
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err
}
err := distribution.RegisterManifestSchema(v1.MediaTypeImageIndex, imageIndexFunc)
if err != nil {
panic(fmt.Sprintf("Unable to register OCI Image Index: %s", err))
}
}
// ImageIndex references manifests for various platforms.
type ImageIndex struct {
manifest.Versioned
// Manifests references a list of manifests
Manifests []distribution.Descriptor `json:"manifests"`
// Annotations is an optional field that contains arbitrary metadata for the
// image index
Annotations map[string]string `json:"annotations,omitempty"`
}
// References returns the distribution descriptors for the referenced image
// manifests.
func (ii ImageIndex) References() []distribution.Descriptor {
return ii.Manifests
}
// DeserializedImageIndex wraps ManifestList with a copy of the original
// JSON.
type DeserializedImageIndex struct {
ImageIndex
// canonical is the canonical byte representation of the Manifest.
canonical []byte
}
// FromDescriptors takes a slice of descriptors and a map of annotations, and
// returns a DeserializedManifestList which contains the resulting manifest list
// and its JSON representation. If annotations is nil or empty then the
// annotations property will be omitted from the JSON representation.
func FromDescriptors(descriptors []distribution.Descriptor, annotations map[string]string) (*DeserializedImageIndex, error) {
return fromDescriptorsWithMediaType(descriptors, annotations, v1.MediaTypeImageIndex)
}
// fromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
func fromDescriptorsWithMediaType(descriptors []distribution.Descriptor, annotations map[string]string, mediaType string) (_ *DeserializedImageIndex, err error) {
m := ImageIndex{
Versioned: manifest.Versioned{
SchemaVersion: IndexSchemaVersion.SchemaVersion,
MediaType: mediaType,
},
Annotations: annotations,
}
m.Manifests = make([]distribution.Descriptor, len(descriptors))
copy(m.Manifests, descriptors)
deserialized := DeserializedImageIndex{
ImageIndex: m,
}
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
return &deserialized, err
}
// UnmarshalJSON populates a new ManifestList struct from JSON data.
func (m *DeserializedImageIndex) UnmarshalJSON(b []byte) error {
m.canonical = make([]byte, len(b))
// store manifest list in canonical
copy(m.canonical, b)
// Unmarshal canonical JSON into ManifestList object
var manifestList ImageIndex
if err := json.Unmarshal(m.canonical, &manifestList); err != nil {
return err
}
m.ImageIndex = manifestList
return nil
}
// MarshalJSON returns the contents of canonical. If canonical is empty,
// marshals the inner contents.
func (m *DeserializedImageIndex) MarshalJSON() ([]byte, error) {
if len(m.canonical) > 0 {
return m.canonical, nil
}
return nil, errors.New("JSON representation not initialized in DeserializedImageIndex")
}
// Payload returns the raw content of the manifest list. The contents can be
// used to calculate the content identifier.
func (m DeserializedImageIndex) Payload() (string, []byte, error) {
mediaType := m.MediaType
if m.MediaType == "" {
mediaType = v1.MediaTypeImageIndex
}
return mediaType, m.canonical, nil
}
// validateIndex returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a manifest
func validateIndex(b []byte) error {
var doc struct {
Config interface{} `json:"config,omitempty"`
Layers interface{} `json:"layers,omitempty"`
}
if err := json.Unmarshal(b, &doc); err != nil {
return err
}
if doc.Config != nil || doc.Layers != nil {
return errors.New("index: expected index but found manifest")
}
return nil
}

@ -0,0 +1,209 @@
package ocischema
import (
"bytes"
"encoding/json"
"reflect"
"testing"
"github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/manifest/schema2"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const expectedOCIImageIndexSerialization = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
"size": 985,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
"size": 985,
"annotations": {
"platform": "none"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608",
"size": 2392,
"annotations": {
"what": "for"
},
"platform": {
"architecture": "sun4m",
"os": "sunos"
}
}
],
"annotations": {
"com.example.favourite-colour": "blue",
"com.example.locale": "en_GB"
}
}`
func makeTestOCIImageIndex(t *testing.T, mediaType string) ([]distribution.Descriptor, *DeserializedImageIndex) {
manifestDescriptors := []distribution.Descriptor{
{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985,
Platform: &v1.Platform{
Architecture: "amd64",
OS: "linux",
},
},
{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985,
Annotations: map[string]string{"platform": "none"},
},
{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:6346340964309634683409684360934680934608934608934608934068934608",
Size: 2392,
Annotations: map[string]string{"what": "for"},
Platform: &v1.Platform{
Architecture: "sun4m",
OS: "sunos",
},
},
}
annotations := map[string]string{
"com.example.favourite-colour": "blue",
"com.example.locale": "en_GB",
}
deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, annotations, mediaType)
if err != nil {
t.Fatalf("error creating DeserializedManifestList: %v", err)
}
return manifestDescriptors, deserialized
}
func TestOCIImageIndex(t *testing.T) {
manifestDescriptors, deserialized := makeTestOCIImageIndex(t, v1.MediaTypeImageIndex)
mediaType, canonical, _ := deserialized.Payload()
if mediaType != v1.MediaTypeImageIndex {
t.Fatalf("unexpected media type: %s", mediaType)
}
// Check that the canonical field is the same as json.MarshalIndent
// with these parameters.
expected, err := json.MarshalIndent(&deserialized.ImageIndex, "", " ")
if err != nil {
t.Fatalf("error marshaling manifest list: %v", err)
}
if !bytes.Equal(expected, canonical) {
t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", string(expected), string(canonical))
}
// Check that the canonical field has the expected value.
if !bytes.Equal([]byte(expectedOCIImageIndexSerialization), canonical) {
t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", expectedOCIImageIndexSerialization, string(canonical))
}
var unmarshalled DeserializedImageIndex
if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil {
t.Fatalf("error unmarshaling manifest: %v", err)
}
if !reflect.DeepEqual(&unmarshalled, deserialized) {
t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized)
}
references := deserialized.References()
if len(references) != 3 {
t.Fatalf("unexpected number of references: %d", len(references))
}
if !reflect.DeepEqual(references, manifestDescriptors) {
t.Errorf("expected references:\n%v\nbut got:\n%v", references, manifestDescriptors)
}
}
func indexMediaTypeTest(contentType string, mediaType string, shouldError bool) func(*testing.T) {
return func(t *testing.T) {
var m *DeserializedImageIndex
_, m = makeTestOCIImageIndex(t, mediaType)
_, canonical, err := m.Payload()
if err != nil {
t.Fatalf("error getting payload, %v", err)
}
unmarshalled, descriptor, err := distribution.UnmarshalManifest(
contentType,
canonical)
if shouldError {
if err == nil {
t.Fatalf("bad content type should have produced error")
}
} else {
if err != nil {
t.Fatalf("error unmarshaling manifest, %v", err)
}
asManifest := unmarshalled.(*DeserializedImageIndex)
if asManifest.MediaType != mediaType {
t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType)
}
if descriptor.MediaType != contentType {
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType)
}
unmarshalledMediaType, _, _ := unmarshalled.Payload()
if unmarshalledMediaType != contentType {
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
}
}
}
}
func TestIndexMediaTypes(t *testing.T) {
t.Run("No_MediaType", indexMediaTypeTest(v1.MediaTypeImageIndex, "", false))
t.Run("ImageIndex", indexMediaTypeTest(v1.MediaTypeImageIndex, v1.MediaTypeImageIndex, false))
t.Run("Bad_MediaType", indexMediaTypeTest(v1.MediaTypeImageIndex, v1.MediaTypeImageIndex+"XXX", true))
}
func TestValidateIndex(t *testing.T) {
manifest := schema2.Manifest{
Config: distribution.Descriptor{Size: 1},
Layers: []distribution.Descriptor{{Size: 2}},
}
index := ImageIndex{
Manifests: []distribution.Descriptor{{Size: 3}},
}
t.Run("valid", func(t *testing.T) {
b, err := json.Marshal(index)
if err != nil {
t.Fatal("unexpected error marshaling index", err)
}
if err := validateIndex(b); err != nil {
t.Error("index should be valid", err)
}
})
t.Run("invalid", func(t *testing.T) {
b, err := json.Marshal(manifest)
if err != nil {
t.Fatal("unexpected error marshaling manifest", err)
}
if err := validateIndex(b); err == nil {
t.Error("manifest should not be valid")
}
})
}

@ -11,10 +11,10 @@ import (
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
) )
// SchemaVersion provides a pre-initialized version structure for this // SchemaVersion provides a pre-initialized version structure for OCI Image
// packages version of the manifest. // Manifests
var SchemaVersion = manifest.Versioned{ var SchemaVersion = manifest.Versioned{
SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version SchemaVersion: 2,
MediaType: v1.MediaTypeImageManifest, MediaType: v1.MediaTypeImageManifest,
} }
@ -124,16 +124,12 @@ func (m DeserializedManifest) Payload() (string, []byte, error) {
return v1.MediaTypeImageManifest, m.canonical, nil return v1.MediaTypeImageManifest, m.canonical, nil
} }
// unknownDocument represents a manifest, manifest list, or index that has not
// yet been validated
type unknownDocument struct {
Manifests interface{} `json:"manifests,omitempty"`
}
// validateManifest returns an error if the byte slice is invalid JSON or if it // validateManifest returns an error if the byte slice is invalid JSON or if it
// contains fields that belong to a index // contains fields that belong to a index
func validateManifest(b []byte) error { func validateManifest(b []byte) error {
var doc unknownDocument var doc struct {
Manifests interface{} `json:"manifests,omitempty"`
}
if err := json.Unmarshal(b, &doc); err != nil { if err := json.Unmarshal(b, &doc); err != nil {
return err return err
} }

@ -142,47 +142,49 @@ func TestManifest(t *testing.T) {
} }
} }
func mediaTypeTest(t *testing.T, mediaType string, shouldError bool) { func manifestMediaTypeTest(mediaType string, shouldError bool) func(*testing.T) {
mfst := makeTestManifest(mediaType) return func(t *testing.T) {
mfst := makeTestManifest(mediaType)
deserialized, err := FromStruct(mfst) deserialized, err := FromStruct(mfst)
if err != nil {
t.Fatalf("error creating DeserializedManifest: %v", err)
}
unmarshalled, descriptor, err := distribution.UnmarshalManifest(
v1.MediaTypeImageManifest,
deserialized.canonical)
if shouldError {
if err == nil {
t.Fatalf("bad content type should have produced error")
}
} else {
if err != nil { if err != nil {
t.Fatalf("error unmarshaling manifest, %v", err) t.Fatalf("error creating DeserializedManifest: %v", err)
} }
asManifest := unmarshalled.(*DeserializedManifest) unmarshalled, descriptor, err := distribution.UnmarshalManifest(
if asManifest.MediaType != mediaType { v1.MediaTypeImageManifest,
t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType) deserialized.canonical)
}
if descriptor.MediaType != v1.MediaTypeImageManifest { if shouldError {
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType) if err == nil {
} t.Fatalf("bad content type should have produced error")
}
} else {
if err != nil {
t.Fatalf("error unmarshaling manifest, %v", err)
}
unmarshalledMediaType, _, _ := unmarshalled.Payload() asManifest := unmarshalled.(*DeserializedManifest)
if unmarshalledMediaType != v1.MediaTypeImageManifest { if asManifest.MediaType != mediaType {
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType) t.Fatalf("Bad media type '%v' as unmarshalled", asManifest.MediaType)
}
if descriptor.MediaType != v1.MediaTypeImageManifest {
t.Fatalf("Bad media type '%v' for descriptor", descriptor.MediaType)
}
unmarshalledMediaType, _, _ := unmarshalled.Payload()
if unmarshalledMediaType != v1.MediaTypeImageManifest {
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
}
} }
} }
} }
func TestMediaTypes(t *testing.T) { func TestManifestMediaTypes(t *testing.T) {
mediaTypeTest(t, "", false) t.Run("No_MediaType", manifestMediaTypeTest("", false))
mediaTypeTest(t, v1.MediaTypeImageManifest, false) t.Run("ImageManifest", manifestMediaTypeTest(v1.MediaTypeImageManifest, false))
mediaTypeTest(t, v1.MediaTypeImageManifest+"XXX", true) t.Run("Bad_MediaType", manifestMediaTypeTest(v1.MediaTypeImageManifest+"XXX", true))
} }
func TestValidateManifest(t *testing.T) { func TestValidateManifest(t *testing.T) {

@ -7,6 +7,7 @@ import (
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
dcontext "github.com/distribution/distribution/v3/context" dcontext "github.com/distribution/distribution/v3/context"
"github.com/distribution/distribution/v3/manifest/manifestlist" "github.com/distribution/distribution/v3/manifest/manifestlist"
"github.com/distribution/distribution/v3/manifest/ocischema"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
@ -33,16 +34,26 @@ func (ms *manifestListHandler) Unmarshal(ctx context.Context, dgst digest.Digest
func (ms *manifestListHandler) Put(ctx context.Context, manifestList distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { func (ms *manifestListHandler) Put(ctx context.Context, manifestList distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
dcontext.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put") dcontext.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put")
m, ok := manifestList.(*manifestlist.DeserializedManifestList) var schemaVersion, expectedSchemaVersion int
if !ok { switch m := manifestList.(type) {
case *manifestlist.DeserializedManifestList:
expectedSchemaVersion = manifestlist.SchemaVersion.SchemaVersion
schemaVersion = m.SchemaVersion
case *ocischema.DeserializedImageIndex:
expectedSchemaVersion = ocischema.IndexSchemaVersion.SchemaVersion
schemaVersion = m.SchemaVersion
default:
return "", fmt.Errorf("wrong type put to manifestListHandler: %T", manifestList) return "", fmt.Errorf("wrong type put to manifestListHandler: %T", manifestList)
} }
if schemaVersion != expectedSchemaVersion {
return "", fmt.Errorf("unrecognized manifest list schema version %d, expected %d", schemaVersion, expectedSchemaVersion)
}
if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil { if err := ms.verifyManifest(ms.ctx, manifestList, skipDependencyVerification); err != nil {
return "", err return "", err
} }
mt, payload, err := m.Payload() mt, payload, err := manifestList.Payload()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -60,13 +71,9 @@ func (ms *manifestListHandler) Put(ctx context.Context, manifestList distributio
// perspective of the registry. As a policy, the registry only tries to // perspective of the registry. As a policy, the registry only tries to
// store valid content, leaving trust policies of that content up to // store valid content, leaving trust policies of that content up to
// consumers. // consumers.
func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst manifestlist.DeserializedManifestList, skipDependencyVerification bool) error { func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst distribution.Manifest, skipDependencyVerification bool) error {
var errs distribution.ErrManifestVerification var errs distribution.ErrManifestVerification
if mnfst.SchemaVersion != 2 {
return fmt.Errorf("unrecognized manifest list schema version %d", mnfst.SchemaVersion)
}
if !skipDependencyVerification { if !skipDependencyVerification {
// This manifest service is different from the blob service // This manifest service is different from the blob service
// returned by Blob. It uses a linked blob store to ensure that // returned by Blob. It uses a linked blob store to ensure that

@ -48,10 +48,11 @@ type manifestStore struct {
skipDependencyVerification bool skipDependencyVerification bool
schema1Handler ManifestHandler schema1Handler ManifestHandler
schema2Handler ManifestHandler schema2Handler ManifestHandler
ocischemaHandler ManifestHandler manifestListHandler ManifestHandler
manifestListHandler ManifestHandler ocischemaHandler ManifestHandler
ocischemaIndexHandler ManifestHandler
} }
var _ distribution.ManifestService = &manifestStore{} var _ distribution.ManifestService = &manifestStore{}
@ -104,14 +105,16 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ..
return ms.schema2Handler.Unmarshal(ctx, dgst, content) return ms.schema2Handler.Unmarshal(ctx, dgst, content)
case v1.MediaTypeImageManifest: case v1.MediaTypeImageManifest:
return ms.ocischemaHandler.Unmarshal(ctx, dgst, content) return ms.ocischemaHandler.Unmarshal(ctx, dgst, content)
case manifestlist.MediaTypeManifestList, v1.MediaTypeImageIndex: case manifestlist.MediaTypeManifestList:
return ms.manifestListHandler.Unmarshal(ctx, dgst, content) return ms.manifestListHandler.Unmarshal(ctx, dgst, content)
case v1.MediaTypeImageIndex:
return ms.ocischemaIndexHandler.Unmarshal(ctx, dgst, content)
case "": case "":
// OCI image or image index - no media type in the content // OCI image or image index - no media type in the content
// First see if it looks like an image index // First see if it looks like an image index
res, err := ms.manifestListHandler.Unmarshal(ctx, dgst, content) res, err := ms.ocischemaIndexHandler.Unmarshal(ctx, dgst, content)
resIndex := res.(*manifestlist.DeserializedManifestList) resIndex := res.(*ocischema.DeserializedImageIndex)
if err == nil && resIndex.Manifests != nil { if err == nil && resIndex.Manifests != nil {
return resIndex, nil return resIndex, nil
} }
@ -138,6 +141,8 @@ func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest
return ms.ocischemaHandler.Put(ctx, manifest, ms.skipDependencyVerification) return ms.ocischemaHandler.Put(ctx, manifest, ms.skipDependencyVerification)
case *manifestlist.DeserializedManifestList: case *manifestlist.DeserializedManifestList:
return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification) return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification)
case *ocischema.DeserializedImageIndex:
return ms.ocischemaIndexHandler.Put(ctx, manifest, ms.skipDependencyVerification)
} }
return "", fmt.Errorf("unrecognized manifest type %T", manifest) return "", fmt.Errorf("unrecognized manifest type %T", manifest)

@ -3,13 +3,13 @@ package storage
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"io" "io"
"reflect" "reflect"
"testing" "testing"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest"
"github.com/distribution/distribution/v3/manifest/manifestlist"
"github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/manifest/ocischema"
"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
"github.com/distribution/distribution/v3/reference" "github.com/distribution/distribution/v3/reference"
@ -464,20 +464,12 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo
t.Fatalf("%s: unexpected error getting manifest descriptor", testname) t.Fatalf("%s: unexpected error getting manifest descriptor", testname)
} }
descriptor.MediaType = v1.MediaTypeImageManifest descriptor.MediaType = v1.MediaTypeImageManifest
descriptor.Platform = &v1.Platform{
platformSpec := manifestlist.PlatformSpec{
Architecture: "atari2600", Architecture: "atari2600",
OS: "CP/M", OS: "CP/M",
} }
manifestDescriptors := []manifestlist.ManifestDescriptor{ imageIndex, err := ociIndexFromDesriptorsWithMediaType([]distribution.Descriptor{descriptor}, indexMediaType)
{
Descriptor: descriptor,
Platform: platformSpec,
},
}
imageIndex, err := manifestlist.FromDescriptorsWithMediaType(manifestDescriptors, indexMediaType)
if err != nil { if err != nil {
t.Fatalf("%s: unexpected error creating image index: %v", testname, err) t.Fatalf("%s: unexpected error creating image index: %v", testname, err)
} }
@ -523,7 +515,7 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo
t.Fatalf("%s: unexpected error fetching image index: %v", testname, err) t.Fatalf("%s: unexpected error fetching image index: %v", testname, err)
} }
fetchedIndex, ok := fromStore.(*manifestlist.DeserializedManifestList) fetchedIndex, ok := fromStore.(*ocischema.DeserializedImageIndex)
if !ok { if !ok {
t.Fatalf("%s: unexpected type for fetched manifest", testname) t.Fatalf("%s: unexpected type for fetched manifest", testname)
} }
@ -574,3 +566,23 @@ func TestLinkPathFuncs(t *testing.T) {
} }
} }
} }
func ociIndexFromDesriptorsWithMediaType(descriptors []distribution.Descriptor, mediaType string) (*ocischema.DeserializedImageIndex, error) {
manifest, err := ocischema.FromDescriptors(descriptors, nil)
if err != nil {
return nil, err
}
manifest.ImageIndex.MediaType = mediaType
rawManifest, err := json.Marshal(manifest.ImageIndex)
if err != nil {
return nil, err
}
var d ocischema.DeserializedImageIndex
if err := d.UnmarshalJSON(rawManifest); err != nil {
return nil, err
}
return &d, nil
}

@ -0,0 +1,28 @@
package storage
import (
"context"
"github.com/distribution/distribution/v3"
dcontext "github.com/distribution/distribution/v3/context"
"github.com/distribution/distribution/v3/manifest/ocischema"
"github.com/opencontainers/go-digest"
)
// ocischemaIndexHandler is a ManifestHandler that covers the OCI Image Index.
type ocischemaIndexHandler struct {
*manifestListHandler
}
var _ ManifestHandler = &manifestListHandler{}
func (ms *ocischemaIndexHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
dcontext.GetLogger(ms.ctx).Debug("(*ociIndexHandler).Unmarshal")
m := &ocischema.DeserializedImageIndex{}
if err := m.UnmarshalJSON(content); err != nil {
return nil, err
}
return m, nil
}

@ -259,6 +259,12 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
} }
} }
manifestListHandler := &manifestListHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
}
ms := &manifestStore{ ms := &manifestStore{
ctx: ctx, ctx: ctx,
repository: repo, repository: repo,
@ -270,17 +276,16 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
blobStore: blobStore, blobStore: blobStore,
manifestURLs: repo.registry.manifestURLs, manifestURLs: repo.registry.manifestURLs,
}, },
manifestListHandler: &manifestListHandler{ manifestListHandler: manifestListHandler,
ctx: ctx,
repository: repo,
blobStore: blobStore,
},
ocischemaHandler: &ocischemaManifestHandler{ ocischemaHandler: &ocischemaManifestHandler{
ctx: ctx, ctx: ctx,
repository: repo, repository: repo,
blobStore: blobStore, blobStore: blobStore,
manifestURLs: repo.registry.manifestURLs, manifestURLs: repo.registry.manifestURLs,
}, },
ocischemaIndexHandler: &ocischemaIndexHandler{
manifestListHandler: manifestListHandler,
},
} }
// Apply options // Apply options