forked from TrueCloudLab/distribution
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:
parent
3277d9fc74
commit
83d62628fc
11 changed files with 789 additions and 149 deletions
159
storage/blobstore.go
Normal file
159
storage/blobstore.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -31,13 +31,18 @@ func TestSimpleLayerUpload(t *testing.T) {
|
|||
}
|
||||
|
||||
imageName := "foo/bar"
|
||||
|
||||
ls := &layerStore{
|
||||
driver: inmemory.New(),
|
||||
pathMapper: &pathMapper{
|
||||
driver := inmemory.New()
|
||||
pm := &pathMapper{
|
||||
root: "/storage/testing",
|
||||
version: storagePathVersion,
|
||||
}
|
||||
ls := &layerStore{
|
||||
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()
|
||||
ls := &layerStore{
|
||||
driver: driver,
|
||||
pathMapper: &pathMapper{
|
||||
pm := &pathMapper{
|
||||
root: "/storage/testing",
|
||||
version: storagePathVersion,
|
||||
}
|
||||
ls := &layerStore{
|
||||
driver: driver,
|
||||
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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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 &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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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{}
|
||||
|
@ -57,141 +67,71 @@ func (errs ErrManifestVerification) Error() string {
|
|||
type manifestStore struct {
|
||||
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:
|
||||
|
|
|
@ -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{
|
||||
driver: driver,
|
||||
pathMapper: &pathMapper{
|
||||
pm := pathMapper{
|
||||
root: "/storage/testing",
|
||||
version: storagePathVersion,
|
||||
}
|
||||
bs := blobStore{
|
||||
driver: driver,
|
||||
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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
217
storage/revisionstore.go
Normal file
217
storage/revisionstore.go
Normal 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)
|
||||
}
|
|
@ -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.
|
||||
|
|
159
storage/tagstore.go
Normal file
159
storage/tagstore.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue