Blob Blame History Raw
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