Implementation of the Manifest Service API refactor.
Add a generic Manifest interface to represent manifests in the registry and remove references to schema specific manifests. Add a ManifestBuilder to construct Manifest objects. Concrete manifest builders will exist for each manifest type and implementations will contain manifest specific data used to build a manifest. Remove Signatures() from Repository interface. Signatures are relevant only to schema1 manifests. Move access to the signature store inside the schema1 manifestStore. Add some API tests to verify signature roundtripping. schema1 ------- Change the way data is stored in schema1.Manifest to enable Payload() to be used to return complete Manifest JSON from the HTTP handler without knowledge of the schema1 protocol. tags ---- Move tag functionality to a seperate TagService and update ManifestService to use the new interfaces. Implement a driver based tagService to be backward compatible with the current tag service. Add a proxyTagService to enable the registry to get a digest for remote manifests from a tag. manifest store -------------- Remove revision store and move all signing functionality into the signed manifeststore. manifest registration --------------------- Add a mechanism to register manifest media types and to allow different manifest types to be Unmarshalled correctly. client ------ Add ManifestServiceOptions to client functions to allow tags to be passed into Put and Get for building correct registry URLs. Change functional arguments to be an interface type to allow passing data without mutating shared state. Signed-off-by: Richard Scothern <richard.scothern@gmail.com> Signed-off-by: Richard Scothern <richard.scothern@docker.com>
This commit is contained in:
parent
0fef25389d
commit
8efb9ca329
18 changed files with 1161 additions and 656 deletions
|
@ -1,6 +1,7 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
|
@ -11,20 +12,21 @@ import (
|
|||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// manifestStore is a storage driver based store for storing schema1 manifests.
|
||||
type manifestStore struct {
|
||||
repository *repository
|
||||
revisionStore *revisionStore
|
||||
tagStore *tagStore
|
||||
blobStore *linkedBlobStore
|
||||
ctx context.Context
|
||||
signatures *signatureStore
|
||||
skipDependencyVerification bool
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
_, err := ms.revisionStore.blobStore.Stat(ms.ctx, dgst)
|
||||
_, err := ms.blobStore.Stat(ms.ctx, dgst)
|
||||
if err != nil {
|
||||
if err == distribution.ErrBlobUnknown {
|
||||
return false, nil
|
||||
|
@ -36,76 +38,131 @@ func (ms *manifestStore) Exists(dgst digest.Digest) (bool, error) {
|
|||
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")
|
||||
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
|
||||
func SkipLayerVerification(ms distribution.ManifestService) error {
|
||||
if ms, ok := ms.(*manifestStore); ok {
|
||||
func SkipLayerVerification() distribution.ManifestServiceOption {
|
||||
return skipLayerOption{}
|
||||
}
|
||||
|
||||
type skipLayerOption struct{}
|
||||
|
||||
func (o skipLayerOption) Apply(m distribution.ManifestService) error {
|
||||
if ms, ok := m.(*manifestStore); ok {
|
||||
ms.skipDependencyVerification = true
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("skip layer verification only valid for manifestStore")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
if err := ms.verifyManifest(ms.ctx, manifest); err != nil {
|
||||
return err
|
||||
sm, ok := manifest.(*schema1.SignedManifest)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("non-v1 manifest put to signed manifestStore: %T", manifest)
|
||||
}
|
||||
|
||||
// Store the revision of the manifest
|
||||
revision, err := ms.revisionStore.put(ms.ctx, manifest)
|
||||
if err := ms.verifyManifest(ms.ctx, *sm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mt := schema1.MediaTypeManifest
|
||||
payload := sm.Canonical
|
||||
|
||||
revision, err := ms.blobStore.Put(ctx, mt, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Now, tag the manifest
|
||||
return ms.tagStore.tag(manifest.Tag, revision.Digest)
|
||||
// Link the revision into the repository.
|
||||
if err := ms.blobStore.linkBlob(ctx, revision); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Grab each json signature and store them.
|
||||
signatures, err := sm.Signatures()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return revision.Digest, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
return ms.revisionStore.delete(ms.ctx, dgst)
|
||||
return ms.blobStore.Delete(ctx, dgst)
|
||||
}
|
||||
|
||||
func (ms *manifestStore) Tags() ([]string, error) {
|
||||
context.GetLogger(ms.ctx).Debug("(*manifestStore).Tags")
|
||||
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)
|
||||
func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
|
||||
return 0, distribution.ErrUnsupported
|
||||
}
|
||||
|
||||
// verifyManifest ensures that the manifest content is valid from the
|
||||
// perspective of the registry. It ensures that the signature is valid for the
|
||||
// enclosed payload. As a policy, the registry only tries to store valid
|
||||
// content, leaving trust policies of that content up to consumers.
|
||||
func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.SignedManifest) error {
|
||||
// content, leaving trust policies of that content up to consumems.
|
||||
func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest) error {
|
||||
var errs distribution.ErrManifestVerification
|
||||
|
||||
if len(mnfst.Name) > reference.NameTotalLengthMax {
|
||||
|
@ -129,7 +186,7 @@ func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.Sign
|
|||
len(mnfst.History), len(mnfst.FSLayers)))
|
||||
}
|
||||
|
||||
if _, err := schema1.Verify(mnfst); err != nil {
|
||||
if _, err := schema1.Verify(&mnfst); err != nil {
|
||||
switch err {
|
||||
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
|
||||
errs = append(errs, distribution.ErrManifestUnverified{})
|
||||
|
@ -143,15 +200,15 @@ func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *schema1.Sign
|
|||
}
|
||||
|
||||
if !ms.skipDependencyVerification {
|
||||
for _, fsLayer := range mnfst.FSLayers {
|
||||
_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.BlobSum)
|
||||
for _, fsLayer := range mnfst.References() {
|
||||
_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest)
|
||||
if err != nil {
|
||||
if err != distribution.ErrBlobUnknown {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
// On error here, we always append unknown blob errors.
|
||||
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.BlobSum})
|
||||
// On error here, we always append unknown blob erroms.
|
||||
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@ type manifestStoreTestEnv struct {
|
|||
func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv {
|
||||
ctx := context.Background()
|
||||
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 {
|
||||
t.Fatalf("error creating registry: %v", err)
|
||||
}
|
||||
|
@ -58,24 +59,6 @@ func TestManifestStorage(t *testing.T) {
|
|||
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{
|
||||
Versioned: manifest.Versioned{
|
||||
SchemaVersion: 1,
|
||||
|
@ -114,7 +97,7 @@ func TestManifestStorage(t *testing.T) {
|
|||
t.Fatalf("error signing manifest: %v", err)
|
||||
}
|
||||
|
||||
err = ms.Put(sm)
|
||||
_, err = ms.Put(ctx, sm)
|
||||
if err == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
exists, err = ms.ExistsByTag(env.tag)
|
||||
exists, err := ms.Exists(ctx, manifestDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking manifest existence: %v", err)
|
||||
t.Fatalf("unexpected error checking manifest existence: %#v", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("manifest should exist")
|
||||
}
|
||||
|
||||
fetchedManifest, err := ms.GetByTag(env.tag)
|
||||
|
||||
fromStore, err := ms.Get(ctx, manifestDigest)
|
||||
if err != nil {
|
||||
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) {
|
||||
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 {
|
||||
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
|
||||
// return by the payload digest.
|
||||
|
||||
dgst := digest.FromBytes(payload)
|
||||
exists, err = ms.Exists(dgst)
|
||||
exists, err = ms.Exists(ctx, dgst)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
fetchedByDigest, err := ms.Get(dgst)
|
||||
fetchedByDigest, err := ms.Get(ctx, dgst)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
pk2, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
|
@ -237,8 +217,12 @@ func TestManifestStorage(t *testing.T) {
|
|||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
if err = ms.Put(sm2); err != nil {
|
||||
if manifestDigest, err = ms.Put(ctx, sm2); err != nil {
|
||||
t.Fatalf("unexpected error putting manifest: %v", err)
|
||||
}
|
||||
|
||||
fetched, err := ms.GetByTag(env.tag)
|
||||
fromStore, err = ms.Get(ctx, manifestDigest)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatalf("unexpected error parsing jws: %v", err)
|
||||
}
|
||||
|
@ -302,12 +296,12 @@ func TestManifestStorage(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test deleting manifests
|
||||
err = ms.Delete(dgst)
|
||||
err = ms.Delete(ctx, dgst)
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Fatalf("Error querying manifest existence")
|
||||
}
|
||||
|
@ -315,7 +309,7 @@ func TestManifestStorage(t *testing.T) {
|
|||
t.Errorf("Deleted manifest should not exist")
|
||||
}
|
||||
|
||||
deletedManifest, err := ms.Get(dgst)
|
||||
deletedManifest, err := ms.Get(ctx, dgst)
|
||||
if err == nil {
|
||||
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
|
||||
err = ms.Put(sm)
|
||||
_, err = ms.Put(ctx, sm)
|
||||
if err != nil {
|
||||
t.Errorf("Error re-uploading deleted manifest")
|
||||
}
|
||||
|
||||
exists, err = ms.Exists(dgst)
|
||||
exists, err = ms.Exists(ctx, dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("Error querying manifest existence")
|
||||
}
|
||||
|
@ -344,7 +338,7 @@ func TestManifestStorage(t *testing.T) {
|
|||
t.Errorf("Restored manifest should exist")
|
||||
}
|
||||
|
||||
deletedManifest, err = ms.Get(dgst)
|
||||
deletedManifest, err = ms.Get(ctx, dgst)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error getting manifest")
|
||||
}
|
||||
|
@ -364,7 +358,7 @@ func TestManifestStorage(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ms.Delete(dgst)
|
||||
err = ms.Delete(ctx, dgst)
|
||||
if err == nil {
|
||||
t.Errorf("Unexpected success deleting while disabled")
|
||||
}
|
||||
|
|
|
@ -145,6 +145,15 @@ func (repo *repository) Name() string {
|
|||
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
|
||||
// may be context sensitive in the future. The instance should be used similar
|
||||
// to a request local.
|
||||
|
@ -159,36 +168,31 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
|
|||
ms := &manifestStore{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
revisionStore: &revisionStore{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
blobStore: &linkedBlobStore{
|
||||
ctx: ctx,
|
||||
blobStore: repo.blobStore,
|
||||
repository: repo,
|
||||
deleteEnabled: repo.registry.deleteEnabled,
|
||||
blobAccessController: &linkedBlobStatter{
|
||||
blobStore: repo.blobStore,
|
||||
repository: repo,
|
||||
linkPathFns: manifestLinkPathFns,
|
||||
},
|
||||
|
||||
// TODO(stevvooe): linkPath limits this blob store to only
|
||||
// manifests. This instance cannot be used for blob checks.
|
||||
linkPathFns: manifestLinkPathFns,
|
||||
resumableDigestEnabled: repo.resumableDigestEnabled,
|
||||
blobStore: &linkedBlobStore{
|
||||
ctx: ctx,
|
||||
blobStore: repo.blobStore,
|
||||
repository: repo,
|
||||
deleteEnabled: repo.registry.deleteEnabled,
|
||||
blobAccessController: &linkedBlobStatter{
|
||||
blobStore: repo.blobStore,
|
||||
repository: repo,
|
||||
linkPathFns: manifestLinkPathFns,
|
||||
},
|
||||
|
||||
// TODO(stevvooe): linkPath limits this blob store to only
|
||||
// manifests. This instance cannot be used for blob checks.
|
||||
linkPathFns: manifestLinkPathFns,
|
||||
},
|
||||
tagStore: &tagStore{
|
||||
signatures: &signatureStore{
|
||||
ctx: ctx,
|
||||
repository: repo,
|
||||
blobStore: repo.registry.blobStore,
|
||||
blobStore: repo.blobStore,
|
||||
},
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, option := range options {
|
||||
err := option(ms)
|
||||
err := option.Apply(ms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -225,11 +229,3 @@ func (repo *repository) Blobs(ctx context.Context) distribution.BlobStore {
|
|||
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"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
@ -15,16 +14,6 @@ type signatureStore struct {
|
|||
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) {
|
||||
signaturesPath, err := pathFor(manifestSignaturesPathSpec{
|
||||
name: s.repository.Name(),
|
||||
|
|
|
@ -9,37 +9,41 @@ import (
|
|||
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
||||
)
|
||||
|
||||
var _ distribution.TagService = &tagStore{}
|
||||
|
||||
// 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 {
|
||||
repository *repository
|
||||
blobStore *blobStore
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// tags lists the manifest tags for the specified repository.
|
||||
func (ts *tagStore) tags() ([]string, error) {
|
||||
p, err := pathFor(manifestTagPathSpec{
|
||||
// All returns all tags
|
||||
func (ts *tagStore) All(ctx context.Context) ([]string, error) {
|
||||
var tags []string
|
||||
|
||||
pathSpec, err := pathFor(manifestTagPathSpec{
|
||||
name: ts.repository.Name(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return tags, err
|
||||
}
|
||||
|
||||
var tags []string
|
||||
entries, err := ts.blobStore.driver.List(ts.ctx, p)
|
||||
entries, err := ts.blobStore.driver.List(ctx, pathSpec)
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case storagedriver.PathNotFoundError:
|
||||
return nil, distribution.ErrRepositoryUnknown{Name: ts.repository.Name()}
|
||||
return tags, distribution.ErrRepositoryUnknown{Name: ts.repository.Name()}
|
||||
default:
|
||||
return nil, err
|
||||
return tags, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
_, filename := path.Split(entry)
|
||||
|
||||
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.
|
||||
func (ts *tagStore) exists(tag string) (bool, error) {
|
||||
func (ts *tagStore) exists(ctx context.Context, tag string) (bool, error) {
|
||||
tagPath, err := pathFor(manifestTagCurrentPathSpec{
|
||||
name: ts.repository.Name(),
|
||||
tag: tag,
|
||||
|
@ -57,7 +61,7 @@ func (ts *tagStore) exists(tag string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
exists, err := exists(ts.ctx, ts.blobStore.driver, tagPath)
|
||||
exists, err := exists(ctx, ts.blobStore.driver, tagPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -65,9 +69,9 @@ func (ts *tagStore) exists(tag string) (bool, error) {
|
|||
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.
|
||||
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{
|
||||
name: ts.repository.Name(),
|
||||
tag: tag,
|
||||
|
@ -77,43 +81,44 @@ func (ts *tagStore) tag(tag string, revision digest.Digest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
nbs := ts.linkedBlobStore(ts.ctx, tag)
|
||||
lbs := ts.linkedBlobStore(ctx, tag)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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{
|
||||
name: ts.repository.Name(),
|
||||
tag: tag,
|
||||
})
|
||||
|
||||
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 {
|
||||
switch err.(type) {
|
||||
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
|
||||
// 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{
|
||||
name: ts.repository.Name(),
|
||||
tag: tag,
|
||||
|
@ -123,7 +128,7 @@ func (ts *tagStore) delete(tag string) error {
|
|||
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
|
||||
|
@ -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
|
||||
}
|
||||
|
|
150
docs/storage/tagstore_test.go
Normal file
150
docs/storage/tagstore_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue