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"` +}