Merge pull request #1268 from RichardScothern/manifest-refactor-impl
Implementation of the Manifest Service API refactor.master
						commit
						67d3675d55
					
				|  | @ -402,6 +402,28 @@ for details): | ||||||
| The client should verify the returned manifest signature for authenticity | The client should verify the returned manifest signature for authenticity | ||||||
| before fetching layers. | before fetching layers. | ||||||
| 
 | 
 | ||||||
|  | ##### Existing Manifests | ||||||
|  | 
 | ||||||
|  | The image manifest can be checked for existence with the following url: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | HEAD /v2/<name>/manifests/<reference> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The `name` and `reference` parameter identify the image and are required. The | ||||||
|  | reference may include a tag or digest. | ||||||
|  | 
 | ||||||
|  | A `404 Not Found` response will be returned if the image is unknown to the | ||||||
|  | registry. If the image exists and the response is successful the response will | ||||||
|  | be as follows: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | 200 OK | ||||||
|  | Content-Length: <length of manifest> | ||||||
|  | Docker-Content-Digest: <digest> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| #### Pulling a Layer | #### Pulling a Layer | ||||||
| 
 | 
 | ||||||
| Layers are stored in the blob portion of the registry, keyed by digest. | Layers are stored in the blob portion of the registry, keyed by digest. | ||||||
|  |  | ||||||
|  | @ -402,6 +402,28 @@ for details): | ||||||
| The client should verify the returned manifest signature for authenticity | The client should verify the returned manifest signature for authenticity | ||||||
| before fetching layers. | before fetching layers. | ||||||
| 
 | 
 | ||||||
|  | ##### Existing Manifests | ||||||
|  | 
 | ||||||
|  | The image manifest can be checked for existence with the following url: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | HEAD /v2/<name>/manifests/<reference> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The `name` and `reference` parameter identify the image and are required. The | ||||||
|  | reference may include a tag or digest. | ||||||
|  | 
 | ||||||
|  | A `404 Not Found` response will be returned if the image is unknown to the | ||||||
|  | registry. If the image exists and the response is successful the response will | ||||||
|  | be as follows: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | 200 OK | ||||||
|  | Content-Length: <length of manifest> | ||||||
|  | Docker-Content-Digest: <digest> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| #### Pulling a Layer | #### Pulling a Layer | ||||||
| 
 | 
 | ||||||
| Layers are stored in the blob portion of the registry, keyed by digest. | Layers are stored in the blob portion of the registry, keyed by digest. | ||||||
|  |  | ||||||
|  | @ -16,6 +16,15 @@ var ErrManifestNotModified = errors.New("manifest not modified") | ||||||
| // performed
 | // performed
 | ||||||
| var ErrUnsupported = errors.New("operation unsupported") | var ErrUnsupported = errors.New("operation unsupported") | ||||||
| 
 | 
 | ||||||
|  | // ErrTagUnknown is returned if the given tag is not known by the tag service
 | ||||||
|  | type ErrTagUnknown struct { | ||||||
|  | 	Tag string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrTagUnknown) Error() string { | ||||||
|  | 	return fmt.Sprintf("unknown tag=%s", err.Tag) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ErrRepositoryUnknown is returned if the named repository is not known by
 | // ErrRepositoryUnknown is returned if the named repository is not known by
 | ||||||
| // the registry.
 | // the registry.
 | ||||||
| type ErrRepositoryUnknown struct { | type ErrRepositoryUnknown struct { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | package schema1 | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"errors" | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/digest" | ||||||
|  | 	"github.com/docker/distribution/manifest" | ||||||
|  | 	"github.com/docker/libtrust" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ManifestBuilder is a type for constructing manifests
 | ||||||
|  | type manifestBuilder 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{ | ||||||
|  | 		Manifest: Manifest{ | ||||||
|  | 			Versioned: manifest.Versioned{ | ||||||
|  | 				SchemaVersion: 1, | ||||||
|  | 			}, | ||||||
|  | 			Name:         name, | ||||||
|  | 			Tag:          tag, | ||||||
|  | 			Architecture: architecture, | ||||||
|  | 		}, | ||||||
|  | 		pk: pk, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Build produces a final manifest from the given references
 | ||||||
|  | func (mb *manifestBuilder) Build() (distribution.Manifest, error) { | ||||||
|  | 	m := mb.Manifest | ||||||
|  | 	if len(m.FSLayers) == 0 { | ||||||
|  | 		return nil, errors.New("cannot build manifest with zero layers or history") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers)) | ||||||
|  | 	m.History = make([]History, len(mb.Manifest.History)) | ||||||
|  | 	copy(m.FSLayers, mb.Manifest.FSLayers) | ||||||
|  | 	copy(m.History, mb.Manifest.History) | ||||||
|  | 
 | ||||||
|  | 	return Sign(&m, mb.pk) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AppendReference adds a reference to the current manifestBuilder
 | ||||||
|  | func (mb *manifestBuilder) AppendReference(d distribution.Describable) error { | ||||||
|  | 	r, ok := d.(Reference) | ||||||
|  | 	if !ok { | ||||||
|  | 		return fmt.Errorf("Unable to add non-reference type to v1 builder") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Entries need to be prepended
 | ||||||
|  | 	mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...) | ||||||
|  | 	mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...) | ||||||
|  | 	return nil | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // References returns the current references added to this builder
 | ||||||
|  | func (mb *manifestBuilder) References() []distribution.Descriptor { | ||||||
|  | 	refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) | ||||||
|  | 	for i := range mb.Manifest.FSLayers { | ||||||
|  | 		layerDigest := mb.Manifest.FSLayers[i].BlobSum | ||||||
|  | 		history := mb.Manifest.History[i] | ||||||
|  | 		ref := Reference{layerDigest, 0, history} | ||||||
|  | 		refs[i] = ref.Descriptor() | ||||||
|  | 	} | ||||||
|  | 	return refs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Reference describes a manifest v2, schema version 1 dependency.
 | ||||||
|  | // An FSLayer associated with a history entry.
 | ||||||
|  | type Reference struct { | ||||||
|  | 	Digest  digest.Digest | ||||||
|  | 	Size    int64 // if we know it, set it for the descriptor.
 | ||||||
|  | 	History History | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Descriptor describes a reference
 | ||||||
|  | func (r Reference) Descriptor() distribution.Descriptor { | ||||||
|  | 	return distribution.Descriptor{ | ||||||
|  | 		MediaType: MediaTypeManifestLayer, | ||||||
|  | 		Digest:    r.Digest, | ||||||
|  | 		Size:      r.Size, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,97 @@ | ||||||
|  | package schema1 | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution/digest" | ||||||
|  | 	"github.com/docker/distribution/manifest" | ||||||
|  | 	"github.com/docker/libtrust" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest { | ||||||
|  | 	u := &Manifest{ | ||||||
|  | 		Versioned: manifest.Versioned{ | ||||||
|  | 			SchemaVersion: 1, | ||||||
|  | 		}, | ||||||
|  | 		Name:         "foo/bar", | ||||||
|  | 		Tag:          "latest", | ||||||
|  | 		Architecture: "amd64", | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := len(refs) - 1; i >= 0; i-- { | ||||||
|  | 		u.FSLayers = append(u.FSLayers, FSLayer{ | ||||||
|  | 			BlobSum: refs[i].Digest, | ||||||
|  | 		}) | ||||||
|  | 		u.History = append(u.History, History{ | ||||||
|  | 			V1Compatibility: refs[i].History.V1Compatibility, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	signedManifest, err := Sign(u, pk) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("unexpected error signing manifest: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return signedManifest | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestBuilder(t *testing.T) { | ||||||
|  | 	pk, err := libtrust.GenerateECP256PrivateKey() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("unexpected error generating private key: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r1 := Reference{ | ||||||
|  | 		Digest:  "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||||||
|  | 		Size:    1, | ||||||
|  | 		History: History{V1Compatibility: "{\"a\" : 1 }"}, | ||||||
|  | 	} | ||||||
|  | 	r2 := Reference{ | ||||||
|  | 		Digest:  "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", | ||||||
|  | 		Size:    2, | ||||||
|  | 		History: History{V1Compatibility: "{\"\a\" : 2 }"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2}) | ||||||
|  | 
 | ||||||
|  | 	b := NewManifestBuilder(pk, handCrafted.Manifest.Name, handCrafted.Manifest.Tag, handCrafted.Manifest.Architecture) | ||||||
|  | 	_, err = b.Build() | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatal("Expected error building zero length manifest") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = b.AppendReference(r1) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = b.AppendReference(r2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	refs := b.References() | ||||||
|  | 	if len(refs) != 2 { | ||||||
|  | 		t.Fatalf("Unexpected reference count : %d != %d", 2, len(refs)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Ensure ordering
 | ||||||
|  | 	if refs[0].Digest != r2.Digest { | ||||||
|  | 		t.Fatalf("Unexpected reference : %v", refs[0]) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	m, err := b.Build() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	built, ok := m.(*SignedManifest) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("unexpected type from Build() : %T", built) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d1 := digest.FromBytes(built.Canonical) | ||||||
|  | 	d2 := digest.FromBytes(handCrafted.Canonical) | ||||||
|  | 	if d1 != d2 { | ||||||
|  | 		t.Errorf("mismatching canonical JSON") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -2,20 +2,22 @@ package schema1 | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest" | 	"github.com/docker/distribution/manifest" | ||||||
| 	"github.com/docker/libtrust" | 	"github.com/docker/libtrust" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // TODO(stevvooe): When we rev the manifest format, the contents of this
 |  | ||||||
| // package should be moved to manifest/v1.
 |  | ||||||
| 
 |  | ||||||
| const ( | const ( | ||||||
| 	// ManifestMediaType specifies the mediaType for the current version. Note
 | 	// MediaTypeManifest specifies the mediaType for the current version. Note
 | ||||||
| 	// that for schema version 1, the the media is optionally
 | 	// that for schema version 1, the the media is optionally "application/json".
 | ||||||
| 	// "application/json".
 | 	MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json" | ||||||
| 	ManifestMediaType = "application/vnd.docker.distribution.manifest.v1+json" | 	// MediaTypeSignedManifest specifies the mediatype for current SignedManifest version
 | ||||||
|  | 	MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" | ||||||
|  | 	// MediaTypeManifestLayer specifies the media type for manifest layers
 | ||||||
|  | 	MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
|  | @ -26,6 +28,47 @@ var ( | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func init() { | ||||||
|  | 	schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { | ||||||
|  | 		sm := new(SignedManifest) | ||||||
|  | 		err := sm.UnmarshalJSON(b) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, distribution.Descriptor{}, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		desc := distribution.Descriptor{ | ||||||
|  | 			Digest:    digest.FromBytes(sm.Canonical), | ||||||
|  | 			Size:      int64(len(sm.Canonical)), | ||||||
|  | 			MediaType: MediaTypeManifest, | ||||||
|  | 		} | ||||||
|  | 		return sm, desc, err | ||||||
|  | 	} | ||||||
|  | 	err := distribution.RegisterManifestSchema(MediaTypeManifest, schema1Func) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(fmt.Sprintf("Unable to register manifest: %s", err)) | ||||||
|  | 	} | ||||||
|  | 	err = distribution.RegisterManifestSchema("", schema1Func) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(fmt.Sprintf("Unable to register manifest: %s", err)) | ||||||
|  | 	} | ||||||
|  | 	err = distribution.RegisterManifestSchema("application/json; charset=utf-8", schema1Func) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(fmt.Sprintf("Unable to register manifest: %s", err)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FSLayer is a container struct for BlobSums defined in an image manifest
 | ||||||
|  | type FSLayer struct { | ||||||
|  | 	// BlobSum is the tarsum of the referenced filesystem image layer
 | ||||||
|  | 	BlobSum digest.Digest `json:"blobSum"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // History stores unstructured v1 compatibility information
 | ||||||
|  | type History struct { | ||||||
|  | 	// V1Compatibility is the raw v1 compatibility information
 | ||||||
|  | 	V1Compatibility string `json:"v1Compatibility"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Manifest provides the base accessible fields for working with V2 image
 | // Manifest provides the base accessible fields for working with V2 image
 | ||||||
| // format in the registry.
 | // format in the registry.
 | ||||||
| type Manifest struct { | type Manifest struct { | ||||||
|  | @ -49,59 +92,64 @@ type Manifest struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SignedManifest provides an envelope for a signed image manifest, including
 | // SignedManifest provides an envelope for a signed image manifest, including
 | ||||||
| // the format sensitive raw bytes. It contains fields to
 | // the format sensitive raw bytes.
 | ||||||
| type SignedManifest struct { | type SignedManifest struct { | ||||||
| 	Manifest | 	Manifest | ||||||
| 
 | 
 | ||||||
| 	// Raw is the byte representation of the ImageManifest, used for signature
 | 	// Canonical is the canonical byte representation of the ImageManifest,
 | ||||||
| 	// verification. The value of Raw must be used directly during
 | 	// without any attached signatures. The manifest byte
 | ||||||
| 	// serialization, or the signature check will fail. The manifest byte
 |  | ||||||
| 	// representation cannot change or it will have to be re-signed.
 | 	// representation cannot change or it will have to be re-signed.
 | ||||||
| 	Raw []byte `json:"-"` | 	Canonical []byte `json:"-"` | ||||||
|  | 
 | ||||||
|  | 	// all contains the byte representation of the Manifest including signatures
 | ||||||
|  | 	// and is retuend by Payload()
 | ||||||
|  | 	all []byte | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UnmarshalJSON populates a new ImageManifest struct from JSON data.
 | // UnmarshalJSON populates a new SignedManifest struct from JSON data.
 | ||||||
| func (sm *SignedManifest) UnmarshalJSON(b []byte) error { | func (sm *SignedManifest) UnmarshalJSON(b []byte) error { | ||||||
| 	sm.Raw = make([]byte, len(b), len(b)) | 	sm.all = make([]byte, len(b), len(b)) | ||||||
| 	copy(sm.Raw, b) | 	// store manifest and signatures in all
 | ||||||
|  | 	copy(sm.all, b) | ||||||
| 
 | 
 | ||||||
| 	p, err := sm.Payload() | 	jsig, err := libtrust.ParsePrettySignature(b, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Resolve the payload in the manifest.
 | ||||||
|  | 	bytes, err := jsig.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sm.Canonical stores the canonical manifest JSON
 | ||||||
|  | 	sm.Canonical = make([]byte, len(bytes), len(bytes)) | ||||||
|  | 	copy(sm.Canonical, bytes) | ||||||
|  | 
 | ||||||
|  | 	// Unmarshal canonical JSON into Manifest object
 | ||||||
| 	var manifest Manifest | 	var manifest Manifest | ||||||
| 	if err := json.Unmarshal(p, &manifest); err != nil { | 	if err := json.Unmarshal(sm.Canonical, &manifest); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	sm.Manifest = manifest | 	sm.Manifest = manifest | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Payload returns the raw, signed content of the signed manifest. The
 | // References returnes the descriptors of this manifests references
 | ||||||
| // contents can be used to calculate the content identifier.
 | func (sm SignedManifest) References() []distribution.Descriptor { | ||||||
| func (sm *SignedManifest) Payload() ([]byte, error) { | 	dependencies := make([]distribution.Descriptor, len(sm.FSLayers)) | ||||||
| 	jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") | 	for i, fsLayer := range sm.FSLayers { | ||||||
| 	if err != nil { | 		dependencies[i] = distribution.Descriptor{ | ||||||
| 		return nil, err | 			MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar", | ||||||
|  | 			Digest:    fsLayer.BlobSum, | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Resolve the payload in the manifest.
 | 	return dependencies | ||||||
| 	return jsig.Payload() |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // Signatures returns the signatures as provided by
 |  | ||||||
| // (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
 |  | ||||||
| // signatures.
 |  | ||||||
| func (sm *SignedManifest) Signatures() ([][]byte, error) { |  | ||||||
| 	jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Resolve the payload in the manifest.
 |  | ||||||
| 	return jsig.Signatures() |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
 | // MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
 | ||||||
|  | @ -109,22 +157,28 @@ func (sm *SignedManifest) Signatures() ([][]byte, error) { | ||||||
| // use Raw directly, since the the content produced by json.Marshal will be
 | // use Raw directly, since the the content produced by json.Marshal will be
 | ||||||
| // compacted and will fail signature checks.
 | // compacted and will fail signature checks.
 | ||||||
| func (sm *SignedManifest) MarshalJSON() ([]byte, error) { | func (sm *SignedManifest) MarshalJSON() ([]byte, error) { | ||||||
| 	if len(sm.Raw) > 0 { | 	if len(sm.all) > 0 { | ||||||
| 		return sm.Raw, nil | 		return sm.all, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If the raw data is not available, just dump the inner content.
 | 	// If the raw data is not available, just dump the inner content.
 | ||||||
| 	return json.Marshal(&sm.Manifest) | 	return json.Marshal(&sm.Manifest) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FSLayer is a container struct for BlobSums defined in an image manifest
 | // Payload returns the signed content of the signed manifest.
 | ||||||
| type FSLayer struct { | func (sm SignedManifest) Payload() (string, []byte, error) { | ||||||
| 	// BlobSum is the digest of the referenced filesystem image layer
 | 	return MediaTypeManifest, sm.all, nil | ||||||
| 	BlobSum digest.Digest `json:"blobSum"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // History stores unstructured v1 compatibility information
 | // Signatures returns the signatures as provided by
 | ||||||
| type History struct { | // (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
 | ||||||
| 	// V1Compatibility is the raw v1 compatibility information
 | // signatures.
 | ||||||
| 	V1Compatibility string `json:"v1Compatibility"` | func (sm *SignedManifest) Signatures() ([][]byte, error) { | ||||||
|  | 	jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Resolve the payload in the manifest.
 | ||||||
|  | 	return jsig.Signatures() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,15 +19,15 @@ type testEnv struct { | ||||||
| func TestManifestMarshaling(t *testing.T) { | func TestManifestMarshaling(t *testing.T) { | ||||||
| 	env := genEnv(t) | 	env := genEnv(t) | ||||||
| 
 | 
 | ||||||
| 	// Check that the Raw field is the same as json.MarshalIndent with these
 | 	// Check that the all field is the same as json.MarshalIndent with these
 | ||||||
| 	// parameters.
 | 	// parameters.
 | ||||||
| 	p, err := json.MarshalIndent(env.signed, "", "   ") | 	p, err := json.MarshalIndent(env.signed, "", "   ") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("error marshaling manifest: %v", err) | 		t.Fatalf("error marshaling manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !bytes.Equal(p, env.signed.Raw) { | 	if !bytes.Equal(p, env.signed.all) { | ||||||
| 		t.Fatalf("manifest bytes not equal: %q != %q", string(env.signed.Raw), string(p)) | 		t.Fatalf("manifest bytes not equal: %q != %q", string(env.signed.all), string(p)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -35,7 +35,7 @@ func TestManifestUnmarshaling(t *testing.T) { | ||||||
| 	env := genEnv(t) | 	env := genEnv(t) | ||||||
| 
 | 
 | ||||||
| 	var signed SignedManifest | 	var signed SignedManifest | ||||||
| 	if err := json.Unmarshal(env.signed.Raw, &signed); err != nil { | 	if err := json.Unmarshal(env.signed.all, &signed); err != nil { | ||||||
| 		t.Fatalf("error unmarshaling signed manifest: %v", err) | 		t.Fatalf("error unmarshaling signed manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31,8 +31,9 @@ func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return &SignedManifest{ | 	return &SignedManifest{ | ||||||
| 		Manifest: *m, | 		Manifest:  *m, | ||||||
| 		Raw:      pretty, | 		all:       pretty, | ||||||
|  | 		Canonical: p, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -60,7 +61,8 @@ func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certifica | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return &SignedManifest{ | 	return &SignedManifest{ | ||||||
| 		Manifest: *m, | 		Manifest:  *m, | ||||||
| 		Raw:      pretty, | 		all:       pretty, | ||||||
|  | 		Canonical: p, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import ( | ||||||
| // Verify verifies the signature of the signed manifest returning the public
 | // Verify verifies the signature of the signed manifest returning the public
 | ||||||
| // keys used during signing.
 | // keys used during signing.
 | ||||||
| func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) { | func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) { | ||||||
| 	js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") | 	js, err := libtrust.ParsePrettySignature(sm.all, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logrus.WithField("err", err).Debugf("(*SignedManifest).Verify") | 		logrus.WithField("err", err).Debugf("(*SignedManifest).Verify") | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -23,7 +23,7 @@ func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) { | ||||||
| // certificate pool returning the list of verified chains. Signatures without
 | // certificate pool returning the list of verified chains. Signatures without
 | ||||||
| // an x509 chain are not checked.
 | // an x509 chain are not checked.
 | ||||||
| func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) { | func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) { | ||||||
| 	js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") | 	js, err := libtrust.ParsePrettySignature(sm.all, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | package distribution | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | 	"github.com/docker/distribution/digest" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Manifest represents a registry object specifying a set of
 | ||||||
|  | // references and an optional target
 | ||||||
|  | type Manifest interface { | ||||||
|  | 	// References returns a list of objects which make up this manifest.
 | ||||||
|  | 	// The references are strictly ordered from base to head. A reference
 | ||||||
|  | 	// is anything which can be represented by a distribution.Descriptor
 | ||||||
|  | 	References() []Descriptor | ||||||
|  | 
 | ||||||
|  | 	// Payload provides the serialized format of the manifest, in addition to
 | ||||||
|  | 	// the mediatype.
 | ||||||
|  | 	Payload() (mediatype string, payload []byte, err error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ManifestBuilder creates a manifest allowing one to include dependencies.
 | ||||||
|  | // Instances can be obtained from a version-specific manifest package.  Manifest
 | ||||||
|  | // specific data is passed into the function which creates the builder.
 | ||||||
|  | type ManifestBuilder interface { | ||||||
|  | 	// Build creates the manifest from his builder.
 | ||||||
|  | 	Build() (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,
 | ||||||
|  | 	// which should be from base to head.
 | ||||||
|  | 	References() []Descriptor | ||||||
|  | 
 | ||||||
|  | 	// AppendReference includes the given object in the manifest after any
 | ||||||
|  | 	// existing dependencies. If the add fails, such as when adding an
 | ||||||
|  | 	// unsupported dependency, an error may be returned.
 | ||||||
|  | 	AppendReference(dependency Describable) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ManifestService describes operations on image manifests.
 | ||||||
|  | type ManifestService interface { | ||||||
|  | 	// Exists returns true if the manifest exists.
 | ||||||
|  | 	Exists(ctx context.Context, dgst digest.Digest) (bool, error) | ||||||
|  | 
 | ||||||
|  | 	// Get retrieves the manifest specified by the given digest
 | ||||||
|  | 	Get(ctx context.Context, dgst digest.Digest, options ...ManifestServiceOption) (Manifest, error) | ||||||
|  | 
 | ||||||
|  | 	// Put creates or updates the given manifest returning the manifest digest
 | ||||||
|  | 	Put(ctx context.Context, manifest Manifest, options ...ManifestServiceOption) (digest.Digest, error) | ||||||
|  | 
 | ||||||
|  | 	// Delete removes the manifest specified by the given digest. Deleting
 | ||||||
|  | 	// a manifest that doesn't exist will return ErrManifestNotFound
 | ||||||
|  | 	Delete(ctx context.Context, dgst digest.Digest) error | ||||||
|  | 
 | ||||||
|  | 	// Enumerate fills 'manifests' with the manifests in this service up
 | ||||||
|  | 	// to the size of 'manifests' and returns 'n' for the number of entries
 | ||||||
|  | 	// which were filled.  'last' contains an offset in the manifest set
 | ||||||
|  | 	// and can be used to resume iteration.
 | ||||||
|  | 	//Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error)
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Describable is an interface for descriptors
 | ||||||
|  | type Describable interface { | ||||||
|  | 	Descriptor() Descriptor | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ManifestMediaTypes returns the supported media types for manifests.
 | ||||||
|  | func ManifestMediaTypes() (mediaTypes []string) { | ||||||
|  | 	for t := range mappings { | ||||||
|  | 		mediaTypes = append(mediaTypes, t) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UnmarshalFunc implements manifest unmarshalling a given MediaType
 | ||||||
|  | type UnmarshalFunc func([]byte) (Manifest, Descriptor, error) | ||||||
|  | 
 | ||||||
|  | var mappings = make(map[string]UnmarshalFunc, 0) | ||||||
|  | 
 | ||||||
|  | // UnmarshalManifest looks up manifest unmarshall functions based on
 | ||||||
|  | // MediaType
 | ||||||
|  | func UnmarshalManifest(mediatype string, p []byte) (Manifest, Descriptor, error) { | ||||||
|  | 	unmarshalFunc, ok := mappings[mediatype] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype: %s", mediatype) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return unmarshalFunc(p) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RegisterManifestSchema registers an UnmarshalFunc for a given schema type.  This
 | ||||||
|  | // should be called from specific
 | ||||||
|  | func RegisterManifestSchema(mediatype string, u UnmarshalFunc) error { | ||||||
|  | 	if _, ok := mappings[mediatype]; ok { | ||||||
|  | 		return fmt.Errorf("manifest mediatype registration would overwrite existing: %s", mediatype) | ||||||
|  | 	} | ||||||
|  | 	mappings[mediatype] = u | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -7,7 +7,6 @@ import ( | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| 	"github.com/docker/distribution/uuid" | 	"github.com/docker/distribution/uuid" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -53,15 +52,15 @@ func NewRequestRecord(id string, r *http.Request) RequestRecord { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *bridge) ManifestPushed(repo string, sm *schema1.SignedManifest) error { | func (b *bridge) ManifestPushed(repo string, sm distribution.Manifest) error { | ||||||
| 	return b.createManifestEventAndWrite(EventActionPush, repo, sm) | 	return b.createManifestEventAndWrite(EventActionPush, repo, sm) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *bridge) ManifestPulled(repo string, sm *schema1.SignedManifest) error { | func (b *bridge) ManifestPulled(repo string, sm distribution.Manifest) error { | ||||||
| 	return b.createManifestEventAndWrite(EventActionPull, repo, sm) | 	return b.createManifestEventAndWrite(EventActionPull, repo, sm) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *bridge) ManifestDeleted(repo string, sm *schema1.SignedManifest) error { | func (b *bridge) ManifestDeleted(repo string, sm distribution.Manifest) error { | ||||||
| 	return b.createManifestEventAndWrite(EventActionDelete, repo, sm) | 	return b.createManifestEventAndWrite(EventActionDelete, repo, sm) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -77,7 +76,7 @@ func (b *bridge) BlobDeleted(repo string, desc distribution.Descriptor) error { | ||||||
| 	return b.createBlobEventAndWrite(EventActionDelete, repo, desc) | 	return b.createBlobEventAndWrite(EventActionDelete, repo, desc) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *bridge) createManifestEventAndWrite(action string, repo string, sm *schema1.SignedManifest) error { | func (b *bridge) createManifestEventAndWrite(action string, repo string, sm distribution.Manifest) error { | ||||||
| 	manifestEvent, err := b.createManifestEvent(action, repo, sm) | 	manifestEvent, err := b.createManifestEvent(action, repo, sm) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -86,21 +85,21 @@ func (b *bridge) createManifestEventAndWrite(action string, repo string, sm *sch | ||||||
| 	return b.sink.Write(*manifestEvent) | 	return b.sink.Write(*manifestEvent) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *bridge) createManifestEvent(action string, repo string, sm *schema1.SignedManifest) (*Event, error) { | func (b *bridge) createManifestEvent(action string, repo string, sm distribution.Manifest) (*Event, error) { | ||||||
| 	event := b.createEvent(action) | 	event := b.createEvent(action) | ||||||
| 	event.Target.MediaType = schema1.ManifestMediaType |  | ||||||
| 	event.Target.Repository = repo | 	event.Target.Repository = repo | ||||||
| 
 | 
 | ||||||
| 	p, err := sm.Payload() | 	mt, p, err := sm.Payload() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	event.Target.MediaType = mt | ||||||
| 	event.Target.Length = int64(len(p)) | 	event.Target.Length = int64(len(p)) | ||||||
| 	event.Target.Size = int64(len(p)) | 	event.Target.Size = int64(len(p)) | ||||||
| 	event.Target.Digest = digest.FromBytes(p) | 	event.Target.Digest = digest.FromBytes(p) | ||||||
| 
 | 
 | ||||||
| 	event.Target.URL, err = b.ub.BuildManifestURL(sm.Name, event.Target.Digest.String()) | 	event.Target.URL, err = b.ub.BuildManifestURL(repo, event.Target.Digest.String()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -85,7 +85,7 @@ func createTestEnv(t *testing.T, fn testSinkFn) Listener { | ||||||
| 		t.Fatalf("error signing manifest: %v", err) | 		t.Fatalf("error signing manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	payload, err = sm.Payload() | 	_, payload, err = sm.Payload() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("error getting manifest payload: %v", err) | 		t.Fatalf("error getting manifest payload: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -109,7 +109,7 @@ func checkCommonManifest(t *testing.T, action string, events ...Event) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if event.Target.URL != u { | 	if event.Target.URL != u { | ||||||
| 		t.Fatalf("incorrect url passed: %q != %q", event.Target.URL, u) | 		t.Fatalf("incorrect url passed: \n%q != \n%q", event.Target.URL, u) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -120,7 +120,7 @@ func TestEventEnvelopeJSONFormat(t *testing.T) { | ||||||
| 	manifestPush.Target.Digest = "sha256:0123456789abcdef0" | 	manifestPush.Target.Digest = "sha256:0123456789abcdef0" | ||||||
| 	manifestPush.Target.Length = 1 | 	manifestPush.Target.Length = 1 | ||||||
| 	manifestPush.Target.Size = 1 | 	manifestPush.Target.Size = 1 | ||||||
| 	manifestPush.Target.MediaType = schema1.ManifestMediaType | 	manifestPush.Target.MediaType = schema1.MediaTypeManifest | ||||||
| 	manifestPush.Target.Repository = "library/test" | 	manifestPush.Target.Repository = "library/test" | ||||||
| 	manifestPush.Target.URL = "http://example.com/v2/library/test/manifests/latest" | 	manifestPush.Target.URL = "http://example.com/v2/library/test/manifests/latest" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -75,12 +75,12 @@ func TestHTTPSink(t *testing.T) { | ||||||
| 		{ | 		{ | ||||||
| 			statusCode: http.StatusOK, | 			statusCode: http.StatusOK, | ||||||
| 			events: []Event{ | 			events: []Event{ | ||||||
| 				createTestEvent("push", "library/test", schema1.ManifestMediaType)}, | 				createTestEvent("push", "library/test", schema1.MediaTypeManifest)}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			statusCode: http.StatusOK, | 			statusCode: http.StatusOK, | ||||||
| 			events: []Event{ | 			events: []Event{ | ||||||
| 				createTestEvent("push", "library/test", schema1.ManifestMediaType), | 				createTestEvent("push", "library/test", schema1.MediaTypeManifest), | ||||||
| 				createTestEvent("push", "library/test", layerMediaType), | 				createTestEvent("push", "library/test", layerMediaType), | ||||||
| 				createTestEvent("push", "library/test", layerMediaType), | 				createTestEvent("push", "library/test", layerMediaType), | ||||||
| 			}, | 			}, | ||||||
|  |  | ||||||
|  | @ -7,18 +7,17 @@ import ( | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ManifestListener describes a set of methods for listening to events related to manifests.
 | // ManifestListener describes a set of methods for listening to events related to manifests.
 | ||||||
| type ManifestListener interface { | type ManifestListener interface { | ||||||
| 	ManifestPushed(repo string, sm *schema1.SignedManifest) error | 	ManifestPushed(repo string, sm distribution.Manifest) error | ||||||
| 	ManifestPulled(repo string, sm *schema1.SignedManifest) error | 	ManifestPulled(repo string, sm distribution.Manifest) error | ||||||
| 
 | 
 | ||||||
| 	// TODO(stevvooe): Please note that delete support is still a little shaky
 | 	// TODO(stevvooe): Please note that delete support is still a little shaky
 | ||||||
| 	// and we'll need to propagate these in the future.
 | 	// and we'll need to propagate these in the future.
 | ||||||
| 
 | 
 | ||||||
| 	ManifestDeleted(repo string, sm *schema1.SignedManifest) error | 	ManifestDeleted(repo string, sm distribution.Manifest) error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // BlobListener describes a listener that can respond to layer related events.
 | // BlobListener describes a listener that can respond to layer related events.
 | ||||||
|  | @ -74,8 +73,8 @@ type manifestServiceListener struct { | ||||||
| 	parent *repositoryListener | 	parent *repositoryListener | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (msl *manifestServiceListener) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { | func (msl *manifestServiceListener) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||||
| 	sm, err := msl.ManifestService.Get(dgst) | 	sm, err := msl.ManifestService.Get(ctx, dgst) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Name(), sm); err != nil { | 		if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Name(), sm); err != nil { | ||||||
| 			logrus.Errorf("error dispatching manifest pull to listener: %v", err) | 			logrus.Errorf("error dispatching manifest pull to listener: %v", err) | ||||||
|  | @ -85,8 +84,8 @@ func (msl *manifestServiceListener) Get(dgst digest.Digest) (*schema1.SignedMani | ||||||
| 	return sm, err | 	return sm, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (msl *manifestServiceListener) Put(sm *schema1.SignedManifest) error { | func (msl *manifestServiceListener) Put(ctx context.Context, sm distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||||
| 	err := msl.ManifestService.Put(sm) | 	dgst, err := msl.ManifestService.Put(ctx, sm, options...) | ||||||
| 
 | 
 | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		if err := msl.parent.listener.ManifestPushed(msl.parent.Repository.Name(), sm); err != nil { | 		if err := msl.parent.listener.ManifestPushed(msl.parent.Repository.Name(), sm); err != nil { | ||||||
|  | @ -94,18 +93,7 @@ func (msl *manifestServiceListener) Put(sm *schema1.SignedManifest) error { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return err | 	return dgst, err | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (msl *manifestServiceListener) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { |  | ||||||
| 	sm, err := msl.ManifestService.GetByTag(tag, options...) |  | ||||||
| 	if err == nil { |  | ||||||
| 		if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Name(), sm); err != nil { |  | ||||||
| 			logrus.Errorf("error dispatching manifest pull to listener: %v", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return sm, err |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type blobServiceListener struct { | type blobServiceListener struct { | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ func TestListener(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	expectedOps := map[string]int{ | 	expectedOps := map[string]int{ | ||||||
| 		"manifest:push": 1, | 		"manifest:push": 1, | ||||||
| 		"manifest:pull": 2, | 		"manifest:pull": 1, | ||||||
| 		// "manifest:delete": 0, // deletes not supported for now
 | 		// "manifest:delete": 0, // deletes not supported for now
 | ||||||
| 		"layer:push": 2, | 		"layer:push": 2, | ||||||
| 		"layer:pull": 2, | 		"layer:pull": 2, | ||||||
|  | @ -55,18 +55,18 @@ type testListener struct { | ||||||
| 	ops map[string]int | 	ops map[string]int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tl *testListener) ManifestPushed(repo string, sm *schema1.SignedManifest) error { | func (tl *testListener) ManifestPushed(repo string, m distribution.Manifest) error { | ||||||
| 	tl.ops["manifest:push"]++ | 	tl.ops["manifest:push"]++ | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tl *testListener) ManifestPulled(repo string, sm *schema1.SignedManifest) error { | func (tl *testListener) ManifestPulled(repo string, m distribution.Manifest) error { | ||||||
| 	tl.ops["manifest:pull"]++ | 	tl.ops["manifest:pull"]++ | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tl *testListener) ManifestDeleted(repo string, sm *schema1.SignedManifest) error { | func (tl *testListener) ManifestDeleted(repo string, m distribution.Manifest) error { | ||||||
| 	tl.ops["manifest:delete"]++ | 	tl.ops["manifest:delete"]++ | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | @ -93,8 +93,11 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository) { | ||||||
| 	// takes the registry through a common set of operations. This could be
 | 	// takes the registry through a common set of operations. This could be
 | ||||||
| 	// used to make cross-cutting updates by changing internals that affect
 | 	// used to make cross-cutting updates by changing internals that affect
 | ||||||
| 	// update counts. Basically, it would make writing tests a lot easier.
 | 	// update counts. Basically, it would make writing tests a lot easier.
 | ||||||
|  | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	tag := "thetag" | 	tag := "thetag" | ||||||
|  | 	// todo: change this to use Builder
 | ||||||
|  | 
 | ||||||
| 	m := schema1.Manifest{ | 	m := schema1.Manifest{ | ||||||
| 		Versioned: manifest.Versioned{ | 		Versioned: manifest.Versioned{ | ||||||
| 			SchemaVersion: 1, | 			SchemaVersion: 1, | ||||||
|  | @ -158,31 +161,19 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository) { | ||||||
| 		t.Fatal(err.Error()) | 		t.Fatal(err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = manifests.Put(sm); err != nil { | 	var digestPut digest.Digest | ||||||
|  | 	if digestPut, err = manifests.Put(ctx, sm); err != nil { | ||||||
| 		t.Fatalf("unexpected error putting the manifest: %v", err) | 		t.Fatalf("unexpected error putting the manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	p, err := sm.Payload() | 	dgst := digest.FromBytes(sm.Canonical) | ||||||
| 	if err != nil { | 	if dgst != digestPut { | ||||||
| 		t.Fatalf("unexpected error getting manifest payload: %v", err) | 		t.Fatalf("mismatching digest from payload and put") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	dgst := digest.FromBytes(p) | 	_, err = manifests.Get(ctx, dgst) | ||||||
| 	fetchedByManifest, err := manifests.Get(dgst) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error fetching manifest: %v", err) | 		t.Fatalf("unexpected error fetching manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if fetchedByManifest.Tag != sm.Tag { |  | ||||||
| 		t.Fatalf("retrieved unexpected manifest: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	fetched, err := manifests.GetByTag(tag) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("unexpected error fetching manifest: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if fetched.Tag != fetchedByManifest.Tag { |  | ||||||
| 		t.Fatalf("retrieved unexpected manifest: %v", err) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										59
									
								
								registry.go
								
								
								
								
							
							
						
						
									
										59
									
								
								registry.go
								
								
								
								
							|  | @ -2,8 +2,6 @@ package distribution | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" |  | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Scope defines the set of items that match a namespace.
 | // Scope defines the set of items that match a namespace.
 | ||||||
|  | @ -44,7 +42,9 @@ type Namespace interface { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ManifestServiceOption is a function argument for Manifest Service methods
 | // ManifestServiceOption is a function argument for Manifest Service methods
 | ||||||
| type ManifestServiceOption func(ManifestService) error | type ManifestServiceOption interface { | ||||||
|  | 	Apply(ManifestService) error | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // Repository is a named collection of manifests and layers.
 | // Repository is a named collection of manifests and layers.
 | ||||||
| type Repository interface { | type Repository interface { | ||||||
|  | @ -62,59 +62,10 @@ type Repository interface { | ||||||
| 	// be a BlobService for use with clients. This will allow such
 | 	// be a BlobService for use with clients. This will allow such
 | ||||||
| 	// implementations to avoid implementing ServeBlob.
 | 	// implementations to avoid implementing ServeBlob.
 | ||||||
| 
 | 
 | ||||||
| 	// Signatures returns a reference to this repository's signatures service.
 | 	// Tags returns a reference to this repositories tag service
 | ||||||
| 	Signatures() SignatureService | 	Tags(ctx context.Context) TagService | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO(stevvooe): Must add close methods to all these. May want to change the
 | // TODO(stevvooe): Must add close methods to all these. May want to change the
 | ||||||
| // way instances are created to better reflect internal dependency
 | // way instances are created to better reflect internal dependency
 | ||||||
| // relationships.
 | // relationships.
 | ||||||
| 
 |  | ||||||
| // ManifestService provides operations on image manifests.
 |  | ||||||
| type ManifestService interface { |  | ||||||
| 	// Exists returns true if the manifest exists.
 |  | ||||||
| 	Exists(dgst digest.Digest) (bool, error) |  | ||||||
| 
 |  | ||||||
| 	// Get retrieves the identified by the digest, if it exists.
 |  | ||||||
| 	Get(dgst digest.Digest) (*schema1.SignedManifest, error) |  | ||||||
| 
 |  | ||||||
| 	// Delete removes the manifest, if it exists.
 |  | ||||||
| 	Delete(dgst digest.Digest) error |  | ||||||
| 
 |  | ||||||
| 	// Put creates or updates the manifest.
 |  | ||||||
| 	Put(manifest *schema1.SignedManifest) error |  | ||||||
| 
 |  | ||||||
| 	// TODO(stevvooe): The methods after this message should be moved to a
 |  | ||||||
| 	// discrete TagService, per active proposals.
 |  | ||||||
| 
 |  | ||||||
| 	// Tags lists the tags under the named repository.
 |  | ||||||
| 	Tags() ([]string, error) |  | ||||||
| 
 |  | ||||||
| 	// ExistsByTag returns true if the manifest exists.
 |  | ||||||
| 	ExistsByTag(tag string) (bool, error) |  | ||||||
| 
 |  | ||||||
| 	// GetByTag retrieves the named manifest, if it exists.
 |  | ||||||
| 	GetByTag(tag string, options ...ManifestServiceOption) (*schema1.SignedManifest, error) |  | ||||||
| 
 |  | ||||||
| 	// TODO(stevvooe): There are several changes that need to be done to this
 |  | ||||||
| 	// interface:
 |  | ||||||
| 	//
 |  | ||||||
| 	//	1. Allow explicit tagging with Tag(digest digest.Digest, tag string)
 |  | ||||||
| 	//	2. Support reading tags with a re-entrant reader to avoid large
 |  | ||||||
| 	//       allocations in the registry.
 |  | ||||||
| 	//	3. Long-term: Provide All() method that lets one scroll through all of
 |  | ||||||
| 	//       the manifest entries.
 |  | ||||||
| 	//	4. Long-term: break out concept of signing from manifests. This is
 |  | ||||||
| 	//       really a part of the distribution sprint.
 |  | ||||||
| 	//	5. Long-term: Manifest should be an interface. This code shouldn't
 |  | ||||||
| 	//       really be concerned with the storage format.
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SignatureService provides operations on signatures.
 |  | ||||||
| type SignatureService interface { |  | ||||||
| 	// Get retrieves all of the signature blobs for the specified digest.
 |  | ||||||
| 	Get(dgst digest.Digest) ([][]byte, error) |  | ||||||
| 
 |  | ||||||
| 	// Put stores the signature for the provided digest.
 |  | ||||||
| 	Put(dgst digest.Digest, signatures ...[]byte) error |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -495,7 +495,7 @@ var routeDescriptors = []RouteDescriptor{ | ||||||
| 		Methods: []MethodDescriptor{ | 		Methods: []MethodDescriptor{ | ||||||
| 			{ | 			{ | ||||||
| 				Method:      "GET", | 				Method:      "GET", | ||||||
| 				Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest.", | 				Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", | ||||||
| 				Requests: []RequestDescriptor{ | 				Requests: []RequestDescriptor{ | ||||||
| 					{ | 					{ | ||||||
| 						Headers: []ParameterDescriptor{ | 						Headers: []ParameterDescriptor{ | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package client | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | @ -14,7 +15,6 @@ import ( | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/docker/distribution/reference" | ||||||
| 	"github.com/docker/distribution/registry/api/v2" | 	"github.com/docker/distribution/registry/api/v2" | ||||||
| 	"github.com/docker/distribution/registry/client/transport" | 	"github.com/docker/distribution/registry/client/transport" | ||||||
|  | @ -156,26 +156,139 @@ func (r *repository) Manifests(ctx context.Context, options ...distribution.Mani | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *repository) Signatures() distribution.SignatureService { | func (r *repository) Tags(ctx context.Context) distribution.TagService { | ||||||
| 	ms, _ := r.Manifests(r.context) | 	return &tags{ | ||||||
| 	return &signatures{ | 		client:  r.client, | ||||||
| 		manifests: ms, | 		ub:      r.ub, | ||||||
|  | 		context: r.context, | ||||||
|  | 		name:    r.Name(), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type signatures struct { | // tags implements remote tagging operations.
 | ||||||
| 	manifests distribution.ManifestService | type tags struct { | ||||||
|  | 	client  *http.Client | ||||||
|  | 	ub      *v2.URLBuilder | ||||||
|  | 	context context.Context | ||||||
|  | 	name    string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *signatures) Get(dgst digest.Digest) ([][]byte, error) { | // All returns all tags
 | ||||||
| 	m, err := s.manifests.Get(dgst) | func (t *tags) All(ctx context.Context) ([]string, error) { | ||||||
|  | 	var tags []string | ||||||
|  | 
 | ||||||
|  | 	u, err := t.ub.BuildTagsURL(t.name) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return tags, err | ||||||
| 	} | 	} | ||||||
| 	return m.Signatures() | 
 | ||||||
|  | 	resp, err := t.client.Get(u) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return tags, err | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	if SuccessStatus(resp.StatusCode) { | ||||||
|  | 		b, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return tags, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tagsResponse := struct { | ||||||
|  | 			Tags []string `json:"tags"` | ||||||
|  | 		}{} | ||||||
|  | 		if err := json.Unmarshal(b, &tagsResponse); err != nil { | ||||||
|  | 			return tags, err | ||||||
|  | 		} | ||||||
|  | 		tags = tagsResponse.Tags | ||||||
|  | 		return tags, nil | ||||||
|  | 	} | ||||||
|  | 	return tags, handleErrorResponse(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *signatures) Put(dgst digest.Digest, signatures ...[]byte) error { | func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) { | ||||||
|  | 	desc := distribution.Descriptor{} | ||||||
|  | 	headers := response.Header | ||||||
|  | 
 | ||||||
|  | 	ctHeader := headers.Get("Content-Type") | ||||||
|  | 	if ctHeader == "" { | ||||||
|  | 		return distribution.Descriptor{}, errors.New("missing or empty Content-Type header") | ||||||
|  | 	} | ||||||
|  | 	desc.MediaType = ctHeader | ||||||
|  | 
 | ||||||
|  | 	digestHeader := headers.Get("Docker-Content-Digest") | ||||||
|  | 	if digestHeader == "" { | ||||||
|  | 		bytes, err := ioutil.ReadAll(response.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return distribution.Descriptor{}, err | ||||||
|  | 		} | ||||||
|  | 		_, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return distribution.Descriptor{}, err | ||||||
|  | 		} | ||||||
|  | 		return desc, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dgst, err := digest.ParseDigest(digestHeader) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return distribution.Descriptor{}, err | ||||||
|  | 	} | ||||||
|  | 	desc.Digest = dgst | ||||||
|  | 
 | ||||||
|  | 	lengthHeader := headers.Get("Content-Length") | ||||||
|  | 	if lengthHeader == "" { | ||||||
|  | 		return distribution.Descriptor{}, errors.New("missing or empty Content-Length header") | ||||||
|  | 	} | ||||||
|  | 	length, err := strconv.ParseInt(lengthHeader, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return distribution.Descriptor{}, err | ||||||
|  | 	} | ||||||
|  | 	desc.Size = length | ||||||
|  | 
 | ||||||
|  | 	return desc, nil | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get issues a HEAD request for a Manifest against its named endpoint in order
 | ||||||
|  | // to construct a descriptor for the tag.  If the registry doesn't support HEADing
 | ||||||
|  | // a manifest, fallback to GET.
 | ||||||
|  | func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { | ||||||
|  | 	u, err := t.ub.BuildManifestURL(t.name, tag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return distribution.Descriptor{}, err | ||||||
|  | 	} | ||||||
|  | 	var attempts int | ||||||
|  | 	resp, err := t.client.Head(u) | ||||||
|  | 
 | ||||||
|  | check: | ||||||
|  | 	if err != nil { | ||||||
|  | 		return distribution.Descriptor{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch { | ||||||
|  | 	case resp.StatusCode >= 200 && resp.StatusCode < 400: | ||||||
|  | 		return descriptorFromResponse(resp) | ||||||
|  | 	case resp.StatusCode == http.StatusMethodNotAllowed: | ||||||
|  | 		resp, err = t.client.Get(u) | ||||||
|  | 		attempts++ | ||||||
|  | 		if attempts > 1 { | ||||||
|  | 			return distribution.Descriptor{}, err | ||||||
|  | 		} | ||||||
|  | 		goto check | ||||||
|  | 	default: | ||||||
|  | 		return distribution.Descriptor{}, handleErrorResponse(resp) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t *tags) Untag(ctx context.Context, tag string) error { | ||||||
| 	panic("not implemented") | 	panic("not implemented") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -186,44 +299,8 @@ type manifests struct { | ||||||
| 	etags  map[string]string | 	etags  map[string]string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifests) Tags() ([]string, error) { | func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { | ||||||
| 	u, err := ms.ub.BuildTagsURL(ms.name) | 	u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	resp, err := ms.client.Get(u) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	defer resp.Body.Close() |  | ||||||
| 
 |  | ||||||
| 	if SuccessStatus(resp.StatusCode) { |  | ||||||
| 		b, err := ioutil.ReadAll(resp.Body) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		tagsResponse := struct { |  | ||||||
| 			Tags []string `json:"tags"` |  | ||||||
| 		}{} |  | ||||||
| 		if err := json.Unmarshal(b, &tagsResponse); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return tagsResponse.Tags, nil |  | ||||||
| 	} |  | ||||||
| 	return nil, handleErrorResponse(resp) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *manifests) Exists(dgst digest.Digest) (bool, error) { |  | ||||||
| 	// Call by Tag endpoint since the API uses the same
 |  | ||||||
| 	// URL endpoint for tags and digests.
 |  | ||||||
| 	return ms.ExistsByTag(dgst.String()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *manifests) ExistsByTag(tag string) (bool, error) { |  | ||||||
| 	u, err := ms.ub.BuildManifestURL(ms.name, tag) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  | @ -241,46 +318,63 @@ func (ms *manifests) ExistsByTag(tag string) (bool, error) { | ||||||
| 	return false, handleErrorResponse(resp) | 	return false, handleErrorResponse(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifests) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { | // AddEtagToTag allows a client to supply an eTag to Get which will be
 | ||||||
| 	// Call by Tag endpoint since the API uses the same
 |  | ||||||
| 	// URL endpoint for tags and digests.
 |  | ||||||
| 	return ms.GetByTag(dgst.String()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // AddEtagToTag allows a client to supply an eTag to GetByTag which will be
 |  | ||||||
| // used for a conditional HTTP request.  If the eTag matches, a nil manifest
 | // used for a conditional HTTP request.  If the eTag matches, a nil manifest
 | ||||||
| // and nil error will be returned. etag is automatically quoted when added to
 | // and ErrManifestNotModified error will be returned. etag is automatically
 | ||||||
| // this map.
 | // quoted when added to this map.
 | ||||||
| func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { | func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { | ||||||
| 	return func(ms distribution.ManifestService) error { | 	return etagOption{tag, etag} | ||||||
| 		if ms, ok := ms.(*manifests); ok { |  | ||||||
| 			ms.etags[tag] = fmt.Sprintf(`"%s"`, etag) |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 		return fmt.Errorf("etag options is a client-only option") |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { | type etagOption struct{ tag, etag string } | ||||||
|  | 
 | ||||||
|  | func (o etagOption) Apply(ms distribution.ManifestService) error { | ||||||
|  | 	if ms, ok := ms.(*manifests); ok { | ||||||
|  | 		ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return fmt.Errorf("etag options is a client-only option") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||||
|  | 
 | ||||||
|  | 	var tag string | ||||||
| 	for _, option := range options { | 	for _, option := range options { | ||||||
| 		err := option(ms) | 		if opt, ok := option.(withTagOption); ok { | ||||||
| 		if err != nil { | 			tag = opt.tag | ||||||
| 			return nil, err | 		} else { | ||||||
|  | 			err := option.Apply(ms) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	u, err := ms.ub.BuildManifestURL(ms.name, tag) | 	var ref string | ||||||
|  | 	if tag != "" { | ||||||
|  | 		ref = tag | ||||||
|  | 	} else { | ||||||
|  | 		ref = dgst.String() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	u, err := ms.ub.BuildManifestURL(ms.name, ref) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	req, err := http.NewRequest("GET", u, nil) | 	req, err := http.NewRequest("GET", u, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, ok := ms.etags[tag]; ok { | 	for _, t := range distribution.ManifestMediaTypes() { | ||||||
| 		req.Header.Set("If-None-Match", ms.etags[tag]) | 		req.Header.Add("Accept", t) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, ok := ms.etags[ref]; ok { | ||||||
|  | 		req.Header.Set("If-None-Match", ms.etags[ref]) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	resp, err := ms.client.Do(req) | 	resp, err := ms.client.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -289,44 +383,89 @@ func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServic | ||||||
| 	if resp.StatusCode == http.StatusNotModified { | 	if resp.StatusCode == http.StatusNotModified { | ||||||
| 		return nil, distribution.ErrManifestNotModified | 		return nil, distribution.ErrManifestNotModified | ||||||
| 	} else if SuccessStatus(resp.StatusCode) { | 	} else if SuccessStatus(resp.StatusCode) { | ||||||
| 		var sm schema1.SignedManifest | 		mt := resp.Header.Get("Content-Type") | ||||||
| 		decoder := json.NewDecoder(resp.Body) | 		body, err := ioutil.ReadAll(resp.Body) | ||||||
| 
 | 
 | ||||||
| 		if err := decoder.Decode(&sm); err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		return &sm, nil | 		m, _, err := distribution.UnmarshalManifest(mt, body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return m, nil | ||||||
| 	} | 	} | ||||||
| 	return nil, handleErrorResponse(resp) | 	return nil, handleErrorResponse(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifests) Put(m *schema1.SignedManifest) error { | // WithTag allows a tag to be passed into Put which enables the client
 | ||||||
| 	manifestURL, err := ms.ub.BuildManifestURL(ms.name, m.Tag) | // to build a correct URL.
 | ||||||
| 	if err != nil { | func WithTag(tag string) distribution.ManifestServiceOption { | ||||||
| 		return err | 	return withTagOption{tag} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type withTagOption struct{ tag string } | ||||||
|  | 
 | ||||||
|  | func (o withTagOption) Apply(m distribution.ManifestService) error { | ||||||
|  | 	if _, ok := m.(*manifests); ok { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return fmt.Errorf("withTagOption is a client-only option") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Put puts a manifest.  A tag can be specified using an options parameter which uses some shared state to hold the
 | ||||||
|  | // tag name in order to build the correct upload URL.  This state is written and read under a lock.
 | ||||||
|  | func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||||
|  | 	var tag string | ||||||
|  | 
 | ||||||
|  | 	for _, option := range options { | ||||||
|  | 		if opt, ok := option.(withTagOption); ok { | ||||||
|  | 			tag = opt.tag | ||||||
|  | 		} else { | ||||||
|  | 			err := option.Apply(ms) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// todo(richardscothern): do something with options here when they become applicable
 | 	manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag) | ||||||
| 
 |  | ||||||
| 	putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(m.Raw)) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	mediaType, p, err := m.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	putRequest.Header.Set("Content-Type", mediaType) | ||||||
|  | 
 | ||||||
| 	resp, err := ms.client.Do(putRequest) | 	resp, err := ms.client.Do(putRequest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 
 | 
 | ||||||
| 	if SuccessStatus(resp.StatusCode) { | 	if SuccessStatus(resp.StatusCode) { | ||||||
| 		// TODO(dmcgowan): make use of digest header
 | 		dgstHeader := resp.Header.Get("Docker-Content-Digest") | ||||||
| 		return nil | 		dgst, err := digest.ParseDigest(dgstHeader) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return dgst, nil | ||||||
| 	} | 	} | ||||||
| 	return handleErrorResponse(resp) | 
 | ||||||
|  | 	return "", handleErrorResponse(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifests) Delete(dgst digest.Digest) error { | func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { | ||||||
| 	u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) | 	u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -348,6 +487,11 @@ func (ms *manifests) Delete(dgst digest.Digest) error { | ||||||
| 	return handleErrorResponse(resp) | 	return handleErrorResponse(resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // todo(richardscothern): Restore interface and implementation with merge of #1050
 | ||||||
|  | /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | ||||||
|  | 	panic("not supported") | ||||||
|  | }*/ | ||||||
|  | 
 | ||||||
| type blobs struct { | type blobs struct { | ||||||
| 	name   string | 	name   string | ||||||
| 	ub     *v2.URLBuilder | 	ub     *v2.URLBuilder | ||||||
|  |  | ||||||
|  | @ -42,7 +42,6 @@ func newRandomBlob(size int) (digest.Digest, []byte) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { | func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { | ||||||
| 
 |  | ||||||
| 	*m = append(*m, testutil.RequestResponseMapping{ | 	*m = append(*m, testutil.RequestResponseMapping{ | ||||||
| 		Request: testutil.Request{ | 		Request: testutil.Request{ | ||||||
| 			Method: "GET", | 			Method: "GET", | ||||||
|  | @ -499,12 +498,7 @@ func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*schema1.Signed | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	p, err := sm.Payload() | 	return sm, digest.FromBytes(sm.Canonical), sm.Canonical | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return sm, digest.FromBytes(p), p |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) { | func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) { | ||||||
|  | @ -525,6 +519,7 @@ func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil | ||||||
| 			Headers: http.Header(map[string][]string{ | 			Headers: http.Header(map[string][]string{ | ||||||
| 				"Content-Length": {"0"}, | 				"Content-Length": {"0"}, | ||||||
| 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | ||||||
|  | 				"Content-Type":   {schema1.MediaTypeManifest}, | ||||||
| 			}), | 			}), | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
|  | @ -534,6 +529,7 @@ func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil | ||||||
| 			Headers: http.Header(map[string][]string{ | 			Headers: http.Header(map[string][]string{ | ||||||
| 				"Content-Length": {fmt.Sprint(len(content))}, | 				"Content-Length": {fmt.Sprint(len(content))}, | ||||||
| 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | ||||||
|  | 				"Content-Type":   {schema1.MediaTypeManifest}, | ||||||
| 			}), | 			}), | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -553,6 +549,7 @@ func addTestManifest(repo, reference string, content []byte, m *testutil.Request | ||||||
| 			Headers: http.Header(map[string][]string{ | 			Headers: http.Header(map[string][]string{ | ||||||
| 				"Content-Length": {fmt.Sprint(len(content))}, | 				"Content-Length": {fmt.Sprint(len(content))}, | ||||||
| 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | ||||||
|  | 				"Content-Type":   {schema1.MediaTypeManifest}, | ||||||
| 			}), | 			}), | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  | @ -566,6 +563,7 @@ func addTestManifest(repo, reference string, content []byte, m *testutil.Request | ||||||
| 			Headers: http.Header(map[string][]string{ | 			Headers: http.Header(map[string][]string{ | ||||||
| 				"Content-Length": {fmt.Sprint(len(content))}, | 				"Content-Length": {fmt.Sprint(len(content))}, | ||||||
| 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | ||||||
|  | 				"Content-Type":   {schema1.MediaTypeManifest}, | ||||||
| 			}), | 			}), | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  | @ -598,12 +596,17 @@ func checkEqualManifest(m1, m2 *schema1.SignedManifest) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestManifestFetch(t *testing.T) { | func TestV1ManifestFetch(t *testing.T) { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	repo := "test.example.com/repo" | 	repo := "test.example.com/repo" | ||||||
| 	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) | 	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) | ||||||
| 	var m testutil.RequestResponseMap | 	var m testutil.RequestResponseMap | ||||||
| 	addTestManifest(repo, dgst.String(), m1.Raw, &m) | 	_, pl, err := m1.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	addTestManifest(repo, dgst.String(), pl, &m) | ||||||
|  | 	addTestManifest(repo, "latest", pl, &m) | ||||||
| 
 | 
 | ||||||
| 	e, c := testServer(m) | 	e, c := testServer(m) | ||||||
| 	defer c() | 	defer c() | ||||||
|  | @ -617,7 +620,7 @@ func TestManifestFetch(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ok, err := ms.Exists(dgst) | 	ok, err := ms.Exists(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -625,11 +628,29 @@ func TestManifestFetch(t *testing.T) { | ||||||
| 		t.Fatal("Manifest does not exist") | 		t.Fatal("Manifest does not exist") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	manifest, err := ms.Get(dgst) | 	manifest, err := ms.Get(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	if err := checkEqualManifest(manifest, m1); err != nil { | 	v1manifest, ok := manifest.(*schema1.SignedManifest) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("Unexpected manifest type from Get: %T", manifest) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := checkEqualManifest(v1manifest, m1); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	manifest, err = ms.Get(ctx, dgst, WithTag("latest")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	v1manifest, ok = manifest.(*schema1.SignedManifest) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("Unexpected manifest type from Get: %T", manifest) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err = checkEqualManifest(v1manifest, m1); err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -643,17 +664,22 @@ func TestManifestFetchWithEtag(t *testing.T) { | ||||||
| 	e, c := testServer(m) | 	e, c := testServer(m) | ||||||
| 	defer c() | 	defer c() | ||||||
| 
 | 
 | ||||||
| 	r, err := NewRepository(context.Background(), repo, e, nil) | 	ctx := context.Background() | ||||||
|  | 	r, err := NewRepository(ctx, repo, e, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	ctx := context.Background() | 
 | ||||||
| 	ms, err := r.Manifests(ctx) | 	ms, err := r.Manifests(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	_, err = ms.GetByTag("latest", AddEtagToTag("latest", d1.String())) | 	clientManifestService, ok := ms.(*manifests) | ||||||
|  | 	if !ok { | ||||||
|  | 		panic("wrong type for client manifest service") | ||||||
|  | 	} | ||||||
|  | 	_, err = clientManifestService.Get(ctx, d1, WithTag("latest"), AddEtagToTag("latest", d1.String())) | ||||||
| 	if err != distribution.ErrManifestNotModified { | 	if err != distribution.ErrManifestNotModified { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -690,10 +716,10 @@ func TestManifestDelete(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := ms.Delete(dgst1); err != nil { | 	if err := ms.Delete(ctx, dgst1); err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	if err := ms.Delete(dgst2); err == nil { | 	if err := ms.Delete(ctx, dgst2); err == nil { | ||||||
| 		t.Fatal("Expected error deleting unknown manifest") | 		t.Fatal("Expected error deleting unknown manifest") | ||||||
| 	} | 	} | ||||||
| 	// TODO(dmcgowan): Check for specific unknown error
 | 	// TODO(dmcgowan): Check for specific unknown error
 | ||||||
|  | @ -702,12 +728,17 @@ func TestManifestDelete(t *testing.T) { | ||||||
| func TestManifestPut(t *testing.T) { | func TestManifestPut(t *testing.T) { | ||||||
| 	repo := "test.example.com/repo/delete" | 	repo := "test.example.com/repo/delete" | ||||||
| 	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6) | 	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6) | ||||||
|  | 
 | ||||||
|  | 	_, payload, err := m1.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
| 	var m testutil.RequestResponseMap | 	var m testutil.RequestResponseMap | ||||||
| 	m = append(m, testutil.RequestResponseMapping{ | 	m = append(m, testutil.RequestResponseMapping{ | ||||||
| 		Request: testutil.Request{ | 		Request: testutil.Request{ | ||||||
| 			Method: "PUT", | 			Method: "PUT", | ||||||
| 			Route:  "/v2/" + repo + "/manifests/other", | 			Route:  "/v2/" + repo + "/manifests/other", | ||||||
| 			Body:   m1.Raw, | 			Body:   payload, | ||||||
| 		}, | 		}, | ||||||
| 		Response: testutil.Response{ | 		Response: testutil.Response{ | ||||||
| 			StatusCode: http.StatusAccepted, | 			StatusCode: http.StatusAccepted, | ||||||
|  | @ -731,7 +762,7 @@ func TestManifestPut(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := ms.Put(m1); err != nil { | 	if _, err := ms.Put(ctx, m1, WithTag(m1.Tag)); err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -751,21 +782,22 @@ func TestManifestTags(t *testing.T) { | ||||||
| } | } | ||||||
| 	`)) | 	`)) | ||||||
| 	var m testutil.RequestResponseMap | 	var m testutil.RequestResponseMap | ||||||
| 	m = append(m, testutil.RequestResponseMapping{ | 	for i := 0; i < 3; i++ { | ||||||
| 		Request: testutil.Request{ | 		m = append(m, testutil.RequestResponseMapping{ | ||||||
| 			Method: "GET", | 			Request: testutil.Request{ | ||||||
| 			Route:  "/v2/" + repo + "/tags/list", | 				Method: "GET", | ||||||
| 		}, | 				Route:  "/v2/" + repo + "/tags/list", | ||||||
| 		Response: testutil.Response{ | 			}, | ||||||
| 			StatusCode: http.StatusOK, | 			Response: testutil.Response{ | ||||||
| 			Body:       tagsList, | 				StatusCode: http.StatusOK, | ||||||
| 			Headers: http.Header(map[string][]string{ | 				Body:       tagsList, | ||||||
| 				"Content-Length": {fmt.Sprint(len(tagsList))}, | 				Headers: http.Header(map[string][]string{ | ||||||
| 				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | 					"Content-Length": {fmt.Sprint(len(tagsList))}, | ||||||
| 			}), | 					"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, | ||||||
| 		}, | 				}), | ||||||
| 	}) | 			}, | ||||||
| 
 | 		}) | ||||||
|  | 	} | ||||||
| 	e, c := testServer(m) | 	e, c := testServer(m) | ||||||
| 	defer c() | 	defer c() | ||||||
| 
 | 
 | ||||||
|  | @ -773,22 +805,29 @@ func TestManifestTags(t *testing.T) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ms, err := r.Manifests(ctx) | 	tagService := r.Tags(ctx) | ||||||
|  | 
 | ||||||
|  | 	tags, err := tagService.All(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	tags, err := ms.Tags() |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(tags) != 3 { | 	if len(tags) != 3 { | ||||||
| 		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags)) | 		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags)) | ||||||
| 	} | 	} | ||||||
| 	// TODO(dmcgowan): Check array
 |  | ||||||
| 
 | 
 | ||||||
|  | 	expected := map[string]struct{}{ | ||||||
|  | 		"tag1":   {}, | ||||||
|  | 		"tag2":   {}, | ||||||
|  | 		"funtag": {}, | ||||||
|  | 	} | ||||||
|  | 	for _, t := range tags { | ||||||
|  | 		delete(expected, t) | ||||||
|  | 	} | ||||||
|  | 	if len(expected) != 0 { | ||||||
|  | 		t.Fatalf("unexpected tags returned: %v", expected) | ||||||
|  | 	} | ||||||
| 	// TODO(dmcgowan): Check for error cases
 | 	// TODO(dmcgowan): Check for error cases
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -821,7 +860,7 @@ func TestManifestUnauthorized(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	_, err = ms.Get(dgst) | 	_, err = ms.Get(ctx, dgst) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Fatal("Expected error fetching manifest") | 		t.Fatal("Expected error fetching manifest") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -871,19 +871,15 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | ||||||
| 		t.Fatalf("unexpected error signing manifest: %v", err) | 		t.Fatalf("unexpected error signing manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	payload, err := signedManifest.Payload() | 	dgst := digest.FromBytes(signedManifest.Canonical) | ||||||
| 	checkErr(t, err, "getting manifest payload") |  | ||||||
| 
 |  | ||||||
| 	dgst := digest.FromBytes(payload) |  | ||||||
| 
 |  | ||||||
| 	args.signedManifest = signedManifest | 	args.signedManifest = signedManifest | ||||||
| 	args.dgst = dgst | 	args.dgst = dgst | ||||||
| 
 | 
 | ||||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) | ||||||
| 	checkErr(t, err, "building manifest url") | 	checkErr(t, err, "building manifest url") | ||||||
| 
 | 
 | ||||||
| 	resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest) | 	resp = putManifest(t, "putting signed manifest no error", manifestURL, signedManifest) | ||||||
| 	checkResponse(t, "putting signed manifest", resp, http.StatusCreated) | 	checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated) | ||||||
| 	checkHeaders(t, resp, http.Header{ | 	checkHeaders(t, resp, http.Header{ | ||||||
| 		"Location":              []string{manifestDigestURL}, | 		"Location":              []string{manifestDigestURL}, | ||||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | 		"Docker-Content-Digest": []string{dgst.String()}, | ||||||
|  | @ -914,11 +910,12 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | ||||||
| 
 | 
 | ||||||
| 	var fetchedManifest schema1.SignedManifest | 	var fetchedManifest schema1.SignedManifest | ||||||
| 	dec := json.NewDecoder(resp.Body) | 	dec := json.NewDecoder(resp.Body) | ||||||
|  | 
 | ||||||
| 	if err := dec.Decode(&fetchedManifest); err != nil { | 	if err := dec.Decode(&fetchedManifest); err != nil { | ||||||
| 		t.Fatalf("error decoding fetched manifest: %v", err) | 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) { | 	if !bytes.Equal(fetchedManifest.Canonical, signedManifest.Canonical) { | ||||||
| 		t.Fatalf("manifests do not match") | 		t.Fatalf("manifests do not match") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -940,10 +937,55 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | ||||||
| 		t.Fatalf("error decoding fetched manifest: %v", err) | 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !bytes.Equal(fetchedManifestByDigest.Raw, signedManifest.Raw) { | 	if !bytes.Equal(fetchedManifestByDigest.Canonical, signedManifest.Canonical) { | ||||||
| 		t.Fatalf("manifests do not match") | 		t.Fatalf("manifests do not match") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// check signature was roundtripped
 | ||||||
|  | 	signatures, err := fetchedManifestByDigest.Signatures() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(signatures) != 1 { | ||||||
|  | 		t.Fatalf("expected 1 signature from manifest, got: %d", len(signatures)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Re-sign, push and pull the same digest
 | ||||||
|  | 	sm2, err := schema1.Sign(&fetchedManifestByDigest.Manifest, env.pk) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, sm2) | ||||||
|  | 	checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) | ||||||
|  | 
 | ||||||
|  | 	resp, err = http.Get(manifestDigestURL) | ||||||
|  | 	checkErr(t, err, "re-fetching manifest by digest") | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	checkResponse(t, "re-fetching uploaded manifest", resp, http.StatusOK) | ||||||
|  | 	checkHeaders(t, resp, http.Header{ | ||||||
|  | 		"Docker-Content-Digest": []string{dgst.String()}, | ||||||
|  | 		"ETag":                  []string{fmt.Sprintf(`"%s"`, dgst)}, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	dec = json.NewDecoder(resp.Body) | ||||||
|  | 	if err := dec.Decode(&fetchedManifestByDigest); err != nil { | ||||||
|  | 		t.Fatalf("error decoding fetched manifest: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// check two signatures were roundtripped
 | ||||||
|  | 	signatures, err = fetchedManifestByDigest.Signatures() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(signatures) != 2 { | ||||||
|  | 		t.Fatalf("expected 2 signature from manifest, got: %d", len(signatures)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Get by name with etag, gives 304
 | 	// Get by name with etag, gives 304
 | ||||||
| 	etag := resp.Header.Get("Etag") | 	etag := resp.Header.Get("Etag") | ||||||
| 	req, err := http.NewRequest("GET", manifestURL, nil) | 	req, err := http.NewRequest("GET", manifestURL, nil) | ||||||
|  | @ -956,7 +998,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | ||||||
| 		t.Fatalf("Error constructing request: %s", err) | 		t.Fatalf("Error constructing request: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) | 	checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) | ||||||
| 
 | 
 | ||||||
| 	// Get by digest with etag, gives 304
 | 	// Get by digest with etag, gives 304
 | ||||||
| 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | 	req, err = http.NewRequest("GET", manifestDigestURL, nil) | ||||||
|  | @ -969,7 +1011,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m | ||||||
| 		t.Fatalf("Error constructing request: %s", err) | 		t.Fatalf("Error constructing request: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) | 	checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) | ||||||
| 
 | 
 | ||||||
| 	// Ensure that the tag is listed.
 | 	// Ensure that the tag is listed.
 | ||||||
| 	resp, err = http.Get(tagsURL) | 	resp, err = http.Get(tagsURL) | ||||||
|  | @ -1143,8 +1185,13 @@ 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 string, v interface{}) *http.Response { | ||||||
| 	var body []byte | 	var body []byte | ||||||
|  | 
 | ||||||
| 	if sm, ok := v.(*schema1.SignedManifest); ok { | 	if sm, ok := v.(*schema1.SignedManifest); ok { | ||||||
| 		body = sm.Raw | 		_, pl, err := sm.Payload() | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("error getting payload: %v", err) | ||||||
|  | 		} | ||||||
|  | 		body = pl | ||||||
| 	} else { | 	} else { | ||||||
| 		var err error | 		var err error | ||||||
| 		body, err = json.MarshalIndent(v, "", "   ") | 		body, err = json.MarshalIndent(v, "", "   ") | ||||||
|  | @ -1435,7 +1482,7 @@ func checkErr(t *testing.T, err error, msg string) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createRepository(env *testEnv, t *testing.T, imageName string, tag string) { | func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest { | ||||||
| 	unsignedManifest := &schema1.Manifest{ | 	unsignedManifest := &schema1.Manifest{ | ||||||
| 		Versioned: manifest.Versioned{ | 		Versioned: manifest.Versioned{ | ||||||
| 			SchemaVersion: 1, | 			SchemaVersion: 1, | ||||||
|  | @ -1459,7 +1506,6 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string) | ||||||
| 
 | 
 | ||||||
| 	for i := range unsignedManifest.FSLayers { | 	for i := range unsignedManifest.FSLayers { | ||||||
| 		rs, dgstStr, err := testutil.CreateRandomTarFile() | 		rs, dgstStr, err := testutil.CreateRandomTarFile() | ||||||
| 
 |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("error creating random layer %d: %v", i, err) | 			t.Fatalf("error creating random layer %d: %v", i, err) | ||||||
| 		} | 		} | ||||||
|  | @ -1477,20 +1523,22 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string) | ||||||
| 		t.Fatalf("unexpected error signing manifest: %v", err) | 		t.Fatalf("unexpected error signing manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	payload, err := signedManifest.Payload() | 	dgst := digest.FromBytes(signedManifest.Canonical) | ||||||
| 	checkErr(t, err, "getting manifest payload") |  | ||||||
| 
 | 
 | ||||||
| 	dgst := digest.FromBytes(payload) | 	// Create this repository by tag to ensure the tag mapping is made in the registry
 | ||||||
| 
 | 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, tag) | ||||||
| 	manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) |  | ||||||
| 	checkErr(t, err, "building manifest url") | 	checkErr(t, err, "building manifest url") | ||||||
| 
 | 
 | ||||||
|  | 	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) | 	checkResponse(t, "putting signed manifest", resp, http.StatusCreated) | ||||||
| 	checkHeaders(t, resp, http.Header{ | 	checkHeaders(t, resp, http.Header{ | ||||||
| 		"Location":              []string{manifestDigestURL}, | 		"Location":              []string{location}, | ||||||
| 		"Docker-Content-Digest": []string{dgst.String()}, | 		"Docker-Content-Digest": []string{dgst.String()}, | ||||||
| 	}) | 	}) | ||||||
|  | 	return dgst | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Test mutation operations on a registry configured as a cache.  Ensure that they return
 | // Test mutation operations on a registry configured as a cache.  Ensure that they return
 | ||||||
|  | @ -1577,3 +1625,64 @@ func TestCheckContextNotifier(t *testing.T) { | ||||||
| 		t.Fatalf("wrong status code - expected 200, got %d", resp.StatusCode) | 		t.Fatalf("wrong status code - expected 200, got %d", resp.StatusCode) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestProxyManifestGetByTag(t *testing.T) { | ||||||
|  | 	truthConfig := configuration.Configuration{ | ||||||
|  | 		Storage: configuration.Storage{ | ||||||
|  | 			"inmemory": configuration.Parameters{}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	truthConfig.HTTP.Headers = headerConfig | ||||||
|  | 
 | ||||||
|  | 	imageName := "foo/bar" | ||||||
|  | 	tag := "latest" | ||||||
|  | 
 | ||||||
|  | 	truthEnv := newTestEnvWithConfig(t, &truthConfig) | ||||||
|  | 	// create a repository in the truth registry
 | ||||||
|  | 	dgst := createRepository(truthEnv, t, imageName, tag) | ||||||
|  | 
 | ||||||
|  | 	proxyConfig := configuration.Configuration{ | ||||||
|  | 		Storage: configuration.Storage{ | ||||||
|  | 			"inmemory": configuration.Parameters{}, | ||||||
|  | 		}, | ||||||
|  | 		Proxy: configuration.Proxy{ | ||||||
|  | 			RemoteURL: truthEnv.server.URL, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	proxyConfig.HTTP.Headers = headerConfig | ||||||
|  | 
 | ||||||
|  | 	proxyEnv := newTestEnvWithConfig(t, &proxyConfig) | ||||||
|  | 
 | ||||||
|  | 	manifestDigestURL, err := proxyEnv.builder.BuildManifestURL(imageName, dgst.String()) | ||||||
|  | 	checkErr(t, err, "building manifest url") | ||||||
|  | 
 | ||||||
|  | 	resp, err := http.Get(manifestDigestURL) | ||||||
|  | 	checkErr(t, err, "fetching manifest from proxy by digest") | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	manifestTagURL, err := proxyEnv.builder.BuildManifestURL(imageName, tag) | ||||||
|  | 	checkErr(t, err, "building manifest url") | ||||||
|  | 
 | ||||||
|  | 	resp, err = http.Get(manifestTagURL) | ||||||
|  | 	checkErr(t, err, "fetching manifest from proxy by tag") | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 	checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) | ||||||
|  | 	checkHeaders(t, resp, http.Header{ | ||||||
|  | 		"Docker-Content-Digest": []string{dgst.String()}, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// Create another manifest in the remote with the same image/tag pair
 | ||||||
|  | 	newDigest := createRepository(truthEnv, t, imageName, tag) | ||||||
|  | 	if dgst == newDigest { | ||||||
|  | 		t.Fatalf("non-random test data") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// fetch it with the same proxy URL as before.  Ensure the updated content is at the same tag
 | ||||||
|  | 	resp, err = http.Get(manifestTagURL) | ||||||
|  | 	checkErr(t, err, "fetching manifest from proxy by tag") | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 	checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) | ||||||
|  | 	checkHeaders(t, resp, http.Header{ | ||||||
|  | 		"Docker-Content-Digest": []string{newDigest.String()}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,19 +2,15 @@ package handlers | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	ctxu "github.com/docker/distribution/context" | 	ctxu "github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| 	"github.com/docker/distribution/registry/api/errcode" | 	"github.com/docker/distribution/registry/api/errcode" | ||||||
| 	"github.com/docker/distribution/registry/api/v2" | 	"github.com/docker/distribution/registry/api/v2" | ||||||
| 	"github.com/gorilla/handlers" | 	"github.com/gorilla/handlers" | ||||||
| 	"golang.org/x/net/context" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // imageManifestDispatcher takes the request context and builds the
 | // imageManifestDispatcher takes the request context and builds the
 | ||||||
|  | @ -33,7 +29,8 @@ func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mhandler := handlers.MethodHandler{ | 	mhandler := handlers.MethodHandler{ | ||||||
| 		"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), | 		"GET":  http.HandlerFunc(imageManifestHandler.GetImageManifest), | ||||||
|  | 		"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !ctx.readOnly { | 	if !ctx.readOnly { | ||||||
|  | @ -54,6 +51,8 @@ type imageManifestHandler struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetImageManifest fetches the image manifest from the storage backend, if it exists.
 | // 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) { | func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { | ||||||
| 	ctxu.GetLogger(imh).Debug("GetImageManifest") | 	ctxu.GetLogger(imh).Debug("GetImageManifest") | ||||||
| 	manifests, err := imh.Repository.Manifests(imh) | 	manifests, err := imh.Repository.Manifests(imh) | ||||||
|  | @ -62,42 +61,38 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var sm *schema1.SignedManifest | 	var manifest distribution.Manifest | ||||||
| 	if imh.Tag != "" { | 	if imh.Tag != "" { | ||||||
| 		sm, err = manifests.GetByTag(imh.Tag) | 		tags := imh.Repository.Tags(imh) | ||||||
| 	} else { | 		desc, err := tags.Get(imh, imh.Tag) | ||||||
| 		if etagMatch(r, imh.Digest.String()) { | 		if err != nil { | ||||||
| 			w.WriteHeader(http.StatusNotModified) | 			imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		sm, err = manifests.Get(imh.Digest) | 		imh.Digest = desc.Digest | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if etagMatch(r, imh.Digest.String()) { | ||||||
|  | 		w.WriteHeader(http.StatusNotModified) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	manifest, err = manifests.Get(imh, imh.Digest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get the digest, if we don't already have it.
 | 	ct, p, err := manifest.Payload() | ||||||
| 	if imh.Digest == "" { | 	if err != nil { | ||||||
| 		dgst, err := digestManifest(imh, sm) | 		return | ||||||
| 		if err != nil { |  | ||||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		if etagMatch(r, dgst.String()) { |  | ||||||
| 			w.WriteHeader(http.StatusNotModified) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		imh.Digest = dgst |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	w.Header().Set("Content-Type", "application/json; charset=utf-8") | 	w.Header().Set("Content-Type", ct) | ||||||
| 	w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw))) | 	w.Header().Set("Content-Length", fmt.Sprint(len(p))) | ||||||
| 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | 	w.Header().Set("Docker-Content-Digest", imh.Digest.String()) | ||||||
| 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) | 	w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) | ||||||
| 	w.Write(sm.Raw) | 	w.Write(p) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func etagMatch(r *http.Request, etag string) bool { | func etagMatch(r *http.Request, etag string) bool { | ||||||
|  | @ -109,7 +104,7 @@ func etagMatch(r *http.Request, etag string) bool { | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // PutImageManifest validates and stores and image in the registry.
 | // PutImageManifest validates and stores an image in the registry.
 | ||||||
| func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { | func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { | ||||||
| 	ctxu.GetLogger(imh).Debug("PutImageManifest") | 	ctxu.GetLogger(imh).Debug("PutImageManifest") | ||||||
| 	manifests, err := imh.Repository.Manifests(imh) | 	manifests, err := imh.Repository.Manifests(imh) | ||||||
|  | @ -124,39 +119,28 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var manifest schema1.SignedManifest | 	mediaType := r.Header.Get("Content-Type") | ||||||
| 	if err := json.Unmarshal(jsonBuf.Bytes(), &manifest); err != nil { | 	manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes()) | ||||||
|  | 	if err != nil { | ||||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) | 		imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	dgst, err := digestManifest(imh, &manifest) | 	if imh.Digest != "" { | ||||||
| 	if err != nil { | 		if desc.Digest != imh.Digest { | ||||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) | 			ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest) | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Validate manifest tag or digest matches payload
 |  | ||||||
| 	if imh.Tag != "" { |  | ||||||
| 		if manifest.Tag != imh.Tag { |  | ||||||
| 			ctxu.GetLogger(imh).Errorf("invalid tag on manifest payload: %q != %q", manifest.Tag, imh.Tag) |  | ||||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		imh.Digest = dgst |  | ||||||
| 	} else if imh.Digest != "" { |  | ||||||
| 		if dgst != imh.Digest { |  | ||||||
| 			ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", dgst, imh.Digest) |  | ||||||
| 			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) | 			imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	} else if imh.Tag != "" { | ||||||
|  | 		imh.Digest = desc.Digest | ||||||
| 	} else { | 	} else { | ||||||
| 		imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified")) | 		imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := manifests.Put(&manifest); err != nil { | 	_, err = manifests.Put(imh, manifest) | ||||||
|  | 	if err != nil { | ||||||
| 		// TODO(stevvooe): These error handling switches really need to be
 | 		// TODO(stevvooe): These error handling switches really need to be
 | ||||||
| 		// handled by an app global mapper.
 | 		// handled by an app global mapper.
 | ||||||
| 		if err == distribution.ErrUnsupported { | 		if err == distribution.ErrUnsupported { | ||||||
|  | @ -188,6 +172,17 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Tag this manifest
 | ||||||
|  | 	if imh.Tag != "" { | ||||||
|  | 		tags := imh.Repository.Tags(imh) | ||||||
|  | 		err = tags.Tag(imh, imh.Tag, desc) | ||||||
|  | 		if err != nil { | ||||||
|  | 			imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Construct a canonical url for the uploaded manifest.
 | 	// Construct a canonical url for the uploaded manifest.
 | ||||||
| 	location, err := imh.urlBuilder.BuildManifestURL(imh.Repository.Name(), imh.Digest.String()) | 	location, err := imh.urlBuilder.BuildManifestURL(imh.Repository.Name(), imh.Digest.String()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -212,7 +207,7 @@ func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *h | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = manifests.Delete(imh.Digest) | 	err = manifests.Delete(imh, imh.Digest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		switch err { | 		switch err { | ||||||
| 		case digest.ErrDigestUnsupported: | 		case digest.ErrDigestUnsupported: | ||||||
|  | @ -233,22 +228,3 @@ func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *h | ||||||
| 
 | 
 | ||||||
| 	w.WriteHeader(http.StatusAccepted) | 	w.WriteHeader(http.StatusAccepted) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // digestManifest takes a digest of the given manifest. This belongs somewhere
 |  | ||||||
| // better but we'll wait for a refactoring cycle to find that real somewhere.
 |  | ||||||
| func digestManifest(ctx context.Context, sm *schema1.SignedManifest) (digest.Digest, error) { |  | ||||||
| 	p, err := sm.Payload() |  | ||||||
| 	if err != nil { |  | ||||||
| 		if !strings.Contains(err.Error(), "missing signature key") { |  | ||||||
| 			ctxu.GetLogger(ctx).Errorf("error getting manifest payload: %v", err) |  | ||||||
| 			return "", err |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// NOTE(stevvooe): There are no signatures but we still have a
 |  | ||||||
| 		// payload. The request will fail later but this is not the
 |  | ||||||
| 		// responsibility of this part of the code.
 |  | ||||||
| 		p = sm.Raw |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return digest.FromBytes(p), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -34,13 +34,9 @@ type tagsAPIResponse struct { | ||||||
| // GetTags returns a json list of tags for a specific image name.
 | // GetTags returns a json list of tags for a specific image name.
 | ||||||
| func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { | func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { | ||||||
| 	defer r.Body.Close() | 	defer r.Body.Close() | ||||||
| 	manifests, err := th.Repository.Manifests(th) |  | ||||||
| 	if err != nil { |  | ||||||
| 		th.Errors = append(th.Errors, err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	tags, err := manifests.Tags() | 	tagService := th.Repository.Tags(th) | ||||||
|  | 	tags, err := tagService.All(th) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		switch err := err.(type) { | 		switch err := err.(type) { | ||||||
| 		case distribution.ErrRepositoryUnknown: | 		case distribution.ErrRepositoryUnknown: | ||||||
|  |  | ||||||
|  | @ -6,8 +6,6 @@ import ( | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| 	"github.com/docker/distribution/registry/client" |  | ||||||
| 	"github.com/docker/distribution/registry/proxy/scheduler" | 	"github.com/docker/distribution/registry/proxy/scheduler" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -24,8 +22,8 @@ type proxyManifestStore struct { | ||||||
| 
 | 
 | ||||||
| var _ distribution.ManifestService = &proxyManifestStore{} | var _ distribution.ManifestService = &proxyManifestStore{} | ||||||
| 
 | 
 | ||||||
| func (pms proxyManifestStore) Exists(dgst digest.Digest) (bool, error) { | func (pms proxyManifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { | ||||||
| 	exists, err := pms.localManifests.Exists(dgst) | 	exists, err := pms.localManifests.Exists(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  | @ -33,117 +31,56 @@ func (pms proxyManifestStore) Exists(dgst digest.Digest) (bool, error) { | ||||||
| 		return true, nil | 		return true, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return pms.remoteManifests.Exists(dgst) | 	return pms.remoteManifests.Exists(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pms proxyManifestStore) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { | func (pms proxyManifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||||
| 	sm, err := pms.localManifests.Get(dgst) | 	// At this point `dgst` was either specified explicitly, or returned by the
 | ||||||
| 	if err == nil { | 	// tagstore with the most recent association.
 | ||||||
| 		proxyMetrics.ManifestPush(uint64(len(sm.Raw))) | 	var fromRemote bool | ||||||
| 		return sm, err | 	manifest, err := pms.localManifests.Get(ctx, dgst, options...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		manifest, err = pms.remoteManifests.Get(ctx, dgst, options...) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		fromRemote = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	sm, err = pms.remoteManifests.Get(dgst) | 	_, payload, err := manifest.Payload() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	proxyMetrics.ManifestPull(uint64(len(sm.Raw))) | 	proxyMetrics.ManifestPush(uint64(len(payload))) | ||||||
| 	err = pms.localManifests.Put(sm) | 	if fromRemote { | ||||||
| 	if err != nil { | 		proxyMetrics.ManifestPull(uint64(len(payload))) | ||||||
| 		return nil, err | 
 | ||||||
|  | 		_, err = pms.localManifests.Put(ctx, manifest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Schedule the repo for removal
 | ||||||
|  | 		pms.scheduler.AddManifest(pms.repositoryName, repositoryTTL) | ||||||
|  | 
 | ||||||
|  | 		// Ensure the manifest blob is cleaned up
 | ||||||
|  | 		pms.scheduler.AddBlob(dgst.String(), repositoryTTL) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Schedule the repo for removal
 | 	return manifest, err | ||||||
| 	pms.scheduler.AddManifest(pms.repositoryName, repositoryTTL) |  | ||||||
| 
 |  | ||||||
| 	// Ensure the manifest blob is cleaned up
 |  | ||||||
| 	pms.scheduler.AddBlob(dgst.String(), repositoryTTL) |  | ||||||
| 
 |  | ||||||
| 	proxyMetrics.ManifestPush(uint64(len(sm.Raw))) |  | ||||||
| 
 |  | ||||||
| 	return sm, err |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pms proxyManifestStore) Tags() ([]string, error) { | func (pms proxyManifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||||
| 	return pms.localManifests.Tags() | 	var d digest.Digest | ||||||
|  | 	return d, distribution.ErrUnsupported | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pms proxyManifestStore) ExistsByTag(tag string) (bool, error) { | func (pms proxyManifestStore) Delete(ctx context.Context, dgst digest.Digest) error { | ||||||
| 	exists, err := pms.localManifests.ExistsByTag(tag) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
| 	if exists { |  | ||||||
| 		return true, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return pms.remoteManifests.ExistsByTag(tag) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (pms proxyManifestStore) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { |  | ||||||
| 	var localDigest digest.Digest |  | ||||||
| 
 |  | ||||||
| 	localManifest, err := pms.localManifests.GetByTag(tag, options...) |  | ||||||
| 	switch err.(type) { |  | ||||||
| 	case distribution.ErrManifestUnknown, distribution.ErrManifestUnknownRevision: |  | ||||||
| 		goto fromremote |  | ||||||
| 	case nil: |  | ||||||
| 		break |  | ||||||
| 	default: |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	localDigest, err = manifestDigest(localManifest) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| fromremote: |  | ||||||
| 	var sm *schema1.SignedManifest |  | ||||||
| 	sm, err = pms.remoteManifests.GetByTag(tag, client.AddEtagToTag(tag, localDigest.String())) |  | ||||||
| 	if err != nil && err != distribution.ErrManifestNotModified { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err == distribution.ErrManifestNotModified { |  | ||||||
| 		context.GetLogger(pms.ctx).Debugf("Local manifest for %q is latest, dgst=%s", tag, localDigest.String()) |  | ||||||
| 		return localManifest, nil |  | ||||||
| 	} |  | ||||||
| 	context.GetLogger(pms.ctx).Debugf("Updated manifest for %q, dgst=%s", tag, localDigest.String()) |  | ||||||
| 
 |  | ||||||
| 	err = pms.localManifests.Put(sm) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	dgst, err := manifestDigest(sm) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	pms.scheduler.AddBlob(dgst.String(), repositoryTTL) |  | ||||||
| 	pms.scheduler.AddManifest(pms.repositoryName, repositoryTTL) |  | ||||||
| 
 |  | ||||||
| 	proxyMetrics.ManifestPull(uint64(len(sm.Raw))) |  | ||||||
| 	proxyMetrics.ManifestPush(uint64(len(sm.Raw))) |  | ||||||
| 
 |  | ||||||
| 	return sm, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func manifestDigest(sm *schema1.SignedManifest) (digest.Digest, error) { |  | ||||||
| 	payload, err := sm.Payload() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 
 |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return digest.FromBytes(payload), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (pms proxyManifestStore) Put(manifest *schema1.SignedManifest) error { |  | ||||||
| 	return distribution.ErrUnsupported | 	return distribution.ErrUnsupported | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pms proxyManifestStore) Delete(dgst digest.Digest) error { | /*func (pms proxyManifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | ||||||
| 	return distribution.ErrUnsupported | 	return 0, distribution.ErrUnsupported | ||||||
| } | } | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | @ -37,40 +37,31 @@ func (te manifestStoreTestEnv) RemoteStats() *map[string]int { | ||||||
| 	return &rs | 	return &rs | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sm statsManifest) Delete(dgst digest.Digest) error { | func (sm statsManifest) Delete(ctx context.Context, dgst digest.Digest) error { | ||||||
| 	sm.stats["delete"]++ | 	sm.stats["delete"]++ | ||||||
| 	return sm.manifests.Delete(dgst) | 	return sm.manifests.Delete(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sm statsManifest) Exists(dgst digest.Digest) (bool, error) { | func (sm statsManifest) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { | ||||||
| 	sm.stats["exists"]++ | 	sm.stats["exists"]++ | ||||||
| 	return sm.manifests.Exists(dgst) | 	return sm.manifests.Exists(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sm statsManifest) ExistsByTag(tag string) (bool, error) { | func (sm statsManifest) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||||
| 	sm.stats["existbytag"]++ |  | ||||||
| 	return sm.manifests.ExistsByTag(tag) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (sm statsManifest) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { |  | ||||||
| 	sm.stats["get"]++ | 	sm.stats["get"]++ | ||||||
| 	return sm.manifests.Get(dgst) | 	return sm.manifests.Get(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sm statsManifest) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { | func (sm statsManifest) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||||
| 	sm.stats["getbytag"]++ |  | ||||||
| 	return sm.manifests.GetByTag(tag, options...) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (sm statsManifest) Put(manifest *schema1.SignedManifest) error { |  | ||||||
| 	sm.stats["put"]++ | 	sm.stats["put"]++ | ||||||
| 	return sm.manifests.Put(manifest) | 	return sm.manifests.Put(ctx, manifest) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sm statsManifest) Tags() ([]string, error) { | /*func (sm statsManifest) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | ||||||
| 	sm.stats["tags"]++ | 	sm.stats["enumerate"]++ | ||||||
| 	return sm.manifests.Tags() | 	return sm.manifests.Enumerate(ctx, manifests, last) | ||||||
| } | } | ||||||
|  | */ | ||||||
| 
 | 
 | ||||||
| func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { | func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  | @ -169,15 +160,12 @@ func populateRepo(t *testing.T, ctx context.Context, repository distribution.Rep | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf(err.Error()) | 		t.Fatalf(err.Error()) | ||||||
| 	} | 	} | ||||||
| 	ms.Put(sm) | 	dgst, err := ms.Put(ctx, sm) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected errors putting manifest: %v", err) | 		t.Fatalf("unexpected errors putting manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 	pl, err := sm.Payload() | 
 | ||||||
| 	if err != nil { | 	return dgst, nil | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	return digest.FromBytes(pl), nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TestProxyManifests contains basic acceptance tests
 | // TestProxyManifests contains basic acceptance tests
 | ||||||
|  | @ -189,8 +177,9 @@ func TestProxyManifests(t *testing.T) { | ||||||
| 	localStats := env.LocalStats() | 	localStats := env.LocalStats() | ||||||
| 	remoteStats := env.RemoteStats() | 	remoteStats := env.RemoteStats() | ||||||
| 
 | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
| 	// Stat - must check local and remote
 | 	// Stat - must check local and remote
 | ||||||
| 	exists, err := env.manifests.ExistsByTag("latest") | 	exists, err := env.manifests.Exists(ctx, env.manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error checking existance") | 		t.Fatalf("Error checking existance") | ||||||
| 	} | 	} | ||||||
|  | @ -198,15 +187,16 @@ func TestProxyManifests(t *testing.T) { | ||||||
| 		t.Errorf("Unexpected non-existant manifest") | 		t.Errorf("Unexpected non-existant manifest") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (*localStats)["existbytag"] != 1 && (*remoteStats)["existbytag"] != 1 { | 	if (*localStats)["exists"] != 1 && (*remoteStats)["exists"] != 1 { | ||||||
| 		t.Errorf("Unexpected exists count") | 		t.Errorf("Unexpected exists count : \n%v \n%v", localStats, remoteStats) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get - should succeed and pull manifest into local
 | 	// Get - should succeed and pull manifest into local
 | ||||||
| 	_, err = env.manifests.Get(env.manifestDigest) | 	_, err = env.manifests.Get(ctx, env.manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if (*localStats)["get"] != 1 && (*remoteStats)["get"] != 1 { | 	if (*localStats)["get"] != 1 && (*remoteStats)["get"] != 1 { | ||||||
| 		t.Errorf("Unexpected get count") | 		t.Errorf("Unexpected get count") | ||||||
| 	} | 	} | ||||||
|  | @ -216,7 +206,7 @@ func TestProxyManifests(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Stat - should only go to local
 | 	// Stat - should only go to local
 | ||||||
| 	exists, err = env.manifests.ExistsByTag("latest") | 	exists, err = env.manifests.Exists(ctx, env.manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -224,19 +214,21 @@ func TestProxyManifests(t *testing.T) { | ||||||
| 		t.Errorf("Unexpected non-existant manifest") | 		t.Errorf("Unexpected non-existant manifest") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (*localStats)["existbytag"] != 2 && (*remoteStats)["existbytag"] != 1 { | 	if (*localStats)["exists"] != 2 && (*remoteStats)["exists"] != 1 { | ||||||
| 		t.Errorf("Unexpected exists count") | 		t.Errorf("Unexpected exists count") | ||||||
| 
 |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get - should get from remote, to test freshness
 | 	// Get - should get from remote, to test freshness
 | ||||||
| 	_, err = env.manifests.Get(env.manifestDigest) | 	_, err = env.manifests.Get(ctx, env.manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (*remoteStats)["get"] != 2 && (*remoteStats)["existsbytag"] != 1 && (*localStats)["put"] != 1 { | 	if (*remoteStats)["get"] != 2 && (*remoteStats)["exists"] != 1 && (*localStats)["put"] != 1 { | ||||||
| 		t.Errorf("Unexpected get count") | 		t.Errorf("Unexpected get count") | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestProxyTagService(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -42,6 +42,7 @@ func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Name | ||||||
| 	s.OnManifestExpire(func(repoName string) error { | 	s.OnManifestExpire(func(repoName string) error { | ||||||
| 		return v.RemoveRepository(repoName) | 		return v.RemoveRepository(repoName) | ||||||
| 	}) | 	}) | ||||||
|  | 
 | ||||||
| 	err = s.Start() | 	err = s.Start() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -78,7 +79,7 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name string) (distri | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	localManifests, err := localRepo.Manifests(ctx, storage.SkipLayerVerification) | 	localManifests, err := localRepo.Manifests(ctx, storage.SkipLayerVerification()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -106,8 +107,11 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name string) (distri | ||||||
| 			ctx:             ctx, | 			ctx:             ctx, | ||||||
| 			scheduler:       pr.scheduler, | 			scheduler:       pr.scheduler, | ||||||
| 		}, | 		}, | ||||||
| 		name:       name, | 		name: name, | ||||||
| 		signatures: localRepo.Signatures(), | 		tags: proxyTagService{ | ||||||
|  | 			localTags:  localRepo.Tags(ctx), | ||||||
|  | 			remoteTags: remoteRepo.Tags(ctx), | ||||||
|  | 		}, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -115,14 +119,13 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name string) (distri | ||||||
| // locally, or pulling it through from a remote and caching it locally if it doesn't
 | // locally, or pulling it through from a remote and caching it locally if it doesn't
 | ||||||
| // already exist
 | // already exist
 | ||||||
| type proxiedRepository struct { | type proxiedRepository struct { | ||||||
| 	blobStore  distribution.BlobStore | 	blobStore distribution.BlobStore | ||||||
| 	manifests  distribution.ManifestService | 	manifests distribution.ManifestService | ||||||
| 	name       string | 	name      string | ||||||
| 	signatures distribution.SignatureService | 	tags      distribution.TagService | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pr *proxiedRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { | func (pr *proxiedRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { | ||||||
| 	// options
 |  | ||||||
| 	return pr.manifests, nil | 	return pr.manifests, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -134,6 +137,6 @@ func (pr *proxiedRepository) Name() string { | ||||||
| 	return pr.name | 	return pr.name | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (pr *proxiedRepository) Signatures() distribution.SignatureService { | func (pr *proxiedRepository) Tags(ctx context.Context) distribution.TagService { | ||||||
| 	return pr.signatures | 	return pr.tags | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | package proxy | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // proxyTagService supports local and remote lookup of tags.
 | ||||||
|  | type proxyTagService struct { | ||||||
|  | 	localTags  distribution.TagService | ||||||
|  | 	remoteTags distribution.TagService | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ distribution.TagService = proxyTagService{} | ||||||
|  | 
 | ||||||
|  | // Get attempts to get the most recent digest for the tag by checking the remote
 | ||||||
|  | // tag service first and then caching it locally.  If the remote is unavailable
 | ||||||
|  | // the local association is returned
 | ||||||
|  | func (pt proxyTagService) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { | ||||||
|  | 	desc, err := pt.remoteTags.Get(ctx, tag) | ||||||
|  | 	if err == nil { | ||||||
|  | 		err := pt.localTags.Tag(ctx, tag, desc) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return distribution.Descriptor{}, err | ||||||
|  | 		} | ||||||
|  | 		return desc, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	desc, err = pt.localTags.Get(ctx, tag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return distribution.Descriptor{}, err | ||||||
|  | 	} | ||||||
|  | 	return desc, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (pt proxyTagService) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { | ||||||
|  | 	return distribution.ErrUnsupported | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (pt proxyTagService) Untag(ctx context.Context, tag string) error { | ||||||
|  | 	err := pt.localTags.Untag(ctx, tag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (pt proxyTagService) All(ctx context.Context) ([]string, error) { | ||||||
|  | 	tags, err := pt.remoteTags.All(ctx) | ||||||
|  | 	if err == nil { | ||||||
|  | 		return tags, err | ||||||
|  | 	} | ||||||
|  | 	return pt.localTags.All(ctx) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (pt proxyTagService) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { | ||||||
|  | 	return []string{}, distribution.ErrUnsupported | ||||||
|  | } | ||||||
|  | @ -0,0 +1,164 @@ | ||||||
|  | package proxy | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"sort" | ||||||
|  | 	"sync" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type mockTagStore struct { | ||||||
|  | 	mapping map[string]distribution.Descriptor | ||||||
|  | 	sync.Mutex | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ distribution.TagService = &mockTagStore{} | ||||||
|  | 
 | ||||||
|  | func (m *mockTagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  | 
 | ||||||
|  | 	if d, ok := m.mapping[tag]; ok { | ||||||
|  | 		return d, nil | ||||||
|  | 	} | ||||||
|  | 	return distribution.Descriptor{}, distribution.ErrTagUnknown{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *mockTagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  | 
 | ||||||
|  | 	m.mapping[tag] = desc | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *mockTagStore) Untag(ctx context.Context, tag string) error { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  | 
 | ||||||
|  | 	if _, ok := m.mapping[tag]; ok { | ||||||
|  | 		delete(m.mapping, tag) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return distribution.ErrTagUnknown{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *mockTagStore) All(ctx context.Context) ([]string, error) { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  | 
 | ||||||
|  | 	var tags []string | ||||||
|  | 	for tag := range m.mapping { | ||||||
|  | 		tags = append(tags, tag) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return tags, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *mockTagStore) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func testProxyTagService(local, remote map[string]distribution.Descriptor) *proxyTagService { | ||||||
|  | 	if local == nil { | ||||||
|  | 		local = make(map[string]distribution.Descriptor) | ||||||
|  | 	} | ||||||
|  | 	if remote == nil { | ||||||
|  | 		remote = make(map[string]distribution.Descriptor) | ||||||
|  | 	} | ||||||
|  | 	return &proxyTagService{ | ||||||
|  | 		localTags:  &mockTagStore{mapping: local}, | ||||||
|  | 		remoteTags: &mockTagStore{mapping: remote}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGet(t *testing.T) { | ||||||
|  | 	remoteDesc := distribution.Descriptor{Size: 42} | ||||||
|  | 	remoteTag := "remote" | ||||||
|  | 	proxyTags := testProxyTagService(map[string]distribution.Descriptor{remoteTag: remoteDesc}, nil) | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	// Get pre-loaded tag
 | ||||||
|  | 	d, err := proxyTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if d != remoteDesc { | ||||||
|  | 		t.Fatal("unable to get put tag") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	local, err := proxyTags.localTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("remote tag not pulled into store") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if local != remoteDesc { | ||||||
|  | 		t.Fatalf("unexpected descriptor pulled through") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Manually overwrite remote tag
 | ||||||
|  | 	newRemoteDesc := distribution.Descriptor{Size: 43} | ||||||
|  | 	err = proxyTags.remoteTags.Tag(ctx, remoteTag, newRemoteDesc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d, err = proxyTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if d != newRemoteDesc { | ||||||
|  | 		t.Fatal("unable to get put tag") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = proxyTags.localTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("remote tag not pulled into store") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// untag, ensure it's removed locally, but present in remote
 | ||||||
|  | 	err = proxyTags.Untag(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = proxyTags.localTags.Get(ctx, remoteTag) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("Expected error getting Untag'd tag") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = proxyTags.remoteTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("remote tag should not be untagged with proxyTag.Untag") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = proxyTags.Get(ctx, remoteTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("untagged tag should be pulled through") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add another tag.  Ensure both tags appear in enumerate
 | ||||||
|  | 	err = proxyTags.remoteTags.Tag(ctx, "funtag", distribution.Descriptor{Size: 42}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	all, err := proxyTags.All(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(all) != 2 { | ||||||
|  | 		t.Fatalf("Unexpected tag length returned from All() : %d ", len(all)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sort.Strings(all) | ||||||
|  | 	if all[0] != "funtag" && all[1] != "remote" { | ||||||
|  | 		t.Fatalf("Unexpected tags returned from All() : %v ", all) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package storage | package storage | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
|  | @ -11,20 +12,21 @@ import ( | ||||||
| 	"github.com/docker/libtrust" | 	"github.com/docker/libtrust" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // manifestStore is a storage driver based store for storing schema1 manifests.
 | ||||||
| type manifestStore struct { | type manifestStore struct { | ||||||
| 	repository                 *repository | 	repository                 *repository | ||||||
| 	revisionStore              *revisionStore | 	blobStore                  *linkedBlobStore | ||||||
| 	tagStore                   *tagStore |  | ||||||
| 	ctx                        context.Context | 	ctx                        context.Context | ||||||
|  | 	signatures                 *signatureStore | ||||||
| 	skipDependencyVerification bool | 	skipDependencyVerification bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var _ distribution.ManifestService = &manifestStore{} | var _ distribution.ManifestService = &manifestStore{} | ||||||
| 
 | 
 | ||||||
| func (ms *manifestStore) Exists(dgst digest.Digest) (bool, error) { | func (ms *manifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Exists") | 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Exists") | ||||||
| 
 | 
 | ||||||
| 	_, err := ms.revisionStore.blobStore.Stat(ms.ctx, dgst) | 	_, err := ms.blobStore.Stat(ms.ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if err == distribution.ErrBlobUnknown { | 		if err == distribution.ErrBlobUnknown { | ||||||
| 			return false, nil | 			return false, nil | ||||||
|  | @ -36,76 +38,131 @@ func (ms *manifestStore) Exists(dgst digest.Digest) (bool, error) { | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifestStore) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { | func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Get") | 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Get") | ||||||
| 	return ms.revisionStore.get(ms.ctx, dgst) | 	// 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.
 | ||||||
|  | 
 | ||||||
|  | 	content, err := ms.blobStore.Get(ctx, dgst) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if err == distribution.ErrBlobUnknown { | ||||||
|  | 			return nil, distribution.ErrManifestUnknownRevision{ | ||||||
|  | 				Name:     ms.repository.Name(), | ||||||
|  | 				Revision: dgst, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SkipLayerVerification allows a manifest to be Put before it's
 | // SkipLayerVerification allows a manifest to be Put before its
 | ||||||
| // layers are on the filesystem
 | // layers are on the filesystem
 | ||||||
| func SkipLayerVerification(ms distribution.ManifestService) error { | func SkipLayerVerification() distribution.ManifestServiceOption { | ||||||
| 	if ms, ok := ms.(*manifestStore); ok { | 	return skipLayerOption{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type skipLayerOption struct{} | ||||||
|  | 
 | ||||||
|  | func (o skipLayerOption) Apply(m distribution.ManifestService) error { | ||||||
|  | 	if ms, ok := m.(*manifestStore); ok { | ||||||
| 		ms.skipDependencyVerification = true | 		ms.skipDependencyVerification = true | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	return fmt.Errorf("skip layer verification only valid for manifestStore") | 	return fmt.Errorf("skip layer verification only valid for manifestStore") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifestStore) Put(manifest *schema1.SignedManifest) error { | func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Put") | 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Put") | ||||||
| 
 | 
 | ||||||
| 	if err := ms.verifyManifest(ms.ctx, manifest); err != nil { | 	sm, ok := manifest.(*schema1.SignedManifest) | ||||||
| 		return err | 	if !ok { | ||||||
|  | 		return "", fmt.Errorf("non-v1 manifest put to signed manifestStore: %T", manifest) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Store the revision of the manifest
 | 	if err := ms.verifyManifest(ms.ctx, *sm); err != nil { | ||||||
| 	revision, err := ms.revisionStore.put(ms.ctx, manifest) | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mt := schema1.MediaTypeManifest | ||||||
|  | 	payload := sm.Canonical | ||||||
|  | 
 | ||||||
|  | 	revision, err := ms.blobStore.Put(ctx, mt, payload) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) | ||||||
|  | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Now, tag the manifest
 | 	// Link the revision into the repository.
 | ||||||
| 	return ms.tagStore.tag(manifest.Tag, revision.Digest) | 	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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Delete removes the revision of the specified manfiest.
 | // Delete removes the revision of the specified manfiest.
 | ||||||
| func (ms *manifestStore) Delete(dgst digest.Digest) error { | func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error { | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Delete") | 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Delete") | ||||||
| 	return ms.revisionStore.delete(ms.ctx, dgst) | 	return ms.blobStore.Delete(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifestStore) Tags() ([]string, error) { | func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).Tags") | 	return 0, distribution.ErrUnsupported | ||||||
| 	return ms.tagStore.tags() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *manifestStore) ExistsByTag(tag string) (bool, error) { |  | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).ExistsByTag") |  | ||||||
| 	return ms.tagStore.exists(tag) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *manifestStore) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { |  | ||||||
| 	for _, option := range options { |  | ||||||
| 		err := option(ms) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	context.GetLogger(ms.ctx).Debug("(*manifestStore).GetByTag") |  | ||||||
| 	dgst, err := ms.tagStore.resolve(tag) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return ms.revisionStore.get(ms.ctx, dgst) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // verifyManifest ensures that the manifest content is valid from the
 | // verifyManifest ensures that the manifest content is valid from the
 | ||||||
| // perspective of the registry. It ensures that the signature is valid for 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
 | // enclosed payload. As a policy, the registry only tries to store valid
 | ||||||
| // content, leaving trust policies of that content up to consumers.
 | // content, leaving trust policies of that content up to consumems.
 | ||||||
| func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.SignedManifest) error { | func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest) error { | ||||||
| 	var errs distribution.ErrManifestVerification | 	var errs distribution.ErrManifestVerification | ||||||
| 
 | 
 | ||||||
| 	if len(mnfst.Name) > reference.NameTotalLengthMax { | 	if len(mnfst.Name) > reference.NameTotalLengthMax { | ||||||
|  | @ -129,7 +186,7 @@ func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.Sign | ||||||
| 			len(mnfst.History), len(mnfst.FSLayers))) | 			len(mnfst.History), len(mnfst.FSLayers))) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := schema1.Verify(mnfst); err != nil { | 	if _, err := schema1.Verify(&mnfst); err != nil { | ||||||
| 		switch err { | 		switch err { | ||||||
| 		case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: | 		case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: | ||||||
| 			errs = append(errs, distribution.ErrManifestUnverified{}) | 			errs = append(errs, distribution.ErrManifestUnverified{}) | ||||||
|  | @ -143,15 +200,15 @@ func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.Sign | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !ms.skipDependencyVerification { | 	if !ms.skipDependencyVerification { | ||||||
| 		for _, fsLayer := range mnfst.FSLayers { | 		for _, fsLayer := range mnfst.References() { | ||||||
| 			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.BlobSum) | 			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if err != distribution.ErrBlobUnknown { | 				if err != distribution.ErrBlobUnknown { | ||||||
| 					errs = append(errs, err) | 					errs = append(errs, err) | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				// On error here, we always append unknown blob errors.
 | 				// On error here, we always append unknown blob erroms.
 | ||||||
| 				errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.BlobSum}) | 				errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest}) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -30,7 +30,8 @@ type manifestStoreTestEnv struct { | ||||||
| func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { | func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	driver := inmemory.New() | 	driver := inmemory.New() | ||||||
| 	registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) | 	registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider( | ||||||
|  | 		memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("error creating registry: %v", err) | 		t.Fatalf("error creating registry: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -58,24 +59,6 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	exists, err := ms.ExistsByTag(env.tag) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("unexpected error checking manifest existence: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if exists { |  | ||||||
| 		t.Fatalf("manifest should not exist") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if _, err := ms.GetByTag(env.tag); true { |  | ||||||
| 		switch err.(type) { |  | ||||||
| 		case distribution.ErrManifestUnknown: |  | ||||||
| 			break |  | ||||||
| 		default: |  | ||||||
| 			t.Fatalf("expected manifest unknown error: %#v", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m := schema1.Manifest{ | 	m := schema1.Manifest{ | ||||||
| 		Versioned: manifest.Versioned{ | 		Versioned: manifest.Versioned{ | ||||||
| 			SchemaVersion: 1, | 			SchemaVersion: 1, | ||||||
|  | @ -114,7 +97,7 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatalf("error signing manifest: %v", err) | 		t.Fatalf("error signing manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = ms.Put(sm) | 	_, err = ms.Put(ctx, sm) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Fatalf("expected errors putting manifest with full verification") | 		t.Fatalf("expected errors putting manifest with full verification") | ||||||
| 	} | 	} | ||||||
|  | @ -150,30 +133,40 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = ms.Put(sm); err != nil { | 	var manifestDigest digest.Digest | ||||||
|  | 	if manifestDigest, err = ms.Put(ctx, sm); err != nil { | ||||||
| 		t.Fatalf("unexpected error putting manifest: %v", err) | 		t.Fatalf("unexpected error putting manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	exists, err = ms.ExistsByTag(env.tag) | 	exists, err := ms.Exists(ctx, manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error checking manifest existence: %v", err) | 		t.Fatalf("unexpected error checking manifest existence: %#v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !exists { | 	if !exists { | ||||||
| 		t.Fatalf("manifest should exist") | 		t.Fatalf("manifest should exist") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fetchedManifest, err := ms.GetByTag(env.tag) | 	fromStore, err := ms.Get(ctx, manifestDigest) | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error fetching manifest: %v", err) | 		t.Fatalf("unexpected error fetching manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	fetchedManifest, ok := fromStore.(*schema1.SignedManifest) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("unexpected manifest type from signedstore") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if !reflect.DeepEqual(fetchedManifest, sm) { | 	if !reflect.DeepEqual(fetchedManifest, sm) { | ||||||
| 		t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm) | 		t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fetchedJWS, err := libtrust.ParsePrettySignature(fetchedManifest.Raw, "signatures") | 	_, pl, err := fetchedManifest.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error getting payload %#v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fetchedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error parsing jws: %v", err) | 		t.Fatalf("unexpected error parsing jws: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -185,8 +178,9 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	// Now that we have a payload, take a moment to check that the manifest is
 | 	// Now that we have a payload, take a moment to check that the manifest is
 | ||||||
| 	// return by the payload digest.
 | 	// return by the payload digest.
 | ||||||
|  | 
 | ||||||
| 	dgst := digest.FromBytes(payload) | 	dgst := digest.FromBytes(payload) | ||||||
| 	exists, err = ms.Exists(dgst) | 	exists, err = ms.Exists(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("error checking manifest existence by digest: %v", err) | 		t.Fatalf("error checking manifest existence by digest: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -195,7 +189,7 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatalf("manifest %s should exist", dgst) | 		t.Fatalf("manifest %s should exist", dgst) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fetchedByDigest, err := ms.Get(dgst) | 	fetchedByDigest, err := ms.Get(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error fetching manifest by digest: %v", err) | 		t.Fatalf("unexpected error fetching manifest by digest: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -213,20 +207,6 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs), 1) | 		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs), 1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Grabs the tags and check that this tagged manifest is present
 |  | ||||||
| 	tags, err := ms.Tags() |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("unexpected error fetching tags: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(tags) != 1 { |  | ||||||
| 		t.Fatalf("unexpected tags returned: %v", tags) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if tags[0] != env.tag { |  | ||||||
| 		t.Fatalf("unexpected tag found in tags: %v != %v", tags, []string{env.tag}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Now, push the same manifest with a different key
 | 	// Now, push the same manifest with a different key
 | ||||||
| 	pk2, err := libtrust.GenerateECP256PrivateKey() | 	pk2, err := libtrust.GenerateECP256PrivateKey() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -237,8 +217,12 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error signing manifest: %v", err) | 		t.Fatalf("unexpected error signing manifest: %v", err) | ||||||
| 	} | 	} | ||||||
|  | 	_, pl, err = sm2.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error getting payload %#v", err) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	jws2, err := libtrust.ParsePrettySignature(sm2.Raw, "signatures") | 	jws2, err := libtrust.ParsePrettySignature(pl, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("error parsing signature: %v", err) | 		t.Fatalf("error parsing signature: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -252,15 +236,20 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1) | 		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = ms.Put(sm2); err != nil { | 	if manifestDigest, err = ms.Put(ctx, sm2); err != nil { | ||||||
| 		t.Fatalf("unexpected error putting manifest: %v", err) | 		t.Fatalf("unexpected error putting manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fetched, err := ms.GetByTag(env.tag) | 	fromStore, err = ms.Get(ctx, manifestDigest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error fetching manifest: %v", err) | 		t.Fatalf("unexpected error fetching manifest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	fetched, ok := fromStore.(*schema1.SignedManifest) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("unexpected type from signed manifeststore : %T", fetched) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if _, err := schema1.Verify(fetched); err != nil { | 	if _, err := schema1.Verify(fetched); err != nil { | ||||||
| 		t.Fatalf("unexpected error verifying manifest: %v", err) | 		t.Fatalf("unexpected error verifying manifest: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -276,7 +265,12 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Fatalf("unexpected error getting expected signatures: %v", err) | 		t.Fatalf("unexpected error getting expected signatures: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	receivedJWS, err := libtrust.ParsePrettySignature(fetched.Raw, "signatures") | 	_, pl, err = fetched.Payload() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("error getting payload %#v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	receivedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error parsing jws: %v", err) | 		t.Fatalf("unexpected error parsing jws: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -302,12 +296,12 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Test deleting manifests
 | 	// Test deleting manifests
 | ||||||
| 	err = ms.Delete(dgst) | 	err = ms.Delete(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected an error deleting manifest by digest: %v", err) | 		t.Fatalf("unexpected an error deleting manifest by digest: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	exists, err = ms.Exists(dgst) | 	exists, err = ms.Exists(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error querying manifest existence") | 		t.Fatalf("Error querying manifest existence") | ||||||
| 	} | 	} | ||||||
|  | @ -315,7 +309,7 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Errorf("Deleted manifest should not exist") | 		t.Errorf("Deleted manifest should not exist") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	deletedManifest, err := ms.Get(dgst) | 	deletedManifest, err := ms.Get(ctx, dgst) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Errorf("Unexpected success getting deleted manifest") | 		t.Errorf("Unexpected success getting deleted manifest") | ||||||
| 	} | 	} | ||||||
|  | @ -331,12 +325,12 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Re-upload should restore manifest to a good state
 | 	// Re-upload should restore manifest to a good state
 | ||||||
| 	err = ms.Put(sm) | 	_, err = ms.Put(ctx, sm) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Error re-uploading deleted manifest") | 		t.Errorf("Error re-uploading deleted manifest") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	exists, err = ms.Exists(dgst) | 	exists, err = ms.Exists(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Error querying manifest existence") | 		t.Fatalf("Error querying manifest existence") | ||||||
| 	} | 	} | ||||||
|  | @ -344,7 +338,7 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 		t.Errorf("Restored manifest should exist") | 		t.Errorf("Restored manifest should exist") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	deletedManifest, err = ms.Get(dgst) | 	deletedManifest, err = ms.Get(ctx, dgst) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error getting manifest") | 		t.Errorf("Unexpected error getting manifest") | ||||||
| 	} | 	} | ||||||
|  | @ -364,7 +358,7 @@ func TestManifestStorage(t *testing.T) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	err = ms.Delete(dgst) | 	err = ms.Delete(ctx, dgst) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Errorf("Unexpected success deleting while disabled") | 		t.Errorf("Unexpected success deleting while disabled") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -145,6 +145,15 @@ func (repo *repository) Name() string { | ||||||
| 	return repo.name | 	return repo.name | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (repo *repository) Tags(ctx context.Context) distribution.TagService { | ||||||
|  | 	tags := &tagStore{ | ||||||
|  | 		repository: repo, | ||||||
|  | 		blobStore:  repo.registry.blobStore, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return tags | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Manifests returns an instance of ManifestService. Instantiation is cheap and
 | // Manifests returns an instance of ManifestService. Instantiation is cheap and
 | ||||||
| // may be context sensitive in the future. The instance should be used similar
 | // may be context sensitive in the future. The instance should be used similar
 | ||||||
| // to a request local.
 | // to a request local.
 | ||||||
|  | @ -159,36 +168,31 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M | ||||||
| 	ms := &manifestStore{ | 	ms := &manifestStore{ | ||||||
| 		ctx:        ctx, | 		ctx:        ctx, | ||||||
| 		repository: repo, | 		repository: repo, | ||||||
| 		revisionStore: &revisionStore{ | 		blobStore: &linkedBlobStore{ | ||||||
| 			ctx:        ctx, | 			ctx:           ctx, | ||||||
| 			repository: repo, | 			blobStore:     repo.blobStore, | ||||||
| 			blobStore: &linkedBlobStore{ | 			repository:    repo, | ||||||
| 				ctx:           ctx, | 			deleteEnabled: repo.registry.deleteEnabled, | ||||||
| 				blobStore:     repo.blobStore, | 			blobAccessController: &linkedBlobStatter{ | ||||||
| 				repository:    repo, | 				blobStore:   repo.blobStore, | ||||||
| 				deleteEnabled: repo.registry.deleteEnabled, | 				repository:  repo, | ||||||
| 				blobAccessController: &linkedBlobStatter{ | 				linkPathFns: manifestLinkPathFns, | ||||||
| 					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, |  | ||||||
| 				resumableDigestEnabled: repo.resumableDigestEnabled, |  | ||||||
| 			}, | 			}, | ||||||
|  | 
 | ||||||
|  | 			// TODO(stevvooe): linkPath limits this blob store to only
 | ||||||
|  | 			// manifests. This instance cannot be used for blob checks.
 | ||||||
|  | 			linkPathFns: manifestLinkPathFns, | ||||||
| 		}, | 		}, | ||||||
| 		tagStore: &tagStore{ | 		signatures: &signatureStore{ | ||||||
| 			ctx:        ctx, | 			ctx:        ctx, | ||||||
| 			repository: repo, | 			repository: repo, | ||||||
| 			blobStore:  repo.registry.blobStore, | 			blobStore:  repo.blobStore, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Apply options
 | 	// Apply options
 | ||||||
| 	for _, option := range options { | 	for _, option := range options { | ||||||
| 		err := option(ms) | 		err := option.Apply(ms) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | @ -225,11 +229,3 @@ func (repo *repository) Blobs(ctx context.Context) distribution.BlobStore { | ||||||
| 		resumableDigestEnabled: repo.resumableDigestEnabled, | 		resumableDigestEnabled: repo.resumableDigestEnabled, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func (repo *repository) Signatures() distribution.SignatureService { |  | ||||||
| 	return &signatureStore{ |  | ||||||
| 		repository: repo, |  | ||||||
| 		blobStore:  repo.blobStore, |  | ||||||
| 		ctx:        repo.ctx, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,111 +0,0 @@ | ||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 
 |  | ||||||
| 	"github.com/docker/distribution" |  | ||||||
| 	"github.com/docker/distribution/context" |  | ||||||
| 	"github.com/docker/distribution/digest" |  | ||||||
| 	"github.com/docker/distribution/manifest/schema1" |  | ||||||
| 	"github.com/docker/libtrust" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // revisionStore supports storing and managing manifest revisions.
 |  | ||||||
| type revisionStore struct { |  | ||||||
| 	repository *repository |  | ||||||
| 	blobStore  *linkedBlobStore |  | ||||||
| 	ctx        context.Context |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // get retrieves the manifest, keyed by revision digest.
 |  | ||||||
| func (rs *revisionStore) get(ctx context.Context, revision digest.Digest) (*schema1.SignedManifest, error) { |  | ||||||
| 	// Ensure that this revision is available in this repository.
 |  | ||||||
| 	_, err := rs.blobStore.Stat(ctx, revision) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if err == distribution.ErrBlobUnknown { |  | ||||||
| 			return nil, distribution.ErrManifestUnknownRevision{ |  | ||||||
| 				Name:     rs.repository.Name(), |  | ||||||
| 				Revision: revision, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// TODO(stevvooe): Need to check descriptor from above to ensure that the
 |  | ||||||
| 	// mediatype is as we expect for the manifest store.
 |  | ||||||
| 
 |  | ||||||
| 	content, err := rs.blobStore.Get(ctx, revision) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if err == distribution.ErrBlobUnknown { |  | ||||||
| 			return nil, distribution.ErrManifestUnknownRevision{ |  | ||||||
| 				Name:     rs.repository.Name(), |  | ||||||
| 				Revision: revision, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Fetch the signatures for the manifest
 |  | ||||||
| 	signatures, err := rs.repository.Signatures().Get(revision) |  | ||||||
| 	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 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // put stores the manifest in the repository, if not already present. Any
 |  | ||||||
| // updated signatures will be stored, as well.
 |  | ||||||
| func (rs *revisionStore) put(ctx context.Context, sm *schema1.SignedManifest) (distribution.Descriptor, error) { |  | ||||||
| 	// Resolve the payload in the manifest.
 |  | ||||||
| 	payload, err := sm.Payload() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return distribution.Descriptor{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Digest and store the manifest payload in the blob store.
 |  | ||||||
| 	revision, err := rs.blobStore.Put(ctx, schema1.ManifestMediaType, payload) |  | ||||||
| 	if err != nil { |  | ||||||
| 		context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) |  | ||||||
| 		return distribution.Descriptor{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Link the revision into the repository.
 |  | ||||||
| 	if err := rs.blobStore.linkBlob(ctx, revision); err != nil { |  | ||||||
| 		return distribution.Descriptor{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Grab each json signature and store them.
 |  | ||||||
| 	signatures, err := sm.Signatures() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return distribution.Descriptor{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := rs.repository.Signatures().Put(revision.Digest, signatures...); err != nil { |  | ||||||
| 		return distribution.Descriptor{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return revision, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (rs *revisionStore) delete(ctx context.Context, revision digest.Digest) error { |  | ||||||
| 	return rs.blobStore.Delete(ctx, revision) |  | ||||||
| } |  | ||||||
|  | @ -4,7 +4,6 @@ import ( | ||||||
| 	"path" | 	"path" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution" |  | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"github.com/docker/distribution/digest" | ||||||
| ) | ) | ||||||
|  | @ -15,16 +14,6 @@ type signatureStore struct { | ||||||
| 	ctx        context.Context | 	ctx        context.Context | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newSignatureStore(ctx context.Context, repo *repository, blobStore *blobStore) *signatureStore { |  | ||||||
| 	return &signatureStore{ |  | ||||||
| 		ctx:        ctx, |  | ||||||
| 		repository: repo, |  | ||||||
| 		blobStore:  blobStore, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var _ distribution.SignatureService = &signatureStore{} |  | ||||||
| 
 |  | ||||||
| func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) { | func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) { | ||||||
| 	signaturesPath, err := pathFor(manifestSignaturesPathSpec{ | 	signaturesPath, err := pathFor(manifestSignaturesPathSpec{ | ||||||
| 		name:     s.repository.Name(), | 		name:     s.repository.Name(), | ||||||
|  |  | ||||||
|  | @ -9,37 +9,41 @@ import ( | ||||||
| 	storagedriver "github.com/docker/distribution/registry/storage/driver" | 	storagedriver "github.com/docker/distribution/registry/storage/driver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var _ distribution.TagService = &tagStore{} | ||||||
|  | 
 | ||||||
| // tagStore provides methods to manage manifest tags in a backend storage driver.
 | // tagStore provides methods to manage manifest tags in a backend storage driver.
 | ||||||
|  | // This implementation uses the same on-disk layout as the (now deleted) tag
 | ||||||
|  | // store.  This provides backward compatibility with current registry deployments
 | ||||||
|  | // which only makes use of the Digest field of the returned distribution.Descriptor
 | ||||||
|  | // but does not enable full roundtripping of Descriptor objects
 | ||||||
| type tagStore struct { | type tagStore struct { | ||||||
| 	repository *repository | 	repository *repository | ||||||
| 	blobStore  *blobStore | 	blobStore  *blobStore | ||||||
| 	ctx        context.Context |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // tags lists the manifest tags for the specified repository.
 | // All returns all tags
 | ||||||
| func (ts *tagStore) tags() ([]string, error) { | func (ts *tagStore) All(ctx context.Context) ([]string, error) { | ||||||
| 	p, err := pathFor(manifestTagPathSpec{ | 	var tags []string | ||||||
|  | 
 | ||||||
|  | 	pathSpec, err := pathFor(manifestTagPathSpec{ | ||||||
| 		name: ts.repository.Name(), | 		name: ts.repository.Name(), | ||||||
| 	}) | 	}) | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return tags, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var tags []string | 	entries, err := ts.blobStore.driver.List(ctx, pathSpec) | ||||||
| 	entries, err := ts.blobStore.driver.List(ts.ctx, p) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		switch err := err.(type) { | 		switch err := err.(type) { | ||||||
| 		case storagedriver.PathNotFoundError: | 		case storagedriver.PathNotFoundError: | ||||||
| 			return nil, distribution.ErrRepositoryUnknown{Name: ts.repository.Name()} | 			return tags, distribution.ErrRepositoryUnknown{Name: ts.repository.Name()} | ||||||
| 		default: | 		default: | ||||||
| 			return nil, err | 			return tags, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, entry := range entries { | 	for _, entry := range entries { | ||||||
| 		_, filename := path.Split(entry) | 		_, filename := path.Split(entry) | ||||||
| 
 |  | ||||||
| 		tags = append(tags, filename) | 		tags = append(tags, filename) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -47,7 +51,7 @@ func (ts *tagStore) tags() ([]string, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // exists returns true if the specified manifest tag exists in the repository.
 | // exists returns true if the specified manifest tag exists in the repository.
 | ||||||
| func (ts *tagStore) exists(tag string) (bool, error) { | func (ts *tagStore) exists(ctx context.Context, tag string) (bool, error) { | ||||||
| 	tagPath, err := pathFor(manifestTagCurrentPathSpec{ | 	tagPath, err := pathFor(manifestTagCurrentPathSpec{ | ||||||
| 		name: ts.repository.Name(), | 		name: ts.repository.Name(), | ||||||
| 		tag:  tag, | 		tag:  tag, | ||||||
|  | @ -57,7 +61,7 @@ func (ts *tagStore) exists(tag string) (bool, error) { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	exists, err := exists(ts.ctx, ts.blobStore.driver, tagPath) | 	exists, err := exists(ctx, ts.blobStore.driver, tagPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  | @ -65,9 +69,9 @@ func (ts *tagStore) exists(tag string) (bool, error) { | ||||||
| 	return exists, nil | 	return exists, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // tag tags the digest with the given tag, updating the the store to point at
 | // Tag tags the digest with the given tag, updating the the store to point at
 | ||||||
| // the current tag. The digest must point to a manifest.
 | // the current tag. The digest must point to a manifest.
 | ||||||
| func (ts *tagStore) tag(tag string, revision digest.Digest) error { | func (ts *tagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { | ||||||
| 	currentPath, err := pathFor(manifestTagCurrentPathSpec{ | 	currentPath, err := pathFor(manifestTagCurrentPathSpec{ | ||||||
| 		name: ts.repository.Name(), | 		name: ts.repository.Name(), | ||||||
| 		tag:  tag, | 		tag:  tag, | ||||||
|  | @ -77,43 +81,44 @@ func (ts *tagStore) tag(tag string, revision digest.Digest) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	nbs := ts.linkedBlobStore(ts.ctx, tag) | 	lbs := ts.linkedBlobStore(ctx, tag) | ||||||
|  | 
 | ||||||
| 	// Link into the index
 | 	// Link into the index
 | ||||||
| 	if err := nbs.linkBlob(ts.ctx, distribution.Descriptor{Digest: revision}); err != nil { | 	if err := lbs.linkBlob(ctx, desc); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Overwrite the current link
 | 	// Overwrite the current link
 | ||||||
| 	return ts.blobStore.link(ts.ctx, currentPath, revision) | 	return ts.blobStore.link(ctx, currentPath, desc.Digest) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // resolve the current revision for name and tag.
 | // resolve the current revision for name and tag.
 | ||||||
| func (ts *tagStore) resolve(tag string) (digest.Digest, error) { | func (ts *tagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { | ||||||
| 	currentPath, err := pathFor(manifestTagCurrentPathSpec{ | 	currentPath, err := pathFor(manifestTagCurrentPathSpec{ | ||||||
| 		name: ts.repository.Name(), | 		name: ts.repository.Name(), | ||||||
| 		tag:  tag, | 		tag:  tag, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return distribution.Descriptor{}, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	revision, err := ts.blobStore.readlink(ts.ctx, currentPath) | 	revision, err := ts.blobStore.readlink(ctx, currentPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		switch err.(type) { | 		switch err.(type) { | ||||||
| 		case storagedriver.PathNotFoundError: | 		case storagedriver.PathNotFoundError: | ||||||
| 			return "", distribution.ErrManifestUnknown{Name: ts.repository.Name(), Tag: tag} | 			return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return "", err | 		return distribution.Descriptor{}, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return revision, nil | 	return distribution.Descriptor{Digest: revision}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // delete removes the tag from repository, including the history of all
 | // delete removes the tag from repository, including the history of all
 | ||||||
| // revisions that have the specified tag.
 | // revisions that have the specified tag.
 | ||||||
| func (ts *tagStore) delete(tag string) error { | func (ts *tagStore) Untag(ctx context.Context, tag string) error { | ||||||
| 	tagPath, err := pathFor(manifestTagPathSpec{ | 	tagPath, err := pathFor(manifestTagPathSpec{ | ||||||
| 		name: ts.repository.Name(), | 		name: ts.repository.Name(), | ||||||
| 		tag:  tag, | 		tag:  tag, | ||||||
|  | @ -123,7 +128,7 @@ func (ts *tagStore) delete(tag string) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ts.blobStore.driver.Delete(ts.ctx, tagPath) | 	return ts.blobStore.driver.Delete(ctx, tagPath) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // linkedBlobStore returns the linkedBlobStore for the named tag, allowing one
 | // linkedBlobStore returns the linkedBlobStore for the named tag, allowing one
 | ||||||
|  | @ -145,3 +150,10 @@ func (ts *tagStore) linkedBlobStore(ctx context.Context, tag string) *linkedBlob | ||||||
| 		}}, | 		}}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // Lookup recovers a list of tags which refer to this digest.  When a manifest is deleted by
 | ||||||
|  | // digest, tag entries which point to it need to be recovered to avoid dangling tags.
 | ||||||
|  | func (ts *tagStore) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { | ||||||
|  | 	// An efficient implementation of this will require changes to the S3 driver.
 | ||||||
|  | 	return make([]string, 0), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,150 @@ | ||||||
|  | package storage | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | 	"github.com/docker/distribution/registry/storage/driver/inmemory" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type tagsTestEnv struct { | ||||||
|  | 	ts  distribution.TagService | ||||||
|  | 	ctx context.Context | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func testTagStore(t *testing.T) *tagsTestEnv { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	d := inmemory.New() | ||||||
|  | 	reg, err := NewRegistry(ctx, d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	repo, err := reg.Repository(ctx, "a/b") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tagsTestEnv{ | ||||||
|  | 		ctx: ctx, | ||||||
|  | 		ts:  repo.Tags(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestTagStoreTag(t *testing.T) { | ||||||
|  | 	env := testTagStore(t) | ||||||
|  | 	tags := env.ts | ||||||
|  | 	ctx := env.ctx | ||||||
|  | 
 | ||||||
|  | 	d := distribution.Descriptor{} | ||||||
|  | 	err := tags.Tag(ctx, "latest", d) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Errorf("unexpected error putting malformed descriptor : %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d.Digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | ||||||
|  | 	err = tags.Tag(ctx, "latest", d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d1, err := tags.Get(ctx, "latest") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if d1.Digest != d.Digest { | ||||||
|  | 		t.Error("put and get digest differ") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Overwrite existing
 | ||||||
|  | 	d.Digest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" | ||||||
|  | 	err = tags.Tag(ctx, "latest", d) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	d1, err = tags.Get(ctx, "latest") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if d1.Digest != d.Digest { | ||||||
|  | 		t.Error("put and get digest differ") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestTagStoreUnTag(t *testing.T) { | ||||||
|  | 	env := testTagStore(t) | ||||||
|  | 	tags := env.ts | ||||||
|  | 	ctx := env.ctx | ||||||
|  | 	desc := distribution.Descriptor{Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} | ||||||
|  | 
 | ||||||
|  | 	err := tags.Untag(ctx, "latest") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Errorf("Expected error untagging non-existant tag") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = tags.Tag(ctx, "latest", desc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = tags.Untag(ctx, "latest") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = tags.Get(ctx, "latest") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Error("Expected error getting untagged tag") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestTagAll(t *testing.T) { | ||||||
|  | 	env := testTagStore(t) | ||||||
|  | 	tagStore := env.ts | ||||||
|  | 	ctx := env.ctx | ||||||
|  | 
 | ||||||
|  | 	alpha := "abcdefghijklmnopqrstuvwxyz" | ||||||
|  | 	for i := 0; i < len(alpha); i++ { | ||||||
|  | 		tag := alpha[i] | ||||||
|  | 		desc := distribution.Descriptor{Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"} | ||||||
|  | 		err := tagStore.Tag(ctx, string(tag), desc) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Error(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	all, err := tagStore.All(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 	if len(all) != len(alpha) { | ||||||
|  | 		t.Errorf("Unexpected count returned from enumerate") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i, c := range all { | ||||||
|  | 		if c != string(alpha[i]) { | ||||||
|  | 			t.Errorf("unexpected tag in enumerate %s", c) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	removed := "a" | ||||||
|  | 	err = tagStore.Untag(ctx, removed) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	all, err = tagStore.All(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 	} | ||||||
|  | 	for _, tag := range all { | ||||||
|  | 		if tag == removed { | ||||||
|  | 			t.Errorf("unexpected tag in enumerate %s", removed) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | package distribution | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TagService provides access to information about tagged objects.
 | ||||||
|  | type TagService interface { | ||||||
|  | 	// Get retrieves the descriptor identified by the tag. Some
 | ||||||
|  | 	// implementations may differentiate between "trusted" tags and
 | ||||||
|  | 	// "untrusted" tags. If a tag is "untrusted", the mapping will be returned
 | ||||||
|  | 	// as an ErrTagUntrusted error, with the target descriptor.
 | ||||||
|  | 	Get(ctx context.Context, tag string) (Descriptor, error) | ||||||
|  | 
 | ||||||
|  | 	// Tag associates the tag with the provided descriptor, updating the
 | ||||||
|  | 	// current association, if needed.
 | ||||||
|  | 	Tag(ctx context.Context, tag string, desc Descriptor) error | ||||||
|  | 
 | ||||||
|  | 	// Untag removes the given tag association
 | ||||||
|  | 	Untag(ctx context.Context, tag string) error | ||||||
|  | 
 | ||||||
|  | 	// All returns the set of tags managed by this tag service
 | ||||||
|  | 	All(ctx context.Context) ([]string, error) | ||||||
|  | 
 | ||||||
|  | 	// Lookup returns the set of tags referencing the given digest.
 | ||||||
|  | 	Lookup(ctx context.Context, digest Descriptor) ([]string, error) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue