adds support for oci manifests and manifestlists
Signed-off-by: Mike Brown <brownwm@us.ibm.com>master
							parent
							
								
									749f6afb45
								
							
						
					
					
						commit
						9986e8ca7c
					
				|  | @ -7,11 +7,17 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/manifest" | 	"github.com/docker/distribution/manifest" | ||||||
|  | 	"github.com/docker/distribution/manifest/ocischema" | ||||||
| 	"github.com/opencontainers/go-digest" | 	"github.com/opencontainers/go-digest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
| 	// MediaTypeManifestList specifies the mediaType for manifest lists.
 | 	// MediaTypeManifestList specifies the mediaType for manifest lists.
 | ||||||
| const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" | 	MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" | ||||||
|  | 	// MediaTypeOCIManifestList specifies the mediaType for OCI compliant manifest
 | ||||||
|  | 	// lists.
 | ||||||
|  | 	MediaTypeOCIManifestList = "application/vnd.oci.image.manifest.list.v1+json" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| // SchemaVersion provides a pre-initialized version structure for this
 | // SchemaVersion provides a pre-initialized version structure for this
 | ||||||
| // packages version of the manifest.
 | // packages version of the manifest.
 | ||||||
|  | @ -20,6 +26,13 @@ var SchemaVersion = manifest.Versioned{ | ||||||
| 	MediaType:     MediaTypeManifestList, | 	MediaType:     MediaTypeManifestList, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // OCISchemaVersion provides a pre-initialized version structure for this
 | ||||||
|  | // packages OCIschema version of the manifest.
 | ||||||
|  | var OCISchemaVersion = manifest.Versioned{ | ||||||
|  | 	SchemaVersion: 2, | ||||||
|  | 	MediaType:     MediaTypeOCIManifestList, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func init() { | func init() { | ||||||
| 	manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { | 	manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { | ||||||
| 		m := new(DeserializedManifestList) | 		m := new(DeserializedManifestList) | ||||||
|  | @ -105,9 +118,16 @@ type DeserializedManifestList struct { | ||||||
| // DeserializedManifestList which contains the resulting manifest list
 | // DeserializedManifestList which contains the resulting manifest list
 | ||||||
| // and its JSON representation.
 | // and its JSON representation.
 | ||||||
| func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { | func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { | ||||||
| 	m := ManifestList{ | 	var m ManifestList | ||||||
|  | 	if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == ocischema.MediaTypeManifest { | ||||||
|  | 		m = ManifestList{ | ||||||
|  | 			Versioned: OCISchemaVersion, | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		m = ManifestList{ | ||||||
| 			Versioned: SchemaVersion, | 			Versioned: SchemaVersion, | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) | 	m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) | ||||||
| 	copy(m.Manifests, descriptors) | 	copy(m.Manifests, descriptors) | ||||||
|  |  | ||||||
|  | @ -69,7 +69,7 @@ func TestManifestList(t *testing.T) { | ||||||
| 		t.Fatalf("error creating DeserializedManifestList: %v", err) | 		t.Fatalf("error creating DeserializedManifestList: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mediaType, canonical, err := deserialized.Payload() | 	mediaType, canonical, _ := deserialized.Payload() | ||||||
| 
 | 
 | ||||||
| 	if mediaType != MediaTypeManifestList { | 	if mediaType != MediaTypeManifestList { | ||||||
| 		t.Fatalf("unexpected media type: %s", mediaType) | 		t.Fatalf("unexpected media type: %s", mediaType) | ||||||
|  | @ -109,3 +109,104 @@ func TestManifestList(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | var expectedOCIManifestListSerialization = []byte(`{ | ||||||
|  |    "schemaVersion": 2, | ||||||
|  |    "mediaType": "application/vnd.oci.image.manifest.list.v1+json", | ||||||
|  |    "manifests": [ | ||||||
|  |       { | ||||||
|  |          "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||||||
|  |          "size": 985, | ||||||
|  |          "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||||
|  |          "platform": { | ||||||
|  |             "architecture": "amd64", | ||||||
|  |             "os": "linux", | ||||||
|  |             "features": [ | ||||||
|  |                "sse4" | ||||||
|  |             ] | ||||||
|  |          } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |          "mediaType": "application/vnd.oci.image.manifest.v1+json", | ||||||
|  |          "size": 2392, | ||||||
|  |          "digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608", | ||||||
|  |          "platform": { | ||||||
|  |             "architecture": "sun4m", | ||||||
|  |             "os": "sunos" | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |    ] | ||||||
|  | }`) | ||||||
|  | 
 | ||||||
|  | func TestOCIManifestList(t *testing.T) { | ||||||
|  | 	manifestDescriptors := []ManifestDescriptor{ | ||||||
|  | 		{ | ||||||
|  | 			Descriptor: distribution.Descriptor{ | ||||||
|  | 				Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||||
|  | 				Size:      985, | ||||||
|  | 				MediaType: "application/vnd.oci.image.manifest.v1+json", | ||||||
|  | 			}, | ||||||
|  | 			Platform: PlatformSpec{ | ||||||
|  | 				Architecture: "amd64", | ||||||
|  | 				OS:           "linux", | ||||||
|  | 				Features:     []string{"sse4"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Descriptor: distribution.Descriptor{ | ||||||
|  | 				Digest:    "sha256:6346340964309634683409684360934680934608934608934608934068934608", | ||||||
|  | 				Size:      2392, | ||||||
|  | 				MediaType: "application/vnd.oci.image.manifest.v1+json", | ||||||
|  | 			}, | ||||||
|  | 			Platform: PlatformSpec{ | ||||||
|  | 				Architecture: "sun4m", | ||||||
|  | 				OS:           "sunos", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	deserialized, err := FromDescriptors(manifestDescriptors) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error creating DeserializedManifestList: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mediaType, canonical, _ := deserialized.Payload() | ||||||
|  | 
 | ||||||
|  | 	if mediaType != MediaTypeOCIManifestList { | ||||||
|  | 		t.Fatalf("unexpected media type: %s", mediaType) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check that the canonical field is the same as json.MarshalIndent
 | ||||||
|  | 	// with these parameters.
 | ||||||
|  | 	p, err := json.MarshalIndent(&deserialized.ManifestList, "", "   ") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error marshaling manifest list: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if !bytes.Equal(p, canonical) { | ||||||
|  | 		t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check that the canonical field has the expected value.
 | ||||||
|  | 	if !bytes.Equal(expectedOCIManifestListSerialization, canonical) { | ||||||
|  | 		t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedOCIManifestListSerialization)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var unmarshalled DeserializedManifestList | ||||||
|  | 	if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil { | ||||||
|  | 		t.Fatalf("error unmarshaling manifest: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !reflect.DeepEqual(&unmarshalled, deserialized) { | ||||||
|  | 		t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	references := deserialized.References() | ||||||
|  | 	if len(references) != 2 { | ||||||
|  | 		t.Fatalf("unexpected number of references: %d", len(references)) | ||||||
|  | 	} | ||||||
|  | 	for i := range references { | ||||||
|  | 		if !reflect.DeepEqual(references[i], manifestDescriptors[i].Descriptor) { | ||||||
|  | 			t.Fatalf("unexpected value %d returned by References: %v", i, references[i]) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,80 @@ | ||||||
|  | package ocischema | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | 	"github.com/opencontainers/go-digest" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // builder is a type for constructing manifests.
 | ||||||
|  | type builder struct { | ||||||
|  | 	// bs is a BlobService used to publish the configuration blob.
 | ||||||
|  | 	bs distribution.BlobService | ||||||
|  | 
 | ||||||
|  | 	// configJSON references
 | ||||||
|  | 	configJSON []byte | ||||||
|  | 
 | ||||||
|  | 	// layers is a list of layer descriptors that gets built by successive
 | ||||||
|  | 	// calls to AppendReference.
 | ||||||
|  | 	layers []distribution.Descriptor | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewManifestBuilder is used to build new manifests for the current schema
 | ||||||
|  | // version. It takes a BlobService so it can publish the configuration blob
 | ||||||
|  | // as part of the Build process.
 | ||||||
|  | func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder { | ||||||
|  | 	mb := &builder{ | ||||||
|  | 		bs:         bs, | ||||||
|  | 		configJSON: make([]byte, len(configJSON)), | ||||||
|  | 	} | ||||||
|  | 	copy(mb.configJSON, configJSON) | ||||||
|  | 
 | ||||||
|  | 	return mb | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Build produces a final manifest from the given references.
 | ||||||
|  | func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) { | ||||||
|  | 	m := Manifest{ | ||||||
|  | 		Versioned: SchemaVersion, | ||||||
|  | 		Layers:    make([]distribution.Descriptor, len(mb.layers)), | ||||||
|  | 	} | ||||||
|  | 	copy(m.Layers, mb.layers) | ||||||
|  | 
 | ||||||
|  | 	configDigest := digest.FromBytes(mb.configJSON) | ||||||
|  | 
 | ||||||
|  | 	var err error | ||||||
|  | 	m.Config, err = mb.bs.Stat(ctx, configDigest) | ||||||
|  | 	switch err { | ||||||
|  | 	case nil: | ||||||
|  | 		// Override MediaType, since Put always replaces the specified media
 | ||||||
|  | 		// type with application/octet-stream in the descriptor it returns.
 | ||||||
|  | 		m.Config.MediaType = MediaTypeConfig | ||||||
|  | 		return FromStruct(m) | ||||||
|  | 	case distribution.ErrBlobUnknown: | ||||||
|  | 		// nop
 | ||||||
|  | 	default: | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add config to the blob store
 | ||||||
|  | 	m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) | ||||||
|  | 	// Override MediaType, since Put always replaces the specified media
 | ||||||
|  | 	// type with application/octet-stream in the descriptor it returns.
 | ||||||
|  | 	m.Config.MediaType = MediaTypeConfig | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return FromStruct(m) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AppendReference adds a reference to the current ManifestBuilder.
 | ||||||
|  | func (mb *builder) AppendReference(d distribution.Describable) error { | ||||||
|  | 	mb.layers = append(mb.layers, d.Descriptor()) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // References returns the current references added to this builder.
 | ||||||
|  | func (mb *builder) References() []distribution.Descriptor { | ||||||
|  | 	return mb.layers | ||||||
|  | } | ||||||
|  | @ -0,0 +1,210 @@ | ||||||
|  | package ocischema | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | 	"github.com/opencontainers/go-digest" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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(`{ | ||||||
|  |     "architecture": "amd64", | ||||||
|  |     "config": { | ||||||
|  |         "AttachStderr": false, | ||||||
|  |         "AttachStdin": false, | ||||||
|  |         "AttachStdout": false, | ||||||
|  |         "Cmd": [ | ||||||
|  |             "/bin/sh", | ||||||
|  |             "-c", | ||||||
|  |             "echo hi" | ||||||
|  |         ], | ||||||
|  |         "Domainname": "", | ||||||
|  |         "Entrypoint": null, | ||||||
|  |         "Env": [ | ||||||
|  |             "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", | ||||||
|  |             "derived=true", | ||||||
|  |             "asdf=true" | ||||||
|  |         ], | ||||||
|  |         "Hostname": "23304fc829f9", | ||||||
|  |         "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", | ||||||
|  |         "Labels": {}, | ||||||
|  |         "OnBuild": [], | ||||||
|  |         "OpenStdin": false, | ||||||
|  |         "StdinOnce": false, | ||||||
|  |         "Tty": false, | ||||||
|  |         "User": "", | ||||||
|  |         "Volumes": null, | ||||||
|  |         "WorkingDir": "" | ||||||
|  |     }, | ||||||
|  |     "container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001", | ||||||
|  |     "container_config": { | ||||||
|  |         "AttachStderr": false, | ||||||
|  |         "AttachStdin": false, | ||||||
|  |         "AttachStdout": false, | ||||||
|  |         "Cmd": [ | ||||||
|  |             "/bin/sh", | ||||||
|  |             "-c", | ||||||
|  |             "#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]" | ||||||
|  |         ], | ||||||
|  |         "Domainname": "", | ||||||
|  |         "Entrypoint": null, | ||||||
|  |         "Env": [ | ||||||
|  |             "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", | ||||||
|  |             "derived=true", | ||||||
|  |             "asdf=true" | ||||||
|  |         ], | ||||||
|  |         "Hostname": "23304fc829f9", | ||||||
|  |         "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", | ||||||
|  |         "Labels": {}, | ||||||
|  |         "OnBuild": [], | ||||||
|  |         "OpenStdin": false, | ||||||
|  |         "StdinOnce": false, | ||||||
|  |         "Tty": false, | ||||||
|  |         "User": "", | ||||||
|  |         "Volumes": null, | ||||||
|  |         "WorkingDir": "" | ||||||
|  |     }, | ||||||
|  |     "created": "2015-11-04T23:06:32.365666163Z", | ||||||
|  |     "docker_version": "1.9.0-dev", | ||||||
|  |     "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\"]" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "created": "2015-11-04T23:06:30.934316144Z", | ||||||
|  |             "created_by": "/bin/sh -c #(nop) ENV derived=true", | ||||||
|  |             "empty_layer": true | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "created": "2015-11-04T23:06:31.192097572Z", | ||||||
|  |             "created_by": "/bin/sh -c #(nop) ENV asdf=true", | ||||||
|  |             "empty_layer": true | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "created": "2015-11-04T23:06:32.083868454Z", | ||||||
|  |             "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "created": "2015-11-04T23:06:32.365666163Z", | ||||||
|  |             "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]", | ||||||
|  |             "empty_layer": true | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "os": "linux", | ||||||
|  |     "rootfs": { | ||||||
|  |         "diff_ids": [ | ||||||
|  |             "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", | ||||||
|  |             "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", | ||||||
|  |             "sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49" | ||||||
|  |         ], | ||||||
|  |         "type": "layers" | ||||||
|  |     } | ||||||
|  | }`) | ||||||
|  | 	configDigest := digest.FromBytes(imgJSON) | ||||||
|  | 
 | ||||||
|  | 	descriptors := []distribution.Descriptor{ | ||||||
|  | 		{ | ||||||
|  | 			Digest:    digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), | ||||||
|  | 			Size:      5312, | ||||||
|  | 			MediaType: MediaTypeLayer, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Digest:    digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"), | ||||||
|  | 			Size:      235231, | ||||||
|  | 			MediaType: MediaTypeLayer, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Digest:    digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), | ||||||
|  | 			Size:      639152, | ||||||
|  | 			MediaType: MediaTypeLayer, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} | ||||||
|  | 	builder := NewManifestBuilder(bs, imgJSON) | ||||||
|  | 
 | ||||||
|  | 	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.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 != MediaTypeConfig { | ||||||
|  | 		t.Fatalf("unexpected media type in target: %s", target.MediaType) | ||||||
|  | 	} | ||||||
|  | 	if target.Size != 3153 { | ||||||
|  | 		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") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,133 @@ | ||||||
|  | package ocischema | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/manifest" | ||||||
|  | 	"github.com/opencontainers/go-digest" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// MediaTypeManifest specifies the mediaType for the current version.
 | ||||||
|  | 	MediaTypeManifest = "application/vnd.oci.image.manifest.v1+json" | ||||||
|  | 
 | ||||||
|  | 	// MediaTypeConfig specifies the mediaType for the image configuration.
 | ||||||
|  | 	MediaTypeConfig = "application/vnd.oci.image.config.v1+json" | ||||||
|  | 
 | ||||||
|  | 	// MediaTypePluginConfig specifies the mediaType for plugin configuration.
 | ||||||
|  | 	MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json" | ||||||
|  | 
 | ||||||
|  | 	// MediaTypeLayer is the mediaType used for layers referenced by the manifest.
 | ||||||
|  | 	MediaTypeLayer = "application/vnd.oci.image.layer.v1.tar+gzip" | ||||||
|  | 
 | ||||||
|  | 	// MediaTypeForeignLayer is the mediaType used for layers that must be
 | ||||||
|  | 	// downloaded from foreign URLs.
 | ||||||
|  | 	MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// SchemaVersion provides a pre-initialized version structure for this
 | ||||||
|  | 	// packages version of the manifest.
 | ||||||
|  | 	SchemaVersion = manifest.Versioned{ | ||||||
|  | 		SchemaVersion: 2, // Mike: todo this could confusing cause oci version 1 is closer to docker 2 than 1
 | ||||||
|  | 		MediaType:     MediaTypeManifest, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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: MediaTypeManifest}, err | ||||||
|  | 	} | ||||||
|  | 	err := distribution.RegisterManifestSchema(MediaTypeManifest, ocischemaFunc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(fmt.Sprintf("Unable to register manifest: %s", err)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Manifest defines a schema2 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"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 signed 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 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,111 @@ | ||||||
|  | package ocischema | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 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" | ||||||
|  |    }, | ||||||
|  |    "layers": [ | ||||||
|  |       { | ||||||
|  |          "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", | ||||||
|  |          "size": 153263, | ||||||
|  |          "digest": "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" | ||||||
|  |       } | ||||||
|  |    ] | ||||||
|  | }`) | ||||||
|  | 
 | ||||||
|  | func TestManifest(t *testing.T) { | ||||||
|  | 	manifest := Manifest{ | ||||||
|  | 		Versioned: SchemaVersion, | ||||||
|  | 		Config: distribution.Descriptor{ | ||||||
|  | 			Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||||
|  | 			Size:      985, | ||||||
|  | 			MediaType: MediaTypeConfig, | ||||||
|  | 		}, | ||||||
|  | 		Layers: []distribution.Descriptor{ | ||||||
|  | 			{ | ||||||
|  | 				Digest:    "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b", | ||||||
|  | 				Size:      153263, | ||||||
|  | 				MediaType: MediaTypeLayer, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	deserialized, err := FromStruct(manifest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error creating DeserializedManifest: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mediaType, canonical, err := deserialized.Payload() | ||||||
|  | 
 | ||||||
|  | 	if mediaType != MediaTypeManifest { | ||||||
|  | 		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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	target := deserialized.Target() | ||||||
|  | 	if target.Digest != "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" { | ||||||
|  | 		t.Fatalf("unexpected digest in target: %s", target.Digest.String()) | ||||||
|  | 	} | ||||||
|  | 	if target.MediaType != MediaTypeConfig { | ||||||
|  | 		t.Fatalf("unexpected media type in target: %s", target.MediaType) | ||||||
|  | 	} | ||||||
|  | 	if target.Size != 985 { | ||||||
|  | 		t.Fatalf("unexpected size in target: %d", target.Size) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 != MediaTypeLayer { | ||||||
|  | 		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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -478,7 +478,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 
 | 
 | ||||||
| 	// -----------------------------------------
 | 	// -----------------------------------------
 | ||||||
| 	// Do layer push with an empty body and different digest
 | 	// 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{})) | 	resp, err = doPushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, bytes.NewReader([]byte{})) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error doing bad layer push: %v", err) | 		t.Fatalf("unexpected error doing bad layer push: %v", err) | ||||||
|  | @ -494,7 +494,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 		t.Fatalf("unexpected error digesting empty buffer: %v", err) | 		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{})) | 	pushLayer(t, env.builder, imageName, zeroDigest, uploadURLBase, bytes.NewReader([]byte{})) | ||||||
| 
 | 
 | ||||||
| 	// -----------------------------------------
 | 	// -----------------------------------------
 | ||||||
|  | @ -507,7 +507,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 		t.Fatalf("unexpected error digesting empty tar: %v", err) | 		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)) | 	pushLayer(t, env.builder, imageName, emptyDigest, uploadURLBase, bytes.NewReader(emptyTar)) | ||||||
| 
 | 
 | ||||||
| 	// ------------------------------------------
 | 	// ------------------------------------------
 | ||||||
|  | @ -515,7 +515,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 	layerLength, _ := layerFile.Seek(0, os.SEEK_END) | 	layerLength, _ := layerFile.Seek(0, os.SEEK_END) | ||||||
| 	layerFile.Seek(0, os.SEEK_SET) | 	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) | 	pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) | ||||||
| 
 | 
 | ||||||
| 	// ------------------------------------------
 | 	// ------------------------------------------
 | ||||||
|  | @ -529,7 +529,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 	canonicalDigest := canonicalDigester.Digest() | 	canonicalDigest := canonicalDigester.Digest() | ||||||
| 
 | 
 | ||||||
| 	layerFile.Seek(0, 0) | 	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) | 	uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength) | ||||||
| 	finishUpload(t, env.builder, imageName, uploadURLBase, dgst) | 	finishUpload(t, env.builder, imageName, uploadURLBase, dgst) | ||||||
| 
 | 
 | ||||||
|  | @ -612,7 +612,7 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { | ||||||
| 		t.Fatalf("Error constructing request: %s", err) | 		t.Fatalf("Error constructing request: %s", err) | ||||||
| 	} | 	} | ||||||
| 	req.Header.Set("If-None-Match", "") | 	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) | 	checkResponse(t, "fetching layer with invalid etag", resp, http.StatusOK) | ||||||
| 
 | 
 | ||||||
| 	// Missing tests:
 | 	// Missing tests:
 | ||||||
|  | @ -1874,7 +1874,7 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { | ||||||
| 	manifest := args.manifest | 	manifest := args.manifest | ||||||
| 
 | 
 | ||||||
| 	ref, _ := reference.WithDigest(imageName, dgst) | 	ref, _ := reference.WithDigest(imageName, dgst) | ||||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(ref) | 	manifestDigestURL, _ := env.builder.BuildManifestURL(ref) | ||||||
| 	// ---------------
 | 	// ---------------
 | ||||||
| 	// Delete by digest
 | 	// Delete by digest
 | ||||||
| 	resp, err := httpDelete(manifestDigestURL) | 	resp, err := httpDelete(manifestDigestURL) | ||||||
|  | @ -1935,7 +1935,7 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { | ||||||
| 	// Upload manifest by tag
 | 	// Upload manifest by tag
 | ||||||
| 	tag := "atag" | 	tag := "atag" | ||||||
| 	tagRef, _ := reference.WithTag(imageName, tag) | 	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) | 	resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest) | ||||||
| 	checkResponse(t, "putting manifest by tag", resp, http.StatusCreated) | 	checkResponse(t, "putting manifest by tag", resp, http.StatusCreated) | ||||||
| 	checkHeaders(t, resp, http.Header{ | 	checkHeaders(t, resp, http.Header{ | ||||||
|  | @ -2502,7 +2502,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) { | ||||||
| 	checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | 	checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | ||||||
| 
 | 
 | ||||||
| 	// Manifest Delete
 | 	// Manifest Delete
 | ||||||
| 	resp, err = httpDelete(manifestURL) | 	resp, _ = httpDelete(manifestURL) | ||||||
| 	checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | 	checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | ||||||
| 
 | 
 | ||||||
| 	// Blob upload initialization
 | 	// Blob upload initialization
 | ||||||
|  | @ -2521,8 +2521,8 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	// Blob Delete
 | 	// Blob Delete
 | ||||||
| 	ref, _ := reference.WithDigest(imageName, digestSha256EmptyTar) | 	ref, _ := reference.WithDigest(imageName, digestSha256EmptyTar) | ||||||
| 	blobURL, err := env.builder.BuildBlobURL(ref) | 	blobURL, _ := env.builder.BuildBlobURL(ref) | ||||||
| 	resp, err = httpDelete(blobURL) | 	resp, _ = httpDelete(blobURL) | ||||||
| 	checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | 	checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -2601,9 +2601,9 @@ func TestProxyManifestGetByTag(t *testing.T) { | ||||||
| 	checkErr(t, err, "building manifest url") | 	checkErr(t, err, "building manifest url") | ||||||
| 
 | 
 | ||||||
| 	resp, err = http.Get(manifestTagURL) | 	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() | 	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{ | 	checkHeaders(t, resp, http.Header{ | ||||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | 		"Docker-Content-Digest": []string{dgst.String()}, | ||||||
| 	}) | 	}) | ||||||
|  | @ -2616,9 +2616,9 @@ func TestProxyManifestGetByTag(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	// fetch it with the same proxy URL as before.  Ensure the updated content is at the same tag
 | 	// fetch it with the same proxy URL as before.  Ensure the updated content is at the same tag
 | ||||||
| 	resp, err = http.Get(manifestTagURL) | 	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() | 	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{ | 	checkHeaders(t, resp, http.Header{ | ||||||
| 		"Docker-Content-Digest": []string{newDigest.String()}, | 		"Docker-Content-Digest": []string{newDigest.String()}, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import ( | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	dcontext "github.com/docker/distribution/context" | 	dcontext "github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/manifest/manifestlist" | 	"github.com/docker/distribution/manifest/manifestlist" | ||||||
|  | 	"github.com/docker/distribution/manifest/ocischema" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" | 	"github.com/docker/distribution/manifest/schema1" | ||||||
| 	"github.com/docker/distribution/manifest/schema2" | 	"github.com/docker/distribution/manifest/schema2" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/docker/distribution/reference" | ||||||
|  | @ -72,43 +73,10 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 		imh.Errors = append(imh.Errors, err) | 		imh.Errors = append(imh.Errors, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	var manifest distribution.Manifest |  | ||||||
| 	if imh.Tag != "" { |  | ||||||
| 		tags := imh.Repository.Tags(imh) |  | ||||||
| 		desc, err := tags.Get(imh, imh.Tag) |  | ||||||
| 		if err != nil { |  | ||||||
| 			if _, ok := err.(distribution.ErrTagUnknown); ok { |  | ||||||
| 				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) |  | ||||||
| 			} else { |  | ||||||
| 				imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.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 { |  | ||||||
| 		if _, ok := err.(distribution.ErrManifestUnknownRevision); ok { |  | ||||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) |  | ||||||
| 		} else { |  | ||||||
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |  | ||||||
| 		} |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	supportsSchema2 := false | 	supportsSchema2 := false | ||||||
| 	supportsManifestList := false | 	supportsManifestList := false | ||||||
|  | 	supportsOCISchema := false | ||||||
|  | 	supportsOCIManifestList := 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
 | 	// 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
 | 	// https://github.com/golang/gddo/blob/e91d4165076d7474d20abda83f92d15c7ebc3e81/httputil/header/header.go#L165-L202
 | ||||||
| 	for _, acceptHeader := range r.Header["Accept"] { | 	for _, acceptHeader := range r.Header["Accept"] { | ||||||
|  | @ -132,16 +100,259 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 			if mediaType == manifestlist.MediaTypeManifestList { | 			if mediaType == manifestlist.MediaTypeManifestList { | ||||||
| 				supportsManifestList = true | 				supportsManifestList = true | ||||||
| 			} | 			} | ||||||
|  | 			if mediaType == ocischema.MediaTypeManifest { | ||||||
|  | 				supportsOCISchema = true | ||||||
| 			} | 			} | ||||||
|  | 			if mediaType == manifestlist.MediaTypeOCIManifestList { | ||||||
|  | 				supportsOCIManifestList = true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	supportsOCI := supportsOCISchema || supportsOCIManifestList | ||||||
|  | 
 | ||||||
|  | 	var manifest distribution.Manifest | ||||||
|  | 	if imh.Tag != "" { | ||||||
|  | 		tags := imh.Repository.Tags(imh) | ||||||
|  | 		var desc distribution.Descriptor | ||||||
|  | 		if !supportsOCI { | ||||||
|  | 			desc, err = tags.Get(imh, imh.Tag) | ||||||
|  | 		} else { | ||||||
|  | 			desc, err = tags.Get(imh, imh.annotatedTag(false)) | ||||||
|  | 		} | ||||||
|  | 		if err != nil { | ||||||
|  | 			if _, ok := err.(distribution.ErrTagUnknown); ok { | ||||||
|  | 				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
|  | 			} else { | ||||||
|  | 				imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.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.annotatedTag(supportsOCI))) | ||||||
|  | 	} | ||||||
|  | 	manifest, err = manifests.Get(imh, imh.Digest, options...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if _, ok := err.(distribution.ErrManifestUnknownRevision); ok { | ||||||
|  | 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
|  | 		} else { | ||||||
|  | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) | 	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) | ||||||
| 	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) | 	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) | ||||||
|  | 	isAnOCIManifest := isSchema2 && (schema2Manifest.MediaType == ocischema.MediaTypeManifest) | ||||||
|  | 	isAnOCIManifestList := isManifestList && (manifestList.MediaType == manifestlist.MediaTypeOCIManifestList) | ||||||
|  | 
 | ||||||
|  | 	if (isSchema2 && !isAnOCIManifest) && (supportsOCISchema && !supportsSchema2) { | ||||||
|  | 		fmt.Printf("\n\nmanifest is schema2, but accept header only supports OCISchema \n\n") | ||||||
|  | 		w.WriteHeader(http.StatusNotFound) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if (isManifestList && !isAnOCIManifestList) && (supportsOCIManifestList && !supportsManifestList) { | ||||||
|  | 		fmt.Printf("\n\nmanifestlist is not OCI, but accept header only supports an OCI manifestlist\n\n") | ||||||
|  | 		w.WriteHeader(http.StatusNotFound) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if isAnOCIManifest && (!supportsOCISchema && supportsSchema2) { | ||||||
|  | 		fmt.Printf("\n\nmanifest is OCI, but accept header only supports schema2\n\n") | ||||||
|  | 		w.WriteHeader(http.StatusNotFound) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if isAnOCIManifestList && (!supportsOCIManifestList && supportsManifestList) { | ||||||
|  | 		fmt.Printf("\n\nmanifestlist is OCI, but accept header only supports non-OCI manifestlists\n\n") | ||||||
|  | 		w.WriteHeader(http.StatusNotFound) | ||||||
|  | 		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 || supportsOCISchema) { | ||||||
|  | 		// Rewrite manifest in schema1 format
 | ||||||
|  | 		ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String()) | ||||||
|  | 
 | ||||||
|  | 		manifest, err = imh.convertSchema2Manifest(schema2Manifest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} else if imh.Tag != "" && isManifestList && !(supportsManifestList || supportsOCIManifestList) { | ||||||
|  | 		// Rewrite manifest in schema1 format
 | ||||||
|  | 		ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String()) | ||||||
|  | 
 | ||||||
|  | 		// Find the image manifest corresponding to the default
 | ||||||
|  | 		// platform
 | ||||||
|  | 		var manifestDigest digest.Digest | ||||||
|  | 		for _, manifestDescriptor := range manifestList.Manifests { | ||||||
|  | 			if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS { | ||||||
|  | 				manifestDigest = manifestDescriptor.Digest | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if manifestDigest == "" { | ||||||
|  | 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		manifest, err = manifests.Get(imh, manifestDigest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if _, ok := err.(distribution.ErrManifestUnknownRevision); ok { | ||||||
|  | 				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
|  | 			} else { | ||||||
|  | 				imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// If necessary, convert the image manifest
 | ||||||
|  | 		if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !(supportsSchema2 || supportsOCISchema) { | ||||||
|  | 			manifest, err = imh.convertSchema2Manifest(schema2Manifest) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			imh.Digest = manifestDigest | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ct, p, err := manifest.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	w.Header().Set("Content-Type", ct) | ||||||
|  | 	w.Header().Set("Content-Length", fmt.Sprint(len(p))) | ||||||
|  | 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | ||||||
|  | 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) | ||||||
|  | 	w.Write(p) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetImageManifest fetches the image manifest from the storage backend, if it exists.
 | ||||||
|  | func (imh *manifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	fmt.Printf("\n\nGetting a manifest!\n\n\n") | ||||||
|  | 	supportsSchema2 := false | ||||||
|  | 	supportsManifestList := false | ||||||
|  | 	supportsOCISchema := false | ||||||
|  | 	supportsOCIManifestList := 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"] { | ||||||
|  | 		// r.Header[...] is a slice in case the request contains the same header more than once
 | ||||||
|  | 		// if the header isn't set, we'll get the zero value, which "range" will handle gracefully
 | ||||||
|  | 
 | ||||||
|  | 		// we need to split each header value on "," to get the full list of "Accept" values (per RFC 2616)
 | ||||||
|  | 		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
 | ||||||
|  | 		for _, mediaType := range strings.Split(acceptHeader, ",") { | ||||||
|  | 			// remove "; q=..." if present
 | ||||||
|  | 			if i := strings.Index(mediaType, ";"); i >= 0 { | ||||||
|  | 				mediaType = mediaType[:i] | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// it's common (but not required) for Accept values to be space separated ("a/b, c/d, e/f")
 | ||||||
|  | 			mediaType = strings.TrimSpace(mediaType) | ||||||
|  | 
 | ||||||
|  | 			if mediaType == schema2.MediaTypeManifest { | ||||||
|  | 				supportsSchema2 = true | ||||||
|  | 			} | ||||||
|  | 			if mediaType == manifestlist.MediaTypeManifestList { | ||||||
|  | 				supportsManifestList = true | ||||||
|  | 			} | ||||||
|  | 			if mediaType == ocischema.MediaTypeManifest { | ||||||
|  | 				supportsOCISchema = true | ||||||
|  | 			} | ||||||
|  | 			if mediaType == manifestlist.MediaTypeOCIManifestList { | ||||||
|  | 				supportsOCIManifestList = true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	supportsOCI := supportsOCISchema || supportsOCIManifestList | ||||||
|  | 
 | ||||||
|  | 	ctxu.GetLogger(imh).Debug("GetImageManifest") | ||||||
|  | 	manifests, err := imh.Repository.Manifests(imh) | ||||||
|  | 	if err != nil { | ||||||
|  | 		imh.Errors = append(imh.Errors, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var manifest distribution.Manifest | ||||||
|  | 	if imh.Tag != "" { | ||||||
|  | 		tags := imh.Repository.Tags(imh) | ||||||
|  | 		var desc distribution.Descriptor | ||||||
|  | 		if !supportsOCI { | ||||||
|  | 			desc, err = tags.Get(imh, imh.Tag) | ||||||
|  | 			if err != nil { | ||||||
|  | 				imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			desc, err = tags.Get(imh, imh.annotatedTag(supportsOCI)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				desc, err = tags.Get(imh, imh.annotatedTag(false)) | ||||||
|  | 				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.annotatedTag(supportsOCI))) | ||||||
|  | 	} | ||||||
|  | 	manifest, err = manifests.Get(imh, imh.Digest, options...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) | ||||||
|  | 	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) | ||||||
|  | 	isAnOCIManifest := isSchema2 && (schema2Manifest.MediaType == ocischema.MediaTypeManifest) | ||||||
|  | 	isAnOCIManifestList := isManifestList && (manifestList.MediaType == manifestlist.MediaTypeOCIManifestList) | ||||||
|  | 
 | ||||||
|  | 	badCombinations := [][]bool{ | ||||||
|  | 		{isSchema2 && !isAnOCIManifest, supportsOCISchema && !supportsSchema2}, | ||||||
|  | 		{isManifestList && !isAnOCIManifestList, supportsOCIManifestList && !supportsManifestList}, | ||||||
|  | 		{isAnOCIManifest, !supportsOCISchema && supportsSchema2}, | ||||||
|  | 		{isAnOCIManifestList, !supportsOCIManifestList && supportsManifestList}, | ||||||
|  | 	} | ||||||
|  | 	for i, combo := range badCombinations { | ||||||
|  | 		if combo[0] && combo[1] { | ||||||
|  | 			fmt.Printf("\n\nbad combo! %d\n\n\n", i) | ||||||
|  | 			w.WriteHeader(http.StatusNotFound) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if isAnOCIManifest { | ||||||
|  | 		fmt.Print("\n\nreturning OCI manifest\n\n") | ||||||
|  | 	} else if isSchema2 { | ||||||
|  | 		fmt.Print("\n\nreturning schema 2 manifest\n\n") | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Print("\n\nreturning schema 1 manifest\n\n") | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// Only rewrite schema2 manifests when they are being fetched by tag.
 | 	// Only rewrite schema2 manifests when they are being fetched by tag.
 | ||||||
| 	// If they are being fetched by digest, we can't return something not
 | 	// If they are being fetched by digest, we can't return something not
 | ||||||
| 	// matching the digest.
 | 	// matching the digest.
 | ||||||
| 	if imh.Tag != "" && isSchema2 && !supportsSchema2 { | 	if imh.Tag != "" && isSchema2 && !(supportsSchema2 || supportsOCISchema) { | ||||||
| 		// Rewrite manifest in schema1 format
 | 		// Rewrite manifest in schema1 format
 | ||||||
| 		dcontext.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String()) | 		dcontext.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String()) | ||||||
| 
 | 
 | ||||||
|  | @ -149,7 +360,7 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} else if imh.Tag != "" && isManifestList && !supportsManifestList { | 	} else if imh.Tag != "" && isManifestList && !(supportsManifestList || supportsOCIManifestList) { | ||||||
| 		// Rewrite manifest in schema1 format
 | 		// Rewrite manifest in schema1 format
 | ||||||
| 		dcontext.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String()) | 		dcontext.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String()) | ||||||
| 
 | 
 | ||||||
|  | @ -199,6 +410,8 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | ||||||
| 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) | 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) | ||||||
| 	w.Write(p) | 	w.Write(p) | ||||||
|  | 
 | ||||||
|  | 	fmt.Printf("\n\nSucceeded in getting the manifest!\n\n\n") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (imh *manifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { | func (imh *manifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { | ||||||
|  | @ -286,9 +499,17 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	isAnOCIManifest := mediaType == ocischema.MediaTypeManifest || mediaType == manifestlist.MediaTypeOCIManifestList | ||||||
|  | 
 | ||||||
|  | 	if isAnOCIManifest { | ||||||
|  | 		fmt.Printf("\n\nPutting an OCI Manifest!\n\n\n") | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Printf("\n\nPutting a Docker Manifest!\n\n\n") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	var options []distribution.ManifestServiceOption | 	var options []distribution.ManifestServiceOption | ||||||
| 	if imh.Tag != "" { | 	if imh.Tag != "" { | ||||||
| 		options = append(options, distribution.WithTag(imh.Tag)) | 		options = append(options, distribution.WithTag(imh.annotatedTag(isAnOCIManifest))) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := imh.applyResourcePolicy(manifest); err != nil { | 	if err := imh.applyResourcePolicy(manifest); err != nil { | ||||||
|  | @ -301,10 +522,12 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 		// TODO(stevvooe): These error handling switches really need to be
 | 		// TODO(stevvooe): These error handling switches really need to be
 | ||||||
| 		// handled by an app global mapper.
 | 		// handled by an app global mapper.
 | ||||||
| 		if err == distribution.ErrUnsupported { | 		if err == distribution.ErrUnsupported { | ||||||
|  | 			fmt.Printf("\n\nXXX 1\n\n\n") | ||||||
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		if err == distribution.ErrAccessDenied { | 		if err == distribution.ErrAccessDenied { | ||||||
|  | 			fmt.Printf("\n\nXXX 2\n\n\n") | ||||||
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied) | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | @ -331,15 +554,16 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 		default: | 		default: | ||||||
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
| 		} | 		} | ||||||
| 
 | 		fmt.Printf("\n\nXXX 3\n\n\n") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Tag this manifest
 | 	// Tag this manifest
 | ||||||
| 	if imh.Tag != "" { | 	if imh.Tag != "" { | ||||||
| 		tags := imh.Repository.Tags(imh) | 		tags := imh.Repository.Tags(imh) | ||||||
| 		err = tags.Tag(imh, imh.Tag, desc) | 		err = tags.Tag(imh, imh.annotatedTag(isAnOCIManifest), desc) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			fmt.Printf("\n\nXXX 4: %T: %v\n\n\n", err, err) | ||||||
| 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | @ -349,6 +573,7 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 	// Construct a canonical url for the uploaded manifest.
 | 	// Construct a canonical url for the uploaded manifest.
 | ||||||
| 	ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest) | 	ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | 		fmt.Printf("\n\nXXX 5\n\n\n") | ||||||
| 		imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | 		imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | @ -364,6 +589,8 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) | ||||||
| 	w.Header().Set("Location", location) | 	w.Header().Set("Location", location) | ||||||
| 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | ||||||
| 	w.WriteHeader(http.StatusCreated) | 	w.WriteHeader(http.StatusCreated) | ||||||
|  | 
 | ||||||
|  | 	fmt.Printf("\n\nSucceeded in putting manifest!\n\n\n") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // applyResourcePolicy checks whether the resource class matches what has
 | // applyResourcePolicy checks whether the resource class matches what has
 | ||||||
|  | @ -478,3 +705,12 @@ func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Reques | ||||||
| 
 | 
 | ||||||
| 	w.WriteHeader(http.StatusAccepted) | 	w.WriteHeader(http.StatusAccepted) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // annotatedTag will annotate OCI tags by prepending a string, and leave docker
 | ||||||
|  | // tags unmodified.
 | ||||||
|  | func (imh *manifestHandler) annotatedTag(oci bool) string { | ||||||
|  | 	if oci { | ||||||
|  | 		return "oci." + imh.Tag | ||||||
|  | 	} | ||||||
|  | 	return imh.Tag | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -116,7 +116,7 @@ type FileWriter interface { | ||||||
| // number of path components separated by slashes, where each component is
 | // number of path components separated by slashes, where each component is
 | ||||||
| // restricted to alphanumeric characters or a period, underscore, or
 | // restricted to alphanumeric characters or a period, underscore, or
 | ||||||
| // hyphen.
 | // hyphen.
 | ||||||
| var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._-]+)+$`) | var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._:-]+)+$`) | ||||||
| 
 | 
 | ||||||
| // ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method.
 | // ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method.
 | ||||||
| type ErrUnsupportedMethod struct { | type ErrUnsupportedMethod struct { | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import ( | ||||||
| 	dcontext "github.com/docker/distribution/context" | 	dcontext "github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/manifest" | 	"github.com/docker/distribution/manifest" | ||||||
| 	"github.com/docker/distribution/manifest/manifestlist" | 	"github.com/docker/distribution/manifest/manifestlist" | ||||||
|  | 	"github.com/docker/distribution/manifest/ocischema" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" | 	"github.com/docker/distribution/manifest/schema1" | ||||||
| 	"github.com/docker/distribution/manifest/schema2" | 	"github.com/docker/distribution/manifest/schema2" | ||||||
| 	"github.com/opencontainers/go-digest" | 	"github.com/opencontainers/go-digest" | ||||||
|  | @ -48,6 +49,7 @@ type manifestStore struct { | ||||||
| 
 | 
 | ||||||
| 	schema1Handler      ManifestHandler | 	schema1Handler      ManifestHandler | ||||||
| 	schema2Handler      ManifestHandler | 	schema2Handler      ManifestHandler | ||||||
|  | 	ocischemaHandler    ManifestHandler | ||||||
| 	manifestListHandler ManifestHandler | 	manifestListHandler ManifestHandler | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -99,7 +101,9 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options .. | ||||||
| 		switch versioned.MediaType { | 		switch versioned.MediaType { | ||||||
| 		case schema2.MediaTypeManifest: | 		case schema2.MediaTypeManifest: | ||||||
| 			return ms.schema2Handler.Unmarshal(ctx, dgst, content) | 			return ms.schema2Handler.Unmarshal(ctx, dgst, content) | ||||||
| 		case manifestlist.MediaTypeManifestList: | 		case ocischema.MediaTypeManifest: | ||||||
|  | 			return ms.ocischemaHandler.Unmarshal(ctx, dgst, content) | ||||||
|  | 		case manifestlist.MediaTypeManifestList, manifestlist.MediaTypeOCIManifestList: | ||||||
| 			return ms.manifestListHandler.Unmarshal(ctx, dgst, content) | 			return ms.manifestListHandler.Unmarshal(ctx, dgst, content) | ||||||
| 		default: | 		default: | ||||||
| 			return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)} | 			return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue