Merge pull request #780 from stevvooe/manifest-storage
Initial implementation of image manifest storage
This commit is contained in:
commit
d825559473
15 changed files with 519 additions and 133 deletions
|
@ -12,17 +12,18 @@ import (
|
|||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
)
|
||||
|
||||
// Client implements the client interface to the registry http api
|
||||
type Client interface {
|
||||
// GetImageManifest returns an image manifest for the image at the given
|
||||
// name, tag pair.
|
||||
GetImageManifest(name, tag string) (*registry.ImageManifest, error)
|
||||
GetImageManifest(name, tag string) (*storage.SignedManifest, error)
|
||||
|
||||
// PutImageManifest uploads an image manifest for the image at the given
|
||||
// name, tag pair.
|
||||
PutImageManifest(name, tag string, imageManifest *registry.ImageManifest) error
|
||||
PutImageManifest(name, tag string, imageManifest *storage.SignedManifest) error
|
||||
|
||||
// DeleteImage removes the image at the given name, tag pair.
|
||||
DeleteImage(name, tag string) error
|
||||
|
@ -81,7 +82,7 @@ type clientImpl struct {
|
|||
|
||||
// TODO(bbland): use consistent route generation between server and client
|
||||
|
||||
func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest, error) {
|
||||
func (r *clientImpl) GetImageManifest(name, tag string) (*storage.SignedManifest, error) {
|
||||
response, err := http.Get(r.imageManifestURL(name, tag))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -108,7 +109,7 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest
|
|||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
||||
manifest := new(registry.ImageManifest)
|
||||
manifest := new(storage.SignedManifest)
|
||||
err = decoder.Decode(manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -116,7 +117,7 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest
|
|||
return manifest, nil
|
||||
}
|
||||
|
||||
func (r *clientImpl) PutImageManifest(name, tag string, manifest *registry.ImageManifest) error {
|
||||
func (r *clientImpl) PutImageManifest(name, tag string, manifest *storage.SignedManifest) error {
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/common/testutil"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
)
|
||||
|
||||
type testBlob struct {
|
||||
|
@ -33,8 +33,8 @@ func TestPush(t *testing.T) {
|
|||
},
|
||||
}
|
||||
uploadLocations := make([]string, len(testBlobs))
|
||||
blobs := make([]registry.FSLayer, len(testBlobs))
|
||||
history := make([]registry.ManifestHistory, len(testBlobs))
|
||||
blobs := make([]storage.FSLayer, len(testBlobs))
|
||||
history := make([]storage.ManifestHistory, len(testBlobs))
|
||||
|
||||
for i, blob := range testBlobs {
|
||||
// TODO(bbland): this is returning the same location for all uploads,
|
||||
|
@ -42,17 +42,21 @@ func TestPush(t *testing.T) {
|
|||
// It's sort of okay because we're using unique digests, but this needs
|
||||
// to change at some point.
|
||||
uploadLocations[i] = fmt.Sprintf("/v2/%s/blob/test-uuid", name)
|
||||
blobs[i] = registry.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
blobs[i] = storage.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = storage.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
}
|
||||
|
||||
manifest := ®istry.ImageManifest{
|
||||
manifest := &storage.SignedManifest{
|
||||
Manifest: storage.Manifest{
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Architecture: "x86",
|
||||
FSLayers: blobs,
|
||||
History: history,
|
||||
Versioned: storage.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
|
||||
|
@ -102,7 +106,7 @@ func TestPush(t *testing.T) {
|
|||
client := New(server.URL)
|
||||
objectStore := &memoryObjectStore{
|
||||
mutex: new(sync.Mutex),
|
||||
manifestStorage: make(map[string]*registry.ImageManifest),
|
||||
manifestStorage: make(map[string]*storage.SignedManifest),
|
||||
layerStorage: make(map[digest.Digest]Layer),
|
||||
}
|
||||
|
||||
|
@ -143,21 +147,25 @@ func TestPull(t *testing.T) {
|
|||
contents: []byte("some other contents"),
|
||||
},
|
||||
}
|
||||
blobs := make([]registry.FSLayer, len(testBlobs))
|
||||
history := make([]registry.ManifestHistory, len(testBlobs))
|
||||
blobs := make([]storage.FSLayer, len(testBlobs))
|
||||
history := make([]storage.ManifestHistory, len(testBlobs))
|
||||
|
||||
for i, blob := range testBlobs {
|
||||
blobs[i] = registry.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
blobs[i] = storage.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = storage.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
}
|
||||
|
||||
manifest := ®istry.ImageManifest{
|
||||
manifest := &storage.SignedManifest{
|
||||
Manifest: storage.Manifest{
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Architecture: "x86",
|
||||
FSLayers: blobs,
|
||||
History: history,
|
||||
Versioned: storage.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
|
||||
|
@ -191,7 +199,7 @@ func TestPull(t *testing.T) {
|
|||
client := New(server.URL)
|
||||
objectStore := &memoryObjectStore{
|
||||
mutex: new(sync.Mutex),
|
||||
manifestStorage: make(map[string]*registry.ImageManifest),
|
||||
manifestStorage: make(map[string]*storage.SignedManifest),
|
||||
layerStorage: make(map[digest.Digest]Layer),
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -27,11 +27,11 @@ var (
|
|||
type ObjectStore interface {
|
||||
// Manifest retrieves the image manifest stored at the given repository name
|
||||
// and tag
|
||||
Manifest(name, tag string) (*registry.ImageManifest, error)
|
||||
Manifest(name, tag string) (*storage.SignedManifest, error)
|
||||
|
||||
// WriteManifest stores an image manifest at the given repository name and
|
||||
// tag
|
||||
WriteManifest(name, tag string, manifest *registry.ImageManifest) error
|
||||
WriteManifest(name, tag string, manifest *storage.SignedManifest) error
|
||||
|
||||
// Layer returns a handle to a layer for reading and writing
|
||||
Layer(dgst digest.Digest) (Layer, error)
|
||||
|
@ -84,11 +84,11 @@ type LayerWriter interface {
|
|||
// memoryObjectStore is an in-memory implementation of the ObjectStore interface
|
||||
type memoryObjectStore struct {
|
||||
mutex *sync.Mutex
|
||||
manifestStorage map[string]*registry.ImageManifest
|
||||
manifestStorage map[string]*storage.SignedManifest
|
||||
layerStorage map[digest.Digest]Layer
|
||||
}
|
||||
|
||||
func (objStore *memoryObjectStore) Manifest(name, tag string) (*registry.ImageManifest, error) {
|
||||
func (objStore *memoryObjectStore) Manifest(name, tag string) (*storage.SignedManifest, error) {
|
||||
objStore.mutex.Lock()
|
||||
defer objStore.mutex.Unlock()
|
||||
|
||||
|
@ -99,7 +99,7 @@ func (objStore *memoryObjectStore) Manifest(name, tag string) (*registry.ImageMa
|
|||
return manifest, nil
|
||||
}
|
||||
|
||||
func (objStore *memoryObjectStore) WriteManifest(name, tag string, manifest *registry.ImageManifest) error {
|
||||
func (objStore *memoryObjectStore) WriteManifest(name, tag string, manifest *storage.SignedManifest) error {
|
||||
objStore.mutex.Lock()
|
||||
defer objStore.mutex.Unlock()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
@ -77,7 +77,7 @@ func Pull(c Client, objectStore ObjectStore, name, tag string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func pullLayer(c Client, objectStore ObjectStore, name string, fsLayer registry.FSLayer) error {
|
||||
func pullLayer(c Client, objectStore ObjectStore, name string, fsLayer storage.FSLayer) error {
|
||||
log.WithField("layer", fsLayer).Info("Pulling layer")
|
||||
|
||||
layer, err := objectStore.Layer(fsLayer.BlobSum)
|
||||
|
|
|
@ -3,7 +3,7 @@ package client
|
|||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
// push window has been successfully pushed.
|
||||
const simultaneousLayerPushWindow = 4
|
||||
|
||||
type pushFunction func(fsLayer registry.FSLayer) error
|
||||
type pushFunction func(fsLayer storage.FSLayer) error
|
||||
|
||||
// Push implements a client push workflow for the image defined by the given
|
||||
// name and tag pair, using the given ObjectStore for local manifest and layer
|
||||
|
@ -72,7 +72,7 @@ func Push(c Client, objectStore ObjectStore, name, tag string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func pushLayer(c Client, objectStore ObjectStore, name string, fsLayer registry.FSLayer) error {
|
||||
func pushLayer(c Client, objectStore ObjectStore, name string, fsLayer storage.FSLayer) error {
|
||||
log.WithField("layer", fsLayer).Info("Pushing layer")
|
||||
|
||||
layer, err := objectStore.Layer(fsLayer.BlobSum)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
)
|
||||
|
||||
// ErrorCode represents the error type. The errors are serialized via strings
|
||||
|
@ -212,7 +213,7 @@ type DetailUnknownLayer struct {
|
|||
|
||||
// Unknown should contain the contents of a layer descriptor, which is a
|
||||
// single FSLayer currently.
|
||||
Unknown FSLayer `json:"unknown"`
|
||||
Unknown storage.FSLayer `json:"unknown"`
|
||||
}
|
||||
|
||||
// RepositoryNotFoundError is returned when making an operation against a
|
||||
|
|
66
images.go
66
images.go
|
@ -1,77 +1,11 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// ImageManifest defines the structure of an image manifest
|
||||
type ImageManifest struct {
|
||||
// Name is the name of the image's repository
|
||||
Name string `json:"name"`
|
||||
|
||||
// Tag is the tag of the image specified by this manifest
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// Architecture is the host architecture on which this image is intended to
|
||||
// run
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
||||
FSLayers []FSLayer `json:"fsLayers"`
|
||||
|
||||
// History is a list of unstructured historical data for v1 compatibility
|
||||
History []ManifestHistory `json:"history"`
|
||||
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
||||
// Raw is the byte representation of the ImageManifest, used for signature
|
||||
// verification
|
||||
Raw []byte `json:"-"`
|
||||
}
|
||||
|
||||
// imageManifest is used to avoid recursion in unmarshaling
|
||||
type imageManifest ImageManifest
|
||||
|
||||
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
|
||||
func (m *ImageManifest) UnmarshalJSON(b []byte) error {
|
||||
var manifest imageManifest
|
||||
err := json.Unmarshal(b, &manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*m = ImageManifest(manifest)
|
||||
m.Raw = b
|
||||
return nil
|
||||
}
|
||||
|
||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum digest.Digest `json:"blobSum"`
|
||||
}
|
||||
|
||||
// ManifestHistory stores unstructured v1 compatibility information
|
||||
type ManifestHistory struct {
|
||||
// V1Compatibility is the raw v1 compatibility information
|
||||
V1Compatibility string `json:"v1Compatibility"`
|
||||
}
|
||||
|
||||
// Checksum is a container struct for an image checksum
|
||||
type Checksum struct {
|
||||
// HashAlgorithm is the algorithm used to compute the checksum
|
||||
// Supported values: md5, sha1, sha256, sha512
|
||||
HashAlgorithm string
|
||||
|
||||
// Sum is the actual checksum value for the given HashAlgorithm
|
||||
Sum string
|
||||
}
|
||||
|
||||
// imageManifestDispatcher takes the request context and builds the
|
||||
// appropriate handler for handling image manifest requests.
|
||||
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
|
|
2
layer.go
2
layer.go
|
@ -52,7 +52,7 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusNotFound)
|
||||
lh.Errors.Push(ErrorCodeUnknownLayer,
|
||||
map[string]interface{}{
|
||||
"unknown": FSLayer{BlobSum: lh.Digest},
|
||||
"unknown": storage.FSLayer{BlobSum: lh.Digest},
|
||||
})
|
||||
return
|
||||
default:
|
||||
|
|
|
@ -8,23 +8,6 @@ import (
|
|||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
// LayerService provides operations on layer files in a backend storage.
|
||||
type LayerService interface {
|
||||
// Exists returns true if the layer exists.
|
||||
Exists(name string, digest digest.Digest) (bool, error)
|
||||
|
||||
// Fetch the layer identifed by TarSum.
|
||||
Fetch(name string, digest digest.Digest) (Layer, error)
|
||||
|
||||
// Upload begins a layer upload to repository identified by name,
|
||||
// returning a handle.
|
||||
Upload(name string) (LayerUpload, error)
|
||||
|
||||
// Resume continues an in progress layer upload, returning the current
|
||||
// state of the upload.
|
||||
Resume(uuid string) (LayerUpload, error)
|
||||
}
|
||||
|
||||
// Layer provides a readable and seekable layer object. Typically,
|
||||
// implementations are *not* goroutine safe.
|
||||
type Layer interface {
|
||||
|
|
125
storage/manifest.go
Normal file
125
storage/manifest.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrManifestUnknown is returned if the manifest is not known by the
|
||||
// registry.
|
||||
ErrManifestUnknown = fmt.Errorf("unknown manifest")
|
||||
|
||||
// ErrManifestUnverified is returned when the registry is unable to verify
|
||||
// the manifest.
|
||||
ErrManifestUnverified = fmt.Errorf("unverified manifest")
|
||||
)
|
||||
|
||||
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
||||
// content with unknown schema version can be decoded against this struct to
|
||||
// check the version.
|
||||
type Versioned struct {
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
|
||||
// Manifest provides the base accessible fields for working with V2 image
|
||||
// format in the registry.
|
||||
type Manifest struct {
|
||||
Versioned
|
||||
|
||||
// Name is the name of the image's repository
|
||||
Name string `json:"name"`
|
||||
|
||||
// Tag is the tag of the image specified by this manifest
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// Architecture is the host architecture on which this image is intended to
|
||||
// run
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
||||
FSLayers []FSLayer `json:"fsLayers"`
|
||||
|
||||
// History is a list of unstructured historical data for v1 compatibility
|
||||
History []ManifestHistory `json:"history"`
|
||||
}
|
||||
|
||||
// Sign signs the manifest with the provided private key, returning a
|
||||
// SignedManifest. This typically won't be used within the registry, except
|
||||
// for testing.
|
||||
func (m *Manifest) Sign(pk libtrust.PrivateKey) (*SignedManifest, error) {
|
||||
p, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
js, err := libtrust.NewJSONSignature(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := js.Sign(pk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pretty, err := js.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SignedManifest{
|
||||
Manifest: *m,
|
||||
Raw: pretty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignedManifest provides an envelope for
|
||||
type SignedManifest struct {
|
||||
Manifest
|
||||
|
||||
// Raw is the byte representation of the ImageManifest, used for signature
|
||||
// verification. The manifest byte representation cannot change or it will
|
||||
// have to be re-signed.
|
||||
Raw []byte `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
|
||||
func (m *SignedManifest) UnmarshalJSON(b []byte) error {
|
||||
var manifest Manifest
|
||||
if err := json.Unmarshal(b, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Manifest = manifest
|
||||
m.Raw = b
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
||||
// contents.
|
||||
func (m *SignedManifest) MarshalJSON() ([]byte, error) {
|
||||
if len(m.Raw) > 0 {
|
||||
return m.Raw, nil
|
||||
}
|
||||
|
||||
// If the raw data is not available, just dump the inner content.
|
||||
return json.Marshal(&m.Manifest)
|
||||
}
|
||||
|
||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum digest.Digest `json:"blobSum"`
|
||||
}
|
||||
|
||||
// ManifestHistory stores unstructured v1 compatibility information
|
||||
type ManifestHistory struct {
|
||||
// V1Compatibility is the raw v1 compatibility information
|
||||
V1Compatibility string `json:"v1Compatibility"`
|
||||
}
|
139
storage/manifest_test.go
Normal file
139
storage/manifest_test.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storagedriver/inmemory"
|
||||
)
|
||||
|
||||
func TestManifestStorage(t *testing.T) {
|
||||
driver := inmemory.New()
|
||||
ms := &manifestStore{
|
||||
driver: driver,
|
||||
pathMapper: &pathMapper{
|
||||
root: "/storage/testing",
|
||||
version: storagePathVersion,
|
||||
},
|
||||
layerService: newMockedLayerService(),
|
||||
}
|
||||
|
||||
name := "foo/bar"
|
||||
tag := "thetag"
|
||||
|
||||
exists, err := ms.Exists(name, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking manifest existence: %v", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
t.Fatalf("manifest should not exist")
|
||||
}
|
||||
|
||||
if _, err := ms.Get(name, tag); err != ErrManifestUnknown {
|
||||
t.Fatalf("expected manifest unknown error: %v != %v", err, ErrManifestUnknown)
|
||||
}
|
||||
|
||||
manifest := Manifest{
|
||||
Versioned: Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
FSLayers: []FSLayer{
|
||||
{
|
||||
BlobSum: "asdf",
|
||||
},
|
||||
{
|
||||
BlobSum: "qwer",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating private key: %v", err)
|
||||
}
|
||||
|
||||
sm, err := manifest.Sign(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("error signing manifest: %v", err)
|
||||
}
|
||||
|
||||
err = ms.Put(name, tag, sm)
|
||||
if err == nil {
|
||||
t.Fatalf("expected errors putting manifest")
|
||||
}
|
||||
|
||||
// TODO(stevvooe): We expect errors describing all of the missing layers.
|
||||
|
||||
ms.layerService.(*mockedExistenceLayerService).add(name, "asdf")
|
||||
ms.layerService.(*mockedExistenceLayerService).add(name, "qwer")
|
||||
|
||||
if err = ms.Put(name, tag, sm); err != nil {
|
||||
t.Fatalf("unexpected error putting manifest: %v", err)
|
||||
}
|
||||
|
||||
exists, err = ms.Exists(name, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking manifest existence: %v", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Fatalf("manifest should exist")
|
||||
}
|
||||
|
||||
fetchedManifest, err := ms.Get(name, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error fetching manifest: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(fetchedManifest, sm) {
|
||||
t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm)
|
||||
}
|
||||
}
|
||||
|
||||
type layerKey struct {
|
||||
name string
|
||||
digest digest.Digest
|
||||
}
|
||||
|
||||
type mockedExistenceLayerService struct {
|
||||
exists map[layerKey]struct{}
|
||||
}
|
||||
|
||||
func newMockedLayerService() *mockedExistenceLayerService {
|
||||
return &mockedExistenceLayerService{
|
||||
exists: make(map[layerKey]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
var _ LayerService = &mockedExistenceLayerService{}
|
||||
|
||||
func (mels *mockedExistenceLayerService) add(name string, digest digest.Digest) {
|
||||
mels.exists[layerKey{name: name, digest: digest}] = struct{}{}
|
||||
}
|
||||
|
||||
func (mels *mockedExistenceLayerService) remove(name string, digest digest.Digest) {
|
||||
delete(mels.exists, layerKey{name: name, digest: digest})
|
||||
}
|
||||
|
||||
func (mels *mockedExistenceLayerService) Exists(name string, digest digest.Digest) (bool, error) {
|
||||
_, ok := mels.exists[layerKey{name: name, digest: digest}]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (mockedExistenceLayerService) Fetch(name string, digest digest.Digest) (Layer, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (mockedExistenceLayerService) Upload(name string) (LayerUpload, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (mockedExistenceLayerService) Resume(uuid string) (LayerUpload, error) {
|
||||
panic("not implemented")
|
||||
}
|
134
storage/manifeststore.go
Normal file
134
storage/manifeststore.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
"github.com/docker/docker-registry/storagedriver"
|
||||
)
|
||||
|
||||
type manifestStore struct {
|
||||
driver storagedriver.StorageDriver
|
||||
pathMapper *pathMapper
|
||||
layerService LayerService
|
||||
}
|
||||
|
||||
var _ ManifestService = &manifestStore{}
|
||||
|
||||
func (ms *manifestStore) Exists(name, tag string) (bool, error) {
|
||||
p, err := ms.path(name, tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
size, err := ms.driver.CurrentSize(p)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ms *manifestStore) Get(name, tag string) (*SignedManifest, error) {
|
||||
p, err := ms.path(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, ErrManifestUnknown
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var 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 *SignedManifest) error {
|
||||
p, err := ms.path(name, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ms.verifyManifest(name, tag, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Should we get manifest first?
|
||||
|
||||
return ms.driver.PutContent(p, manifest.Raw)
|
||||
}
|
||||
|
||||
func (ms *manifestStore) Delete(name, tag string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (ms *manifestStore) path(name, tag string) (string, error) {
|
||||
return ms.pathMapper.path(manifestPathSpec{
|
||||
name: name,
|
||||
tag: tag,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManifest) error {
|
||||
if manifest.Name != name {
|
||||
return fmt.Errorf("name does not match manifest name")
|
||||
}
|
||||
|
||||
if manifest.Tag != tag {
|
||||
return fmt.Errorf("tag does not match manifest tag")
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
for _, fsLayer := range manifest.FSLayers {
|
||||
exists, err := ms.layerService.Exists(name, fsLayer.BlobSum)
|
||||
if err != nil {
|
||||
// TODO(stevvooe): Need to store information about missing blob.
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
errs = append(errs, fmt.Errorf("missing layer %v", fsLayer.BlobSum))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
// TODO(stevvooe): These need to be recoverable by a caller.
|
||||
return fmt.Errorf("missing layers: %v", errs)
|
||||
}
|
||||
|
||||
js, err := libtrust.ParsePrettySignature(manifest.Raw, "signatures")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = js.Verify() // These pubkeys need to be checked.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(sday): Pubkey checks need to go here. This where things get fancy.
|
||||
// Perhaps, an injected service would reduce coupling here.
|
||||
|
||||
return nil
|
||||
}
|
|
@ -24,7 +24,7 @@ const storagePathVersion = "v2"
|
|||
// <root>/v2
|
||||
// -> repositories/
|
||||
// -><name>/
|
||||
// -> images/
|
||||
// -> manifests/
|
||||
// <manifests by tag name>
|
||||
// -> layers/
|
||||
// -> tarsum/
|
||||
|
@ -48,6 +48,7 @@ const storagePathVersion = "v2"
|
|||
//
|
||||
// We cover the path formats implemented by this path mapper below.
|
||||
//
|
||||
// manifestPathSpec: <root>/v2/repositories/<name>/manifests/<tag>
|
||||
// layerLinkPathSpec: <root>/v2/repositories/<name>/layers/tarsum/<tarsum version>/<tarsum hash alg>/<tarsum hash>
|
||||
// layerIndexLinkPathSpec: <root>/v2/layerindex/tarsum/<tarsum version>/<tarsum hash alg>/<tarsum hash>
|
||||
// blobPathSpec: <root>/v2/blob/sha256/<first two hex bytes of digest>/<hex digest>
|
||||
|
@ -84,7 +85,13 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
|||
// to an intermediate path object, than can be consumed and mapped by the
|
||||
// other version.
|
||||
|
||||
rootPrefix := []string{pm.root, pm.version}
|
||||
repoPrefix := append(rootPrefix, "repositories")
|
||||
|
||||
switch v := spec.(type) {
|
||||
case manifestPathSpec:
|
||||
// TODO(sday): May need to store manifest by architecture.
|
||||
return path.Join(append(repoPrefix, v.name, "manifests", v.tag)...), nil
|
||||
case layerLinkPathSpec:
|
||||
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
||||
// Only tarsum is supported, for now
|
||||
|
@ -101,9 +108,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
p := path.Join(append([]string{pm.root, pm.version, "repositories", v.name, "layers"}, tarSumInfoPathComponents(tsi)...)...)
|
||||
|
||||
return p, nil
|
||||
return path.Join(append(append(repoPrefix, v.name, "layers"),
|
||||
tarSumInfoPathComponents(tsi)...)...), nil
|
||||
case layerIndexLinkPathSpec:
|
||||
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
||||
// Only tarsum is supported, for now
|
||||
|
@ -120,9 +126,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
p := path.Join(append([]string{pm.root, pm.version, "layerindex"}, tarSumInfoPathComponents(tsi)...)...)
|
||||
|
||||
return p, nil
|
||||
return path.Join(append(append(rootPrefix, "layerindex"),
|
||||
tarSumInfoPathComponents(tsi)...)...), nil
|
||||
case blobPathSpec:
|
||||
p := path.Join([]string{pm.root, pm.version, "blob", v.alg, v.digest[:2], v.digest}...)
|
||||
return p, nil
|
||||
|
@ -139,6 +144,15 @@ type pathSpec interface {
|
|||
pathSpec()
|
||||
}
|
||||
|
||||
// manifestPathSpec describes the path elements used to build a manifest path.
|
||||
// The contents should be a signed manifest json file.
|
||||
type manifestPathSpec struct {
|
||||
name string
|
||||
tag string
|
||||
}
|
||||
|
||||
func (manifestPathSpec) pathSpec() {}
|
||||
|
||||
// layerLink specifies a path for a layer link, which is a file with a blob
|
||||
// id. The layer link will contain a content addressable blob id reference
|
||||
// into the blob store. The format of the contents is as follows:
|
||||
|
|
|
@ -16,6 +16,13 @@ func TestPathMapper(t *testing.T) {
|
|||
expected string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
spec: manifestPathSpec{
|
||||
name: "foo/bar",
|
||||
tag: "thetag",
|
||||
},
|
||||
expected: "/pathmapper-test/repositories/foo/bar/manifests/thetag",
|
||||
},
|
||||
{
|
||||
spec: layerLinkPathSpec{
|
||||
name: "foo/bar",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storagedriver"
|
||||
)
|
||||
|
||||
|
@ -41,3 +42,42 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
|
|||
func (ss *Services) Layers() LayerService {
|
||||
return &layerStore{driver: ss.driver, pathMapper: ss.pathMapper, uploadStore: ss.layerUploadStore}
|
||||
}
|
||||
|
||||
// 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()}
|
||||
}
|
||||
|
||||
// ManifestService provides operations on image manifests.
|
||||
type ManifestService interface {
|
||||
// Exists returns true if the layer exists.
|
||||
Exists(name, tag string) (bool, error)
|
||||
|
||||
// Get retrieves the named manifest, if it exists.
|
||||
Get(name, tag string) (*SignedManifest, error)
|
||||
|
||||
// Put creates or updates the named manifest.
|
||||
Put(name, tag string, manifest *SignedManifest) error
|
||||
|
||||
// Delete removes the named manifest, if it exists.
|
||||
Delete(name, tag string) error
|
||||
}
|
||||
|
||||
// LayerService provides operations on layer files in a backend storage.
|
||||
type LayerService interface {
|
||||
// Exists returns true if the layer exists.
|
||||
Exists(name string, digest digest.Digest) (bool, error)
|
||||
|
||||
// Fetch the layer identifed by TarSum.
|
||||
Fetch(name string, digest digest.Digest) (Layer, error)
|
||||
|
||||
// Upload begins a layer upload to repository identified by name,
|
||||
// returning a handle.
|
||||
Upload(name string) (LayerUpload, error)
|
||||
|
||||
// Resume continues an in progress layer upload, returning the current
|
||||
// state of the upload.
|
||||
Resume(uuid string) (LayerUpload, error)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue