From a4283bf81187a1bd572adfddaeed64086f5e7c83 Mon Sep 17 00:00:00 2001
From: Mike Brown <brownwm@us.ibm.com>
Date: Thu, 31 Aug 2017 21:14:40 -0500
Subject: [PATCH] adds support for oci manifests and manifestlists
https://github.com/docker/distribution/pull/2076
Signed-off-by: Mike Brown <brownwm@us.ibm.com>
---
blobs.go | 8 +
manifest/manifestlist/manifestlist.go | 25 ++-
manifest/manifestlist/manifestlist_test.go | 134 ++++++++++++++-
manifest/ocischema/builder.go | 86 ++++++++++
manifest/ocischema/builder_test.go | 173 +++++++++++++++++++
manifest/ocischema/manifest.go | 119 +++++++++++++
manifest/ocischema/manifest_test.go | 133 ++++++++++++++
manifest/schema2/manifest.go | 2 +-
registry/handlers/api_test.go | 26 +--
registry/handlers/images.go | 137 ++++++++++-----
registry/storage/manifeststore.go | 9 +-
registry/storage/ocimanifesthandler.go | 129 ++++++++++++++
registry/storage/ocimanifesthandler_test.go | 138 +++++++++++++++
registry/storage/registry.go | 6 +
.../github.com/opencontainers/image-spec/LICENSE | 191 +++++++++++++++++++++
.../github.com/opencontainers/image-spec/README.md | 167 ++++++++++++++++++
.../image-spec/specs-go/v1/annotations.go | 56 ++++++
.../image-spec/specs-go/v1/config.go | 103 +++++++++++
.../image-spec/specs-go/v1/descriptor.go | 64 +++++++
.../opencontainers/image-spec/specs-go/v1/index.go | 29 ++++
.../image-spec/specs-go/v1/layout.go | 28 +++
.../image-spec/specs-go/v1/manifest.go | 32 ++++
.../image-spec/specs-go/v1/mediatype.go | 48 ++++++
.../opencontainers/image-spec/specs-go/version.go | 32 ++++
.../image-spec/specs-go/versioned.go | 23 +++
25 files changed, 1835 insertions(+), 63 deletions(-)
create mode 100644 manifest/ocischema/builder.go
create mode 100644 manifest/ocischema/builder_test.go
create mode 100644 manifest/ocischema/manifest.go
create mode 100644 manifest/ocischema/manifest_test.go
create mode 100644 registry/storage/ocimanifesthandler.go
create mode 100644 registry/storage/ocimanifesthandler_test.go
create mode 100644 vendor/github.com/opencontainers/image-spec/LICENSE
create mode 100644 vendor/github.com/opencontainers/image-spec/README.md
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/version.go
create mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/versioned.go
diff --git a/blobs.go b/blobs.go
index 1f91ae21..b9ebbf0e 100644
--- a/blobs.go
+++ b/blobs.go
@@ -10,6 +10,7 @@ import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
+ "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
@@ -72,6 +73,13 @@ type Descriptor struct {
// URLs contains the source URLs of this content.
URLs []string `json:"urls,omitempty"`
+ // Annotations contains arbitrary metadata relating to the targeted content.
+ Annotations map[string]string `json:"annotations,omitempty"`
+
+ // Platform describes the platform which the image in the manifest runs on.
+ // This should only be used when referring to a manifest.
+ Platform *v1.Platform `json:"platform,omitempty"`
+
// NOTE: Before adding a field here, please ensure that all
// other options have been exhausted. Much of the type relationships
// depend on the simplicity of this type.
diff --git a/manifest/manifestlist/manifestlist.go b/manifest/manifestlist/manifestlist.go
index a2082ec0..007eae75 100644
--- a/manifest/manifestlist/manifestlist.go
+++ b/manifest/manifestlist/manifestlist.go
@@ -8,10 +8,13 @@ import (
"github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
)
-// 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"
+)
// SchemaVersion provides a pre-initialized version structure for this
// packages version of the manifest.
@@ -20,6 +23,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: v1.MediaTypeImageIndex,
+}
+
func init() {
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
m := new(DeserializedManifestList)
@@ -105,8 +115,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 == v1.MediaTypeImageManifest {
+ m = ManifestList{
+ Versioned: OCISchemaVersion,
+ }
+ } else {
+ m = ManifestList{
+ Versioned: SchemaVersion,
+ }
}
m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors))
diff --git a/manifest/manifestlist/manifestlist_test.go b/manifest/manifestlist/manifestlist_test.go
index 09e6ed1f..720b911b 100644
--- a/manifest/manifestlist/manifestlist_test.go
+++ b/manifest/manifestlist/manifestlist_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/docker/distribution"
+ "github.com/opencontainers/image-spec/specs-go/v1"
)
var expectedManifestListSerialization = []byte(`{
@@ -69,7 +70,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)
@@ -109,3 +110,134 @@ 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 docker/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.
+var expectedOCIImageIndexSerialization = []byte(`{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.oci.image.index.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": 985,
+ "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
+ "annotations": {
+ "platform": "none"
+ },
+ "platform": {
+ "architecture": "",
+ "os": ""
+ }
+ },
+ {
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "size": 2392,
+ "digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608",
+ "annotations": {
+ "what": "for"
+ },
+ "platform": {
+ "architecture": "sun4m",
+ "os": "sunos"
+ }
+ }
+ ]
+}`)
+
+func TestOCIImageIndex(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:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
+ Size: 985,
+ MediaType: "application/vnd.oci.image.manifest.v1+json",
+ Annotations: map[string]string{"platform": "none"},
+ },
+ },
+ {
+ Descriptor: distribution.Descriptor{
+ Digest: "sha256:6346340964309634683409684360934680934608934608934608934068934608",
+ Size: 2392,
+ MediaType: "application/vnd.oci.image.manifest.v1+json",
+ Annotations: map[string]string{"what": "for"},
+ },
+ 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 != v1.MediaTypeImageIndex {
+ 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(expectedOCIImageIndexSerialization, canonical) {
+ t.Fatalf("manifest bytes not equal to expected: %q != %q", string(canonical), string(expectedOCIImageIndexSerialization))
+ }
+
+ 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 {
+ if !reflect.DeepEqual(references[i], manifestDescriptors[i].Descriptor) {
+ t.Fatalf("unexpected value %d returned by References: %v", i, references[i])
+ }
+ }
+}
diff --git a/manifest/ocischema/builder.go b/manifest/ocischema/builder.go
new file mode 100644
index 00000000..c1a615e8
--- /dev/null
+++ b/manifest/ocischema/builder.go
@@ -0,0 +1,86 @@
+package ocischema
+
+import (
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// 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
+
+ // Annotations contains arbitrary metadata relating to the targeted content.
+ annotations map[string]string
+}
+
+// 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, and annotations.
+func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotations map[string]string) distribution.ManifestBuilder {
+ mb := &builder{
+ bs: bs,
+ configJSON: make([]byte, len(configJSON)),
+ annotations: annotations,
+ }
+ 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)),
+ Annotations: mb.annotations,
+ }
+ 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 = v1.MediaTypeImageConfig
+ return FromStruct(m)
+ case distribution.ErrBlobUnknown:
+ // nop
+ default:
+ return nil, err
+ }
+
+ // Add config to the blob store
+ m.Config, err = mb.bs.Put(ctx, v1.MediaTypeImageConfig, mb.configJSON)
+ // Override MediaType, since Put always replaces the specified media
+ // type with application/octet-stream in the descriptor it returns.
+ m.Config.MediaType = v1.MediaTypeImageConfig
+ 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
+}
diff --git a/manifest/ocischema/builder_test.go b/manifest/ocischema/builder_test.go
new file mode 100644
index 00000000..dbbb83a8
--- /dev/null
+++ b/manifest/ocischema/builder_test.go
@@ -0,0 +1,173 @@
+package ocischema
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+type mockBlobService struct {
+ descriptors map[digest.Digest]distribution.Descriptor
+}
+
+func (bs *mockBlobService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
+ if descriptor, ok := bs.descriptors[dgst]; ok {
+ return descriptor, nil
+ }
+ return distribution.Descriptor{}, distribution.ErrBlobUnknown
+}
+
+func (bs *mockBlobService) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
+ panic("not implemented")
+}
+
+func (bs *mockBlobService) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
+ panic("not implemented")
+}
+
+func (bs *mockBlobService) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
+ d := distribution.Descriptor{
+ Digest: digest.FromBytes(p),
+ Size: int64(len(p)),
+ MediaType: "application/octet-stream",
+ }
+ bs.descriptors[d.Digest] = d
+ return d, nil
+}
+
+func (bs *mockBlobService) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
+ panic("not implemented")
+}
+
+func (bs *mockBlobService) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
+ panic("not implemented")
+}
+
+func TestBuilder(t *testing.T) {
+ imgJSON := []byte(`{
+ "created": "2015-10-31T22:22:56.015925234Z",
+ "author": "Alyssa P. Hacker <alyspdev@example.com>",
+ "architecture": "amd64",
+ "os": "linux",
+ "config": {
+ "User": "alice",
+ "ExposedPorts": {
+ "8080/tcp": {}
+ },
+ "Env": [
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "FOO=oci_is_a",
+ "BAR=well_written_spec"
+ ],
+ "Entrypoint": [
+ "/bin/my-app-binary"
+ ],
+ "Cmd": [
+ "--foreground",
+ "--config",
+ "/etc/my-app.d/default.cfg"
+ ],
+ "Volumes": {
+ "/var/job-result-data": {},
+ "/var/log/my-app-logs": {}
+ },
+ "WorkingDir": "/home/alice",
+ "Labels": {
+ "com.example.project.git.url": "https://example.com/project.git",
+ "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
+ }
+ },
+ "rootfs": {
+ "diff_ids": [
+ "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
+ "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
+ ],
+ "type": "layers"
+ },
+ "annotations": {
+ "hot": "potato"
+ }
+ "history": [
+ {
+ "created": "2015-10-31T22:22:54.690851953Z",
+ "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
+ },
+ {
+ "created": "2015-10-31T22:22:55.613815829Z",
+ "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
+ "empty_layer": true
+ }
+ ]
+}`)
+ configDigest := digest.FromBytes(imgJSON)
+
+ descriptors := []distribution.Descriptor{
+ {
+ Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
+ Size: 5312,
+ MediaType: v1.MediaTypeImageLayerGzip,
+ Annotations: map[string]string{"apple": "orange", "lettuce": "wrap"},
+ },
+ {
+ Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
+ Size: 235231,
+ MediaType: v1.MediaTypeImageLayerGzip,
+ },
+ {
+ Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
+ Size: 639152,
+ MediaType: v1.MediaTypeImageLayerGzip,
+ },
+ }
+ annotations := map[string]string{"hot": "potato"}
+
+ bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)}
+ builder := NewManifestBuilder(bs, imgJSON, annotations)
+
+ for _, d := range descriptors {
+ if err := builder.AppendReference(d); err != nil {
+ t.Fatalf("AppendReference returned error: %v", err)
+ }
+ }
+
+ built, err := builder.Build(context.Background())
+ if err != nil {
+ t.Fatalf("Build returned error: %v", err)
+ }
+
+ // Check that the config was put in the blob store
+ _, err = bs.Stat(context.Background(), configDigest)
+ if err != nil {
+ t.Fatal("config was not put in the blob store")
+ }
+
+ manifest := built.(*DeserializedManifest).Manifest
+ if manifest.Annotations["hot"] != "potato" {
+ t.Fatalf("unexpected annotation in manifest: %s", manifest.Annotations["hot"])
+ }
+
+ if manifest.Versioned.SchemaVersion != 2 {
+ t.Fatal("SchemaVersion != 2")
+ }
+
+ target := manifest.Target()
+ if target.Digest != configDigest {
+ t.Fatalf("unexpected digest in target: %s", target.Digest.String())
+ }
+ if target.MediaType != v1.MediaTypeImageConfig {
+ t.Fatalf("unexpected media type in target: %s", target.MediaType)
+ }
+ if target.Size != 1632 {
+ t.Fatalf("unexpected size in target: %d", target.Size)
+ }
+
+ references := manifest.References()
+ expected := append([]distribution.Descriptor{manifest.Target()}, descriptors...)
+ if !reflect.DeepEqual(references, expected) {
+ t.Fatal("References() does not match the descriptors added")
+ }
+}
diff --git a/manifest/ocischema/manifest.go b/manifest/ocischema/manifest.go
new file mode 100644
index 00000000..e9460102
--- /dev/null
+++ b/manifest/ocischema/manifest.go
@@ -0,0 +1,119 @@
+package ocischema
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/digest"
+ "github.com/docker/distribution/manifest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+var (
+ // SchemaVersion provides a pre-initialized version structure for this
+ // packages version of the manifest.
+ SchemaVersion = manifest.Versioned{
+ SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version
+ MediaType: v1.MediaTypeImageManifest,
+ }
+)
+
+func init() {
+ ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
+ m := new(DeserializedManifest)
+ err := m.UnmarshalJSON(b)
+ if err != nil {
+ return nil, distribution.Descriptor{}, err
+ }
+
+ dgst := digest.FromBytes(b)
+ return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageManifest}, err
+ }
+ err := distribution.RegisterManifestSchema(v1.MediaTypeImageManifest, ocischemaFunc)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
+ }
+}
+
+// Manifest defines a ocischema manifest.
+type Manifest struct {
+ manifest.Versioned
+
+ // Config references the image configuration as a blob.
+ Config distribution.Descriptor `json:"config"`
+
+ // Layers lists descriptors for the layers referenced by the
+ // configuration.
+ Layers []distribution.Descriptor `json:"layers"`
+
+ // Annotations contains arbitrary metadata for the image manifest.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
+
+// References returnes the descriptors of this manifests references.
+func (m Manifest) References() []distribution.Descriptor {
+ references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
+ references = append(references, m.Config)
+ references = append(references, m.Layers...)
+ return references
+}
+
+// Target returns the target of this manifest.
+func (m Manifest) Target() distribution.Descriptor {
+ return m.Config
+}
+
+// DeserializedManifest wraps Manifest with a copy of the original JSON.
+// It satisfies the distribution.Manifest interface.
+type DeserializedManifest struct {
+ Manifest
+
+ // canonical is the canonical byte representation of the Manifest.
+ canonical []byte
+}
+
+// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
+// DeserializedManifest which contains the manifest and its JSON representation.
+func FromStruct(m Manifest) (*DeserializedManifest, error) {
+ var deserialized DeserializedManifest
+ deserialized.Manifest = m
+
+ var err error
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
+ return &deserialized, err
+}
+
+// UnmarshalJSON populates a new Manifest struct from JSON data.
+func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
+ m.canonical = make([]byte, len(b), len(b))
+ // store manifest in canonical
+ copy(m.canonical, b)
+
+ // Unmarshal canonical JSON into Manifest object
+ var manifest Manifest
+ if err := json.Unmarshal(m.canonical, &manifest); err != nil {
+ return err
+ }
+
+ m.Manifest = manifest
+
+ return nil
+}
+
+// MarshalJSON returns the contents of canonical. If canonical is empty,
+// marshals the inner contents.
+func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
+ if len(m.canonical) > 0 {
+ return m.canonical, nil
+ }
+
+ return nil, errors.New("JSON representation not initialized in DeserializedManifest")
+}
+
+// Payload returns the raw content of the manifest. The contents can be used to
+// calculate the content identifier.
+func (m DeserializedManifest) Payload() (string, []byte, error) {
+ return m.MediaType, m.canonical, nil
+}
diff --git a/manifest/ocischema/manifest_test.go b/manifest/ocischema/manifest_test.go
new file mode 100644
index 00000000..749cb859
--- /dev/null
+++ b/manifest/ocischema/manifest_test.go
@@ -0,0 +1,133 @@
+package ocischema
+
+import (
+ "bytes"
+ "encoding/json"
+ "reflect"
+ "testing"
+
+ "github.com/docker/distribution"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+var expectedManifestSerialization = []byte(`{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
+ "config": {
+ "mediaType": "application/vnd.oci.image.config.v1+json",
+ "size": 985,
+ "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
+ "annotations": {
+ "apple": "orange"
+ }
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
+ "size": 153263,
+ "digest": "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b",
+ "annotations": {
+ "lettuce": "wrap"
+ }
+ }
+ ],
+ "annotations": {
+ "hot": "potato"
+ }
+}`)
+
+func TestManifest(t *testing.T) {
+ manifest := Manifest{
+ Versioned: SchemaVersion,
+ Config: distribution.Descriptor{
+ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
+ Size: 985,
+ MediaType: v1.MediaTypeImageConfig,
+ Annotations: map[string]string{"apple": "orange"},
+ },
+ Layers: []distribution.Descriptor{
+ {
+ Digest: "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b",
+ Size: 153263,
+ MediaType: v1.MediaTypeImageLayerGzip,
+ Annotations: map[string]string{"lettuce": "wrap"},
+ },
+ },
+ Annotations: map[string]string{"hot": "potato"},
+ }
+
+ deserialized, err := FromStruct(manifest)
+ if err != nil {
+ t.Fatalf("error creating DeserializedManifest: %v", err)
+ }
+
+ mediaType, canonical, _ := deserialized.Payload()
+
+ if mediaType != v1.MediaTypeImageManifest {
+ 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(&manifest, "", " ")
+ if err != nil {
+ t.Fatalf("error marshaling manifest: %v", err)
+ }
+ if !bytes.Equal(p, canonical) {
+ t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p))
+ }
+
+ // Check that canonical field matches expected value.
+ if !bytes.Equal(expectedManifestSerialization, canonical) {
+ t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedManifestSerialization))
+ }
+
+ var unmarshalled DeserializedManifest
+ 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)
+ }
+ if deserialized.Annotations["hot"] != "potato" {
+ t.Fatalf("unexpected annotation in manifest: %s", deserialized.Annotations["hot"])
+ }
+
+ target := deserialized.Target()
+ if target.Digest != "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" {
+ t.Fatalf("unexpected digest in target: %s", target.Digest.String())
+ }
+ if target.MediaType != v1.MediaTypeImageConfig {
+ t.Fatalf("unexpected media type in target: %s", target.MediaType)
+ }
+ if target.Size != 985 {
+ t.Fatalf("unexpected size in target: %d", target.Size)
+ }
+ if target.Annotations["apple"] != "orange" {
+ t.Fatalf("unexpected annotation in target: %s", target.Annotations["apple"])
+ }
+
+ references := deserialized.References()
+ if len(references) != 2 {
+ t.Fatalf("unexpected number of references: %d", len(references))
+ }
+
+ if !reflect.DeepEqual(references[0], target) {
+ t.Fatalf("first reference should be target: %v != %v", references[0], target)
+ }
+
+ // Test the second reference
+ if references[1].Digest != "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" {
+ t.Fatalf("unexpected digest in reference: %s", references[0].Digest.String())
+ }
+ if references[1].MediaType != v1.MediaTypeImageLayerGzip {
+ t.Fatalf("unexpected media type in reference: %s", references[0].MediaType)
+ }
+ if references[1].Size != 153263 {
+ t.Fatalf("unexpected size in reference: %d", references[0].Size)
+ }
+ if references[1].Annotations["lettuce"] != "wrap" {
+ t.Fatalf("unexpected annotation in reference: %s", references[1].Annotations["lettuce"])
+ }
+}
diff --git a/manifest/schema2/manifest.go b/manifest/schema2/manifest.go
index 741998d0..7f27bc73 100644
--- a/manifest/schema2/manifest.go
+++ b/manifest/schema2/manifest.go
@@ -75,7 +75,7 @@ func (m Manifest) References() []distribution.Descriptor {
return references
}
-// Target returns the target of this signed manifest.
+// Target returns the target of this manifest.
func (m Manifest) Target() distribution.Descriptor {
return m.Config
}
diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go
index 9d64fbbf..987a3f96 100644
--- a/registry/handlers/api_test.go
+++ b/registry/handlers/api_test.go
@@ -470,7 +470,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
// -----------------------------------------
// Do layer push with an empty body and different digest
- uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
+ uploadURLBase, _ = startPushLayer(t, env, imageName)
resp, err = doPushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, bytes.NewReader([]byte{}))
if err != nil {
t.Fatalf("unexpected error doing bad layer push: %v", err)
@@ -486,7 +486,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
t.Fatalf("unexpected error digesting empty buffer: %v", err)
}
- uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
+ uploadURLBase, _ = startPushLayer(t, env, imageName)
pushLayer(t, env.builder, imageName, zeroDigest, uploadURLBase, bytes.NewReader([]byte{}))
// -----------------------------------------
@@ -499,7 +499,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
t.Fatalf("unexpected error digesting empty tar: %v", err)
}
- uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
+ uploadURLBase, _ = startPushLayer(t, env, imageName)
pushLayer(t, env.builder, imageName, emptyDigest, uploadURLBase, bytes.NewReader(emptyTar))
// ------------------------------------------
@@ -507,7 +507,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
layerFile.Seek(0, os.SEEK_SET)
- uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
+ uploadURLBase, _ = startPushLayer(t, env, imageName)
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
// ------------------------------------------
@@ -521,7 +521,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
canonicalDigest := canonicalDigester.Digest()
layerFile.Seek(0, 0)
- uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
+ uploadURLBase, _ = startPushLayer(t, env, imageName)
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
@@ -607,7 +607,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
t.Fatalf("Error constructing request: %s", err)
}
req.Header.Set("If-None-Match", "")
- resp, err = http.DefaultClient.Do(req)
+ resp, _ = http.DefaultClient.Do(req)
checkResponse(t, "fetching layer with invalid etag", resp, http.StatusOK)
// Missing tests:
@@ -1762,7 +1762,7 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
manifest := args.manifest
ref, _ := reference.WithDigest(imageName, dgst)
- manifestDigestURL, err := env.builder.BuildManifestURL(ref)
+ manifestDigestURL, _ := env.builder.BuildManifestURL(ref)
// ---------------
// Delete by digest
resp, err := httpDelete(manifestDigestURL)
@@ -1823,7 +1823,7 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
// Upload manifest by tag
tag := "atag"
tagRef, _ := reference.WithTag(imageName, tag)
- manifestTagURL, err := env.builder.BuildManifestURL(tagRef)
+ manifestTagURL, _ := env.builder.BuildManifestURL(tagRef)
resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest)
checkResponse(t, "putting manifest by tag", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
@@ -2390,7 +2390,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) {
checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
// Manifest Delete
- resp, err = httpDelete(manifestURL)
+ resp, _ = httpDelete(manifestURL)
checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
// Blob upload initialization
@@ -2489,9 +2489,9 @@ func TestProxyManifestGetByTag(t *testing.T) {
checkErr(t, err, "building manifest url")
resp, err = http.Get(manifestTagURL)
- checkErr(t, err, "fetching manifest from proxy by tag")
+ checkErr(t, err, "fetching manifest from proxy by tag (error check 1)")
defer resp.Body.Close()
- checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK)
+ checkResponse(t, "fetching manifest from proxy by tag (response check 1)", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
})
@@ -2504,9 +2504,9 @@ func TestProxyManifestGetByTag(t *testing.T) {
// fetch it with the same proxy URL as before. Ensure the updated content is at the same tag
resp, err = http.Get(manifestTagURL)
- checkErr(t, err, "fetching manifest from proxy by tag")
+ checkErr(t, err, "fetching manifest from proxy by tag (error check 2)")
defer resp.Body.Close()
- checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK)
+ checkResponse(t, "fetching manifest from proxy by tag (response check 2)", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{newDigest.String()},
})
diff --git a/registry/handlers/images.go b/registry/handlers/images.go
index 3ee207b6..f5900c13 100644
--- a/registry/handlers/images.go
+++ b/registry/handlers/images.go
@@ -10,6 +10,7 @@ import (
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/manifestlist"
+ "github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
@@ -17,6 +18,7 @@ import (
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/auth"
"github.com/gorilla/handlers"
+ "github.com/opencontainers/image-spec/specs-go/v1"
)
// These constants determine which architecture and OS to choose from a
@@ -25,6 +27,18 @@ const (
defaultArch = "amd64"
defaultOS = "linux"
maxManifestBodySize = 4 << 20
+ imageClass = "image"
+)
+
+type storageType int
+
+const (
+ manifestSchema1 storageType = iota // 0
+ manifestSchema2 // 1
+ manifestlistSchema // 2
+ ociSchema // 3
+ ociImageIndexSchema // 4
+ numStorageTypes // 5
)
// imageManifestDispatcher takes the request context and builds the
@@ -72,35 +86,8 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
imh.Errors = append(imh.Errors, err)
return
}
+ var supports [numStorageTypes]bool
- var manifest distribution.Manifest
- if imh.Tag != "" {
- tags := imh.Repository.Tags(imh)
- desc, err := tags.Get(imh, imh.Tag)
- if err != nil {
- imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
- return
- }
- imh.Digest = desc.Digest
- }
-
- if etagMatch(r, imh.Digest.String()) {
- w.WriteHeader(http.StatusNotModified)
- return
- }
-
- var options []distribution.ManifestServiceOption
- if imh.Tag != "" {
- options = append(options, distribution.WithTag(imh.Tag))
- }
- manifest, err = manifests.Get(imh, imh.Digest, options...)
- if err != nil {
- imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
- return
- }
-
- supportsSchema2 := false
- supportsManifestList := false
// this parsing of Accept headers is not quite as full-featured as godoc.org's parser, but we don't care about "q=" values
// https://github.com/golang/gddo/blob/e91d4165076d7474d20abda83f92d15c7ebc3e81/httputil/header/header.go#L165-L202
for _, acceptHeader := range r.Header["Accept"] {
@@ -119,21 +106,72 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
mediaType = strings.TrimSpace(mediaType)
if mediaType == schema2.MediaTypeManifest {
- supportsSchema2 = true
+ supports[manifestSchema2] = true
}
if mediaType == manifestlist.MediaTypeManifestList {
- supportsManifestList = true
+ supports[manifestlistSchema] = true
+ }
+ if mediaType == v1.MediaTypeImageManifest {
+ supports[ociSchema] = true
+ }
+ if mediaType == v1.MediaTypeImageIndex {
+ supports[ociImageIndexSchema] = true
}
}
}
+ if imh.Tag != "" {
+ tags := imh.Repository.Tags(imh)
+ desc, err := tags.Get(imh, imh.Tag)
+ if err != nil {
+ imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
+ return
+ }
+ imh.Digest = desc.Digest
+ }
+
+ if etagMatch(r, imh.Digest.String()) {
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+
+ var options []distribution.ManifestServiceOption
+ if imh.Tag != "" {
+ options = append(options, distribution.WithTag(imh.Tag))
+ }
+ manifest, err := manifests.Get(imh, imh.Digest, options...)
+ if err != nil {
+ imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
+ return
+ }
+ // determine the type of the returned manifest
+ manifestType := manifestSchema1
schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
+ if isSchema2 {
+ manifestType = manifestSchema2
+ } else if _, isOCImanifest := manifest.(*ocischema.DeserializedManifest); isOCImanifest {
+ manifestType = ociSchema
+ } else if isManifestList {
+ if manifestList.MediaType == manifestlist.MediaTypeManifestList {
+ manifestType = manifestlistSchema
+ } else if manifestList.MediaType == v1.MediaTypeImageIndex {
+ manifestType = ociImageIndexSchema
+ }
+ }
+ if manifestType == ociSchema && !supports[ociSchema] {
+ imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithMessage("OCI manifest found, but accept header does not support OCI manifests"))
+ return
+ }
+ if manifestType == ociImageIndexSchema && !supports[ociImageIndexSchema] {
+ imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithMessage("OCI index found, but accept header does not support OCI indexes"))
+ return
+ }
// Only rewrite schema2 manifests when they are being fetched by tag.
// If they are being fetched by digest, we can't return something not
// matching the digest.
- if imh.Tag != "" && isSchema2 && !supportsSchema2 {
+ if imh.Tag != "" && manifestType == manifestSchema2 && !supports[manifestSchema2] {
// Rewrite manifest in schema1 format
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
@@ -141,7 +179,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
if err != nil {
return
}
- } else if imh.Tag != "" && isManifestList && !supportsManifestList {
+ } else if imh.Tag != "" && manifestType == manifestlistSchema && !supports[manifestlistSchema] {
// Rewrite manifest in schema1 format
ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
@@ -167,7 +205,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
}
// If necessary, convert the image manifest
- if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
+ if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supports[manifestSchema2] {
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
if err != nil {
return
@@ -268,6 +306,14 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
return
}
+ isAnOCIManifest := mediaType == v1.MediaTypeImageManifest || mediaType == v1.MediaTypeImageIndex
+
+ if isAnOCIManifest {
+ ctxu.GetLogger(imh).Debug("Putting an OCI Manifest!")
+ } else {
+ ctxu.GetLogger(imh).Debug("Putting a Docker Manifest!")
+ }
+
var options []distribution.ManifestServiceOption
if imh.Tag != "" {
options = append(options, distribution.WithTag(imh.Tag))
@@ -313,7 +359,6 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
default:
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
-
return
}
@@ -346,6 +391,8 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
w.Header().Set("Location", location)
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
w.WriteHeader(http.StatusCreated)
+
+ ctxu.GetLogger(imh).Debug("Succeeded in putting manifest!")
}
// applyResourcePolicy checks whether the resource class matches what has
@@ -359,16 +406,22 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
var class string
switch m := manifest.(type) {
case *schema1.SignedManifest:
- class = "image"
+ class = imageClass
case *schema2.DeserializedManifest:
switch m.Config.MediaType {
case schema2.MediaTypeConfig:
- class = "image"
+ class = imageClass
case schema2.MediaTypePluginConfig:
class = "plugin"
default:
- message := fmt.Sprintf("unknown manifest class for %s", m.Config.MediaType)
- return errcode.ErrorCodeDenied.WithMessage(message)
+ return errcode.ErrorCodeDenied.WithMessage("unknown manifest class for " + m.Config.MediaType)
+ }
+ case *ocischema.DeserializedManifest:
+ switch m.Config.MediaType {
+ case v1.MediaTypeImageConfig:
+ class = imageClass
+ default:
+ return errcode.ErrorCodeDenied.WithMessage("unknown manifest class for " + m.Config.MediaType)
}
}
@@ -385,8 +438,7 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
}
}
if !allowedClass {
- message := fmt.Sprintf("registry does not allow %s manifest", class)
- return errcode.ErrorCodeDenied.WithMessage(message)
+ return errcode.ErrorCodeDenied.WithMessage(fmt.Sprintf("registry does not allow %s manifest", class))
}
resources := auth.AuthorizedResources(imh)
@@ -396,7 +448,7 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
for _, r := range resources {
if r.Name == n {
if r.Class == "" {
- r.Class = "image"
+ r.Class = imageClass
}
if r.Class == class {
return nil
@@ -407,8 +459,7 @@ func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manif
// resource was found but no matching class was found
if foundResource {
- message := fmt.Sprintf("repository not authorized for %s manifest", class)
- return errcode.ErrorCodeDenied.WithMessage(message)
+ return errcode.ErrorCodeDenied.WithMessage(fmt.Sprintf("repository not authorized for %s manifest", class))
}
return nil
diff --git a/registry/storage/manifeststore.go b/registry/storage/manifeststore.go
index 9e8065bb..b65d7d49 100644
--- a/registry/storage/manifeststore.go
+++ b/registry/storage/manifeststore.go
@@ -9,8 +9,10 @@ import (
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/manifestlist"
+ "github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
+ "github.com/opencontainers/image-spec/specs-go/v1"
)
// A ManifestHandler gets and puts manifests of a particular type.
@@ -47,6 +49,7 @@ type manifestStore struct {
schema1Handler ManifestHandler
schema2Handler ManifestHandler
+ ocischemaHandler ManifestHandler
manifestListHandler ManifestHandler
}
@@ -98,7 +101,9 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ..
switch versioned.MediaType {
case schema2.MediaTypeManifest:
return ms.schema2Handler.Unmarshal(ctx, dgst, content)
- case manifestlist.MediaTypeManifestList:
+ case v1.MediaTypeImageManifest:
+ return ms.ocischemaHandler.Unmarshal(ctx, dgst, content)
+ case manifestlist.MediaTypeManifestList, v1.MediaTypeImageIndex:
return ms.manifestListHandler.Unmarshal(ctx, dgst, content)
default:
return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)}
@@ -116,6 +121,8 @@ func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest
return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification)
case *schema2.DeserializedManifest:
return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification)
+ case *ocischema.DeserializedManifest:
+ return ms.ocischemaHandler.Put(ctx, manifest, ms.skipDependencyVerification)
case *manifestlist.DeserializedManifestList:
return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification)
}
diff --git a/registry/storage/ocimanifesthandler.go b/registry/storage/ocimanifesthandler.go
new file mode 100644
index 00000000..ec4adafb
--- /dev/null
+++ b/registry/storage/ocimanifesthandler.go
@@ -0,0 +1,129 @@
+package storage
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/docker/distribution/manifest/ocischema"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+//ocischemaManifestHandler is a ManifestHandler that covers ocischema manifests.
+type ocischemaManifestHandler struct {
+ repository distribution.Repository
+ blobStore distribution.BlobStore
+ ctx context.Context
+ manifestURLs manifestURLs
+}
+
+var _ ManifestHandler = &ocischemaManifestHandler{}
+
+func (ms *ocischemaManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
+ context.GetLogger(ms.ctx).Debug("(*ocischemaManifestHandler).Unmarshal")
+
+ var m ocischema.DeserializedManifest
+ if err := json.Unmarshal(content, &m); err != nil {
+ return nil, err
+ }
+
+ return &m, nil
+}
+
+func (ms *ocischemaManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
+ context.GetLogger(ms.ctx).Debug("(*ocischemaManifestHandler).Put")
+
+ m, ok := manifest.(*ocischema.DeserializedManifest)
+ if !ok {
+ return "", fmt.Errorf("non-ocischema manifest put to ocischemaManifestHandler: %T", manifest)
+ }
+
+ if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil {
+ return "", err
+ }
+
+ mt, payload, err := m.Payload()
+ if err != nil {
+ return "", err
+ }
+
+ revision, err := ms.blobStore.Put(ctx, mt, payload)
+ if err != nil {
+ context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
+ return "", err
+ }
+
+ return revision.Digest, nil
+}
+
+// verifyManifest ensures that the manifest content is valid from the
+// 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 *ocischemaManifestHandler) verifyManifest(ctx context.Context, mnfst ocischema.DeserializedManifest, skipDependencyVerification bool) error {
+ var errs distribution.ErrManifestVerification
+
+ if skipDependencyVerification {
+ return nil
+ }
+
+ manifestService, err := ms.repository.Manifests(ctx)
+ if err != nil {
+ return err
+ }
+
+ blobsService := ms.repository.Blobs(ctx)
+
+ for _, descriptor := range mnfst.References() {
+ var err error
+
+ switch descriptor.MediaType {
+ case v1.MediaTypeImageLayer, v1.MediaTypeImageLayerGzip, v1.MediaTypeImageLayerNonDistributable, v1.MediaTypeImageLayerNonDistributableGzip:
+ allow := ms.manifestURLs.allow
+ deny := ms.manifestURLs.deny
+ for _, u := range descriptor.URLs {
+ var pu *url.URL
+ pu, err = url.Parse(u)
+ if err != nil || (pu.Scheme != "http" && pu.Scheme != "https") || pu.Fragment != "" || (allow != nil && !allow.MatchString(u)) || (deny != nil && deny.MatchString(u)) {
+ err = errInvalidURL
+ break
+ }
+ }
+ if err == nil && len(descriptor.URLs) == 0 {
+ // If no URLs, require that the blob exists
+ _, err = blobsService.Stat(ctx, descriptor.Digest)
+ }
+
+ case v1.MediaTypeImageManifest:
+ var exists bool
+ exists, err = manifestService.Exists(ctx, descriptor.Digest)
+ if err != nil || !exists {
+ err = distribution.ErrBlobUnknown // just coerce to unknown.
+ }
+
+ fallthrough // double check the blob store.
+ default:
+ // forward all else to blob storage
+ if len(descriptor.URLs) == 0 {
+ _, err = blobsService.Stat(ctx, descriptor.Digest)
+ }
+ }
+
+ if err != nil {
+ if err != distribution.ErrBlobUnknown {
+ errs = append(errs, err)
+ }
+
+ // On error here, we always append unknown blob errors.
+ errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: descriptor.Digest})
+ }
+ }
+
+ if len(errs) != 0 {
+ return errs
+ }
+
+ return nil
+}
diff --git a/registry/storage/ocimanifesthandler_test.go b/registry/storage/ocimanifesthandler_test.go
new file mode 100644
index 00000000..302294af
--- /dev/null
+++ b/registry/storage/ocimanifesthandler_test.go
@@ -0,0 +1,138 @@
+package storage
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/manifest"
+ "github.com/docker/distribution/manifest/ocischema"
+ "github.com/docker/distribution/registry/storage/driver/inmemory"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+func TestVerifyOCIManifestNonDistributableLayer(t *testing.T) {
+ ctx := context.Background()
+ inmemoryDriver := inmemory.New()
+ registry := createRegistry(t, inmemoryDriver,
+ ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")),
+ ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope")))
+ repo := makeRepository(t, registry, "test")
+ manifestService := makeManifestService(t, repo)
+
+ config, err := repo.Blobs(ctx).Put(ctx, v1.MediaTypeImageConfig, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ layer, err := repo.Blobs(ctx).Put(ctx, v1.MediaTypeImageLayerGzip, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nonDistributableLayer := distribution.Descriptor{
+ Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a",
+ Size: 6323,
+ MediaType: v1.MediaTypeImageLayerNonDistributableGzip,
+ }
+
+ template := ocischema.Manifest{
+ Versioned: manifest.Versioned{
+ SchemaVersion: 2,
+ MediaType: v1.MediaTypeImageManifest,
+ },
+ Config: config,
+ }
+
+ type testcase struct {
+ BaseLayer distribution.Descriptor
+ URLs []string
+ Err error
+ }
+
+ cases := []testcase{
+ {
+ nonDistributableLayer,
+ nil,
+ distribution.ErrManifestBlobUnknown{Digest: nonDistributableLayer.Digest},
+ },
+ {
+ layer,
+ []string{"http://foo/bar"},
+ nil,
+ },
+ {
+ nonDistributableLayer,
+ []string{"file:///local/file"},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"http://foo/bar#baz"},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{""},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"https://foo/bar", ""},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"", "https://foo/bar"},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"http://nope/bar"},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"http://foo/nope"},
+ errInvalidURL,
+ },
+ {
+ nonDistributableLayer,
+ []string{"http://foo/bar"},
+ nil,
+ },
+ {
+ nonDistributableLayer,
+ []string{"https://foo/bar"},
+ nil,
+ },
+ }
+
+ for _, c := range cases {
+ m := template
+ l := c.BaseLayer
+ l.URLs = c.URLs
+ m.Layers = []distribution.Descriptor{l}
+ dm, err := ocischema.FromStruct(m)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ _, err = manifestService.Put(ctx, dm)
+ if verr, ok := err.(distribution.ErrManifestVerification); ok {
+ // Extract the first error
+ if len(verr) == 2 {
+ if _, ok = verr[1].(distribution.ErrManifestBlobUnknown); ok {
+ err = verr[0]
+ }
+ } else if len(verr) == 1 {
+ err = verr[0]
+ }
+ }
+ if err != c.Err {
+ t.Errorf("%#v: expected %v, got %v", l, c.Err, err)
+ }
+ }
+}
diff --git a/registry/storage/registry.go b/registry/storage/registry.go
index 20525ffb..fde594a6 100644
--- a/registry/storage/registry.go
+++ b/registry/storage/registry.go
@@ -258,6 +258,12 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
repository: repo,
blobStore: blobStore,
},
+ ocischemaHandler: &ocischemaManifestHandler{
+ ctx: ctx,
+ repository: repo,
+ blobStore: blobStore,
+ manifestURLs: repo.registry.manifestURLs,
+ },
}
// Apply options
diff --git a/vendor/github.com/opencontainers/image-spec/LICENSE b/vendor/github.com/opencontainers/image-spec/LICENSE
new file mode 100644
index 00000000..9fdc20fd
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/LICENSE
@@ -0,0 +1,191 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2016 The Linux Foundation.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/opencontainers/image-spec/README.md b/vendor/github.com/opencontainers/image-spec/README.md
new file mode 100644
index 00000000..5ab5554e
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/README.md
@@ -0,0 +1,167 @@
+# OCI Image Format Specification
+<div>
+<a href="https://travis-ci.org/opencontainers/image-spec">
+<img src="https://travis-ci.org/opencontainers/image-spec.svg?branch=master"></img>
+</a>
+</div>
+
+The OCI Image Format project creates and maintains the software shipping container image format spec (OCI Image Format).
+
+**[The specification can be found here](spec.md).**
+
+This repository also provides [Go types](specs-go), [intra-blob validation tooling, and JSON Schema](schema).
+The Go types and validation should be compatible with the current Go release; earlier Go releases are not supported.
+
+Additional documentation about how this group operates:
+
+- [Code of Conduct](https://github.com/opencontainers/tob/blob/d2f9d68c1332870e40693fe077d311e0742bc73d/code-of-conduct.md)
+- [Roadmap](#roadmap)
+- [Releases](RELEASES.md)
+- [Project Documentation](project.md)
+
+The _optional_ and _base_ layers of all OCI projects are tracked in the [OCI Scope Table](https://www.opencontainers.org/about/oci-scope-table).
+
+## Running an OCI Image
+
+The OCI Image Format partner project is the [OCI Runtime Spec project](https://github.com/opencontainers/runtime-spec).
+The Runtime Specification outlines how to run a "[filesystem bundle](https://github.com/opencontainers/runtime-spec/blob/master/bundle.md)" that is unpacked on disk.
+At a high-level an OCI implementation would download an OCI Image then unpack that image into an OCI Runtime filesystem bundle.
+At this point the OCI Runtime Bundle would be run by an OCI Runtime.
+
+This entire workflow supports the UX that users have come to expect from container engines like Docker and rkt: primarily, the ability to run an image with no additional arguments:
+
+* docker run example.com/org/app:v1.0.0
+* rkt run example.com/org/app,version=v1.0.0
+
+To support this UX the OCI Image Format contains sufficient information to launch the application on the target platform (e.g. command, arguments, environment variables, etc).
+
+## FAQ
+
+**Q: Why doesn't this project mention distribution?**
+
+A: Distribution, for example using HTTP as both Docker v2.2 and AppC do today, is currently out of scope on the [OCI Scope Table](https://www.opencontainers.org/about/oci-scope-table).
+There has been [some discussion on the TOB mailing list](https://groups.google.com/a/opencontainers.org/d/msg/tob/A3JnmI-D-6Y/tLuptPDHAgAJ) to make distribution an optional layer, but this topic is a work in progress.
+
+**Q: What happens to AppC or Docker Image Formats?**
+
+A: Existing formats can continue to be a proving ground for technologies, as needed.
+The OCI Image Format project strives to provide a dependable open specification that can be shared between different tools and be evolved for years or decades of compatibility; as the deb and rpm format have.
+
+Find more [FAQ on the OCI site](https://www.opencontainers.org/faq).
+
+## Roadmap
+
+The [GitHub milestones](https://github.com/opencontainers/image-spec/milestones) lay out the path to the OCI v1.0.0 release in late 2016.
+
+# Contributing
+
+Development happens on GitHub for the spec.
+Issues are used for bugs and actionable items and longer discussions can happen on the [mailing list](#mailing-list).
+
+The specification and code is licensed under the Apache 2.0 license found in the `LICENSE` file of this repository.
+
+## Discuss your design
+
+The project welcomes submissions, but please let everyone know what you are working on.
+
+Before undertaking a nontrivial change to this specification, send mail to the [mailing list](#mailing-list) to discuss what you plan to do.
+This gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits.
+It also guarantees that the design is sound before code is written; a GitHub pull-request is not the place for high-level discussions.
+
+Typos and grammatical errors can go straight to a pull-request.
+When in doubt, start on the [mailing-list](#mailing-list).
+
+## Weekly Call
+
+The contributors and maintainers of all OCI projects have a weekly meeting Wednesdays at 2:00 PM (USA Pacific).
+Everyone is welcome to participate via [UberConference web][UberConference] or audio-only: +1-415-968-0849 (no PIN needed).
+An initial agenda will be posted to the [mailing list](#mailing-list) earlier in the week, and everyone is welcome to propose additional topics or suggest other agenda alterations there.
+Minutes are posted to the [mailing list](#mailing-list) and minutes from past calls are archived [here][minutes].
+
+## Mailing List
+
+You can subscribe and join the mailing list on [Google Groups](https://groups.google.com/a/opencontainers.org/forum/#!forum/dev).
+
+## IRC
+
+OCI discussion happens on #opencontainers on Freenode ([logs][irc-logs]).
+
+## Markdown style
+
+To keep consistency throughout the Markdown files in the Open Container spec all files should be formatted one sentence per line.
+This fixes two things: it makes diffing easier with git and it resolves fights about line wrapping length.
+For example, this paragraph will span three lines in the Markdown source.
+
+## Git commit
+
+### Sign your work
+
+The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch.
+The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)):
+
+```
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+660 York Street, Suite 102,
+San Francisco, CA 94110 USA
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+```
+
+then you just add a line to every git commit message:
+
+ Signed-off-by: Joe Smith <joe@gmail.com>
+
+using your real name (sorry, no pseudonyms or anonymous contributions.)
+
+You can add the sign off when creating the git commit via `git commit -s`.
+
+### Commit Style
+
+Simple house-keeping for clean git history.
+Read more on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) or the Discussion section of [`git-commit(1)`](http://git-scm.com/docs/git-commit).
+
+1. Separate the subject from body with a blank line
+2. Limit the subject line to 50 characters
+3. Capitalize the subject line
+4. Do not end the subject line with a period
+5. Use the imperative mood in the subject line
+6. Wrap the body at 72 characters
+7. Use the body to explain what and why vs. how
+ * If there was important/useful/essential conversation or information, copy or include a reference
+8. When possible, one keyword to scope the change in the subject (i.e. "README: ...", "runtime: ...")
+
+
+[UberConference]: https://www.uberconference.com/opencontainers
+[irc-logs]: http://ircbot.wl.linuxfoundation.org/eavesdrop/%23opencontainers/
+[minutes]: http://ircbot.wl.linuxfoundation.org/meetings/opencontainers/
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go
new file mode 100644
index 00000000..35d81089
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go
@@ -0,0 +1,56 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+const (
+ // AnnotationCreated is the annotation key for the date and time on which the image was built (date-time string as defined by RFC 3339).
+ AnnotationCreated = "org.opencontainers.image.created"
+
+ // AnnotationAuthors is the annotation key for the contact details of the people or organization responsible for the image (freeform string).
+ AnnotationAuthors = "org.opencontainers.image.authors"
+
+ // AnnotationURL is the annotation key for the URL to find more information on the image.
+ AnnotationURL = "org.opencontainers.image.url"
+
+ // AnnotationDocumentation is the annotation key for the URL to get documentation on the image.
+ AnnotationDocumentation = "org.opencontainers.image.documentation"
+
+ // AnnotationSource is the annotation key for the URL to get source code for building the image.
+ AnnotationSource = "org.opencontainers.image.source"
+
+ // AnnotationVersion is the annotation key for the version of the packaged software.
+ // The version MAY match a label or tag in the source code repository.
+ // The version MAY be Semantic versioning-compatible.
+ AnnotationVersion = "org.opencontainers.image.version"
+
+ // AnnotationRevision is the annotation key for the source control revision identifier for the packaged software.
+ AnnotationRevision = "org.opencontainers.image.revision"
+
+ // AnnotationVendor is the annotation key for the name of the distributing entity, organization or individual.
+ AnnotationVendor = "org.opencontainers.image.vendor"
+
+ // AnnotationLicenses is the annotation key for the license(s) under which contained software is distributed as an SPDX License Expression.
+ AnnotationLicenses = "org.opencontainers.image.licenses"
+
+ // AnnotationRefName is the annotation key for the name of the reference for a target.
+ // SHOULD only be considered valid when on descriptors on `index.json` within image layout.
+ AnnotationRefName = "org.opencontainers.image.ref.name"
+
+ // AnnotationTitle is the annotation key for the human-readable title of the image.
+ AnnotationTitle = "org.opencontainers.image.title"
+
+ // AnnotationDescription is the annotation key for the human-readable description of the software packaged in the image.
+ AnnotationDescription = "org.opencontainers.image.description"
+)
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go
new file mode 100644
index 00000000..2743c299
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go
@@ -0,0 +1,103 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "time"
+
+ "github.com/docker/distribution/digest"
+)
+
+// ImageConfig defines the execution parameters which should be used as a base when running a container using an image.
+type ImageConfig struct {
+ // User defines the username or UID which the process in the container should run as.
+ User string `json:"User,omitempty"`
+
+ // ExposedPorts a set of ports to expose from a container running this image.
+ ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
+
+ // Env is a list of environment variables to be used in a container.
+ Env []string `json:"Env,omitempty"`
+
+ // Entrypoint defines a list of arguments to use as the command to execute when the container starts.
+ Entrypoint []string `json:"Entrypoint,omitempty"`
+
+ // Cmd defines the default arguments to the entrypoint of the container.
+ Cmd []string `json:"Cmd,omitempty"`
+
+ // Volumes is a set of directories describing where the process is likely write data specific to a container instance.
+ Volumes map[string]struct{} `json:"Volumes,omitempty"`
+
+ // WorkingDir sets the current working directory of the entrypoint process in the container.
+ WorkingDir string `json:"WorkingDir,omitempty"`
+
+ // Labels contains arbitrary metadata for the container.
+ Labels map[string]string `json:"Labels,omitempty"`
+
+ // StopSignal contains the system call signal that will be sent to the container to exit.
+ StopSignal string `json:"StopSignal,omitempty"`
+}
+
+// RootFS describes a layer content addresses
+type RootFS struct {
+ // Type is the type of the rootfs.
+ Type string `json:"type"`
+
+ // DiffIDs is an array of layer content hashes (DiffIDs), in order from bottom-most to top-most.
+ DiffIDs []digest.Digest `json:"diff_ids"`
+}
+
+// History describes the history of a layer.
+type History struct {
+ // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6.
+ Created *time.Time `json:"created,omitempty"`
+
+ // CreatedBy is the command which created the layer.
+ CreatedBy string `json:"created_by,omitempty"`
+
+ // Author is the author of the build point.
+ Author string `json:"author,omitempty"`
+
+ // Comment is a custom message set when creating the layer.
+ Comment string `json:"comment,omitempty"`
+
+ // EmptyLayer is used to mark if the history item created a filesystem diff.
+ EmptyLayer bool `json:"empty_layer,omitempty"`
+}
+
+// Image is the JSON structure which describes some basic information about the image.
+// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON.
+type Image struct {
+ // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6.
+ Created *time.Time `json:"created,omitempty"`
+
+ // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image.
+ Author string `json:"author,omitempty"`
+
+ // Architecture is the CPU architecture which the binaries in this image are built to run on.
+ Architecture string `json:"architecture"`
+
+ // OS is the name of the operating system which the image is built to run on.
+ OS string `json:"os"`
+
+ // Config defines the execution parameters which should be used as a base when running a container using the image.
+ Config ImageConfig `json:"config,omitempty"`
+
+ // RootFS references the layer content addresses used by the image.
+ RootFS RootFS `json:"rootfs"`
+
+ // History describes the history of each layer.
+ History []History `json:"history,omitempty"`
+}
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go
new file mode 100644
index 00000000..0aa049df
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go
@@ -0,0 +1,64 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import "github.com/docker/distribution/digest"
+
+// Descriptor describes the disposition of targeted content.
+// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype
+// when marshalled to JSON.
+type Descriptor struct {
+ // MediaType is the media type of the object this schema refers to.
+ MediaType string `json:"mediaType,omitempty"`
+
+ // Digest is the digest of the targeted content.
+ Digest digest.Digest `json:"digest"`
+
+ // Size specifies the size in bytes of the blob.
+ Size int64 `json:"size"`
+
+ // URLs specifies a list of URLs from which this object MAY be downloaded
+ URLs []string `json:"urls,omitempty"`
+
+ // Annotations contains arbitrary metadata relating to the targeted content.
+ Annotations map[string]string `json:"annotations,omitempty"`
+
+ // Platform describes the platform which the image in the manifest runs on.
+ //
+ // This should only be used when referring to a manifest.
+ Platform *Platform `json:"platform,omitempty"`
+}
+
+// Platform describes the platform which the image in the manifest runs on.
+type Platform struct {
+ // Architecture field specifies the CPU architecture, for example
+ // `amd64` or `ppc64`.
+ Architecture string `json:"architecture"`
+
+ // OS specifies the operating system, for example `linux` or `windows`.
+ OS string `json:"os"`
+
+ // OSVersion is an optional field specifying the operating system
+ // version, for example on Windows `10.0.14393.1066`.
+ OSVersion string `json:"os.version,omitempty"`
+
+ // OSFeatures is an optional field specifying an array of strings,
+ // each listing a required OS feature (for example on Windows `win32k`).
+ OSFeatures []string `json:"os.features,omitempty"`
+
+ // Variant is an optional field specifying a variant of the CPU, for
+ // example `v7` to specify ARMv7 when architecture is `arm`.
+ Variant string `json:"variant,omitempty"`
+}
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go
new file mode 100644
index 00000000..4e6c4b23
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go
@@ -0,0 +1,29 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import "github.com/opencontainers/image-spec/specs-go"
+
+// Index references manifests for various platforms.
+// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON.
+type Index struct {
+ specs.Versioned
+
+ // Manifests references platform specific manifests.
+ Manifests []Descriptor `json:"manifests"`
+
+ // Annotations contains arbitrary metadata for the image index.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go
new file mode 100644
index 00000000..fc79e9e0
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go
@@ -0,0 +1,28 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+const (
+ // ImageLayoutFile is the file name of oci image layout file
+ ImageLayoutFile = "oci-layout"
+ // ImageLayoutVersion is the version of ImageLayout
+ ImageLayoutVersion = "1.0.0"
+)
+
+// ImageLayout is the structure in the "oci-layout" file, found in the root
+// of an OCI Image-layout directory.
+type ImageLayout struct {
+ Version string `json:"imageLayoutVersion"`
+}
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go
new file mode 100644
index 00000000..7ff32c40
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go
@@ -0,0 +1,32 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import "github.com/opencontainers/image-spec/specs-go"
+
+// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON.
+type Manifest struct {
+ specs.Versioned
+
+ // Config references a configuration object for a container, by digest.
+ // The referenced configuration object is a JSON blob that the runtime uses to set up the container.
+ Config Descriptor `json:"config"`
+
+ // Layers is an indexed list of layers referenced by the manifest.
+ Layers []Descriptor `json:"layers"`
+
+ // Annotations contains arbitrary metadata for the image manifest.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go
new file mode 100644
index 00000000..bad7bb97
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go
@@ -0,0 +1,48 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+const (
+ // MediaTypeDescriptor specifies the media type for a content descriptor.
+ MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json"
+
+ // MediaTypeLayoutHeader specifies the media type for the oci-layout.
+ MediaTypeLayoutHeader = "application/vnd.oci.layout.header.v1+json"
+
+ // MediaTypeImageManifest specifies the media type for an image manifest.
+ MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json"
+
+ // MediaTypeImageIndex specifies the media type for an image index.
+ MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json"
+
+ // MediaTypeImageLayer is the media type used for layers referenced by the manifest.
+ MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar"
+
+ // MediaTypeImageLayerGzip is the media type used for gzipped layers
+ // referenced by the manifest.
+ MediaTypeImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip"
+
+ // MediaTypeImageLayerNonDistributable is the media type for layers referenced by
+ // the manifest but with distribution restrictions.
+ MediaTypeImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar"
+
+ // MediaTypeImageLayerNonDistributableGzip is the media type for
+ // gzipped layers referenced by the manifest but with distribution
+ // restrictions.
+ MediaTypeImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
+
+ // MediaTypeImageConfig specifies the media type for the image configuration.
+ MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json"
+)
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/version.go b/vendor/github.com/opencontainers/image-spec/specs-go/version.go
new file mode 100644
index 00000000..e3eee29b
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/version.go
@@ -0,0 +1,32 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package specs
+
+import "fmt"
+
+const (
+ // VersionMajor is for an API incompatible changes
+ VersionMajor = 1
+ // VersionMinor is for functionality in a backwards-compatible manner
+ VersionMinor = 0
+ // VersionPatch is for backwards-compatible bug fixes
+ VersionPatch = 0
+
+ // VersionDev indicates development branch. Releases will be empty string.
+ VersionDev = ""
+)
+
+// Version is the specification version that the package types support.
+var Version = fmt.Sprintf("%d.%d.%d%s", VersionMajor, VersionMinor, VersionPatch, VersionDev)
diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go b/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go
new file mode 100644
index 00000000..58a1510f
--- /dev/null
+++ b/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go
@@ -0,0 +1,23 @@
+// Copyright 2016 The Linux Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package specs
+
+// Versioned provides a struct with the manifest schemaVersion and mediaType.
+// Incoming content with unknown schema version can be decoded against this
+// struct to check the version.
+type Versioned struct {
+ // SchemaVersion is the image manifest schema that this image follows
+ SchemaVersion int `json:"schemaVersion"`
+}
--
2.13.5