forked from TrueCloudLab/distribution
Rich error reporting for manifest push
To provide rich error reporting during manifest pushes, the storage layers verifyManifest stage has been modified to provide the necessary granularity. Along with this comes with a partial shift to explicit error types, which represents a small move in larger refactoring of error handling. Signature methods from libtrust have been added to the various Manifest types to clean up the verification code. A primitive deletion implementation for manifests has been added. It only deletes the manifest file and doesn't attempt to add some of the richer features request, such as layer cleanup.
This commit is contained in:
parent
59c399cb25
commit
6fead90736
7 changed files with 168 additions and 53 deletions
|
@ -53,9 +53,6 @@ type LayerUpload interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrLayerUnknown returned when layer cannot be found.
|
|
||||||
ErrLayerUnknown = fmt.Errorf("unknown layer")
|
|
||||||
|
|
||||||
// ErrLayerExists returned when layer already exists
|
// ErrLayerExists returned when layer already exists
|
||||||
ErrLayerExists = fmt.Errorf("layer exists")
|
ErrLayerExists = fmt.Errorf("layer exists")
|
||||||
|
|
||||||
|
@ -65,9 +62,6 @@ var (
|
||||||
// ErrLayerUploadUnknown returned when upload is not found.
|
// ErrLayerUploadUnknown returned when upload is not found.
|
||||||
ErrLayerUploadUnknown = fmt.Errorf("layer upload unknown")
|
ErrLayerUploadUnknown = fmt.Errorf("layer upload unknown")
|
||||||
|
|
||||||
// ErrLayerInvalidDigest returned when tarsum check fails.
|
|
||||||
ErrLayerInvalidDigest = fmt.Errorf("invalid layer digest")
|
|
||||||
|
|
||||||
// ErrLayerInvalidLength returned when length check fails.
|
// ErrLayerInvalidLength returned when length check fails.
|
||||||
ErrLayerInvalidLength = fmt.Errorf("invalid layer length")
|
ErrLayerInvalidLength = fmt.Errorf("invalid layer length")
|
||||||
|
|
||||||
|
@ -75,3 +69,21 @@ var (
|
||||||
// Layer or LayerUpload.
|
// Layer or LayerUpload.
|
||||||
ErrLayerClosed = fmt.Errorf("layer closed")
|
ErrLayerClosed = fmt.Errorf("layer closed")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnknownLayer returned when layer cannot be found.
|
||||||
|
type ErrUnknownLayer struct {
|
||||||
|
FSLayer FSLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUnknownLayer) Error() string {
|
||||||
|
return fmt.Sprintf("unknown layer %v", err.FSLayer.BlobSum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLayerInvalidDigest returned when tarsum check fails.
|
||||||
|
type ErrLayerInvalidDigest struct {
|
||||||
|
FSLayer FSLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLayerInvalidDigest) Error() string {
|
||||||
|
return fmt.Sprintf("invalid digest for referenced layer: %v", err.FSLayer.BlobSum)
|
||||||
|
}
|
||||||
|
|
|
@ -169,11 +169,13 @@ func TestSimpleLayerRead(t *testing.T) {
|
||||||
t.Fatalf("error expected fetching unknown layer")
|
t.Fatalf("error expected fetching unknown layer")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != ErrLayerUnknown {
|
switch err.(type) {
|
||||||
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
|
case ErrUnknownLayer:
|
||||||
} else {
|
|
||||||
err = nil
|
err = nil
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
randomLayerDigest, err := writeTestLayer(driver, ls.pathMapper, imageName, dgst, randomLayerReader)
|
randomLayerDigest, err := writeTestLayer(driver, ls.pathMapper, imageName, dgst, randomLayerReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error writing test layer: %v", err)
|
t.Fatalf("unexpected error writing test layer: %v", err)
|
||||||
|
|
|
@ -19,7 +19,8 @@ func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) {
|
||||||
_, err := ls.Fetch(name, digest)
|
_, err := ls.Fetch(name, digest)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrLayerUnknown {
|
switch err.(type) {
|
||||||
|
case ErrUnknownLayer:
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
return nil, ErrLayerUnknown
|
return nil, ErrUnknownLayer{FSLayer{BlobSum: digest}}
|
||||||
default:
|
default:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -44,7 +45,7 @@ func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
return nil, ErrLayerUnknown
|
return nil, ErrUnknownLayer{FSLayer{BlobSum: digest}}
|
||||||
default:
|
default:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,7 +275,7 @@ func (luc *layerUploadController) validateLayer(fp layerFile, size int64, dgst d
|
||||||
}
|
}
|
||||||
|
|
||||||
if !digestVerifier.Verified() {
|
if !digestVerifier.Verified() {
|
||||||
return "", ErrLayerInvalidDigest
|
return "", ErrLayerInvalidDigest{FSLayer{BlobSum: dgst}}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dgst, nil
|
return dgst, nil
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
"github.com/docker/libtrust"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/digest"
|
"github.com/docker/docker-registry/digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// ErrUnknownManifest is returned if the manifest is not known by the
|
||||||
// ErrManifestUnknown is returned if the manifest is not known by the
|
// registry.
|
||||||
// registry.
|
type ErrUnknownManifest struct {
|
||||||
ErrManifestUnknown = fmt.Errorf("unknown manifest")
|
Name string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
// ErrManifestUnverified is returned when the registry is unable to verify
|
func (err ErrUnknownManifest) Error() string {
|
||||||
// the manifest.
|
return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag)
|
||||||
ErrManifestUnverified = fmt.Errorf("unverified manifest")
|
}
|
||||||
)
|
|
||||||
|
// ErrManifestUnverified is returned when the registry is unable to verify
|
||||||
|
// the manifest.
|
||||||
|
type ErrManifestUnverified struct{}
|
||||||
|
|
||||||
|
func (ErrManifestUnverified) Error() string {
|
||||||
|
return fmt.Sprintf("unverified manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrManifestVerification provides a type to collect errors encountered
|
||||||
|
// during manifest verification. Currently, it accepts errors of all types,
|
||||||
|
// but it may be narrowed to those involving manifest verification.
|
||||||
|
type ErrManifestVerification []error
|
||||||
|
|
||||||
|
func (errs ErrManifestVerification) Error() string {
|
||||||
|
var parts []string
|
||||||
|
for _, err := range errs {
|
||||||
|
parts = append(parts, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
||||||
// content with unknown schema version can be decoded against this struct to
|
// content with unknown schema version can be decoded against this struct to
|
||||||
|
@ -78,7 +103,37 @@ func (m *Manifest) Sign(pk libtrust.PrivateKey) (*SignedManifest, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignedManifest provides an envelope for
|
// SignWithChain signs the manifest with the given private key and x509 chain.
|
||||||
|
// The public key of the first element in the chain must be the public key
|
||||||
|
// corresponding with the sign key.
|
||||||
|
func (m *Manifest) SignWithChain(key libtrust.PrivateKey, chain []*x509.Certificate) (*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.SignWithChain(key, chain); 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 a signed image manifest, including
|
||||||
|
// the format sensitive raw bytes. It contains fields to
|
||||||
type SignedManifest struct {
|
type SignedManifest struct {
|
||||||
Manifest
|
Manifest
|
||||||
|
|
||||||
|
@ -88,28 +143,51 @@ type SignedManifest struct {
|
||||||
Raw []byte `json:"-"`
|
Raw []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify verifies the signature of the signed manifest returning the public
|
||||||
|
// keys used during signing.
|
||||||
|
func (sm *SignedManifest) Verify() ([]libtrust.PublicKey, error) {
|
||||||
|
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return js.Verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyChains verifies the signature of the signed manifest against the
|
||||||
|
// certificate pool returning the list of verified chains. Signatures without
|
||||||
|
// an x509 chain are not checked.
|
||||||
|
func (sm *SignedManifest) VerifyChains(ca *x509.CertPool) ([][]*x509.Certificate, error) {
|
||||||
|
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return js.VerifyChains(ca)
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
|
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
|
||||||
func (m *SignedManifest) UnmarshalJSON(b []byte) error {
|
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
|
||||||
var manifest Manifest
|
var manifest Manifest
|
||||||
if err := json.Unmarshal(b, &manifest); err != nil {
|
if err := json.Unmarshal(b, &manifest); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Manifest = manifest
|
sm.Manifest = manifest
|
||||||
m.Raw = b
|
sm.Raw = b
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
||||||
// contents.
|
// contents.
|
||||||
func (m *SignedManifest) MarshalJSON() ([]byte, error) {
|
func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
|
||||||
if len(m.Raw) > 0 {
|
if len(sm.Raw) > 0 {
|
||||||
return m.Raw, nil
|
return sm.Raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the raw data is not available, just dump the inner content.
|
// If the raw data is not available, just dump the inner content.
|
||||||
return json.Marshal(&m.Manifest)
|
return json.Marshal(&sm.Manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||||
|
|
|
@ -33,8 +33,13 @@ func TestManifestStorage(t *testing.T) {
|
||||||
t.Fatalf("manifest should not exist")
|
t.Fatalf("manifest should not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := ms.Get(name, tag); err != ErrManifestUnknown {
|
if _, err := ms.Get(name, tag); true {
|
||||||
t.Fatalf("expected manifest unknown error: %v != %v", err, ErrManifestUnknown)
|
switch err.(type) {
|
||||||
|
case ErrUnknownManifest:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected manifest unknown error: %#v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest := Manifest{
|
manifest := Manifest{
|
||||||
|
|
|
@ -4,9 +4,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
|
|
||||||
"github.com/docker/docker-registry/storagedriver"
|
"github.com/docker/docker-registry/storagedriver"
|
||||||
|
"github.com/docker/libtrust"
|
||||||
)
|
)
|
||||||
|
|
||||||
type manifestStore struct {
|
type manifestStore struct {
|
||||||
|
@ -45,7 +44,7 @@ func (ms *manifestStore) Get(name, tag string) (*SignedManifest, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
return nil, ErrManifestUnknown
|
return nil, ErrUnknownManifest{Name: name, Tag: tag}
|
||||||
default:
|
default:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -73,13 +72,28 @@ func (ms *manifestStore) Put(name, tag string, manifest *SignedManifest) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(stevvooe): Should we get manifest first?
|
// TODO(stevvooe): Should we get old manifest first? Perhaps, write, then
|
||||||
|
// move to ensure a valid manifest?
|
||||||
|
|
||||||
return ms.driver.PutContent(p, manifest.Raw)
|
return ms.driver.PutContent(p, manifest.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *manifestStore) Delete(name, tag string) error {
|
func (ms *manifestStore) Delete(name, tag string) error {
|
||||||
panic("not implemented")
|
p, err := ms.path(name, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ms.driver.Delete(p); err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
|
return ErrUnknownManifest{Name: name, Tag: tag}
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *manifestStore) path(name, tag string) (string, error) {
|
func (ms *manifestStore) path(name, tag string) (string, error) {
|
||||||
|
@ -90,6 +104,12 @@ func (ms *manifestStore) path(name, tag string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManifest) error {
|
func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManifest) error {
|
||||||
|
// TODO(stevvooe): This verification is present here, but this needs to be
|
||||||
|
// lifted out of the storage infrastructure and moved into a package
|
||||||
|
// oriented towards defining verifiers and reporting them with
|
||||||
|
// granularity.
|
||||||
|
|
||||||
|
var errs ErrManifestVerification
|
||||||
if manifest.Name != name {
|
if manifest.Name != name {
|
||||||
return fmt.Errorf("name does not match manifest name")
|
return fmt.Errorf("name does not match manifest name")
|
||||||
}
|
}
|
||||||
|
@ -98,37 +118,34 @@ func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManife
|
||||||
return fmt.Errorf("tag does not match manifest tag")
|
return fmt.Errorf("tag does not match manifest tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
// TODO(stevvooe): These pubkeys need to be checked with either Verify or
|
||||||
|
// VerifyWithChains. We need to define the exact source of the CA.
|
||||||
|
// Perhaps, its a configuration value injected into manifest store.
|
||||||
|
|
||||||
|
if _, err := manifest.Verify(); err != nil {
|
||||||
|
switch err {
|
||||||
|
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
|
||||||
|
errs = append(errs, ErrManifestUnverified{})
|
||||||
|
default:
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fsLayer := range manifest.FSLayers {
|
for _, fsLayer := range manifest.FSLayers {
|
||||||
exists, err := ms.layerService.Exists(name, fsLayer.BlobSum)
|
exists, err := ms.layerService.Exists(name, fsLayer.BlobSum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(stevvooe): Need to store information about missing blob.
|
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
errs = append(errs, fmt.Errorf("missing layer %v", fsLayer.BlobSum))
|
errs = append(errs, ErrUnknownLayer{FSLayer: fsLayer})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
// TODO(stevvooe): These need to be recoverable by a caller.
|
// TODO(stevvooe): These need to be recoverable by a caller.
|
||||||
return fmt.Errorf("missing layers: %v", errs)
|
return 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue