Merge pull request #3869 from brackendawson/split-oci-index
Split OCI Image Index from Docker Manifest List
This commit is contained in:
commit
46b3d62016
11 changed files with 548 additions and 332 deletions
|
@ -23,13 +23,6 @@ var SchemaVersion = manifest.Versioned{
|
|||
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() {
|
||||
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
||||
m := new(DeserializedManifestList)
|
||||
|
@ -52,31 +45,6 @@ func init() {
|
|||
if err != nil {
|
||||
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
|
||||
|
@ -154,21 +122,14 @@ type DeserializedManifestList struct {
|
|||
// DeserializedManifestList which contains the resulting manifest list
|
||||
// and its JSON representation.
|
||||
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
|
||||
var mediaType string
|
||||
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest {
|
||||
mediaType = v1.MediaTypeImageIndex
|
||||
} else {
|
||||
mediaType = MediaTypeManifestList
|
||||
}
|
||||
|
||||
return FromDescriptorsWithMediaType(descriptors, mediaType)
|
||||
return fromDescriptorsWithMediaType(descriptors, MediaTypeManifestList)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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) {
|
||||
m := ManifestList{
|
||||
Versioned: manifest.Versioned{
|
||||
SchemaVersion: 2,
|
||||
SchemaVersion: SchemaVersion.SchemaVersion,
|
||||
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
|
||||
// used to calculate the content identifier.
|
||||
func (m DeserializedManifestList) Payload() (string, []byte, error) {
|
||||
var mediaType string
|
||||
if m.MediaType == "" {
|
||||
mediaType = v1.MediaTypeImageIndex
|
||||
} else {
|
||||
mediaType = m.MediaType
|
||||
}
|
||||
|
||||
return mediaType, m.canonical, nil
|
||||
return m.MediaType, m.canonical, nil
|
||||
}
|
||||
|
||||
// unknownDocument represents a manifest, manifest list, or index that has not
|
||||
// yet been validated
|
||||
type unknownDocument struct {
|
||||
// validateManifestList returns an error if the byte slice is invalid JSON or if it
|
||||
// contains fields that belong to a manifest
|
||||
func validateManifestList(b []byte) error {
|
||||
var doc 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
|
||||
func validateIndex(b []byte) error {
|
||||
var doc unknownDocument
|
||||
}
|
||||
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 errors.New("manifestlist: expected list but found manifest")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
@ -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 {
|
||||
t.Fatalf("error creating DeserializedManifestList: %v", err)
|
||||
}
|
||||
|
@ -130,162 +130,10 @@ func TestManifestList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO (mikebrow): add annotations on the manifest list (index) and support for
|
||||
// empty platform structs (move to Platform *Platform `json:"platform,omitempty"`
|
||||
// from current Platform PlatformSpec `json:"platform"`) in the manifest descriptor.
|
||||
// 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) {
|
||||
func mediaTypeTest(contentType string, mediaType string, shouldError bool) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
var m *DeserializedManifestList
|
||||
if contentType == MediaTypeManifestList {
|
||||
_, m = makeTestManifestList(t, mediaType)
|
||||
} else {
|
||||
_, m = makeTestOCIImageIndex(t, mediaType)
|
||||
}
|
||||
|
||||
_, canonical, err := m.Payload()
|
||||
if err != nil {
|
||||
|
@ -319,34 +167,32 @@ func mediaTypeTest(t *testing.T, contentType string, mediaType string, shouldErr
|
|||
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaTypes(t *testing.T) {
|
||||
mediaTypeTest(t, MediaTypeManifestList, "", true)
|
||||
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList, false)
|
||||
mediaTypeTest(t, MediaTypeManifestList, MediaTypeManifestList+"XXX", true)
|
||||
mediaTypeTest(t, v1.MediaTypeImageIndex, "", false)
|
||||
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex, false)
|
||||
mediaTypeTest(t, v1.MediaTypeImageIndex, v1.MediaTypeImageIndex+"XXX", true)
|
||||
t.Run("ManifestList_No_MediaType", mediaTypeTest(MediaTypeManifestList, "", true))
|
||||
t.Run("ManifestList", mediaTypeTest(MediaTypeManifestList, MediaTypeManifestList, false))
|
||||
t.Run("ManifestList_Bad_MediaType", mediaTypeTest(MediaTypeManifestList, MediaTypeManifestList+"XXX", true))
|
||||
}
|
||||
|
||||
func TestValidateManifest(t *testing.T) {
|
||||
manifest := ocischema.Manifest{
|
||||
func TestValidateManifestList(t *testing.T) {
|
||||
manifest := schema2.Manifest{
|
||||
Config: distribution.Descriptor{Size: 1},
|
||||
Layers: []distribution.Descriptor{{Size: 2}},
|
||||
}
|
||||
index := ManifestList{
|
||||
manifestList := ManifestList{
|
||||
Manifests: []ManifestDescriptor{
|
||||
{Descriptor: distribution.Descriptor{Size: 3}},
|
||||
},
|
||||
}
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
b, err := json.Marshal(index)
|
||||
b, err := json.Marshal(manifestList)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error marshaling index", err)
|
||||
t.Fatal("unexpected error marshaling manifest list", err)
|
||||
}
|
||||
if err := validateIndex(b); err != nil {
|
||||
t.Error("index should be valid", err)
|
||||
if err := validateManifestList(b); err != nil {
|
||||
t.Error("list should be valid", err)
|
||||
}
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
|
@ -354,7 +200,7 @@ func TestValidateManifest(t *testing.T) {
|
|||
if err != nil {
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
|
156
manifest/ocischema/index.go
Normal file
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
|
||||
}
|
209
manifest/ocischema/index_test.go
Normal file
209
manifest/ocischema/index_test.go
Normal file
|
@ -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"
|
||||
)
|
||||
|
||||
// SchemaVersion provides a pre-initialized version structure for this
|
||||
// packages version of the manifest.
|
||||
// SchemaVersion provides a pre-initialized version structure for OCI Image
|
||||
// Manifests
|
||||
var SchemaVersion = manifest.Versioned{
|
||||
SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version
|
||||
SchemaVersion: 2,
|
||||
MediaType: v1.MediaTypeImageManifest,
|
||||
}
|
||||
|
||||
|
@ -124,16 +124,12 @@ func (m DeserializedManifest) Payload() (string, []byte, error) {
|
|||
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
|
||||
// contains fields that belong to a index
|
||||
func validateManifest(b []byte) error {
|
||||
var doc unknownDocument
|
||||
var doc struct {
|
||||
Manifests interface{} `json:"manifests,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -142,7 +142,8 @@ func TestManifest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func mediaTypeTest(t *testing.T, mediaType string, shouldError bool) {
|
||||
func manifestMediaTypeTest(mediaType string, shouldError bool) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
mfst := makeTestManifest(mediaType)
|
||||
|
||||
deserialized, err := FromStruct(mfst)
|
||||
|
@ -177,12 +178,13 @@ func mediaTypeTest(t *testing.T, mediaType string, shouldError bool) {
|
|||
t.Fatalf("Bad media type '%v' for payload", unmarshalledMediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaTypes(t *testing.T) {
|
||||
mediaTypeTest(t, "", false)
|
||||
mediaTypeTest(t, v1.MediaTypeImageManifest, false)
|
||||
mediaTypeTest(t, v1.MediaTypeImageManifest+"XXX", true)
|
||||
func TestManifestMediaTypes(t *testing.T) {
|
||||
t.Run("No_MediaType", manifestMediaTypeTest("", false))
|
||||
t.Run("ImageManifest", manifestMediaTypeTest(v1.MediaTypeImageManifest, false))
|
||||
t.Run("Bad_MediaType", manifestMediaTypeTest(v1.MediaTypeImageManifest+"XXX", true))
|
||||
}
|
||||
|
||||
func TestValidateManifest(t *testing.T) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/distribution/distribution/v3"
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
||||
"github.com/distribution/distribution/v3/manifest/ocischema"
|
||||
"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) {
|
||||
dcontext.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put")
|
||||
|
||||
m, ok := manifestList.(*manifestlist.DeserializedManifestList)
|
||||
if !ok {
|
||||
var schemaVersion, expectedSchemaVersion int
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
mt, payload, err := m.Payload()
|
||||
mt, payload, err := manifestList.Payload()
|
||||
if err != nil {
|
||||
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
|
||||
// store valid content, leaving trust policies of that content up to
|
||||
// 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
|
||||
|
||||
if mnfst.SchemaVersion != 2 {
|
||||
return fmt.Errorf("unrecognized manifest list schema version %d", mnfst.SchemaVersion)
|
||||
}
|
||||
|
||||
if !skipDependencyVerification {
|
||||
// This manifest service is different from the blob service
|
||||
// returned by Blob. It uses a linked blob store to ensure that
|
||||
|
|
|
@ -50,8 +50,9 @@ type manifestStore struct {
|
|||
|
||||
schema1Handler ManifestHandler
|
||||
schema2Handler ManifestHandler
|
||||
ocischemaHandler ManifestHandler
|
||||
manifestListHandler ManifestHandler
|
||||
ocischemaHandler ManifestHandler
|
||||
ocischemaIndexHandler ManifestHandler
|
||||
}
|
||||
|
||||
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)
|
||||
case v1.MediaTypeImageManifest:
|
||||
return ms.ocischemaHandler.Unmarshal(ctx, dgst, content)
|
||||
case manifestlist.MediaTypeManifestList, v1.MediaTypeImageIndex:
|
||||
case manifestlist.MediaTypeManifestList:
|
||||
return ms.manifestListHandler.Unmarshal(ctx, dgst, content)
|
||||
case v1.MediaTypeImageIndex:
|
||||
return ms.ocischemaIndexHandler.Unmarshal(ctx, dgst, content)
|
||||
case "":
|
||||
// OCI image or image index - no media type in the content
|
||||
|
||||
// First see if it looks like an image index
|
||||
res, err := ms.manifestListHandler.Unmarshal(ctx, dgst, content)
|
||||
resIndex := res.(*manifestlist.DeserializedManifestList)
|
||||
res, err := ms.ocischemaIndexHandler.Unmarshal(ctx, dgst, content)
|
||||
resIndex := res.(*ocischema.DeserializedImageIndex)
|
||||
if err == nil && resIndex.Manifests != 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)
|
||||
case *manifestlist.DeserializedManifestList:
|
||||
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)
|
||||
|
|
|
@ -3,13 +3,13 @@ package storage
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
"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/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"
|
||||
|
@ -464,20 +464,12 @@ func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes boo
|
|||
t.Fatalf("%s: unexpected error getting manifest descriptor", testname)
|
||||
}
|
||||
descriptor.MediaType = v1.MediaTypeImageManifest
|
||||
|
||||
platformSpec := manifestlist.PlatformSpec{
|
||||
descriptor.Platform = &v1.Platform{
|
||||
Architecture: "atari2600",
|
||||
OS: "CP/M",
|
||||
}
|
||||
|
||||
manifestDescriptors := []manifestlist.ManifestDescriptor{
|
||||
{
|
||||
Descriptor: descriptor,
|
||||
Platform: platformSpec,
|
||||
},
|
||||
}
|
||||
|
||||
imageIndex, err := manifestlist.FromDescriptorsWithMediaType(manifestDescriptors, indexMediaType)
|
||||
imageIndex, err := ociIndexFromDesriptorsWithMediaType([]distribution.Descriptor{descriptor}, indexMediaType)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
fetchedIndex, ok := fromStore.(*manifestlist.DeserializedManifestList)
|
||||
fetchedIndex, ok := fromStore.(*ocischema.DeserializedImageIndex)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
|
28
registry/storage/ociindexhandler.go
Normal file
28
registry/storage/ociindexhandler.go
Normal file
|
@ -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{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
|
@ -270,17 +276,16 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
|
|||
blobStore: blobStore,
|
||||
manifestURLs: repo.registry.manifestURLs,
|
||||
},
|
||||
manifestListHandler: &manifestListHandler{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
blobStore: blobStore,
|
||||
},
|
||||
manifestListHandler: manifestListHandler,
|
||||
ocischemaHandler: &ocischemaManifestHandler{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
blobStore: blobStore,
|
||||
manifestURLs: repo.registry.manifestURLs,
|
||||
},
|
||||
ocischemaIndexHandler: &ocischemaIndexHandler{
|
||||
manifestListHandler: manifestListHandler,
|
||||
},
|
||||
}
|
||||
|
||||
// Apply options
|
||||
|
|
Loading…
Reference in a new issue