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.
This commit is contained in:
Stephen J Day 2014-11-21 19:29:08 -08:00
parent 4bbabc6e36
commit eaadb82e1e
9 changed files with 179 additions and 108 deletions

View file

@ -12,17 +12,18 @@ import (
"github.com/docker/docker-registry" "github.com/docker/docker-registry"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
) )
// Client implements the client interface to the registry http api // Client implements the client interface to the registry http api
type Client interface { type Client interface {
// GetImageManifest returns an image manifest for the image at the given // GetImageManifest returns an image manifest for the image at the given
// name, tag pair. // 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 // PutImageManifest uploads an image manifest for the image at the given
// name, tag pair. // 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 removes the image at the given name, tag pair.
DeleteImage(name, tag string) error DeleteImage(name, tag string) error
@ -81,7 +82,7 @@ type clientImpl struct {
// TODO(bbland): use consistent route generation between server and client // 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)) response, err := http.Get(r.imageManifestURL(name, tag))
if err != nil { if err != nil {
return nil, err return nil, err
@ -108,7 +109,7 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
manifest := new(registry.ImageManifest) manifest := new(storage.SignedManifest)
err = decoder.Decode(manifest) err = decoder.Decode(manifest)
if err != nil { if err != nil {
return nil, err return nil, err
@ -116,7 +117,7 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest
return manifest, nil 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) manifestBytes, err := json.Marshal(manifest)
if err != nil { if err != nil {
return err return err

View file

@ -9,9 +9,9 @@ import (
"sync" "sync"
"testing" "testing"
"github.com/docker/docker-registry"
"github.com/docker/docker-registry/common/testutil" "github.com/docker/docker-registry/common/testutil"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
) )
type testBlob struct { type testBlob struct {
@ -33,8 +33,8 @@ func TestPush(t *testing.T) {
}, },
} }
uploadLocations := make([]string, len(testBlobs)) uploadLocations := make([]string, len(testBlobs))
blobs := make([]registry.FSLayer, len(testBlobs)) blobs := make([]storage.FSLayer, len(testBlobs))
history := make([]registry.ManifestHistory, len(testBlobs)) history := make([]storage.ManifestHistory, len(testBlobs))
for i, blob := range testBlobs { for i, blob := range testBlobs {
// TODO(bbland): this is returning the same location for all uploads, // 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 // It's sort of okay because we're using unique digests, but this needs
// to change at some point. // to change at some point.
uploadLocations[i] = fmt.Sprintf("/v2/%s/blob/test-uuid", name) uploadLocations[i] = fmt.Sprintf("/v2/%s/blob/test-uuid", name)
blobs[i] = registry.FSLayer{BlobSum: blob.digest} blobs[i] = storage.FSLayer{BlobSum: blob.digest}
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()} history[i] = storage.ManifestHistory{V1Compatibility: blob.digest.String()}
} }
manifest := &registry.ImageManifest{ manifest := &storage.SignedManifest{
Manifest: storage.Manifest{
Name: name, Name: name,
Tag: tag, Tag: tag,
Architecture: "x86", Architecture: "x86",
FSLayers: blobs, FSLayers: blobs,
History: history, History: history,
Versioned: storage.Versioned{
SchemaVersion: 1, SchemaVersion: 1,
},
},
} }
manifestBytes, err := json.Marshal(manifest) manifestBytes, err := json.Marshal(manifest)
@ -102,7 +106,7 @@ func TestPush(t *testing.T) {
client := New(server.URL) client := New(server.URL)
objectStore := &memoryObjectStore{ objectStore := &memoryObjectStore{
mutex: new(sync.Mutex), mutex: new(sync.Mutex),
manifestStorage: make(map[string]*registry.ImageManifest), manifestStorage: make(map[string]*storage.SignedManifest),
layerStorage: make(map[digest.Digest]Layer), layerStorage: make(map[digest.Digest]Layer),
} }
@ -142,21 +146,25 @@ func TestPull(t *testing.T) {
contents: []byte("some other contents"), contents: []byte("some other contents"),
}, },
} }
blobs := make([]registry.FSLayer, len(testBlobs)) blobs := make([]storage.FSLayer, len(testBlobs))
history := make([]registry.ManifestHistory, len(testBlobs)) history := make([]storage.ManifestHistory, len(testBlobs))
for i, blob := range testBlobs { for i, blob := range testBlobs {
blobs[i] = registry.FSLayer{BlobSum: blob.digest} blobs[i] = storage.FSLayer{BlobSum: blob.digest}
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()} history[i] = storage.ManifestHistory{V1Compatibility: blob.digest.String()}
} }
manifest := &registry.ImageManifest{ manifest := &storage.SignedManifest{
Manifest: storage.Manifest{
Name: name, Name: name,
Tag: tag, Tag: tag,
Architecture: "x86", Architecture: "x86",
FSLayers: blobs, FSLayers: blobs,
History: history, History: history,
Versioned: storage.Versioned{
SchemaVersion: 1, SchemaVersion: 1,
},
},
} }
manifestBytes, err := json.Marshal(manifest) manifestBytes, err := json.Marshal(manifest)
@ -190,7 +198,7 @@ func TestPull(t *testing.T) {
client := New(server.URL) client := New(server.URL)
objectStore := &memoryObjectStore{ objectStore := &memoryObjectStore{
mutex: new(sync.Mutex), mutex: new(sync.Mutex),
manifestStorage: make(map[string]*registry.ImageManifest), manifestStorage: make(map[string]*storage.SignedManifest),
layerStorage: make(map[digest.Digest]Layer), layerStorage: make(map[digest.Digest]Layer),
} }

View file

@ -8,8 +8,8 @@ import (
"io/ioutil" "io/ioutil"
"sync" "sync"
"github.com/docker/docker-registry"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
) )
var ( var (
@ -28,11 +28,11 @@ var (
type ObjectStore interface { type ObjectStore interface {
// Manifest retrieves the image manifest stored at the given repository name // Manifest retrieves the image manifest stored at the given repository name
// and tag // 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 // WriteManifest stores an image manifest at the given repository name and
// tag // 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 returns a handle to a layer for reading and writing
Layer(dgst digest.Digest) (Layer, error) Layer(dgst digest.Digest) (Layer, error)
@ -56,11 +56,11 @@ type Layer interface {
// memoryObjectStore is an in-memory implementation of the ObjectStore interface // memoryObjectStore is an in-memory implementation of the ObjectStore interface
type memoryObjectStore struct { type memoryObjectStore struct {
mutex *sync.Mutex mutex *sync.Mutex
manifestStorage map[string]*registry.ImageManifest manifestStorage map[string]*storage.SignedManifest
layerStorage map[digest.Digest]Layer 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() objStore.mutex.Lock()
defer objStore.mutex.Unlock() defer objStore.mutex.Unlock()
@ -71,7 +71,7 @@ func (objStore *memoryObjectStore) Manifest(name, tag string) (*registry.ImageMa
return manifest, nil 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() objStore.mutex.Lock()
defer objStore.mutex.Unlock() defer objStore.mutex.Unlock()

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/docker/docker-registry" "github.com/docker/docker-registry/storage"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
) )
@ -77,7 +77,7 @@ func Pull(c Client, objectStore ObjectStore, name, tag string) error {
return nil 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") log.WithField("layer", fsLayer).Info("Pulling layer")
layer, err := objectStore.Layer(fsLayer.BlobSum) layer, err := objectStore.Layer(fsLayer.BlobSum)

View file

@ -5,7 +5,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"github.com/docker/docker-registry" "github.com/docker/docker-registry/storage"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
) )
@ -15,7 +15,7 @@ import (
// push window has been successfully pushed. // push window has been successfully pushed.
const simultaneousLayerPushWindow = 4 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 // 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 // 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 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") log.WithField("layer", fsLayer).Info("Pushing layer")
layer, err := objectStore.Layer(fsLayer.BlobSum) layer, err := objectStore.Layer(fsLayer.BlobSum)

View file

@ -212,7 +212,7 @@ type DetailUnknownLayer struct {
// Unknown should contain the contents of a layer descriptor, which is a // Unknown should contain the contents of a layer descriptor, which is a
// single FSLayer currently. // single FSLayer currently.
Unknown FSLayer `json:"unknown"` Unknown storage.FSLayer `json:"unknown"`
} }
// RepositoryNotFoundError is returned when making an operation against a // RepositoryNotFoundError is returned when making an operation against a

View file

@ -2,76 +2,13 @@ package registry
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "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 // imageManifestDispatcher takes the request context and builds the
// appropriate handler for handling image manifest requests. // appropriate handler for handling image manifest requests.
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {

View file

@ -52,7 +52,7 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
lh.Errors.Push(ErrorCodeUnknownLayer, lh.Errors.Push(ErrorCodeUnknownLayer,
map[string]interface{}{ map[string]interface{}{
"unknown": FSLayer{BlobSum: lh.Digest}, "unknown": storage.FSLayer{BlobSum: lh.Digest},
}) })
return return
default: default:

125
storage/manifest.go Normal file
View 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"`
}