diff --git a/storage/blobstore.go b/storage/blobstore.go new file mode 100644 index 000000000..bd7b3fc83 --- /dev/null +++ b/storage/blobstore.go @@ -0,0 +1,159 @@ +package storage + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + + "github.com/docker/distribution/digest" + "github.com/docker/distribution/storagedriver" +) + +// TODO(stevvooe): Currently, the blobStore implementation used by the +// manifest store. The layer store should be refactored to better leverage the +// blobStore, reducing duplicated code. + +// blobStore implements a generalized blob store over a driver, supporting the +// read side and link management. This object is intentionally a leaky +// abstraction, providing utility methods that support creating and traversing +// backend links. +type blobStore struct { + driver storagedriver.StorageDriver + pm *pathMapper +} + +// exists reports whether or not the path exists. If the driver returns error +// other than storagedriver.PathNotFound, an error may be returned. +func (bs *blobStore) exists(dgst digest.Digest) (bool, error) { + path, err := bs.path(dgst) + + if err != nil { + return false, err + } + + ok, err := exists(bs.driver, path) + if err != nil { + return false, err + } + + return ok, nil +} + +// get retrieves the blob by digest, returning it a byte slice. This should +// only be used for small objects. +func (bs *blobStore) get(dgst digest.Digest) ([]byte, error) { + bp, err := bs.path(dgst) + if err != nil { + return nil, err + } + + return bs.driver.GetContent(bp) +} + +// link links the path to the provided digest by writing the digest into the +// target file. +func (bs *blobStore) link(path string, dgst digest.Digest) error { + if exists, err := bs.exists(dgst); err != nil { + return err + } else if !exists { + return fmt.Errorf("cannot link non-existent blob") + } + + // The contents of the "link" file are the exact string contents of the + // digest, which is specified in that package. + return bs.driver.PutContent(path, []byte(dgst)) +} + +// linked reads the link at path and returns the content. +func (bs *blobStore) linked(path string) ([]byte, error) { + linked, err := bs.readlink(path) + if err != nil { + return nil, err + } + + return bs.get(linked) +} + +// readlink returns the linked digest at path. +func (bs *blobStore) readlink(path string) (digest.Digest, error) { + content, err := bs.driver.GetContent(path) + if err != nil { + return "", err + } + + linked, err := digest.ParseDigest(string(content)) + if err != nil { + return "", err + } + + if exists, err := bs.exists(linked); err != nil { + return "", err + } else if !exists { + return "", fmt.Errorf("link %q invalid: blob %s does not exist", path, linked) + } + + return linked, nil +} + +// resolve reads the digest link at path and returns the blob store link. +func (bs *blobStore) resolve(path string) (string, error) { + dgst, err := bs.readlink(path) + if err != nil { + return "", err + } + + return bs.path(dgst) +} + +// put stores the content p in the blob store, calculating the digest. If the +// content is already present, only the digest will be returned. This should +// only be used for small objects, such as manifests. +func (bs *blobStore) put(p []byte) (digest.Digest, error) { + dgst, err := digest.FromBytes(p) + if err != nil { + logrus.Errorf("error digesting content: %v, %s", err, string(p)) + return "", err + } + + bp, err := bs.path(dgst) + if err != nil { + return "", err + } + + // If the content already exists, just return the digest. + if exists, err := bs.exists(dgst); err != nil { + return "", err + } else if exists { + return dgst, nil + } + + return dgst, bs.driver.PutContent(bp, p) +} + +// path returns the canonical path for the blob identified by digest. The blob +// may or may not exist. +func (bs *blobStore) path(dgst digest.Digest) (string, error) { + bp, err := bs.pm.path(blobDataPathSpec{ + digest: dgst, + }) + + if err != nil { + return "", err + } + + return bp, nil +} + +// exists provides a utility method to test whether or not +func exists(driver storagedriver.StorageDriver, path string) (bool, error) { + if _, err := driver.Stat(path); err != nil { + switch err := err.(type) { + case storagedriver.PathNotFoundError: + return false, nil + default: + return false, err + } + } + + return true, nil +} diff --git a/storage/delegatelayerhandler.go b/storage/delegatelayerhandler.go index cc0622bf5..5c30f4db3 100644 --- a/storage/delegatelayerhandler.go +++ b/storage/delegatelayerhandler.go @@ -54,12 +54,17 @@ func (lh *delegateLayerHandler) Resolve(layer Layer) (http.Handler, error) { // urlFor returns a download URL for the given layer, or the empty string if // unsupported. func (lh *delegateLayerHandler) urlFor(layer Layer) (string, error) { - blobPath, err := resolveBlobPath(lh.storageDriver, lh.pathMapper, layer.Name(), layer.Digest()) - if err != nil { - return "", err + // Crack open the layer to get at the layerStore + layerRd, ok := layer.(*layerReader) + if !ok { + // TODO(stevvooe): We probably want to find a better way to get at the + // underlying filesystem path for a given layer. Perhaps, the layer + // handler should have its own layer store but right now, it is not + // request scoped. + return "", fmt.Errorf("unsupported layer type: cannot resolve blob path: %v", layer) } - layerURL, err := lh.storageDriver.URLFor(blobPath, map[string]interface{}{"expiry": time.Now().Add(lh.duration)}) + layerURL, err := lh.storageDriver.URLFor(layerRd.path, map[string]interface{}{"expiry": time.Now().Add(lh.duration)}) if err != nil { return "", err } diff --git a/storage/layer_test.go b/storage/layer_test.go index d6f4718aa..c6b7b0d8a 100644 --- a/storage/layer_test.go +++ b/storage/layer_test.go @@ -31,13 +31,18 @@ func TestSimpleLayerUpload(t *testing.T) { } imageName := "foo/bar" - + driver := inmemory.New() + pm := &pathMapper{ + root: "/storage/testing", + version: storagePathVersion, + } ls := &layerStore{ - driver: inmemory.New(), - pathMapper: &pathMapper{ - root: "/storage/testing", - version: storagePathVersion, + driver: driver, + blobStore: &blobStore{ + driver: driver, + pm: pm, }, + pathMapper: pm, } h := sha256.New() @@ -140,12 +145,17 @@ func TestSimpleLayerUpload(t *testing.T) { func TestSimpleLayerRead(t *testing.T) { imageName := "foo/bar" driver := inmemory.New() + pm := &pathMapper{ + root: "/storage/testing", + version: storagePathVersion, + } ls := &layerStore{ driver: driver, - pathMapper: &pathMapper{ - root: "/storage/testing", - version: storagePathVersion, + blobStore: &blobStore{ + driver: driver, + pm: pm, }, + pathMapper: pm, } randomLayerReader, tarSumStr, err := testutil.CreateRandomTarFile() @@ -307,7 +317,7 @@ func writeTestLayer(driver storagedriver.StorageDriver, pathMapper *pathMapper, blobDigestSHA := digest.NewDigest("sha256", h) - blobPath, err := pathMapper.path(blobPathSpec{ + blobPath, err := pathMapper.path(blobDataPathSpec{ digest: dgst, }) diff --git a/storage/layerstore.go b/storage/layerstore.go index 41227cc5b..6d399af0e 100644 --- a/storage/layerstore.go +++ b/storage/layerstore.go @@ -12,6 +12,7 @@ import ( type layerStore struct { driver storagedriver.StorageDriver pathMapper *pathMapper + blobStore *blobStore } func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) { @@ -31,31 +32,21 @@ func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) { return true, nil } -func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) { - blobPath, err := resolveBlobPath(ls.driver, ls.pathMapper, name, digest) +func (ls *layerStore) Fetch(name string, dgst digest.Digest) (Layer, error) { + bp, err := ls.path(name, dgst) if err != nil { - switch err := err.(type) { - case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: - return nil, ErrUnknownLayer{manifest.FSLayer{BlobSum: digest}} - default: - return nil, err - } + return nil, err } - fr, err := newFileReader(ls.driver, blobPath) + fr, err := newFileReader(ls.driver, bp) if err != nil { - switch err := err.(type) { - case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: - return nil, ErrUnknownLayer{manifest.FSLayer{BlobSum: digest}} - default: - return nil, err - } + return nil, err } return &layerReader{ fileReader: *fr, name: name, - digest: digest, + digest: dgst, }, nil } @@ -151,3 +142,24 @@ func (ls *layerStore) newLayerUpload(name, uuid, path string, startedAt time.Tim fileWriter: *fw, }, nil } + +func (ls *layerStore) path(name string, dgst digest.Digest) (string, error) { + // We must traverse this path through the link to enforce ownership. + layerLinkPath, err := ls.pathMapper.path(layerLinkPathSpec{name: name, digest: dgst}) + if err != nil { + return "", err + } + + blobPath, err := ls.blobStore.resolve(layerLinkPath) + + if err != nil { + switch err := err.(type) { + case storagedriver.PathNotFoundError: + return "", ErrUnknownLayer{manifest.FSLayer{BlobSum: dgst}} + default: + return "", err + } + } + + return blobPath, nil +} diff --git a/storage/layerupload.go b/storage/layerupload.go index b9953b236..c71176350 100644 --- a/storage/layerupload.go +++ b/storage/layerupload.go @@ -112,7 +112,7 @@ func (luc *layerUploadController) validateLayer(dgst digest.Digest) (digest.Dige // sink. Instead, its read driven. This might be okay. // Calculate an updated digest with the latest version. - canonical, err := digest.FromReader(tr) + canonical, err := digest.FromTarArchive(tr) if err != nil { return "", err } @@ -128,7 +128,7 @@ func (luc *layerUploadController) validateLayer(dgst digest.Digest) (digest.Dige // identified by dgst. The layer should be validated before commencing the // move. func (luc *layerUploadController) moveLayer(dgst digest.Digest) error { - blobPath, err := luc.layerStore.pathMapper.path(blobPathSpec{ + blobPath, err := luc.layerStore.pathMapper.path(blobDataPathSpec{ digest: dgst, }) diff --git a/storage/manifeststore.go b/storage/manifeststore.go index af16dcf33..2a8c5f18a 100644 --- a/storage/manifeststore.go +++ b/storage/manifeststore.go @@ -1,11 +1,10 @@ package storage import ( - "encoding/json" "fmt" - "path" "strings" + "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/storagedriver" "github.com/docker/libtrust" @@ -32,6 +31,17 @@ func (err ErrUnknownManifest) Error() string { return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag) } +// ErrUnknownManifestRevision is returned when a manifest cannot be found by +// revision within a repository. +type ErrUnknownManifestRevision struct { + Name string + Revision digest.Digest +} + +func (err ErrUnknownManifestRevision) Error() string { + return fmt.Sprintf("unknown manifest name=%s revision=%s", err.Name, err.Revision) +} + // ErrManifestUnverified is returned when the registry is unable to verify // the manifest. type ErrManifestUnverified struct{} @@ -55,143 +65,73 @@ func (errs ErrManifestVerification) Error() string { } type manifestStore struct { - driver storagedriver.StorageDriver - pathMapper *pathMapper - layerService LayerService + driver storagedriver.StorageDriver + pathMapper *pathMapper + revisionStore *revisionStore + tagStore *tagStore + blobStore *blobStore + layerService LayerService } var _ ManifestService = &manifestStore{} func (ms *manifestStore) Tags(name string) ([]string, error) { - p, err := ms.pathMapper.path(manifestTagsPath{ - name: name, - }) - if err != nil { - return nil, err - } - - var tags []string - entries, err := ms.driver.List(p) - if err != nil { - switch err := err.(type) { - case storagedriver.PathNotFoundError: - return nil, ErrUnknownRepository{Name: name} - default: - return nil, err - } - } - - for _, entry := range entries { - _, filename := path.Split(entry) - - tags = append(tags, filename) - } - - return tags, nil + return ms.tagStore.tags(name) } func (ms *manifestStore) Exists(name, tag string) (bool, error) { - p, err := ms.path(name, tag) - if err != nil { - return false, err - } - - fi, err := ms.driver.Stat(p) - if err != nil { - switch err.(type) { - case storagedriver.PathNotFoundError: - return false, nil - default: - return false, err - } - } - - if fi.IsDir() { - return false, fmt.Errorf("unexpected directory at path: %v, name=%s tag=%s", p, name, tag) - } - - if fi.Size() == 0 { - return false, nil - } - - return true, nil + return ms.tagStore.exists(name, tag) } func (ms *manifestStore) Get(name, tag string) (*manifest.SignedManifest, error) { - p, err := ms.path(name, tag) + dgst, err := ms.tagStore.resolve(name, tag) if err != nil { return nil, err } - content, err := ms.driver.GetContent(p) - if err != nil { - switch err := err.(type) { - case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: - return nil, ErrUnknownManifest{Name: name, Tag: tag} - default: - return nil, err - } - } - - var manifest manifest.SignedManifest - - if err := json.Unmarshal(content, &manifest); err != nil { - // TODO(stevvooe): Corrupted manifest error? - return nil, err - } - - // TODO(stevvooe): Verify the manifest here? - - return &manifest, nil + return ms.revisionStore.get(name, dgst) } func (ms *manifestStore) Put(name, tag string, manifest *manifest.SignedManifest) error { - p, err := ms.path(name, tag) - if err != nil { - return err - } - + // Verify the manifest. if err := ms.verifyManifest(name, tag, manifest); err != nil { return err } - // TODO(stevvooe): Should we get old manifest first? Perhaps, write, then - // move to ensure a valid manifest? - - return ms.driver.PutContent(p, manifest.Raw) -} - -func (ms *manifestStore) Delete(name, tag string) error { - p, err := ms.path(name, tag) + // Store the revision of the manifest + revision, err := ms.revisionStore.put(name, manifest) if err != nil { return err } - if err := ms.driver.Delete(p); err != nil { - switch err := err.(type) { - case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: - return ErrUnknownManifest{Name: name, Tag: tag} - default: + // Now, tag the manifest + return ms.tagStore.tag(name, tag, revision) +} + +// Delete removes all revisions of the given tag. We may want to change these +// semantics in the future, but this will maintain consistency. The underlying +// blobs are left alone. +func (ms *manifestStore) Delete(name, tag string) error { + revisions, err := ms.tagStore.revisions(name, tag) + if err != nil { + return err + } + + for _, revision := range revisions { + if err := ms.revisionStore.delete(name, revision); err != nil { return err } } - return nil -} - -func (ms *manifestStore) path(name, tag string) (string, error) { - return ms.pathMapper.path(manifestPathSpec{ - name: name, - tag: tag, - }) + return ms.tagStore.delete(name, tag) } +// verifyManifest ensures that the manifest content is valid from the +// perspective of the registry. It ensures that the name and tag match and +// 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(name, tag string, mnfst *manifest.SignedManifest) error { - // TODO(stevvooe): This verification is present here, but this needs to be - // lifted out of the storage infrastructure and moved into a package - // oriented towards defining verifiers and reporting them with - // granularity. - var errs ErrManifestVerification if mnfst.Name != name { // TODO(stevvooe): This needs to be an exported error @@ -203,10 +143,6 @@ func (ms *manifestStore) verifyManifest(name, tag string, mnfst *manifest.Signed errs = append(errs, fmt.Errorf("tag does not match manifest tag")) } - // TODO(stevvooe): These pubkeys need to be checked with either Verify or - // VerifyWithChains. We need to define the exact source of the CA. - // Perhaps, its a configuration value injected into manifest store. - if _, err := manifest.Verify(mnfst); err != nil { switch err { case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: diff --git a/storage/manifeststore_test.go b/storage/manifeststore_test.go index a6cca9627..5f9b3f379 100644 --- a/storage/manifeststore_test.go +++ b/storage/manifeststore_test.go @@ -1,6 +1,7 @@ package storage import ( + "bytes" "reflect" "testing" @@ -12,12 +13,28 @@ import ( func TestManifestStorage(t *testing.T) { driver := inmemory.New() - ms := &manifestStore{ + pm := pathMapper{ + root: "/storage/testing", + version: storagePathVersion, + } + bs := blobStore{ driver: driver, - pathMapper: &pathMapper{ - root: "/storage/testing", - version: storagePathVersion, + pm: &pm, + } + ms := &manifestStore{ + driver: driver, + pathMapper: &pm, + revisionStore: &revisionStore{ + driver: driver, + pathMapper: &pm, + blobStore: &bs, }, + tagStore: &tagStore{ + driver: driver, + pathMapper: &pm, + blobStore: &bs, + }, + blobStore: &bs, layerService: newMockedLayerService(), } @@ -100,6 +117,25 @@ func TestManifestStorage(t *testing.T) { t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm) } + fetchedJWS, err := libtrust.ParsePrettySignature(fetchedManifest.Raw, "signatures") + if err != nil { + t.Fatalf("unexpected error parsing jws: %v", err) + } + + payload, err := fetchedJWS.Payload() + if err != nil { + t.Fatalf("unexpected error extracting payload: %v", err) + } + + sigs, err := fetchedJWS.Signatures() + if err != nil { + t.Fatalf("unable to extract signatures: %v", err) + } + + if 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(name) if err != nil { @@ -113,6 +149,84 @@ func TestManifestStorage(t *testing.T) { if tags[0] != tag { t.Fatalf("unexpected tag found in tags: %v != %v", tags, []string{tag}) } + + // Now, push the same manifest with a different key + pk2, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + t.Fatalf("unexpected error generating private key: %v", err) + } + + sm2, err := manifest.Sign(&m, pk2) + if err != nil { + t.Fatalf("unexpected error signing manifest: %v", err) + } + + jws2, err := libtrust.ParsePrettySignature(sm2.Raw, "signatures") + if err != nil { + t.Fatalf("error parsing signature: %v", err) + } + + sigs2, err := jws2.Signatures() + if err != nil { + t.Fatalf("unable to extract signatures: %v", err) + } + + if len(sigs2) != 1 { + t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1) + } + + if err = ms.Put(name, tag, sm2); err != nil { + t.Fatalf("unexpected error putting manifest: %v", err) + } + + fetched, err := ms.Get(name, tag) + if err != nil { + t.Fatalf("unexpected error fetching manifest: %v", err) + } + + if _, err := manifest.Verify(fetched); err != nil { + t.Fatalf("unexpected error verifying manifest: %v", err) + } + + // Assemble our payload and two signatures to get what we expect! + expectedJWS, err := libtrust.NewJSONSignature(payload, sigs[0], sigs2[0]) + if err != nil { + t.Fatalf("unexpected error merging jws: %v", err) + } + + expectedSigs, err := expectedJWS.Signatures() + if err != nil { + t.Fatalf("unexpected error getting expected signatures: %v", err) + } + + receivedJWS, err := libtrust.ParsePrettySignature(fetched.Raw, "signatures") + if err != nil { + t.Fatalf("unexpected error parsing jws: %v", err) + } + + receivedPayload, err := receivedJWS.Payload() + if err != nil { + t.Fatalf("unexpected error extracting received payload: %v", err) + } + + if !bytes.Equal(receivedPayload, payload) { + t.Fatalf("payloads are not equal") + } + + receivedSigs, err := receivedJWS.Signatures() + if err != nil { + t.Fatalf("error getting signatures: %v", err) + } + + for i, sig := range receivedSigs { + if !bytes.Equal(sig, expectedSigs[i]) { + t.Fatalf("mismatched signatures from remote: %v != %v", string(sig), string(expectedSigs[i])) + } + } + + if err := ms.Delete(name, tag); err != nil { + t.Fatalf("unexpected error deleting manifest: %v", err) + } } type layerKey struct { diff --git a/storage/paths.go b/storage/paths.go index 13777ed73..f393a62ab 100644 --- a/storage/paths.go +++ b/storage/paths.go @@ -188,7 +188,7 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) { return "", err } - return path.Join(root, "current/link"), nil + return path.Join(root, "current", "link"), nil case manifestTagIndexPathSpec: root, err := pm.path(manifestTagPathSpec{ name: v.name, diff --git a/storage/revisionstore.go b/storage/revisionstore.go new file mode 100644 index 000000000..ff286cab8 --- /dev/null +++ b/storage/revisionstore.go @@ -0,0 +1,217 @@ +package storage + +import ( + "encoding/json" + "path" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest" + "github.com/docker/distribution/storagedriver" + "github.com/docker/libtrust" +) + +// revisionStore supports storing and managing manifest revisions. +type revisionStore struct { + driver storagedriver.StorageDriver + pathMapper *pathMapper + blobStore *blobStore +} + +// exists returns true if the revision is available in the named repository. +func (rs *revisionStore) exists(name string, revision digest.Digest) (bool, error) { + revpath, err := rs.pathMapper.path(manifestRevisionPathSpec{ + name: name, + revision: revision, + }) + + if err != nil { + return false, err + } + + exists, err := exists(rs.driver, revpath) + if err != nil { + return false, err + } + + return exists, nil +} + +// get retrieves the manifest, keyed by revision digest. +func (rs *revisionStore) get(name string, revision digest.Digest) (*manifest.SignedManifest, error) { + // Ensure that this revision is available in this repository. + if exists, err := rs.exists(name, revision); err != nil { + return nil, err + } else if !exists { + return nil, ErrUnknownManifestRevision{ + Name: name, + Revision: revision, + } + } + + content, err := rs.blobStore.get(revision) + if err != nil { + return nil, err + } + + // Fetch the signatures for the manifest + signatures, err := rs.getSignatures(name, revision) + if err != nil { + return nil, err + } + + logrus.Infof("retrieved signatures: %v", string(signatures[0])) + + 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 manifest.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(name string, sm *manifest.SignedManifest) (digest.Digest, error) { + jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures") + if err != nil { + return "", err + } + + // Resolve the payload in the manifest. + payload, err := jsig.Payload() + if err != nil { + return "", err + } + + // Digest and store the manifest payload in the blob store. + revision, err := rs.blobStore.put(payload) + if err != nil { + logrus.Errorf("error putting payload into blobstore: %v", err) + return "", err + } + + // Link the revision into the repository. + if err := rs.link(name, revision); err != nil { + return "", err + } + + // Grab each json signature and store them. + signatures, err := jsig.Signatures() + if err != nil { + return "", err + } + + for _, signature := range signatures { + if err := rs.putSignature(name, revision, signature); err != nil { + return "", err + } + } + + return revision, nil +} + +// link links the revision into the repository. +func (rs *revisionStore) link(name string, revision digest.Digest) error { + revisionPath, err := rs.pathMapper.path(manifestRevisionLinkPathSpec{ + name: name, + revision: revision, + }) + + if err != nil { + return err + } + + if exists, err := exists(rs.driver, revisionPath); err != nil { + return err + } else if exists { + // Revision has already been linked! + return nil + } + + return rs.blobStore.link(revisionPath, revision) +} + +// delete removes the specified manifest revision from storage. +func (rs *revisionStore) delete(name string, revision digest.Digest) error { + revisionPath, err := rs.pathMapper.path(manifestRevisionPathSpec{ + name: name, + revision: revision, + }) + + if err != nil { + return err + } + + return rs.driver.Delete(revisionPath) +} + +// getSignatures retrieves all of the signature blobs for the specified +// manifest revision. +func (rs *revisionStore) getSignatures(name string, revision digest.Digest) ([][]byte, error) { + signaturesPath, err := rs.pathMapper.path(manifestSignaturesPathSpec{ + name: name, + revision: revision, + }) + + if err != nil { + return nil, err + } + + // Need to append signature digest algorithm to path to get all items. + // Perhaps, this should be in the pathMapper but it feels awkward. This + // can be eliminated by implementing listAll on drivers. + signaturesPath = path.Join(signaturesPath, "sha256") + + signaturePaths, err := rs.driver.List(signaturesPath) + if err != nil { + return nil, err + } + + var signatures [][]byte + for _, sigPath := range signaturePaths { + // Append the link portion + sigPath = path.Join(sigPath, "link") + + // TODO(stevvooe): These fetches should be parallelized for performance. + p, err := rs.blobStore.linked(sigPath) + if err != nil { + return nil, err + } + + signatures = append(signatures, p) + } + + return signatures, nil +} + +// putSignature stores the signature for the provided manifest revision. +func (rs *revisionStore) putSignature(name string, revision digest.Digest, signature []byte) error { + signatureDigest, err := rs.blobStore.put(signature) + if err != nil { + return err + } + + signaturePath, err := rs.pathMapper.path(manifestSignatureLinkPathSpec{ + name: name, + revision: revision, + signature: signatureDigest, + }) + + if err != nil { + return err + } + + return rs.blobStore.link(signaturePath, signatureDigest) +} diff --git a/storage/services.go b/storage/services.go index 97edca3fc..81b25025e 100644 --- a/storage/services.go +++ b/storage/services.go @@ -28,14 +28,42 @@ func NewServices(driver storagedriver.StorageDriver) *Services { // may be context sensitive in the future. The instance should be used similar // to a request local. func (ss *Services) Layers() LayerService { - return &layerStore{driver: ss.driver, pathMapper: ss.pathMapper} + return &layerStore{ + driver: ss.driver, + blobStore: &blobStore{ + driver: ss.driver, + pm: ss.pathMapper, + }, + pathMapper: ss.pathMapper, + } } // 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. func (ss *Services) Manifests() ManifestService { - return &manifestStore{driver: ss.driver, pathMapper: ss.pathMapper, layerService: ss.Layers()} + // TODO(stevvooe): Lose this kludge. An intermediary object is clearly + // missing here. This initialization is a mess. + bs := &blobStore{ + driver: ss.driver, + pm: ss.pathMapper, + } + + return &manifestStore{ + driver: ss.driver, + pathMapper: ss.pathMapper, + revisionStore: &revisionStore{ + driver: ss.driver, + pathMapper: ss.pathMapper, + blobStore: bs, + }, + tagStore: &tagStore{ + driver: ss.driver, + blobStore: bs, + pathMapper: ss.pathMapper, + }, + blobStore: bs, + layerService: ss.Layers()} } // ManifestService provides operations on image manifests. @@ -43,7 +71,7 @@ type ManifestService interface { // Tags lists the tags under the named repository. Tags(name string) ([]string, error) - // Exists returns true if the layer exists. + // Exists returns true if the manifest exists. Exists(name, tag string) (bool, error) // Get retrieves the named manifest, if it exists. diff --git a/storage/tagstore.go b/storage/tagstore.go new file mode 100644 index 000000000..a3fd6da2c --- /dev/null +++ b/storage/tagstore.go @@ -0,0 +1,159 @@ +package storage + +import ( + "path" + + "github.com/docker/distribution/digest" + "github.com/docker/distribution/storagedriver" +) + +// tagStore provides methods to manage manifest tags in a backend storage driver. +type tagStore struct { + driver storagedriver.StorageDriver + blobStore *blobStore + pathMapper *pathMapper +} + +// tags lists the manifest tags for the specified repository. +func (ts *tagStore) tags(name string) ([]string, error) { + p, err := ts.pathMapper.path(manifestTagPathSpec{ + name: name, + }) + if err != nil { + return nil, err + } + + var tags []string + entries, err := ts.driver.List(p) + if err != nil { + switch err := err.(type) { + case storagedriver.PathNotFoundError: + return nil, ErrUnknownRepository{Name: name} + default: + return nil, err + } + } + + for _, entry := range entries { + _, filename := path.Split(entry) + + tags = append(tags, filename) + } + + return tags, nil +} + +// exists returns true if the specified manifest tag exists in the repository. +func (ts *tagStore) exists(name, tag string) (bool, error) { + tagPath, err := ts.pathMapper.path(manifestTagCurrentPathSpec{ + name: name, + tag: tag, + }) + if err != nil { + return false, err + } + + exists, err := exists(ts.driver, tagPath) + if err != nil { + return false, err + } + + return exists, nil +} + +// 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(name, tag string, revision digest.Digest) error { + indexEntryPath, err := ts.pathMapper.path(manifestTagIndexEntryPathSpec{ + name: name, + tag: tag, + revision: revision, + }) + + if err != nil { + return err + } + + currentPath, err := ts.pathMapper.path(manifestTagCurrentPathSpec{ + name: name, + tag: tag, + }) + + if err != nil { + return err + } + + // Link into the index + if err := ts.blobStore.link(indexEntryPath, revision); err != nil { + return err + } + + // Overwrite the current link + return ts.blobStore.link(currentPath, revision) +} + +// resolve the current revision for name and tag. +func (ts *tagStore) resolve(name, tag string) (digest.Digest, error) { + currentPath, err := ts.pathMapper.path(manifestTagCurrentPathSpec{ + name: name, + tag: tag, + }) + + if err != nil { + return "", err + } + + if exists, err := exists(ts.driver, currentPath); err != nil { + return "", err + } else if !exists { + return "", ErrUnknownManifest{Name: name, Tag: tag} + } + + revision, err := ts.blobStore.readlink(currentPath) + if err != nil { + return "", err + } + + return revision, nil +} + +// revisions returns all revisions with the specified name and tag. +func (ts *tagStore) revisions(name, tag string) ([]digest.Digest, error) { + manifestTagIndexPath, err := ts.pathMapper.path(manifestTagIndexPathSpec{ + name: name, + tag: tag, + }) + + if err != nil { + return nil, err + } + + // TODO(stevvooe): Need to append digest alg to get listing of revisions. + manifestTagIndexPath = path.Join(manifestTagIndexPath, "sha256") + + entries, err := ts.driver.List(manifestTagIndexPath) + if err != nil { + return nil, err + } + + var revisions []digest.Digest + for _, entry := range entries { + revisions = append(revisions, digest.NewDigestFromHex("sha256", path.Base(entry))) + } + + return revisions, nil +} + +// delete removes the tag from repository, including the history of all +// revisions that have the specified tag. +func (ts *tagStore) delete(name, tag string) error { + tagPath, err := ts.pathMapper.path(manifestTagPathSpec{ + name: name, + tag: tag, + }) + if err != nil { + return err + } + + return ts.driver.Delete(tagPath) +}