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:
Stephen J Day 2014-11-26 12:52:52 -08:00
parent 59c399cb25
commit 6fead90736
7 changed files with 168 additions and 53 deletions

View file

@ -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)
}

View file

@ -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)

View file

@ -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
} }

View file

@ -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

View file

@ -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

View file

@ -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{

View file

@ -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
} }