Refactor storage to use new backend layout

This change refactors the storage backend to use the new path layout. To
facilitate this, manifest storage has been separated into a revision store and
tag store, supported by a more general blob store. The blob store is a hybrid
object, effectively providing both small object access, keyed by content
address, as well as methods that can be used to manage and traverse links to
underlying blobs. This covers common operations used in the revision store and
tag store, such as linking and traversal. The blob store can also be updated to
better support layer reading but this refactoring has been left for another
day.

The revision store and tag store support the manifest store's compound view of
data. These underlying stores provide facilities for richer access models, such
as content-addressable access and a richer tagging model. The highlight of this
change is the ability to sign a manifest from different hosts and have the
registry merge and serve those signatures as part of the manifest package.

Various other items, such as the delegate layer handler, were updated to more
directly use the blob store or other mechanism to fit with the changes.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2015-01-14 12:02:43 -08:00
parent 3277d9fc74
commit 83d62628fc
11 changed files with 789 additions and 149 deletions

159
storage/blobstore.go Normal file
View File

@ -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
}

View File

@ -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 // urlFor returns a download URL for the given layer, or the empty string if
// unsupported. // unsupported.
func (lh *delegateLayerHandler) urlFor(layer Layer) (string, error) { func (lh *delegateLayerHandler) urlFor(layer Layer) (string, error) {
blobPath, err := resolveBlobPath(lh.storageDriver, lh.pathMapper, layer.Name(), layer.Digest()) // Crack open the layer to get at the layerStore
if err != nil { layerRd, ok := layer.(*layerReader)
return "", err 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 { if err != nil {
return "", err return "", err
} }

View File

@ -31,13 +31,18 @@ func TestSimpleLayerUpload(t *testing.T) {
} }
imageName := "foo/bar" imageName := "foo/bar"
driver := inmemory.New()
pm := &pathMapper{
root: "/storage/testing",
version: storagePathVersion,
}
ls := &layerStore{ ls := &layerStore{
driver: inmemory.New(), driver: driver,
pathMapper: &pathMapper{ blobStore: &blobStore{
root: "/storage/testing", driver: driver,
version: storagePathVersion, pm: pm,
}, },
pathMapper: pm,
} }
h := sha256.New() h := sha256.New()
@ -140,12 +145,17 @@ func TestSimpleLayerUpload(t *testing.T) {
func TestSimpleLayerRead(t *testing.T) { func TestSimpleLayerRead(t *testing.T) {
imageName := "foo/bar" imageName := "foo/bar"
driver := inmemory.New() driver := inmemory.New()
pm := &pathMapper{
root: "/storage/testing",
version: storagePathVersion,
}
ls := &layerStore{ ls := &layerStore{
driver: driver, driver: driver,
pathMapper: &pathMapper{ blobStore: &blobStore{
root: "/storage/testing", driver: driver,
version: storagePathVersion, pm: pm,
}, },
pathMapper: pm,
} }
randomLayerReader, tarSumStr, err := testutil.CreateRandomTarFile() randomLayerReader, tarSumStr, err := testutil.CreateRandomTarFile()
@ -307,7 +317,7 @@ func writeTestLayer(driver storagedriver.StorageDriver, pathMapper *pathMapper,
blobDigestSHA := digest.NewDigest("sha256", h) blobDigestSHA := digest.NewDigest("sha256", h)
blobPath, err := pathMapper.path(blobPathSpec{ blobPath, err := pathMapper.path(blobDataPathSpec{
digest: dgst, digest: dgst,
}) })

View File

@ -12,6 +12,7 @@ import (
type layerStore struct { type layerStore struct {
driver storagedriver.StorageDriver driver storagedriver.StorageDriver
pathMapper *pathMapper pathMapper *pathMapper
blobStore *blobStore
} }
func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) { 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 return true, nil
} }
func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) { func (ls *layerStore) Fetch(name string, dgst digest.Digest) (Layer, error) {
blobPath, err := resolveBlobPath(ls.driver, ls.pathMapper, name, digest) bp, err := ls.path(name, dgst)
if err != nil { if err != nil {
switch err := err.(type) { return nil, err
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return nil, ErrUnknownLayer{manifest.FSLayer{BlobSum: digest}}
default:
return nil, err
}
} }
fr, err := newFileReader(ls.driver, blobPath) fr, err := newFileReader(ls.driver, bp)
if err != nil { if err != nil {
switch err := err.(type) { return nil, err
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return nil, ErrUnknownLayer{manifest.FSLayer{BlobSum: digest}}
default:
return nil, err
}
} }
return &layerReader{ return &layerReader{
fileReader: *fr, fileReader: *fr,
name: name, name: name,
digest: digest, digest: dgst,
}, nil }, nil
} }
@ -151,3 +142,24 @@ func (ls *layerStore) newLayerUpload(name, uuid, path string, startedAt time.Tim
fileWriter: *fw, fileWriter: *fw,
}, nil }, 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
}

View File

@ -112,7 +112,7 @@ func (luc *layerUploadController) validateLayer(dgst digest.Digest) (digest.Dige
// sink. Instead, its read driven. This might be okay. // sink. Instead, its read driven. This might be okay.
// Calculate an updated digest with the latest version. // Calculate an updated digest with the latest version.
canonical, err := digest.FromReader(tr) canonical, err := digest.FromTarArchive(tr)
if err != nil { if err != nil {
return "", err 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 // identified by dgst. The layer should be validated before commencing the
// move. // move.
func (luc *layerUploadController) moveLayer(dgst digest.Digest) error { func (luc *layerUploadController) moveLayer(dgst digest.Digest) error {
blobPath, err := luc.layerStore.pathMapper.path(blobPathSpec{ blobPath, err := luc.layerStore.pathMapper.path(blobDataPathSpec{
digest: dgst, digest: dgst,
}) })

View File

@ -1,11 +1,10 @@
package storage package storage
import ( import (
"encoding/json"
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/storagedriver" "github.com/docker/distribution/storagedriver"
"github.com/docker/libtrust" "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) 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 // ErrManifestUnverified is returned when the registry is unable to verify
// the manifest. // the manifest.
type ErrManifestUnverified struct{} type ErrManifestUnverified struct{}
@ -55,143 +65,73 @@ func (errs ErrManifestVerification) Error() string {
} }
type manifestStore struct { type manifestStore struct {
driver storagedriver.StorageDriver driver storagedriver.StorageDriver
pathMapper *pathMapper pathMapper *pathMapper
layerService LayerService revisionStore *revisionStore
tagStore *tagStore
blobStore *blobStore
layerService LayerService
} }
var _ ManifestService = &manifestStore{} var _ ManifestService = &manifestStore{}
func (ms *manifestStore) Tags(name string) ([]string, error) { func (ms *manifestStore) Tags(name string) ([]string, error) {
p, err := ms.pathMapper.path(manifestTagsPath{ return ms.tagStore.tags(name)
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
} }
func (ms *manifestStore) Exists(name, tag string) (bool, error) { func (ms *manifestStore) Exists(name, tag string) (bool, error) {
p, err := ms.path(name, tag) return ms.tagStore.exists(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
} }
func (ms *manifestStore) Get(name, tag string) (*manifest.SignedManifest, error) { 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 { if err != nil {
return nil, err return nil, err
} }
content, err := ms.driver.GetContent(p) return ms.revisionStore.get(name, dgst)
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
} }
func (ms *manifestStore) Put(name, tag string, manifest *manifest.SignedManifest) error { func (ms *manifestStore) Put(name, tag string, manifest *manifest.SignedManifest) error {
p, err := ms.path(name, tag) // Verify the manifest.
if err != nil {
return err
}
if err := ms.verifyManifest(name, tag, manifest); err != nil { if err := ms.verifyManifest(name, tag, manifest); err != nil {
return err return err
} }
// TODO(stevvooe): Should we get old manifest first? Perhaps, write, then // Store the revision of the manifest
// move to ensure a valid manifest? revision, err := ms.revisionStore.put(name, manifest)
return ms.driver.PutContent(p, manifest.Raw)
}
func (ms *manifestStore) Delete(name, tag string) error {
p, err := ms.path(name, tag)
if err != nil { if err != nil {
return err return err
} }
if err := ms.driver.Delete(p); err != nil { // Now, tag the manifest
switch err := err.(type) { return ms.tagStore.tag(name, tag, revision)
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: }
return ErrUnknownManifest{Name: name, Tag: tag}
default: // 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 err
} }
} }
return nil return ms.tagStore.delete(name, tag)
}
func (ms *manifestStore) path(name, tag string) (string, error) {
return ms.pathMapper.path(manifestPathSpec{
name: name,
tag: 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 { 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 var errs ErrManifestVerification
if mnfst.Name != name { if mnfst.Name != name {
// TODO(stevvooe): This needs to be an exported error // 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")) 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 { if _, err := manifest.Verify(mnfst); err != nil {
switch err { switch err {
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:

View File

@ -1,6 +1,7 @@
package storage package storage
import ( import (
"bytes"
"reflect" "reflect"
"testing" "testing"
@ -12,12 +13,28 @@ import (
func TestManifestStorage(t *testing.T) { func TestManifestStorage(t *testing.T) {
driver := inmemory.New() driver := inmemory.New()
ms := &manifestStore{ pm := pathMapper{
root: "/storage/testing",
version: storagePathVersion,
}
bs := blobStore{
driver: driver, driver: driver,
pathMapper: &pathMapper{ pm: &pm,
root: "/storage/testing", }
version: storagePathVersion, 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(), layerService: newMockedLayerService(),
} }
@ -100,6 +117,25 @@ func TestManifestStorage(t *testing.T) {
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")
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 // Grabs the tags and check that this tagged manifest is present
tags, err := ms.Tags(name) tags, err := ms.Tags(name)
if err != nil { if err != nil {
@ -113,6 +149,84 @@ func TestManifestStorage(t *testing.T) {
if tags[0] != tag { if tags[0] != tag {
t.Fatalf("unexpected tag found in tags: %v != %v", tags, []string{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 { type layerKey struct {

View File

@ -188,7 +188,7 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
return "", err return "", err
} }
return path.Join(root, "current/link"), nil return path.Join(root, "current", "link"), nil
case manifestTagIndexPathSpec: case manifestTagIndexPathSpec:
root, err := pm.path(manifestTagPathSpec{ root, err := pm.path(manifestTagPathSpec{
name: v.name, name: v.name,

217
storage/revisionstore.go Normal file
View File

@ -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)
}

View File

@ -28,14 +28,42 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
// 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.
func (ss *Services) Layers() LayerService { 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 // 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.
func (ss *Services) Manifests() ManifestService { 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. // ManifestService provides operations on image manifests.
@ -43,7 +71,7 @@ type ManifestService interface {
// Tags lists the tags under the named repository. // Tags lists the tags under the named repository.
Tags(name string) ([]string, error) 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) Exists(name, tag string) (bool, error)
// Get retrieves the named manifest, if it exists. // Get retrieves the named manifest, if it exists.

159
storage/tagstore.go Normal file
View File

@ -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)
}