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"
|
||||||
"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
|
||||||
|
|
|
@ -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 := ®istry.ImageManifest{
|
manifest := &storage.SignedManifest{
|
||||||
Name: name,
|
Manifest: storage.Manifest{
|
||||||
Tag: tag,
|
Name: name,
|
||||||
Architecture: "x86",
|
Tag: tag,
|
||||||
FSLayers: blobs,
|
Architecture: "x86",
|
||||||
History: history,
|
FSLayers: blobs,
|
||||||
SchemaVersion: 1,
|
History: history,
|
||||||
|
Versioned: storage.Versioned{
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,21 +147,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 := ®istry.ImageManifest{
|
manifest := &storage.SignedManifest{
|
||||||
Name: name,
|
Manifest: storage.Manifest{
|
||||||
Tag: tag,
|
Name: name,
|
||||||
Architecture: "x86",
|
Tag: tag,
|
||||||
FSLayers: blobs,
|
Architecture: "x86",
|
||||||
History: history,
|
FSLayers: blobs,
|
||||||
SchemaVersion: 1,
|
History: history,
|
||||||
|
Versioned: storage.Versioned{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
manifestBytes, err := json.Marshal(manifest)
|
manifestBytes, err := json.Marshal(manifest)
|
||||||
|
|
||||||
|
@ -191,7 +199,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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"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 (
|
||||||
|
@ -27,11 +27,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)
|
||||||
|
@ -84,11 +84,11 @@ type LayerWriter 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()
|
||||||
|
|
||||||
|
@ -99,7 +99,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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -3,7 +3,7 @@ package client
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/docker/docker-registry"
|
"github.com/docker/docker-registry/storage"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,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
|
||||||
|
@ -72,7 +72,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)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/digest"
|
"github.com/docker/docker-registry/digest"
|
||||||
|
"github.com/docker/docker-registry/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrorCode represents the error type. The errors are serialized via strings
|
// 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
|
// 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
|
||||||
|
|
66
images.go
66
images.go
|
@ -1,77 +1,11 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/digest"
|
|
||||||
"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 {
|
||||||
|
|
2
layer.go
2
layer.go
|
@ -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:
|
||||||
|
|
|
@ -8,23 +8,6 @@ import (
|
||||||
"github.com/docker/docker-registry/digest"
|
"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,
|
// Layer provides a readable and seekable layer object. Typically,
|
||||||
// implementations are *not* goroutine safe.
|
// implementations are *not* goroutine safe.
|
||||||
type Layer interface {
|
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
|
// <root>/v2
|
||||||
// -> repositories/
|
// -> repositories/
|
||||||
// -><name>/
|
// -><name>/
|
||||||
// -> images/
|
// -> manifests/
|
||||||
// <manifests by tag name>
|
// <manifests by tag name>
|
||||||
// -> layers/
|
// -> layers/
|
||||||
// -> tarsum/
|
// -> tarsum/
|
||||||
|
@ -48,6 +48,7 @@ const storagePathVersion = "v2"
|
||||||
//
|
//
|
||||||
// We cover the path formats implemented by this path mapper below.
|
// 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>
|
// 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>
|
// 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>
|
// 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
|
// to an intermediate path object, than can be consumed and mapped by the
|
||||||
// other version.
|
// other version.
|
||||||
|
|
||||||
|
rootPrefix := []string{pm.root, pm.version}
|
||||||
|
repoPrefix := append(rootPrefix, "repositories")
|
||||||
|
|
||||||
switch v := spec.(type) {
|
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:
|
case layerLinkPathSpec:
|
||||||
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
||||||
// Only tarsum is supported, for now
|
// Only tarsum is supported, for now
|
||||||
|
@ -101,9 +108,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := path.Join(append([]string{pm.root, pm.version, "repositories", v.name, "layers"}, tarSumInfoPathComponents(tsi)...)...)
|
return path.Join(append(append(repoPrefix, v.name, "layers"),
|
||||||
|
tarSumInfoPathComponents(tsi)...)...), nil
|
||||||
return p, nil
|
|
||||||
case layerIndexLinkPathSpec:
|
case layerIndexLinkPathSpec:
|
||||||
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
if !strings.HasPrefix(v.digest.Algorithm(), "tarsum") {
|
||||||
// Only tarsum is supported, for now
|
// Only tarsum is supported, for now
|
||||||
|
@ -120,9 +126,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := path.Join(append([]string{pm.root, pm.version, "layerindex"}, tarSumInfoPathComponents(tsi)...)...)
|
return path.Join(append(append(rootPrefix, "layerindex"),
|
||||||
|
tarSumInfoPathComponents(tsi)...)...), nil
|
||||||
return p, nil
|
|
||||||
case blobPathSpec:
|
case blobPathSpec:
|
||||||
p := path.Join([]string{pm.root, pm.version, "blob", v.alg, v.digest[:2], v.digest}...)
|
p := path.Join([]string{pm.root, pm.version, "blob", v.alg, v.digest[:2], v.digest}...)
|
||||||
return p, nil
|
return p, nil
|
||||||
|
@ -139,6 +144,15 @@ type pathSpec interface {
|
||||||
pathSpec()
|
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
|
// 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
|
// id. The layer link will contain a content addressable blob id reference
|
||||||
// into the blob store. The format of the contents is as follows:
|
// into the blob store. The format of the contents is as follows:
|
||||||
|
|
|
@ -16,6 +16,13 @@ func TestPathMapper(t *testing.T) {
|
||||||
expected string
|
expected string
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
spec: manifestPathSpec{
|
||||||
|
name: "foo/bar",
|
||||||
|
tag: "thetag",
|
||||||
|
},
|
||||||
|
expected: "/pathmapper-test/repositories/foo/bar/manifests/thetag",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
spec: layerLinkPathSpec{
|
spec: layerLinkPathSpec{
|
||||||
name: "foo/bar",
|
name: "foo/bar",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/docker/docker-registry/digest"
|
||||||
"github.com/docker/docker-registry/storagedriver"
|
"github.com/docker/docker-registry/storagedriver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,3 +42,42 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
|
||||||
func (ss *Services) Layers() LayerService {
|
func (ss *Services) Layers() LayerService {
|
||||||
return &layerStore{driver: ss.driver, pathMapper: ss.pathMapper, uploadStore: ss.layerUploadStore}
|
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