Merge pull request #1281 from aaronlehmann/new-manifest
Implement schema2 manifest formats
This commit is contained in:
		
						commit
						a7ae88da45
					
				
					 22 changed files with 2513 additions and 231 deletions
				
			
		
							
								
								
									
										9
									
								
								blobs.go
									
										
									
									
									
								
							
							
						
						
									
										9
									
								
								blobs.go
									
										
									
									
									
								
							|  | @ -61,6 +61,15 @@ type Descriptor struct { | |||
| 	// depend on the simplicity of this type. | ||||
| } | ||||
| 
 | ||||
| // Descriptor returns the descriptor, to make it satisfy the Describable | ||||
| // interface. Note that implementations of Describable are generally objects | ||||
| // which can be described, not simply descriptors; this exception is in place | ||||
| // to make it more convenient to pass actual descriptors to functions that | ||||
| // expect Describable objects. | ||||
| func (d Descriptor) Descriptor() Descriptor { | ||||
| 	return d | ||||
| } | ||||
| 
 | ||||
| // BlobStatter makes blob descriptors available by digest. The service may | ||||
| // provide a descriptor of a different digest if the provided digest is not | ||||
| // canonical. | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ image manifest based on the Content-Type returned in the HTTP response. | |||
| 
 | ||||
|         - **`os`** *string* | ||||
| 
 | ||||
|             The architecture field specifies the operating system, for example | ||||
|             The os field specifies the operating system, for example | ||||
|             `linux` or `windows`. | ||||
| 
 | ||||
|         - **`variant`** *string* | ||||
|  |  | |||
							
								
								
									
										147
									
								
								manifest/manifestlist/manifestlist.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								manifest/manifestlist/manifestlist.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,147 @@ | |||
| package manifestlist | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| ) | ||||
| 
 | ||||
| // MediaTypeManifestList specifies the mediaType for manifest lists. | ||||
| const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" | ||||
| 
 | ||||
| // SchemaVersion provides a pre-initialized version structure for this | ||||
| // packages version of the manifest. | ||||
| var SchemaVersion = manifest.Versioned{ | ||||
| 	SchemaVersion: 2, | ||||
| 	MediaType:     MediaTypeManifestList, | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { | ||||
| 		m := new(DeserializedManifestList) | ||||
| 		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: MediaTypeManifestList}, err | ||||
| 	} | ||||
| 	err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) | ||||
| 	if err != nil { | ||||
| 		panic(fmt.Sprintf("Unable to register manifest: %s", err)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // PlatformSpec specifies a platform where a particular image manifest is | ||||
| // applicable. | ||||
| type PlatformSpec 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"` | ||||
| 
 | ||||
| 	// Variant is an optional field specifying a variant of the CPU, for | ||||
| 	// example `ppc64le` to specify a little-endian version of a PowerPC CPU. | ||||
| 	Variant string `json:"variant,omitempty"` | ||||
| 
 | ||||
| 	// Features is an optional field specifuing an array of strings, each | ||||
| 	// listing a required CPU feature (for example `sse4` or `aes`). | ||||
| 	Features []string `json:"features,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // A ManifestDescriptor references a platform-specific manifest. | ||||
| type ManifestDescriptor struct { | ||||
| 	distribution.Descriptor | ||||
| 
 | ||||
| 	// Platform specifies which platform the manifest pointed to by the | ||||
| 	// descriptor runs on. | ||||
| 	Platform PlatformSpec `json:"platform"` | ||||
| } | ||||
| 
 | ||||
| // ManifestList references manifests for various platforms. | ||||
| type ManifestList struct { | ||||
| 	manifest.Versioned | ||||
| 
 | ||||
| 	// Config references the image configuration as a blob. | ||||
| 	Manifests []ManifestDescriptor `json:"manifests"` | ||||
| } | ||||
| 
 | ||||
| // References returnes the distribution descriptors for the referenced image | ||||
| // manifests. | ||||
| func (m ManifestList) References() []distribution.Descriptor { | ||||
| 	dependencies := make([]distribution.Descriptor, len(m.Manifests)) | ||||
| 	for i := range m.Manifests { | ||||
| 		dependencies[i] = m.Manifests[i].Descriptor | ||||
| 	} | ||||
| 
 | ||||
| 	return dependencies | ||||
| } | ||||
| 
 | ||||
| // DeserializedManifestList wraps ManifestList with a copy of the original | ||||
| // JSON. | ||||
| type DeserializedManifestList struct { | ||||
| 	ManifestList | ||||
| 
 | ||||
| 	// canonical is the canonical byte representation of the Manifest. | ||||
| 	canonical []byte | ||||
| } | ||||
| 
 | ||||
| // FromDescriptors takes a slice of descriptors, and returns a | ||||
| // DeserializedManifestList which contains the resulting manifest list | ||||
| // and its JSON representation. | ||||
| func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { | ||||
| 	m := ManifestList{ | ||||
| 		Versioned: SchemaVersion, | ||||
| 	} | ||||
| 
 | ||||
| 	m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) | ||||
| 	copy(m.Manifests, descriptors) | ||||
| 
 | ||||
| 	deserialized := DeserializedManifestList{ | ||||
| 		ManifestList: m, | ||||
| 	} | ||||
| 
 | ||||
| 	var err error | ||||
| 	deserialized.canonical, err = json.MarshalIndent(&m, "", "   ") | ||||
| 	return &deserialized, err | ||||
| } | ||||
| 
 | ||||
| // UnmarshalJSON populates a new ManifestList struct from JSON data. | ||||
| func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error { | ||||
| 	m.canonical = make([]byte, len(b), len(b)) | ||||
| 	// store manifest list in canonical | ||||
| 	copy(m.canonical, b) | ||||
| 
 | ||||
| 	// Unmarshal canonical JSON into ManifestList object | ||||
| 	var manifestList ManifestList | ||||
| 	if err := json.Unmarshal(m.canonical, &manifestList); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	m.ManifestList = manifestList | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // MarshalJSON returns the contents of canonical. If canonical is empty, | ||||
| // marshals the inner contents. | ||||
| func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) { | ||||
| 	if len(m.canonical) > 0 { | ||||
| 		return m.canonical, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, errors.New("JSON representation not initialized in DeserializedManifestList") | ||||
| } | ||||
| 
 | ||||
| // Payload returns the raw content of the manifest list. The contents can be | ||||
| // used to calculate the content identifier. | ||||
| func (m DeserializedManifestList) Payload() (string, []byte, error) { | ||||
| 	return m.MediaType, m.canonical, nil | ||||
| } | ||||
							
								
								
									
										111
									
								
								manifest/manifestlist/manifestlist_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								manifest/manifestlist/manifestlist_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | |||
| package manifestlist | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| ) | ||||
| 
 | ||||
| var expectedManifestListSerialization = []byte(`{ | ||||
|    "schemaVersion": 2, | ||||
|    "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", | ||||
|    "manifests": [ | ||||
|       { | ||||
|          "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | ||||
|          "size": 985, | ||||
|          "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||
|          "platform": { | ||||
|             "architecture": "amd64", | ||||
|             "os": "linux", | ||||
|             "features": [ | ||||
|                "sse4" | ||||
|             ] | ||||
|          } | ||||
|       }, | ||||
|       { | ||||
|          "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | ||||
|          "size": 2392, | ||||
|          "digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608", | ||||
|          "platform": { | ||||
|             "architecture": "sun4m", | ||||
|             "os": "sunos" | ||||
|          } | ||||
|       } | ||||
|    ] | ||||
| }`) | ||||
| 
 | ||||
| func TestManifestList(t *testing.T) { | ||||
| 	manifestDescriptors := []ManifestDescriptor{ | ||||
| 		{ | ||||
| 			Descriptor: distribution.Descriptor{ | ||||
| 				Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||
| 				Size:      985, | ||||
| 				MediaType: "application/vnd.docker.distribution.manifest.v2+json", | ||||
| 			}, | ||||
| 			Platform: PlatformSpec{ | ||||
| 				Architecture: "amd64", | ||||
| 				OS:           "linux", | ||||
| 				Features:     []string{"sse4"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Descriptor: distribution.Descriptor{ | ||||
| 				Digest:    "sha256:6346340964309634683409684360934680934608934608934608934068934608", | ||||
| 				Size:      2392, | ||||
| 				MediaType: "application/vnd.docker.distribution.manifest.v2+json", | ||||
| 			}, | ||||
| 			Platform: PlatformSpec{ | ||||
| 				Architecture: "sun4m", | ||||
| 				OS:           "sunos", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	deserialized, err := FromDescriptors(manifestDescriptors) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error creating DeserializedManifestList: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	mediaType, canonical, err := deserialized.Payload() | ||||
| 
 | ||||
| 	if mediaType != MediaTypeManifestList { | ||||
| 		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(expectedManifestListSerialization, canonical) { | ||||
| 		t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedManifestListSerialization)) | ||||
| 	} | ||||
| 
 | ||||
| 	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]) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										278
									
								
								manifest/schema1/config_builder.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								manifest/schema1/config_builder.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,278 @@ | |||
| package schema1 | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/libtrust" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| ) | ||||
| 
 | ||||
| type diffID digest.Digest | ||||
| 
 | ||||
| // gzippedEmptyTar is a gzip-compressed version of an empty tar file | ||||
| // (1024 NULL bytes) | ||||
| var gzippedEmptyTar = []byte{ | ||||
| 	31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88, | ||||
| 	0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0, | ||||
| } | ||||
| 
 | ||||
| // digestSHA256GzippedEmptyTar is the canonical sha256 digest of | ||||
| // gzippedEmptyTar | ||||
| const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") | ||||
| 
 | ||||
| // configManifestBuilder is a type for constructing manifests from an image | ||||
| // configuration and generic descriptors. | ||||
| type configManifestBuilder struct { | ||||
| 	// bs is a BlobService used to create empty layer tars in the | ||||
| 	// blob store if necessary. | ||||
| 	bs distribution.BlobService | ||||
| 	// pk is the libtrust private key used to sign the final manifest. | ||||
| 	pk libtrust.PrivateKey | ||||
| 	// configJSON is configuration supplied when the ManifestBuilder was | ||||
| 	// created. | ||||
| 	configJSON []byte | ||||
| 	// name is the name provided to NewConfigManifestBuilder | ||||
| 	name string | ||||
| 	// tag is the tag provided to NewConfigManifestBuilder | ||||
| 	tag string | ||||
| 	// descriptors is the set of descriptors referencing the layers. | ||||
| 	descriptors []distribution.Descriptor | ||||
| 	// emptyTarDigest is set to a valid digest if an empty tar has been | ||||
| 	// put in the blob store; otherwise it is empty. | ||||
| 	emptyTarDigest digest.Digest | ||||
| } | ||||
| 
 | ||||
| // NewConfigManifestBuilder is used to build new manifests for the current | ||||
| // schema version from an image configuration and a set of descriptors. | ||||
| // It takes a BlobService so that it can add an empty tar to the blob store | ||||
| // if the resulting manifest needs empty layers. | ||||
| func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, name, tag string, configJSON []byte) distribution.ManifestBuilder { | ||||
| 	return &configManifestBuilder{ | ||||
| 		bs:         bs, | ||||
| 		pk:         pk, | ||||
| 		configJSON: configJSON, | ||||
| 		name:       name, | ||||
| 		tag:        tag, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Build produces a final manifest from the given references | ||||
| func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) { | ||||
| 	type imageRootFS struct { | ||||
| 		Type      string   `json:"type"` | ||||
| 		DiffIDs   []diffID `json:"diff_ids,omitempty"` | ||||
| 		BaseLayer string   `json:"base_layer,omitempty"` | ||||
| 	} | ||||
| 
 | ||||
| 	type imageHistory struct { | ||||
| 		Created    time.Time `json:"created"` | ||||
| 		Author     string    `json:"author,omitempty"` | ||||
| 		CreatedBy  string    `json:"created_by,omitempty"` | ||||
| 		Comment    string    `json:"comment,omitempty"` | ||||
| 		EmptyLayer bool      `json:"empty_layer,omitempty"` | ||||
| 	} | ||||
| 
 | ||||
| 	type imageConfig struct { | ||||
| 		RootFS       *imageRootFS   `json:"rootfs,omitempty"` | ||||
| 		History      []imageHistory `json:"history,omitempty"` | ||||
| 		Architecture string         `json:"architecture,omitempty"` | ||||
| 	} | ||||
| 
 | ||||
| 	var img imageConfig | ||||
| 
 | ||||
| 	if err := json.Unmarshal(mb.configJSON, &img); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if len(img.History) == 0 { | ||||
| 		return nil, errors.New("empty history when trying to create schema1 manifest") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(img.RootFS.DiffIDs) != len(mb.descriptors) { | ||||
| 		return nil, errors.New("number of descriptors and number of layers in rootfs must match") | ||||
| 	} | ||||
| 
 | ||||
| 	// Generate IDs for each layer | ||||
| 	// For non-top-level layers, create fake V1Compatibility strings that | ||||
| 	// fit the format and don't collide with anything else, but don't | ||||
| 	// result in runnable images on their own. | ||||
| 	type v1Compatibility struct { | ||||
| 		ID              string    `json:"id"` | ||||
| 		Parent          string    `json:"parent,omitempty"` | ||||
| 		Comment         string    `json:"comment,omitempty"` | ||||
| 		Created         time.Time `json:"created"` | ||||
| 		ContainerConfig struct { | ||||
| 			Cmd []string | ||||
| 		} `json:"container_config,omitempty"` | ||||
| 		ThrowAway bool `json:"throwaway,omitempty"` | ||||
| 	} | ||||
| 
 | ||||
| 	fsLayerList := make([]FSLayer, len(img.History)) | ||||
| 	history := make([]History, len(img.History)) | ||||
| 
 | ||||
| 	parent := "" | ||||
| 	layerCounter := 0 | ||||
| 	for i, h := range img.History[:len(img.History)-1] { | ||||
| 		var blobsum digest.Digest | ||||
| 		if h.EmptyLayer { | ||||
| 			if blobsum, err = mb.emptyTar(ctx); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} else { | ||||
| 			if len(img.RootFS.DiffIDs) <= layerCounter { | ||||
| 				return nil, errors.New("too many non-empty layers in History section") | ||||
| 			} | ||||
| 			blobsum = mb.descriptors[layerCounter].Digest | ||||
| 			layerCounter++ | ||||
| 		} | ||||
| 
 | ||||
| 		v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex() | ||||
| 
 | ||||
| 		if i == 0 && img.RootFS.BaseLayer != "" { | ||||
| 			// windows-only baselayer setup | ||||
| 			baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer)) | ||||
| 			parent = fmt.Sprintf("%x", baseID[:32]) | ||||
| 		} | ||||
| 
 | ||||
| 		v1Compatibility := v1Compatibility{ | ||||
| 			ID:      v1ID, | ||||
| 			Parent:  parent, | ||||
| 			Comment: h.Comment, | ||||
| 			Created: h.Created, | ||||
| 		} | ||||
| 		v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy} | ||||
| 		if h.EmptyLayer { | ||||
| 			v1Compatibility.ThrowAway = true | ||||
| 		} | ||||
| 		jsonBytes, err := json.Marshal(&v1Compatibility) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		reversedIndex := len(img.History) - i - 1 | ||||
| 		history[reversedIndex].V1Compatibility = string(jsonBytes) | ||||
| 		fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum} | ||||
| 
 | ||||
| 		parent = v1ID | ||||
| 	} | ||||
| 
 | ||||
| 	latestHistory := img.History[len(img.History)-1] | ||||
| 
 | ||||
| 	var blobsum digest.Digest | ||||
| 	if latestHistory.EmptyLayer { | ||||
| 		if blobsum, err = mb.emptyTar(ctx); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		if len(img.RootFS.DiffIDs) <= layerCounter { | ||||
| 			return nil, errors.New("too many non-empty layers in History section") | ||||
| 		} | ||||
| 		blobsum = mb.descriptors[layerCounter].Digest | ||||
| 	} | ||||
| 
 | ||||
| 	fsLayerList[0] = FSLayer{BlobSum: blobsum} | ||||
| 	dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON))) | ||||
| 
 | ||||
| 	// Top-level v1compatibility string should be a modified version of the | ||||
| 	// image config. | ||||
| 	transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	history[0].V1Compatibility = string(transformedConfig) | ||||
| 
 | ||||
| 	mfst := Manifest{ | ||||
| 		Versioned: manifest.Versioned{ | ||||
| 			SchemaVersion: 1, | ||||
| 		}, | ||||
| 		Name:         mb.name, | ||||
| 		Tag:          mb.tag, | ||||
| 		Architecture: img.Architecture, | ||||
| 		FSLayers:     fsLayerList, | ||||
| 		History:      history, | ||||
| 	} | ||||
| 
 | ||||
| 	return Sign(&mfst, mb.pk) | ||||
| } | ||||
| 
 | ||||
| // emptyTar pushes a compressed empty tar to the blob store if one doesn't | ||||
| // already exist, and returns its blobsum. | ||||
| func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) { | ||||
| 	if mb.emptyTarDigest != "" { | ||||
| 		// Already put an empty tar | ||||
| 		return mb.emptyTarDigest, nil | ||||
| 	} | ||||
| 
 | ||||
| 	descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar) | ||||
| 	switch err { | ||||
| 	case nil: | ||||
| 		mb.emptyTarDigest = descriptor.Digest | ||||
| 		return descriptor.Digest, nil | ||||
| 	case distribution.ErrBlobUnknown: | ||||
| 		// nop | ||||
| 	default: | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	// Add gzipped empty tar to the blob store | ||||
| 	descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	mb.emptyTarDigest = descriptor.Digest | ||||
| 
 | ||||
| 	return descriptor.Digest, nil | ||||
| } | ||||
| 
 | ||||
| // AppendReference adds a reference to the current ManifestBuilder | ||||
| func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error { | ||||
| 	// todo: verification here? | ||||
| 	mb.descriptors = append(mb.descriptors, d.Descriptor()) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // References returns the current references added to this builder | ||||
| func (mb *configManifestBuilder) References() []distribution.Descriptor { | ||||
| 	return mb.descriptors | ||||
| } | ||||
| 
 | ||||
| // MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON | ||||
| func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) { | ||||
| 	// Top-level v1compatibility string should be a modified version of the | ||||
| 	// image config. | ||||
| 	var configAsMap map[string]*json.RawMessage | ||||
| 	if err := json.Unmarshal(configJSON, &configAsMap); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Delete fields that didn't exist in old manifest | ||||
| 	delete(configAsMap, "rootfs") | ||||
| 	delete(configAsMap, "history") | ||||
| 	configAsMap["id"] = rawJSON(v1ID) | ||||
| 	if parentV1ID != "" { | ||||
| 		configAsMap["parent"] = rawJSON(parentV1ID) | ||||
| 	} | ||||
| 	if throwaway { | ||||
| 		configAsMap["throwaway"] = rawJSON(true) | ||||
| 	} | ||||
| 
 | ||||
| 	return json.Marshal(configAsMap) | ||||
| } | ||||
| 
 | ||||
| func rawJSON(value interface{}) *json.RawMessage { | ||||
| 	jsonval, err := json.Marshal(value) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return (*json.RawMessage)(&jsonval) | ||||
| } | ||||
							
								
								
									
										264
									
								
								manifest/schema1/config_builder_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								manifest/schema1/config_builder_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,264 @@ | |||
| package schema1 | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"compress/gzip" | ||||
| 	"io" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/libtrust" | ||||
| ) | ||||
| 
 | ||||
| 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: mediaType, | ||||
| 	} | ||||
| 	bs.descriptors[d.Digest] = d | ||||
| 	return d, nil | ||||
| } | ||||
| 
 | ||||
| func (bs *mockBlobService) Create(ctx context.Context) (distribution.BlobWriter, error) { | ||||
| 	panic("not implemented") | ||||
| } | ||||
| 
 | ||||
| func (bs *mockBlobService) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { | ||||
| 	panic("not implemented") | ||||
| } | ||||
| 
 | ||||
| func TestEmptyTar(t *testing.T) { | ||||
| 	// Confirm that gzippedEmptyTar expands to 1024 NULL bytes. | ||||
| 	var decompressed [2048]byte | ||||
| 	gzipReader, err := gzip.NewReader(bytes.NewReader(gzippedEmptyTar)) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("NewReader returned error: %v", err) | ||||
| 	} | ||||
| 	n, err := gzipReader.Read(decompressed[:]) | ||||
| 	if n != 1024 { | ||||
| 		t.Fatalf("read returned %d bytes; expected 1024", n) | ||||
| 	} | ||||
| 	n, err = gzipReader.Read(decompressed[1024:]) | ||||
| 	if n != 0 { | ||||
| 		t.Fatalf("read returned %d bytes; expected 0", n) | ||||
| 	} | ||||
| 	if err != io.EOF { | ||||
| 		t.Fatal("read did not return io.EOF") | ||||
| 	} | ||||
| 	gzipReader.Close() | ||||
| 	for _, b := range decompressed[:1024] { | ||||
| 		if b != 0 { | ||||
| 			t.Fatal("nonzero byte in decompressed tar") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Confirm that digestSHA256EmptyTar is the digest of gzippedEmptyTar. | ||||
| 	dgst := digest.FromBytes(gzippedEmptyTar) | ||||
| 	if dgst != digestSHA256GzippedEmptyTar { | ||||
| 		t.Fatalf("digest mismatch for empty tar: expected %s got %s", digestSHA256GzippedEmptyTar, dgst) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestConfigBuilder(t *testing.T) { | ||||
| 	imgJSON := `{ | ||||
|     "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" | ||||
|     } | ||||
| }` | ||||
| 
 | ||||
| 	descriptors := []distribution.Descriptor{ | ||||
| 		{Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 		{Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, | ||||
| 		{Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 	} | ||||
| 
 | ||||
| 	pk, err := libtrust.GenerateECP256PrivateKey() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not generate key for testing: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} | ||||
| 	builder := NewConfigManifestBuilder(bs, pk, "testrepo", "testtag", []byte(imgJSON)) | ||||
| 
 | ||||
| 	for _, d := range descriptors { | ||||
| 		if err := builder.AppendReference(d); err != nil { | ||||
| 			t.Fatalf("AppendReference returned error: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	signed, err := builder.Build(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Build returned error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check that the gzipped empty layer tar was put in the blob store | ||||
| 	_, err = bs.Stat(context.Background(), digestSHA256GzippedEmptyTar) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("gzipped empty tar was not put in the blob store") | ||||
| 	} | ||||
| 
 | ||||
| 	manifest := signed.(*SignedManifest).Manifest | ||||
| 
 | ||||
| 	if manifest.Versioned.SchemaVersion != 1 { | ||||
| 		t.Fatal("SchemaVersion != 1") | ||||
| 	} | ||||
| 	if manifest.Name != "testrepo" { | ||||
| 		t.Fatal("incorrect name in manifest") | ||||
| 	} | ||||
| 	if manifest.Tag != "testtag" { | ||||
| 		t.Fatal("incorrect tag in manifest") | ||||
| 	} | ||||
| 	if manifest.Architecture != "amd64" { | ||||
| 		t.Fatal("incorrect arch in manifest") | ||||
| 	} | ||||
| 
 | ||||
| 	expectedFSLayers := []FSLayer{ | ||||
| 		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 		{BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 		{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, | ||||
| 		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, | ||||
| 	} | ||||
| 
 | ||||
| 	if len(manifest.FSLayers) != len(expectedFSLayers) { | ||||
| 		t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers)) | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) { | ||||
| 		t.Fatal("wrong FSLayers list") | ||||
| 	} | ||||
| 
 | ||||
| 	expectedV1Compatibility := []string{ | ||||
| 		`{"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","id":"0850bfdeb7b060b1004a09099846c2f023a3f2ecbf33f56b4774384b00ce0323","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`, | ||||
| 		`{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]}}`, | ||||
| 		`{"id":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","parent":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`, | ||||
| 		`{"id":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`, | ||||
| 		`{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`, | ||||
| 		`{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`, | ||||
| 	} | ||||
| 
 | ||||
| 	if len(manifest.History) != len(expectedV1Compatibility) { | ||||
| 		t.Fatalf("wrong number of history entries: %d", len(manifest.History)) | ||||
| 	} | ||||
| 	for i := range expectedV1Compatibility { | ||||
| 		if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] { | ||||
| 			t.Errorf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -5,21 +5,23 @@ import ( | |||
| 
 | ||||
| 	"errors" | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| 	"github.com/docker/libtrust" | ||||
| ) | ||||
| 
 | ||||
| // ManifestBuilder is a type for constructing manifests | ||||
| type manifestBuilder struct { | ||||
| // referenceManifestBuilder is a type for constructing manifests from schema1 | ||||
| // dependencies. | ||||
| type referenceManifestBuilder struct { | ||||
| 	Manifest | ||||
| 	pk libtrust.PrivateKey | ||||
| } | ||||
| 
 | ||||
| // NewManifestBuilder is used to build new manifests for the current schema | ||||
| // version. | ||||
| func NewManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) distribution.ManifestBuilder { | ||||
| 	return &manifestBuilder{ | ||||
| // NewReferenceManifestBuilder is used to build new manifests for the current | ||||
| // schema version using schema1 dependencies. | ||||
| func NewReferenceManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) distribution.ManifestBuilder { | ||||
| 	return &referenceManifestBuilder{ | ||||
| 		Manifest: Manifest{ | ||||
| 			Versioned: manifest.Versioned{ | ||||
| 				SchemaVersion: 1, | ||||
|  | @ -32,8 +34,7 @@ func NewManifestBuilder(pk libtrust.PrivateKey, name, tag, architecture string) | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Build produces a final manifest from the given references | ||||
| func (mb *manifestBuilder) Build() (distribution.Manifest, error) { | ||||
| func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) { | ||||
| 	m := mb.Manifest | ||||
| 	if len(m.FSLayers) == 0 { | ||||
| 		return nil, errors.New("cannot build manifest with zero layers or history") | ||||
|  | @ -47,8 +48,8 @@ func (mb *manifestBuilder) Build() (distribution.Manifest, error) { | |||
| 	return Sign(&m, mb.pk) | ||||
| } | ||||
| 
 | ||||
| // AppendReference adds a reference to the current manifestBuilder | ||||
| func (mb *manifestBuilder) AppendReference(d distribution.Describable) error { | ||||
| // AppendReference adds a reference to the current ManifestBuilder | ||||
| func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error { | ||||
| 	r, ok := d.(Reference) | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("Unable to add non-reference type to v1 builder") | ||||
|  | @ -62,7 +63,7 @@ func (mb *manifestBuilder) AppendReference(d distribution.Describable) error { | |||
| } | ||||
| 
 | ||||
| // References returns the current references added to this builder | ||||
| func (mb *manifestBuilder) References() []distribution.Descriptor { | ||||
| func (mb *referenceManifestBuilder) References() []distribution.Descriptor { | ||||
| 	refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) | ||||
| 	for i := range mb.Manifest.FSLayers { | ||||
| 		layerDigest := mb.Manifest.FSLayers[i].BlobSum | ||||
|  | @ -3,6 +3,7 @@ package schema1 | |||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| 	"github.com/docker/libtrust" | ||||
|  | @ -34,7 +35,7 @@ func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) | |||
| 	return signedManifest | ||||
| } | ||||
| 
 | ||||
| func TestBuilder(t *testing.T) { | ||||
| func TestReferenceBuilder(t *testing.T) { | ||||
| 	pk, err := libtrust.GenerateECP256PrivateKey() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error generating private key: %v", err) | ||||
|  | @ -53,8 +54,8 @@ func TestBuilder(t *testing.T) { | |||
| 
 | ||||
| 	handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2}) | ||||
| 
 | ||||
| 	b := NewManifestBuilder(pk, handCrafted.Manifest.Name, handCrafted.Manifest.Tag, handCrafted.Manifest.Architecture) | ||||
| 	_, err = b.Build() | ||||
| 	b := NewReferenceManifestBuilder(pk, handCrafted.Manifest.Name, handCrafted.Manifest.Tag, handCrafted.Manifest.Architecture) | ||||
| 	_, err = b.Build(context.Background()) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Expected error building zero length manifest") | ||||
| 	} | ||||
|  | @ -79,7 +80,7 @@ func TestBuilder(t *testing.T) { | |||
| 		t.Fatalf("Unexpected reference : %v", refs[0]) | ||||
| 	} | ||||
| 
 | ||||
| 	m, err := b.Build() | ||||
| 	m, err := b.Build(context.Background()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
							
								
								
									
										74
									
								
								manifest/schema2/builder.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								manifest/schema2/builder.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| package schema2 | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/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: | ||||
| 		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) | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										210
									
								
								manifest/schema2/builder_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								manifest/schema2/builder_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,210 @@ | |||
| package schema2 | ||||
| 
 | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/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: mediaType, | ||||
| 	} | ||||
| 	bs.descriptors[d.Digest] = d | ||||
| 	return d, nil | ||||
| } | ||||
| 
 | ||||
| func (bs *mockBlobService) Create(ctx context.Context) (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() | ||||
| 
 | ||||
| 	if !reflect.DeepEqual(references, descriptors) { | ||||
| 		t.Fatal("References() does not match the descriptors added") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										125
									
								
								manifest/schema2/manifest.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								manifest/schema2/manifest.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | |||
| package schema2 | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// MediaTypeManifest specifies the mediaType for the current version. | ||||
| 	MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" | ||||
| 
 | ||||
| 	// MediaTypeConfig specifies the mediaType for the image configuration. | ||||
| 	MediaTypeConfig = "application/vnd.docker.container.image.v1+json" | ||||
| 
 | ||||
| 	// MediaTypeLayer is the mediaType used for layers referenced by the | ||||
| 	// manifest. | ||||
| 	MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// SchemaVersion provides a pre-initialized version structure for this | ||||
| 	// packages version of the manifest. | ||||
| 	SchemaVersion = manifest.Versioned{ | ||||
| 		SchemaVersion: 2, | ||||
| 		MediaType:     MediaTypeManifest, | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	schema2Func := 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, schema2Func) | ||||
| 	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 { | ||||
| 	return m.Layers | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										105
									
								
								manifest/schema2/manifest_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								manifest/schema2/manifest_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| package schema2 | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| ) | ||||
| 
 | ||||
| var expectedManifestSerialization = []byte(`{ | ||||
|    "schemaVersion": 2, | ||||
|    "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | ||||
|    "config": { | ||||
|       "mediaType": "application/vnd.docker.container.image.v1+json", | ||||
|       "size": 985, | ||||
|       "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" | ||||
|    }, | ||||
|    "layers": [ | ||||
|       { | ||||
|          "mediaType": "application/vnd.docker.image.rootfs.diff.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) != 1 { | ||||
| 		t.Fatalf("unexpected number of references: %d", len(references)) | ||||
| 	} | ||||
| 	if references[0].Digest != "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" { | ||||
| 		t.Fatalf("unexpected digest in reference: %s", references[0].Digest.String()) | ||||
| 	} | ||||
| 	if references[0].MediaType != MediaTypeLayer { | ||||
| 		t.Fatalf("unexpected media type in reference: %s", references[0].MediaType) | ||||
| 	} | ||||
| 	if references[0].Size != 153263 { | ||||
| 		t.Fatalf("unexpected size in reference: %d", references[0].Size) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,9 +1,12 @@ | |||
| package manifest | ||||
| 
 | ||||
| // Versioned provides a struct with just the manifest schemaVersion. Incoming | ||||
| // Versioned provides a struct with the manifest schemaVersion and . 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"` | ||||
| 
 | ||||
| 	// MediaType is the media type of this schema. | ||||
| 	MediaType string `json:"mediaType,omitempty"` | ||||
| } | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ type Manifest interface { | |||
| // specific data is passed into the function which creates the builder. | ||||
| type ManifestBuilder interface { | ||||
| 	// Build creates the manifest from his builder. | ||||
| 	Build() (Manifest, error) | ||||
| 	Build(ctx context.Context) (Manifest, error) | ||||
| 
 | ||||
| 	// References returns a list of objects which have been added to this | ||||
| 	// builder. The dependencies are returned in the order they were added, | ||||
|  |  | |||
|  | @ -18,11 +18,14 @@ import ( | |||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/configuration" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| 	"github.com/docker/distribution/manifest/manifestlist" | ||||
| 	"github.com/docker/distribution/manifest/schema1" | ||||
| 	"github.com/docker/distribution/manifest/schema2" | ||||
| 	"github.com/docker/distribution/registry/api/errcode" | ||||
| 	"github.com/docker/distribution/registry/api/v2" | ||||
| 	_ "github.com/docker/distribution/registry/storage/driver/inmemory" | ||||
|  | @ -690,48 +693,42 @@ func httpDelete(url string) (*http.Response, error) { | |||
| } | ||||
| 
 | ||||
| type manifestArgs struct { | ||||
| 	imageName      string | ||||
| 	signedManifest *schema1.SignedManifest | ||||
| 	dgst           digest.Digest | ||||
| } | ||||
| 
 | ||||
| func makeManifestArgs(t *testing.T) manifestArgs { | ||||
| 	args := manifestArgs{ | ||||
| 		imageName: "foo/bar", | ||||
| 	} | ||||
| 
 | ||||
| 	return args | ||||
| 	imageName string | ||||
| 	mediaType string | ||||
| 	manifest  distribution.Manifest | ||||
| 	dgst      digest.Digest | ||||
| } | ||||
| 
 | ||||
| func TestManifestAPI(t *testing.T) { | ||||
| 	deleteEnabled := false | ||||
| 	env := newTestEnv(t, deleteEnabled) | ||||
| 	args := makeManifestArgs(t) | ||||
| 	testManifestAPI(t, env, args) | ||||
| 	testManifestAPISchema1(t, env, "foo/schema1") | ||||
| 	schema2Args := testManifestAPISchema2(t, env, "foo/schema2") | ||||
| 	testManifestAPIManifestList(t, env, schema2Args) | ||||
| 
 | ||||
| 	deleteEnabled = true | ||||
| 	env = newTestEnv(t, deleteEnabled) | ||||
| 	args = makeManifestArgs(t) | ||||
| 	testManifestAPI(t, env, args) | ||||
| 	testManifestAPISchema1(t, env, "foo/schema1") | ||||
| 	schema2Args = testManifestAPISchema2(t, env, "foo/schema2") | ||||
| 	testManifestAPIManifestList(t, env, schema2Args) | ||||
| } | ||||
| 
 | ||||
| func TestManifestDelete(t *testing.T) { | ||||
| 	deleteEnabled := true | ||||
| 	env := newTestEnv(t, deleteEnabled) | ||||
| 	args := makeManifestArgs(t) | ||||
| 	env, args = testManifestAPI(t, env, args) | ||||
| 	testManifestDelete(t, env, args) | ||||
| 	schema1Args := testManifestAPISchema1(t, env, "foo/schema1") | ||||
| 	testManifestDelete(t, env, schema1Args) | ||||
| 	schema2Args := testManifestAPISchema2(t, env, "foo/schema2") | ||||
| 	testManifestDelete(t, env, schema2Args) | ||||
| } | ||||
| 
 | ||||
| func TestManifestDeleteDisabled(t *testing.T) { | ||||
| 	deleteEnabled := false | ||||
| 	env := newTestEnv(t, deleteEnabled) | ||||
| 	args := makeManifestArgs(t) | ||||
| 	testManifestDeleteDisabled(t, env, args) | ||||
| 	testManifestDeleteDisabled(t, env, "foo/schema1") | ||||
| } | ||||
| 
 | ||||
| func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) *testEnv { | ||||
| 	imageName := args.imageName | ||||
| func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName string) { | ||||
| 	manifestURL, err := env.builder.BuildManifestURL(imageName, digest.DigestSha256EmptyTar) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting manifest url: %v", err) | ||||
|  | @ -744,12 +741,11 @@ func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) * | |||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, manifestArgs) { | ||||
| 	imageName := args.imageName | ||||
| func testManifestAPISchema1(t *testing.T, env *testEnv, imageName string) manifestArgs { | ||||
| 	tag := "thetag" | ||||
| 	args := manifestArgs{imageName: imageName} | ||||
| 
 | ||||
| 	manifestURL, err := env.builder.BuildManifestURL(imageName, tag) | ||||
| 	if err != nil { | ||||
|  | @ -808,10 +804,10 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest) | ||||
| 	resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest) | ||||
| 	defer resp.Body.Close() | ||||
| 	checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest) | ||||
| 	_, p, counts := checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestInvalid) | ||||
| 	_, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid) | ||||
| 
 | ||||
| 	expectedCounts := map[errcode.ErrorCode]int{ | ||||
| 		v2.ErrorCodeManifestInvalid: 1, | ||||
|  | @ -827,7 +823,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 		t.Fatalf("error signing manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting signed manifest with errors", manifestURL, sm) | ||||
| 	resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm) | ||||
| 	defer resp.Body.Close() | ||||
| 	checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest) | ||||
| 	_, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp, | ||||
|  | @ -872,13 +868,13 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 	} | ||||
| 
 | ||||
| 	dgst := digest.FromBytes(signedManifest.Canonical) | ||||
| 	args.signedManifest = signedManifest | ||||
| 	args.manifest = signedManifest | ||||
| 	args.dgst = dgst | ||||
| 
 | ||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||
| 	checkErr(t, err, "building manifest url") | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting signed manifest no error", manifestURL, signedManifest) | ||||
| 	resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest) | ||||
| 	checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
|  | @ -887,7 +883,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 
 | ||||
| 	// -------------------- | ||||
| 	// Push by digest -- should get same result | ||||
| 	resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest) | ||||
| 	resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) | ||||
| 	checkResponse(t, "putting signed manifest", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
|  | @ -958,7 +954,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, sm2) | ||||
| 	resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "", sm2) | ||||
| 	checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) | ||||
| 
 | ||||
| 	resp, err = http.Get(manifestDigestURL) | ||||
|  | @ -1020,8 +1016,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	// Check that we get an unknown repository error when asking for tags | ||||
| 	checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) | ||||
| 	checkResponse(t, "getting tags", resp, http.StatusOK) | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	var tagsResponse tagsAPIResponse | ||||
|  | @ -1052,16 +1047,581 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | |||
| 		t.Fatalf("error signing manifest") | ||||
| 	} | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, invalidSigned) | ||||
| 	resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned) | ||||
| 	checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest) | ||||
| 
 | ||||
| 	return env, args | ||||
| 	return args | ||||
| } | ||||
| 
 | ||||
| func testManifestAPISchema2(t *testing.T, env *testEnv, imageName string) manifestArgs { | ||||
| 	tag := "schema2tag" | ||||
| 	args := manifestArgs{ | ||||
| 		imageName: imageName, | ||||
| 		mediaType: schema2.MediaTypeManifest, | ||||
| 	} | ||||
| 
 | ||||
| 	manifestURL, err := env.builder.BuildManifestURL(imageName, tag) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting manifest url: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// ----------------------------- | ||||
| 	// Attempt to fetch the manifest | ||||
| 	resp, err := http.Get(manifestURL) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting manifest: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) | ||||
| 	checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) | ||||
| 
 | ||||
| 	tagsURL, err := env.builder.BuildTagsURL(imageName) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error building tags url: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err = http.Get(tagsURL) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting unknown tags: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	// Check that we get an unknown repository error when asking for tags | ||||
| 	checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) | ||||
| 	checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) | ||||
| 
 | ||||
| 	// -------------------------------- | ||||
| 	// Attempt to push manifest with missing config and missing layers | ||||
| 	manifest := &schema2.Manifest{ | ||||
| 		Versioned: manifest.Versioned{ | ||||
| 			SchemaVersion: 2, | ||||
| 			MediaType:     schema2.MediaTypeManifest, | ||||
| 		}, | ||||
| 		Config: distribution.Descriptor{ | ||||
| 			Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||
| 			Size:      3253, | ||||
| 			MediaType: schema2.MediaTypeConfig, | ||||
| 		}, | ||||
| 		Layers: []distribution.Descriptor{ | ||||
| 			{ | ||||
| 				Digest:    "sha256:463434349086340864309863409683460843608348608934092322395278926a", | ||||
| 				Size:      6323, | ||||
| 				MediaType: schema2.MediaTypeLayer, | ||||
| 			}, | ||||
| 			{ | ||||
| 				Digest:    "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa", | ||||
| 				Size:      6863, | ||||
| 				MediaType: schema2.MediaTypeLayer, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest) | ||||
| 	defer resp.Body.Close() | ||||
| 	checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest) | ||||
| 	_, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown) | ||||
| 
 | ||||
| 	expectedCounts := map[errcode.ErrorCode]int{ | ||||
| 		v2.ErrorCodeManifestBlobUnknown: 3, | ||||
| 	} | ||||
| 
 | ||||
| 	if !reflect.DeepEqual(counts, expectedCounts) { | ||||
| 		t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Push a config, and reference it in the manifest | ||||
| 	sampleConfig := []byte(`{ | ||||
| 		"architecture": "amd64", | ||||
| 		"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\"]" | ||||
| 		  } | ||||
| 		], | ||||
| 		"rootfs": { | ||||
| 		  "diff_ids": [ | ||||
| 		    "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", | ||||
| 		    "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" | ||||
| 		  ], | ||||
| 		  "type": "layers" | ||||
| 		} | ||||
| 	}`) | ||||
| 	sampleConfigDigest := digest.FromBytes(sampleConfig) | ||||
| 
 | ||||
| 	uploadURLBase, _ := startPushLayer(t, env.builder, imageName) | ||||
| 	pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig)) | ||||
| 	manifest.Config.Digest = sampleConfigDigest | ||||
| 	manifest.Config.Size = int64(len(sampleConfig)) | ||||
| 
 | ||||
| 	// The manifest should still be invalid, because its layer doesnt exist | ||||
| 	resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest) | ||||
| 	defer resp.Body.Close() | ||||
| 	checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest) | ||||
| 	_, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown) | ||||
| 
 | ||||
| 	expectedCounts = map[errcode.ErrorCode]int{ | ||||
| 		v2.ErrorCodeManifestBlobUnknown: 2, | ||||
| 	} | ||||
| 
 | ||||
| 	if !reflect.DeepEqual(counts, expectedCounts) { | ||||
| 		t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Push 2 random layers | ||||
| 	expectedLayers := make(map[digest.Digest]io.ReadSeeker) | ||||
| 
 | ||||
| 	for i := range manifest.Layers { | ||||
| 		rs, dgstStr, err := testutil.CreateRandomTarFile() | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("error creating random layer %d: %v", i, err) | ||||
| 		} | ||||
| 		dgst := digest.Digest(dgstStr) | ||||
| 
 | ||||
| 		expectedLayers[dgst] = rs | ||||
| 		manifest.Layers[i].Digest = dgst | ||||
| 
 | ||||
| 		uploadURLBase, _ := startPushLayer(t, env.builder, imageName) | ||||
| 		pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) | ||||
| 	} | ||||
| 
 | ||||
| 	// ------------------- | ||||
| 	// Push the manifest with all layers pushed. | ||||
| 	deserializedManifest, err := schema2.FromStruct(*manifest) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not create DeserializedManifest: %v", err) | ||||
| 	} | ||||
| 	_, canonical, err := deserializedManifest.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not get manifest payload: %v", err) | ||||
| 	} | ||||
| 	dgst := digest.FromBytes(canonical) | ||||
| 	args.dgst = dgst | ||||
| 	args.manifest = deserializedManifest | ||||
| 
 | ||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||
| 	checkErr(t, err, "building manifest url") | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest) | ||||
| 	checkResponse(t, "putting manifest no error", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// -------------------- | ||||
| 	// Push by digest -- should get same result | ||||
| 	resp = putManifest(t, "putting manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest) | ||||
| 	checkResponse(t, "putting manifest by digest", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// ------------------ | ||||
| 	// Fetch by tag name | ||||
| 	req, err := http.NewRequest("GET", manifestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("Accept", schema2.MediaTypeManifest) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error fetching manifest: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedManifest schema2.DeserializedManifest | ||||
| 	dec := json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	if err := dec.Decode(&fetchedManifest); err != nil { | ||||
| 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	_, fetchedCanonical, err := fetchedManifest.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error getting manifest payload: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !bytes.Equal(fetchedCanonical, canonical) { | ||||
| 		t.Fatalf("manifests do not match") | ||||
| 	} | ||||
| 
 | ||||
| 	// --------------- | ||||
| 	// Fetch by digest | ||||
| 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("Accept", schema2.MediaTypeManifest) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	checkErr(t, err, "fetching manifest by digest") | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedManifestByDigest schema2.DeserializedManifest | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 	if err := dec.Decode(&fetchedManifestByDigest); err != nil { | ||||
| 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	_, fetchedCanonical, err = fetchedManifest.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error getting manifest payload: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !bytes.Equal(fetchedCanonical, canonical) { | ||||
| 		t.Fatalf("manifests do not match") | ||||
| 	} | ||||
| 
 | ||||
| 	// Get by name with etag, gives 304 | ||||
| 	etag := resp.Header.Get("Etag") | ||||
| 	req, err = http.NewRequest("GET", manifestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("If-None-Match", etag) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) | ||||
| 
 | ||||
| 	// Get by digest with etag, gives 304 | ||||
| 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("If-None-Match", etag) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) | ||||
| 
 | ||||
| 	// Ensure that the tag is listed. | ||||
| 	resp, err = http.Get(tagsURL) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting unknown tags: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	var tagsResponse tagsAPIResponse | ||||
| 
 | ||||
| 	if err := dec.Decode(&tagsResponse); err != nil { | ||||
| 		t.Fatalf("unexpected error decoding error response: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if tagsResponse.Name != imageName { | ||||
| 		t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(tagsResponse.Tags) != 1 { | ||||
| 		t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) | ||||
| 	} | ||||
| 
 | ||||
| 	if tagsResponse.Tags[0] != tag { | ||||
| 		t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) | ||||
| 	} | ||||
| 
 | ||||
| 	// ------------------ | ||||
| 	// Fetch as a schema1 manifest | ||||
| 	resp, err = http.Get(manifestURL) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error fetching manifest as schema1: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedSchema1Manifest schema1.SignedManifest | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	if err := dec.Decode(&fetchedSchema1Manifest); err != nil { | ||||
| 		t.Fatalf("error decoding fetched schema1 manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { | ||||
| 		t.Fatal("wrong schema version") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Architecture != "amd64" { | ||||
| 		t.Fatal("wrong architecture") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Name != imageName { | ||||
| 		t.Fatal("wrong image name") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Tag != tag { | ||||
| 		t.Fatal("wrong tag") | ||||
| 	} | ||||
| 	if len(fetchedSchema1Manifest.FSLayers) != 2 { | ||||
| 		t.Fatal("wrong number of FSLayers") | ||||
| 	} | ||||
| 	for i := range manifest.Layers { | ||||
| 		if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest { | ||||
| 			t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(fetchedSchema1Manifest.History) != 2 { | ||||
| 		t.Fatal("wrong number of History entries") | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't check V1Compatibility fields becuase we're using randomly-generated | ||||
| 	// layers. | ||||
| 
 | ||||
| 	return args | ||||
| } | ||||
| 
 | ||||
| func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) { | ||||
| 	imageName := args.imageName | ||||
| 	tag := "manifestlisttag" | ||||
| 
 | ||||
| 	manifestURL, err := env.builder.BuildManifestURL(imageName, tag) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error getting manifest url: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// -------------------------------- | ||||
| 	// Attempt to push manifest list that refers to an unknown manifest | ||||
| 	manifestList := &manifestlist.ManifestList{ | ||||
| 		Versioned: manifest.Versioned{ | ||||
| 			SchemaVersion: 2, | ||||
| 			MediaType:     manifestlist.MediaTypeManifestList, | ||||
| 		}, | ||||
| 		Manifests: []manifestlist.ManifestDescriptor{ | ||||
| 			{ | ||||
| 				Descriptor: distribution.Descriptor{ | ||||
| 					Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", | ||||
| 					Size:      3253, | ||||
| 					MediaType: schema2.MediaTypeManifest, | ||||
| 				}, | ||||
| 				Platform: manifestlist.PlatformSpec{ | ||||
| 					Architecture: "amd64", | ||||
| 					OS:           "linux", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList) | ||||
| 	defer resp.Body.Close() | ||||
| 	checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest) | ||||
| 	_, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown) | ||||
| 
 | ||||
| 	expectedCounts := map[errcode.ErrorCode]int{ | ||||
| 		v2.ErrorCodeManifestBlobUnknown: 1, | ||||
| 	} | ||||
| 
 | ||||
| 	if !reflect.DeepEqual(counts, expectedCounts) { | ||||
| 		t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) | ||||
| 	} | ||||
| 
 | ||||
| 	// ------------------- | ||||
| 	// Push a manifest list that references an actual manifest | ||||
| 	manifestList.Manifests[0].Digest = args.dgst | ||||
| 	deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not create DeserializedManifestList: %v", err) | ||||
| 	} | ||||
| 	_, canonical, err := deserializedManifestList.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("could not get manifest list payload: %v", err) | ||||
| 	} | ||||
| 	dgst := digest.FromBytes(canonical) | ||||
| 
 | ||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||
| 	checkErr(t, err, "building manifest url") | ||||
| 
 | ||||
| 	resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) | ||||
| 	checkResponse(t, "putting manifest list no error", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// -------------------- | ||||
| 	// Push by digest -- should get same result | ||||
| 	resp = putManifest(t, "putting manifest list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) | ||||
| 	checkResponse(t, "putting manifest list by digest", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// ------------------ | ||||
| 	// Fetch by tag name | ||||
| 	req, err := http.NewRequest("GET", manifestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("Accept", manifestlist.MediaTypeManifestList) | ||||
| 	req.Header.Add("Accept", schema1.MediaTypeManifest) | ||||
| 	req.Header.Add("Accept", schema2.MediaTypeManifest) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error fetching manifest list: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedManifestList manifestlist.DeserializedManifestList | ||||
| 	dec := json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	if err := dec.Decode(&fetchedManifestList); err != nil { | ||||
| 		t.Fatalf("error decoding fetched manifest list: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	_, fetchedCanonical, err := fetchedManifestList.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error getting manifest list payload: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !bytes.Equal(fetchedCanonical, canonical) { | ||||
| 		t.Fatalf("manifest lists do not match") | ||||
| 	} | ||||
| 
 | ||||
| 	// --------------- | ||||
| 	// Fetch by digest | ||||
| 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("Accept", manifestlist.MediaTypeManifestList) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	checkErr(t, err, "fetching manifest list by digest") | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedManifestListByDigest manifestlist.DeserializedManifestList | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 	if err := dec.Decode(&fetchedManifestListByDigest); err != nil { | ||||
| 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	_, fetchedCanonical, err = fetchedManifestListByDigest.Payload() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error getting manifest list payload: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !bytes.Equal(fetchedCanonical, canonical) { | ||||
| 		t.Fatalf("manifests do not match") | ||||
| 	} | ||||
| 
 | ||||
| 	// Get by name with etag, gives 304 | ||||
| 	etag := resp.Header.Get("Etag") | ||||
| 	req, err = http.NewRequest("GET", manifestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("If-None-Match", etag) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) | ||||
| 
 | ||||
| 	// Get by digest with etag, gives 304 | ||||
| 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 	req.Header.Set("If-None-Match", etag) | ||||
| 	resp, err = http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error constructing request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) | ||||
| 
 | ||||
| 	// ------------------ | ||||
| 	// Fetch as a schema1 manifest | ||||
| 	resp, err = http.Get(manifestURL) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error fetching manifest list as schema1: %v", err) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
| 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||
| 	}) | ||||
| 
 | ||||
| 	var fetchedSchema1Manifest schema1.SignedManifest | ||||
| 	dec = json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	if err := dec.Decode(&fetchedSchema1Manifest); err != nil { | ||||
| 		t.Fatalf("error decoding fetched schema1 manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { | ||||
| 		t.Fatal("wrong schema version") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Architecture != "amd64" { | ||||
| 		t.Fatal("wrong architecture") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Name != imageName { | ||||
| 		t.Fatal("wrong image name") | ||||
| 	} | ||||
| 	if fetchedSchema1Manifest.Tag != tag { | ||||
| 		t.Fatal("wrong tag") | ||||
| 	} | ||||
| 	if len(fetchedSchema1Manifest.FSLayers) != 2 { | ||||
| 		t.Fatal("wrong number of FSLayers") | ||||
| 	} | ||||
| 	layers := args.manifest.(*schema2.DeserializedManifest).Layers | ||||
| 	for i := range layers { | ||||
| 		if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest { | ||||
| 			t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(fetchedSchema1Manifest.History) != 2 { | ||||
| 		t.Fatal("wrong number of History entries") | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't check V1Compatibility fields becuase we're using randomly-generated | ||||
| 	// layers. | ||||
| } | ||||
| 
 | ||||
| func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { | ||||
| 	imageName := args.imageName | ||||
| 	dgst := args.dgst | ||||
| 	signedManifest := args.signedManifest | ||||
| 	manifest := args.manifest | ||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||
| 	// --------------- | ||||
| 	// Delete by digest | ||||
|  | @ -1090,8 +1650,8 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { | |||
| 
 | ||||
| 	// -------------------- | ||||
| 	// Re-upload manifest by digest | ||||
| 	resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest) | ||||
| 	checkResponse(t, "putting signed manifest", resp, http.StatusCreated) | ||||
| 	resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest) | ||||
| 	checkResponse(t, "putting manifest", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{manifestDigestURL}, | ||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | ||||
|  | @ -1183,16 +1743,23 @@ func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *te | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response { | ||||
| func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response { | ||||
| 	var body []byte | ||||
| 
 | ||||
| 	if sm, ok := v.(*schema1.SignedManifest); ok { | ||||
| 		_, pl, err := sm.Payload() | ||||
| 	switch m := v.(type) { | ||||
| 	case *schema1.SignedManifest: | ||||
| 		_, pl, err := m.Payload() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("error getting payload: %v", err) | ||||
| 		} | ||||
| 		body = pl | ||||
| 	} else { | ||||
| 	case *manifestlist.DeserializedManifestList: | ||||
| 		_, pl, err := m.Payload() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("error getting payload: %v", err) | ||||
| 		} | ||||
| 		body = pl | ||||
| 	default: | ||||
| 		var err error | ||||
| 		body, err = json.MarshalIndent(v, "", "   ") | ||||
| 		if err != nil { | ||||
|  | @ -1205,6 +1772,10 @@ func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response { | |||
| 		t.Fatalf("error creating request for %s: %v", msg, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if contentType != "" { | ||||
| 		req.Header.Set("Content-Type", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error doing put request while %s: %v", msg, err) | ||||
|  | @ -1532,7 +2103,7 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string) | |||
| 	location, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||
| 	checkErr(t, err, "building location URL") | ||||
| 
 | ||||
| 	resp := putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest) | ||||
| 	resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) | ||||
| 	checkResponse(t, "putting signed manifest", resp, http.StatusCreated) | ||||
| 	checkHeaders(t, resp, http.Header{ | ||||
| 		"Location":              []string{location}, | ||||
|  | @ -1570,7 +2141,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) { | |||
| 		t.Fatalf("error signing manifest: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	resp := putManifest(t, "putting unsigned manifest", manifestURL, sm) | ||||
| 	resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm) | ||||
| 	checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) | ||||
| 
 | ||||
| 	// Manifest Delete | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import ( | |||
| 	storagedriver "github.com/docker/distribution/registry/storage/driver" | ||||
| 	"github.com/docker/distribution/registry/storage/driver/factory" | ||||
| 	storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" | ||||
| 	"github.com/docker/libtrust" | ||||
| 	"github.com/garyburd/redigo/redis" | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"golang.org/x/net/context" | ||||
|  | @ -67,10 +68,15 @@ type App struct { | |||
| 
 | ||||
| 	redis *redis.Pool | ||||
| 
 | ||||
| 	// true if this registry is configured as a pull through cache | ||||
| 	// trustKey is a deprecated key used to sign manifests converted to | ||||
| 	// schema1 for backward compatibility. It should not be used for any | ||||
| 	// other purposes. | ||||
| 	trustKey libtrust.PrivateKey | ||||
| 
 | ||||
| 	// isCache is true if this registry is configured as a pull through cache | ||||
| 	isCache bool | ||||
| 
 | ||||
| 	// true if the registry is in a read-only maintenance mode | ||||
| 	// readOnly is true if the registry is in a read-only maintenance mode | ||||
| 	readOnly bool | ||||
| } | ||||
| 
 | ||||
|  | @ -139,6 +145,13 @@ func NewApp(ctx context.Context, configuration *configuration.Configuration) *Ap | |||
| 	app.configureRedis(configuration) | ||||
| 	app.configureLogHook(configuration) | ||||
| 
 | ||||
| 	// Generate an ephemeral key to be used for signing converted manifests | ||||
| 	// for clients that don't support schema2. | ||||
| 	app.trustKey, err = libtrust.GenerateECP256PrivateKey() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if configuration.HTTP.Host != "" { | ||||
| 		u, err := url.Parse(configuration.HTTP.Host) | ||||
| 		if err != nil { | ||||
|  |  | |||
|  | @ -8,11 +8,21 @@ import ( | |||
| 	"github.com/docker/distribution" | ||||
| 	ctxu "github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest/manifestlist" | ||||
| 	"github.com/docker/distribution/manifest/schema1" | ||||
| 	"github.com/docker/distribution/manifest/schema2" | ||||
| 	"github.com/docker/distribution/registry/api/errcode" | ||||
| 	"github.com/docker/distribution/registry/api/v2" | ||||
| 	"github.com/gorilla/handlers" | ||||
| ) | ||||
| 
 | ||||
| // These constants determine which architecture and OS to choose from a | ||||
| // manifest list when downconverting it to a schema1 manifest. | ||||
| const ( | ||||
| 	defaultArch = "amd64" | ||||
| 	defaultOS   = "linux" | ||||
| ) | ||||
| 
 | ||||
| // imageManifestDispatcher takes the request context and builds the | ||||
| // appropriate handler for handling image manifest requests. | ||||
| func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { | ||||
|  | @ -51,8 +61,6 @@ type imageManifestHandler struct { | |||
| } | ||||
| 
 | ||||
| // GetImageManifest fetches the image manifest from the storage backend, if it exists. | ||||
| // todo(richardscothern): this assumes v2 schema 1 manifests for now but in the future | ||||
| // get the version from the Accept HTTP header | ||||
| func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { | ||||
| 	ctxu.GetLogger(imh).Debug("GetImageManifest") | ||||
| 	manifests, err := imh.Repository.Manifests(imh) | ||||
|  | @ -83,6 +91,67 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	supportsSchema2 := false | ||||
| 	supportsManifestList := false | ||||
| 	if acceptHeaders, ok := r.Header["Accept"]; ok { | ||||
| 		for _, mediaType := range acceptHeaders { | ||||
| 			if mediaType == schema2.MediaTypeManifest { | ||||
| 				supportsSchema2 = true | ||||
| 			} | ||||
| 			if mediaType == manifestlist.MediaTypeManifestList { | ||||
| 				supportsManifestList = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) | ||||
| 	manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		// 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 { | ||||
| 		// 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 { | ||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// If necessary, convert the image manifest | ||||
| 		if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 { | ||||
| 			manifest, err = imh.convertSchema2Manifest(schema2Manifest) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ct, p, err := manifest.Payload() | ||||
| 	if err != nil { | ||||
| 		return | ||||
|  | @ -95,6 +164,31 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http | |||
| 	w.Write(p) | ||||
| } | ||||
| 
 | ||||
| func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { | ||||
| 	targetDescriptor := schema2Manifest.Target() | ||||
| 	blobs := imh.Repository.Blobs(imh) | ||||
| 	configJSON, err := blobs.Get(imh, targetDescriptor.Digest) | ||||
| 	if err != nil { | ||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, imh.Repository.Name(), imh.Tag, configJSON) | ||||
| 	for _, d := range schema2Manifest.References() { | ||||
| 		if err := builder.AppendReference(d); err != nil { | ||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	manifest, err := builder.Build(imh) | ||||
| 	if err != nil { | ||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return manifest, nil | ||||
| } | ||||
| 
 | ||||
| func etagMatch(r *http.Request, etag string) bool { | ||||
| 	for _, headerVal := range r.Header["If-None-Match"] { | ||||
| 		if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted | ||||
|  |  | |||
							
								
								
									
										96
									
								
								registry/storage/manifestlisthandler.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								registry/storage/manifestlisthandler.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"encoding/json" | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest/manifestlist" | ||||
| ) | ||||
| 
 | ||||
| // manifestListHandler is a ManifestHandler that covers schema2 manifest lists. | ||||
| type manifestListHandler struct { | ||||
| 	repository *repository | ||||
| 	blobStore  *linkedBlobStore | ||||
| 	ctx        context.Context | ||||
| } | ||||
| 
 | ||||
| var _ ManifestHandler = &manifestListHandler{} | ||||
| 
 | ||||
| func (ms *manifestListHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Unmarshal") | ||||
| 
 | ||||
| 	var m manifestlist.DeserializedManifestList | ||||
| 	if err := json.Unmarshal(content, &m); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &m, nil | ||||
| } | ||||
| 
 | ||||
| func (ms *manifestListHandler) Put(ctx context.Context, manifestList distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put") | ||||
| 
 | ||||
| 	m, ok := manifestList.(*manifestlist.DeserializedManifestList) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("wrong type put to manifestListHandler: %T", manifestList) | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| 	} | ||||
| 
 | ||||
| 	// Link the revision into the repository. | ||||
| 	if err := ms.blobStore.linkBlob(ctx, revision); err != nil { | ||||
| 		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 *manifestListHandler) verifyManifest(ctx context.Context, mnfst manifestlist.DeserializedManifestList, skipDependencyVerification bool) error { | ||||
| 	var errs distribution.ErrManifestVerification | ||||
| 
 | ||||
| 	if !skipDependencyVerification { | ||||
| 		// This manifest service is different from the blob service | ||||
| 		// returned by Blob. It uses a linked blob store to ensure that | ||||
| 		// only manifests are accessible. | ||||
| 		manifestService, err := ms.repository.Manifests(ctx) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, manifestDescriptor := range mnfst.References() { | ||||
| 			exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest) | ||||
| 			if err != nil && err != distribution.ErrBlobUnknown { | ||||
| 				errs = append(errs, err) | ||||
| 			} | ||||
| 			if err != nil || !exists { | ||||
| 				// On error here, we always append unknown blob errors. | ||||
| 				errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: manifestDescriptor.Digest}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if len(errs) != 0 { | ||||
| 		return errs | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,24 +1,53 @@ | |||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"encoding/json" | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest" | ||||
| 	"github.com/docker/distribution/manifest/manifestlist" | ||||
| 	"github.com/docker/distribution/manifest/schema1" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/docker/libtrust" | ||||
| 	"github.com/docker/distribution/manifest/schema2" | ||||
| ) | ||||
| 
 | ||||
| // manifestStore is a storage driver based store for storing schema1 manifests. | ||||
| // A ManifestHandler gets and puts manifests of a particular type. | ||||
| type ManifestHandler interface { | ||||
| 	// Unmarshal unmarshals the manifest from a byte slice. | ||||
| 	Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) | ||||
| 
 | ||||
| 	// Put creates or updates the given manifest returning the manifest digest. | ||||
| 	Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) | ||||
| } | ||||
| 
 | ||||
| // SkipLayerVerification allows a manifest to be Put before its | ||||
| // layers are on the filesystem | ||||
| func SkipLayerVerification() distribution.ManifestServiceOption { | ||||
| 	return skipLayerOption{} | ||||
| } | ||||
| 
 | ||||
| type skipLayerOption struct{} | ||||
| 
 | ||||
| func (o skipLayerOption) Apply(m distribution.ManifestService) error { | ||||
| 	if ms, ok := m.(*manifestStore); ok { | ||||
| 		ms.skipDependencyVerification = true | ||||
| 		return nil | ||||
| 	} | ||||
| 	return fmt.Errorf("skip layer verification only valid for manifestStore") | ||||
| } | ||||
| 
 | ||||
| type manifestStore struct { | ||||
| 	repository                 *repository | ||||
| 	blobStore                  *linkedBlobStore | ||||
| 	ctx                        context.Context | ||||
| 	signatures                 *signatureStore | ||||
| 	repository *repository | ||||
| 	blobStore  *linkedBlobStore | ||||
| 	ctx        context.Context | ||||
| 
 | ||||
| 	skipDependencyVerification bool | ||||
| 
 | ||||
| 	schema1Handler      ManifestHandler | ||||
| 	schema2Handler      ManifestHandler | ||||
| 	manifestListHandler ManifestHandler | ||||
| } | ||||
| 
 | ||||
| var _ distribution.ManifestService = &manifestStore{} | ||||
|  | @ -40,18 +69,6 @@ func (ms *manifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, | |||
| 
 | ||||
| func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Get") | ||||
| 	// Ensure that this revision is available in this repository. | ||||
| 	_, err := ms.blobStore.Stat(ctx, dgst) | ||||
| 	if err != nil { | ||||
| 		if err == distribution.ErrBlobUnknown { | ||||
| 			return nil, distribution.ErrManifestUnknownRevision{ | ||||
| 				Name:     ms.repository.Name(), | ||||
| 				Revision: dgst, | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(stevvooe): Need to check descriptor from above to ensure that the | ||||
| 	// mediatype is as we expect for the manifest store. | ||||
|  | @ -68,84 +85,42 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options .. | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Fetch the signatures for the manifest | ||||
| 	signatures, err := ms.signatures.Get(dgst) | ||||
| 	if err != nil { | ||||
| 	var versioned manifest.Versioned | ||||
| 	if err = json.Unmarshal(content, &versioned); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	jsig, err := libtrust.NewJSONSignature(content, signatures...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	switch versioned.SchemaVersion { | ||||
| 	case 1: | ||||
| 		return ms.schema1Handler.Unmarshal(ctx, dgst, content) | ||||
| 	case 2: | ||||
| 		// This can be an image manifest or a manifest list | ||||
| 		switch versioned.MediaType { | ||||
| 		case schema2.MediaTypeManifest: | ||||
| 			return ms.schema2Handler.Unmarshal(ctx, dgst, content) | ||||
| 		case manifestlist.MediaTypeManifestList: | ||||
| 			return ms.manifestListHandler.Unmarshal(ctx, dgst, content) | ||||
| 		default: | ||||
| 			return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Extract the pretty JWS | ||||
| 	raw, err := jsig.PrettySignature("signatures") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var sm schema1.SignedManifest | ||||
| 	if err := json.Unmarshal(raw, &sm); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &sm, nil | ||||
| } | ||||
| 
 | ||||
| // SkipLayerVerification allows a manifest to be Put before its | ||||
| // layers are on the filesystem | ||||
| func SkipLayerVerification() distribution.ManifestServiceOption { | ||||
| 	return skipLayerOption{} | ||||
| } | ||||
| 
 | ||||
| type skipLayerOption struct{} | ||||
| 
 | ||||
| func (o skipLayerOption) Apply(m distribution.ManifestService) error { | ||||
| 	if ms, ok := m.(*manifestStore); ok { | ||||
| 		ms.skipDependencyVerification = true | ||||
| 		return nil | ||||
| 	} | ||||
| 	return fmt.Errorf("skip layer verification only valid for manifestStore") | ||||
| 	return nil, fmt.Errorf("unrecognized manifest schema version %d", versioned.SchemaVersion) | ||||
| } | ||||
| 
 | ||||
| func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Put") | ||||
| 
 | ||||
| 	sm, ok := manifest.(*schema1.SignedManifest) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("non-v1 manifest put to signed manifestStore: %T", manifest) | ||||
| 	switch manifest.(type) { | ||||
| 	case *schema1.SignedManifest: | ||||
| 		return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification) | ||||
| 	case *schema2.DeserializedManifest: | ||||
| 		return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification) | ||||
| 	case *manifestlist.DeserializedManifestList: | ||||
| 		return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ms.verifyManifest(ms.ctx, *sm); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	mt := schema1.MediaTypeManifest | ||||
| 	payload := sm.Canonical | ||||
| 
 | ||||
| 	revision, err := ms.blobStore.Put(ctx, mt, payload) | ||||
| 	if err != nil { | ||||
| 		context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	// Link the revision into the repository. | ||||
| 	if err := ms.blobStore.linkBlob(ctx, revision); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	// Grab each json signature and store them. | ||||
| 	signatures, err := sm.Signatures() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ms.signatures.Put(revision.Digest, signatures...); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return revision.Digest, nil | ||||
| 	return "", fmt.Errorf("unrecognized manifest type %T", manifest) | ||||
| } | ||||
| 
 | ||||
| // Delete removes the revision of the specified manfiest. | ||||
|  | @ -157,64 +132,3 @@ func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error { | |||
| func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | ||||
| 	return 0, distribution.ErrUnsupported | ||||
| } | ||||
| 
 | ||||
| // verifyManifest ensures that the manifest content is valid from the | ||||
| // perspective of the registry. It ensures that the signature is valid for the | ||||
| // enclosed payload. As a policy, the registry only tries to store valid | ||||
| // content, leaving trust policies of that content up to consumems. | ||||
| func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest) error { | ||||
| 	var errs distribution.ErrManifestVerification | ||||
| 
 | ||||
| 	if len(mnfst.Name) > reference.NameTotalLengthMax { | ||||
| 		errs = append(errs, | ||||
| 			distribution.ErrManifestNameInvalid{ | ||||
| 				Name:   mnfst.Name, | ||||
| 				Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax), | ||||
| 			}) | ||||
| 	} | ||||
| 
 | ||||
| 	if !reference.NameRegexp.MatchString(mnfst.Name) { | ||||
| 		errs = append(errs, | ||||
| 			distribution.ErrManifestNameInvalid{ | ||||
| 				Name:   mnfst.Name, | ||||
| 				Reason: fmt.Errorf("invalid manifest name format"), | ||||
| 			}) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(mnfst.History) != len(mnfst.FSLayers) { | ||||
| 		errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d", | ||||
| 			len(mnfst.History), len(mnfst.FSLayers))) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := schema1.Verify(&mnfst); err != nil { | ||||
| 		switch err { | ||||
| 		case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: | ||||
| 			errs = append(errs, distribution.ErrManifestUnverified{}) | ||||
| 		default: | ||||
| 			if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust | ||||
| 				errs = append(errs, distribution.ErrManifestUnverified{}) | ||||
| 			} else { | ||||
| 				errs = append(errs, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !ms.skipDependencyVerification { | ||||
| 		for _, fsLayer := range mnfst.References() { | ||||
| 			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest) | ||||
| 			if err != nil { | ||||
| 				if err != distribution.ErrBlobUnknown { | ||||
| 					errs = append(errs, err) | ||||
| 				} | ||||
| 
 | ||||
| 				// On error here, we always append unknown blob erroms. | ||||
| 				errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if len(errs) != 0 { | ||||
| 		return errs | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -165,28 +165,45 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M | |||
| 		blobLinkPath, | ||||
| 	} | ||||
| 
 | ||||
| 	blobStore := &linkedBlobStore{ | ||||
| 		ctx:           ctx, | ||||
| 		blobStore:     repo.blobStore, | ||||
| 		repository:    repo, | ||||
| 		deleteEnabled: repo.registry.deleteEnabled, | ||||
| 		blobAccessController: &linkedBlobStatter{ | ||||
| 			blobStore:   repo.blobStore, | ||||
| 			repository:  repo, | ||||
| 			linkPathFns: manifestLinkPathFns, | ||||
| 		}, | ||||
| 
 | ||||
| 		// TODO(stevvooe): linkPath limits this blob store to only | ||||
| 		// manifests. This instance cannot be used for blob checks. | ||||
| 		linkPathFns: manifestLinkPathFns, | ||||
| 	} | ||||
| 
 | ||||
| 	ms := &manifestStore{ | ||||
| 		ctx:        ctx, | ||||
| 		repository: repo, | ||||
| 		blobStore: &linkedBlobStore{ | ||||
| 			ctx:           ctx, | ||||
| 			blobStore:     repo.blobStore, | ||||
| 			repository:    repo, | ||||
| 			deleteEnabled: repo.registry.deleteEnabled, | ||||
| 			blobAccessController: &linkedBlobStatter{ | ||||
| 				blobStore:   repo.blobStore, | ||||
| 				repository:  repo, | ||||
| 				linkPathFns: manifestLinkPathFns, | ||||
| 			}, | ||||
| 
 | ||||
| 			// TODO(stevvooe): linkPath limits this blob store to only | ||||
| 			// manifests. This instance cannot be used for blob checks. | ||||
| 			linkPathFns: manifestLinkPathFns, | ||||
| 		}, | ||||
| 		signatures: &signatureStore{ | ||||
| 		blobStore:  blobStore, | ||||
| 		schema1Handler: &signedManifestHandler{ | ||||
| 			ctx:        ctx, | ||||
| 			repository: repo, | ||||
| 			blobStore:  repo.blobStore, | ||||
| 			blobStore:  blobStore, | ||||
| 			signatures: &signatureStore{ | ||||
| 				ctx:        ctx, | ||||
| 				repository: repo, | ||||
| 				blobStore:  repo.blobStore, | ||||
| 			}, | ||||
| 		}, | ||||
| 		schema2Handler: &schema2ManifestHandler{ | ||||
| 			ctx:        ctx, | ||||
| 			repository: repo, | ||||
| 			blobStore:  blobStore, | ||||
| 		}, | ||||
| 		manifestListHandler: &manifestListHandler{ | ||||
| 			ctx:        ctx, | ||||
| 			repository: repo, | ||||
| 			blobStore:  blobStore, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										99
									
								
								registry/storage/schema2manifesthandler.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								registry/storage/schema2manifesthandler.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | |||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"encoding/json" | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest/schema2" | ||||
| ) | ||||
| 
 | ||||
| //schema2ManifestHandler is a ManifestHandler that covers schema2 manifests. | ||||
| type schema2ManifestHandler struct { | ||||
| 	repository *repository | ||||
| 	blobStore  *linkedBlobStore | ||||
| 	ctx        context.Context | ||||
| } | ||||
| 
 | ||||
| var _ ManifestHandler = &schema2ManifestHandler{} | ||||
| 
 | ||||
| func (ms *schema2ManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Unmarshal") | ||||
| 
 | ||||
| 	var m schema2.DeserializedManifest | ||||
| 	if err := json.Unmarshal(content, &m); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &m, nil | ||||
| } | ||||
| 
 | ||||
| func (ms *schema2ManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Put") | ||||
| 
 | ||||
| 	m, ok := manifest.(*schema2.DeserializedManifest) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("non-schema2 manifest put to schema2ManifestHandler: %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 | ||||
| 	} | ||||
| 
 | ||||
| 	// Link the revision into the repository. | ||||
| 	if err := ms.blobStore.linkBlob(ctx, revision); err != nil { | ||||
| 		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 *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst schema2.DeserializedManifest, skipDependencyVerification bool) error { | ||||
| 	var errs distribution.ErrManifestVerification | ||||
| 
 | ||||
| 	if !skipDependencyVerification { | ||||
| 		target := mnfst.Target() | ||||
| 		_, err := ms.repository.Blobs(ctx).Stat(ctx, target.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: target.Digest}) | ||||
| 		} | ||||
| 
 | ||||
| 		for _, fsLayer := range mnfst.References() { | ||||
| 			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.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: fsLayer.Digest}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if len(errs) != 0 { | ||||
| 		return errs | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										150
									
								
								registry/storage/signedmanifesthandler.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								registry/storage/signedmanifesthandler.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,150 @@ | |||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/docker/distribution" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	"github.com/docker/distribution/digest" | ||||
| 	"github.com/docker/distribution/manifest/schema1" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/docker/libtrust" | ||||
| ) | ||||
| 
 | ||||
| // signedManifestHandler is a ManifestHandler that covers schema1 manifests. It | ||||
| // can unmarshal and put schema1 manifests that have been signed by libtrust. | ||||
| type signedManifestHandler struct { | ||||
| 	repository *repository | ||||
| 	blobStore  *linkedBlobStore | ||||
| 	ctx        context.Context | ||||
| 	signatures *signatureStore | ||||
| } | ||||
| 
 | ||||
| var _ ManifestHandler = &signedManifestHandler{} | ||||
| 
 | ||||
| func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal") | ||||
| 	// Fetch the signatures for the manifest | ||||
| 	signatures, err := ms.signatures.Get(dgst) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	jsig, err := libtrust.NewJSONSignature(content, signatures...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Extract the pretty JWS | ||||
| 	raw, err := jsig.PrettySignature("signatures") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var sm schema1.SignedManifest | ||||
| 	if err := json.Unmarshal(raw, &sm); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &sm, nil | ||||
| } | ||||
| 
 | ||||
| func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { | ||||
| 	context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Put") | ||||
| 
 | ||||
| 	sm, ok := manifest.(*schema1.SignedManifest) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("non-schema1 manifest put to signedManifestHandler: %T", manifest) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ms.verifyManifest(ms.ctx, *sm, skipDependencyVerification); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	mt := schema1.MediaTypeManifest | ||||
| 	payload := sm.Canonical | ||||
| 
 | ||||
| 	revision, err := ms.blobStore.Put(ctx, mt, payload) | ||||
| 	if err != nil { | ||||
| 		context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	// Link the revision into the repository. | ||||
| 	if err := ms.blobStore.linkBlob(ctx, revision); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	// Grab each json signature and store them. | ||||
| 	signatures, err := sm.Signatures() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ms.signatures.Put(revision.Digest, signatures...); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return revision.Digest, nil | ||||
| } | ||||
| 
 | ||||
| // verifyManifest ensures that the manifest content is valid from the | ||||
| // perspective of the registry. It ensures that the signature is valid for the | ||||
| // enclosed payload. As a policy, the registry only tries to store valid | ||||
| // content, leaving trust policies of that content up to consumers. | ||||
| func (ms *signedManifestHandler) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest, skipDependencyVerification bool) error { | ||||
| 	var errs distribution.ErrManifestVerification | ||||
| 
 | ||||
| 	if len(mnfst.Name) > reference.NameTotalLengthMax { | ||||
| 		errs = append(errs, | ||||
| 			distribution.ErrManifestNameInvalid{ | ||||
| 				Name:   mnfst.Name, | ||||
| 				Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax), | ||||
| 			}) | ||||
| 	} | ||||
| 
 | ||||
| 	if !reference.NameRegexp.MatchString(mnfst.Name) { | ||||
| 		errs = append(errs, | ||||
| 			distribution.ErrManifestNameInvalid{ | ||||
| 				Name:   mnfst.Name, | ||||
| 				Reason: fmt.Errorf("invalid manifest name format"), | ||||
| 			}) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(mnfst.History) != len(mnfst.FSLayers) { | ||||
| 		errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d", | ||||
| 			len(mnfst.History), len(mnfst.FSLayers))) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := schema1.Verify(&mnfst); err != nil { | ||||
| 		switch err { | ||||
| 		case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: | ||||
| 			errs = append(errs, distribution.ErrManifestUnverified{}) | ||||
| 		default: | ||||
| 			if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust | ||||
| 				errs = append(errs, distribution.ErrManifestUnverified{}) | ||||
| 			} else { | ||||
| 				errs = append(errs, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !skipDependencyVerification { | ||||
| 		for _, fsLayer := range mnfst.References() { | ||||
| 			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.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: fsLayer.Digest}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if len(errs) != 0 { | ||||
| 		return errs | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue