forked from TrueCloudLab/distribution
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,
|
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
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"
|
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
|
||||||
|
}
|
||||||
|
|
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{
|
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
|
||||||
|
|
Loading…
Reference in a new issue