From eaadb82e1e45547726d53c8c36a34efdc6015024 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Fri, 21 Nov 2014 19:29:08 -0800 Subject: [PATCH] Move Manifest type into storage package This changeset move the Manifest type into the storage package to make the type accessible to client and registry without import cycles. The structure of the manifest was also changed to accuratle reflect the stages of the signing process. A straw man Manifest.Sign method has been added to start testing this concept out but will probably be accompanied by the more import SignedManifest.Verify method as the security model develops. This is probably the start of a concerted effort to consolidate types across the client and server portions of the code base but we may want to see how such a handy type, like the Manifest and SignedManifest, would work in docker core. --- client/client.go | 11 ++-- client/client_test.go | 58 +++++++++++--------- client/objectstore.go | 12 ++-- client/pull.go | 4 +- client/push.go | 6 +- errors.go | 2 +- images.go | 67 +--------------------- layer.go | 2 +- storage/manifest.go | 125 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 179 insertions(+), 108 deletions(-) create mode 100644 storage/manifest.go diff --git a/client/client.go b/client/client.go index 944050e0..e51476cd 100644 --- a/client/client.go +++ b/client/client.go @@ -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 diff --git a/client/client_test.go b/client/client_test.go index a77e7665..dc75789d 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -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{ - Name: name, - Tag: tag, - Architecture: "x86", - FSLayers: blobs, - History: history, - SchemaVersion: 1, + 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), } @@ -142,21 +146,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{ - Name: name, - Tag: tag, - Architecture: "x86", - FSLayers: blobs, - History: history, - SchemaVersion: 1, + 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) @@ -190,7 +198,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), } diff --git a/client/objectstore.go b/client/objectstore.go index bee73ff0..177f9aca 100644 --- a/client/objectstore.go +++ b/client/objectstore.go @@ -8,8 +8,8 @@ import ( "io/ioutil" "sync" - "github.com/docker/docker-registry" "github.com/docker/docker-registry/digest" + "github.com/docker/docker-registry/storage" ) var ( @@ -28,11 +28,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) @@ -56,11 +56,11 @@ type Layer 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() @@ -71,7 +71,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() diff --git a/client/pull.go b/client/pull.go index bce06756..435e40b9 100644 --- a/client/pull.go +++ b/client/pull.go @@ -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) diff --git a/client/push.go b/client/push.go index 08726058..c0ff10d1 100644 --- a/client/push.go +++ b/client/push.go @@ -5,7 +5,7 @@ import ( "io" "io/ioutil" - "github.com/docker/docker-registry" + "github.com/docker/docker-registry/storage" log "github.com/Sirupsen/logrus" ) @@ -15,7 +15,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 @@ -74,7 +74,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) diff --git a/errors.go b/errors.go index e2f16ba0..b6170430 100644 --- a/errors.go +++ b/errors.go @@ -212,7 +212,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 diff --git a/images.go b/images.go index 534069b2..317651e2 100644 --- a/images.go +++ b/images.go @@ -2,76 +2,13 @@ package registry import ( "encoding/json" + "fmt" "net/http" - "github.com/docker/docker-registry/digest" + "github.com/docker/docker-registry/storage" "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 { diff --git a/layer.go b/layer.go index 38fdfe39..5e1c6f45 100644 --- a/layer.go +++ b/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: diff --git a/storage/manifest.go b/storage/manifest.go new file mode 100644 index 00000000..9921fbea --- /dev/null +++ b/storage/manifest.go @@ -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"` +}