Skip to content

Commit

Permalink
adds support for oci manifests and manifestlists
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Brown <[email protected]>
  • Loading branch information
mikebrow committed Jun 18, 2018
1 parent 749f6af commit 9986e8c
Show file tree
Hide file tree
Showing 10 changed files with 924 additions and 29 deletions.
28 changes: 24 additions & 4 deletions manifest/manifestlist/manifestlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ import (

"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/ocischema"
"github.com/opencontainers/go-digest"
)

// MediaTypeManifestList specifies the mediaType for manifest lists.
const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
const (
// MediaTypeManifestList specifies the mediaType for manifest lists.
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
// MediaTypeOCIManifestList specifies the mediaType for OCI compliant manifest
// lists.
MediaTypeOCIManifestList = "application/vnd.oci.image.manifest.list.v1+json"
)

// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
Expand All @@ -20,6 +26,13 @@ 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: MediaTypeOCIManifestList,
}

func init() {
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifestList)
Expand Down Expand Up @@ -105,8 +118,15 @@ type DeserializedManifestList struct {
// DeserializedManifestList which contains the resulting manifest list
// and its JSON representation.
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
m := ManifestList{
Versioned: SchemaVersion,
var m ManifestList
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == ocischema.MediaTypeManifest {
m = ManifestList{
Versioned: OCISchemaVersion,
}
} else {
m = ManifestList{
Versioned: SchemaVersion,
}
}

m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors))
Expand Down
103 changes: 102 additions & 1 deletion manifest/manifestlist/manifestlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestManifestList(t *testing.T) {
t.Fatalf("error creating DeserializedManifestList: %v", err)
}

mediaType, canonical, err := deserialized.Payload()
mediaType, canonical, _ := deserialized.Payload()

if mediaType != MediaTypeManifestList {
t.Fatalf("unexpected media type: %s", mediaType)
Expand Down Expand Up @@ -109,3 +109,104 @@ func TestManifestList(t *testing.T) {
}
}
}

var expectedOCIManifestListSerialization = []byte(`{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.list.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 985,
"digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse4"
]
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 2392,
"digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608",
"platform": {
"architecture": "sun4m",
"os": "sunos"
}
}
]
}`)

func TestOCIManifestList(t *testing.T) {
manifestDescriptors := []ManifestDescriptor{
{
Descriptor: distribution.Descriptor{
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 985,
MediaType: "application/vnd.oci.image.manifest.v1+json",
},
Platform: PlatformSpec{
Architecture: "amd64",
OS: "linux",
Features: []string{"sse4"},
},
},
{
Descriptor: distribution.Descriptor{
Digest: "sha256:6346340964309634683409684360934680934608934608934608934068934608",
Size: 2392,
MediaType: "application/vnd.oci.image.manifest.v1+json",
},
Platform: PlatformSpec{
Architecture: "sun4m",
OS: "sunos",
},
},
}

deserialized, err := FromDescriptors(manifestDescriptors)
if err != nil {
t.Fatalf("error creating DeserializedManifestList: %v", err)
}

mediaType, canonical, _ := deserialized.Payload()

if mediaType != MediaTypeOCIManifestList {
t.Fatalf("unexpected media type: %s", mediaType)
}

// Check that the canonical field is the same as json.MarshalIndent
// with these parameters.
p, err := json.MarshalIndent(&deserialized.ManifestList, "", " ")
if err != nil {
t.Fatalf("error marshaling manifest list: %v", err)
}
if !bytes.Equal(p, canonical) {
t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p))
}

// Check that the canonical field has the expected value.
if !bytes.Equal(expectedOCIManifestListSerialization, canonical) {
t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedOCIManifestListSerialization))
}

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) != 2 {
t.Fatalf("unexpected number of references: %d", len(references))
}
for i := range references {
if !reflect.DeepEqual(references[i], manifestDescriptors[i].Descriptor) {
t.Fatalf("unexpected value %d returned by References: %v", i, references[i])
}
}
}
80 changes: 80 additions & 0 deletions manifest/ocischema/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ocischema

import (
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/opencontainers/go-digest"
)

// builder is a type for constructing manifests.
type builder struct {
// bs is a BlobService used to publish the configuration blob.
bs distribution.BlobService

// configJSON references
configJSON []byte

// layers is a list of layer descriptors that gets built by successive
// calls to AppendReference.
layers []distribution.Descriptor
}

// NewManifestBuilder is used to build new manifests for the current schema
// version. It takes a BlobService so it can publish the configuration blob
// as part of the Build process.
func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder {
mb := &builder{
bs: bs,
configJSON: make([]byte, len(configJSON)),
}
copy(mb.configJSON, configJSON)

return mb
}

// Build produces a final manifest from the given references.
func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
m := Manifest{
Versioned: SchemaVersion,
Layers: make([]distribution.Descriptor, len(mb.layers)),
}
copy(m.Layers, mb.layers)

configDigest := digest.FromBytes(mb.configJSON)

var err error
m.Config, err = mb.bs.Stat(ctx, configDigest)
switch err {
case nil:
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = MediaTypeConfig
return FromStruct(m)
case distribution.ErrBlobUnknown:
// nop
default:
return nil, err
}

// Add config to the blob store
m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON)
// Override MediaType, since Put always replaces the specified media
// type with application/octet-stream in the descriptor it returns.
m.Config.MediaType = MediaTypeConfig
if err != nil {
return nil, err
}

return FromStruct(m)
}

// AppendReference adds a reference to the current ManifestBuilder.
func (mb *builder) AppendReference(d distribution.Describable) error {
mb.layers = append(mb.layers, d.Descriptor())
return nil
}

// References returns the current references added to this builder.
func (mb *builder) References() []distribution.Descriptor {
return mb.layers
}
Loading

0 comments on commit 9986e8c

Please sign in to comment.