Lock down HTTP API error codes

This commit locks down the set of http error codes that will be part of the
inital V2 specification, proposed in docker/docker#9015. The naming order has
been slightly changed and there are few tweaks to ensure all conditions are
captured but this will be set the docker core will be impleemnted against.

To support this, the errors have been moved into an api/errors package. A new
type, ErrorDescriptor, has been defined to centralize the code, message and
definitions used with each type. The information therein can be used to
generate documentation and response code mappings (yet to come...).

In addition to the refactoring that came along with this change, several tests
have been added to ensure serialization round trips are reliable. This allows
better support for using these error types on the client and server side. This
is coupled with some tweaks in the client code to fix issues with error
reporting.

Other fixes in the client include moving client-specific errors out of the base
package and ensuring that we have correct parameters for finishing uploads.
This commit is contained in:
Stephen J Day 2014-12-09 21:25:54 -08:00
parent 33d89b4bca
commit 7b56d10076
14 changed files with 716 additions and 514 deletions

135
api/errors/descriptors.go Normal file
View file

@ -0,0 +1,135 @@
package errors
import "net/http"
// ErrorDescriptor provides relevant information about a given error code.
type ErrorDescriptor struct {
// Code is the error code that this descriptor describes.
Code ErrorCode
// Value provides a unique, string key, often captilized with
// underscores, to identify the error code. This value is used as the
// keyed value when serializing api errors.
Value string
// Message is a short, human readable decription of the error condition
// included in API responses.
Message string
// Description provides a complete account of the errors purpose, suitable
// for use in documentation.
Description string
// DefaultStatusCode should to be returned via the HTTP API. Some error
// may have different status codes depending on the situation.
DefaultStatusCode int
}
var descriptors = []ErrorDescriptor{
{
Code: ErrorCodeUnknown,
Value: "UNKNOWN",
Message: "unknown error",
},
{
Code: ErrorCodeDigestInvalid,
Value: "DIGEST_INVALID",
Message: "provided digest did not match uploaded content",
Description: `When a blob is uploaded, the registry will check that
the content matches the digest provided by the client. The error may
include a detail structure with the key "digest", including the
invalid digest string. This error may also be returned when a manifest
includes an invalid layer digest.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeSizeInvalid,
Value: "SIZE_INVALID",
Message: "provided length did not match content length",
Description: `When a layer is uploaded, the provided size will be
checked against the uploaded content. If they do not match, this error
will be returned.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeNameInvalid,
Value: "NAME_INVALID",
Message: "manifest name did not match URI",
Description: `During a manifest upload, if the name in the manifest
does not match the uri name, this error will be returned.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeTagInvalid,
Value: "TAG_INVALID",
Message: "manifest tag did not match URI",
Description: `During a manifest upload, if the tag in the manifest
does not match the uri tag, this error will be returned.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeNameUnknown,
Value: "NAME_UNKNOWN",
Message: "repository name not known to registry",
Description: `This is returned if the name used during an operation is
unknown to the registry.`,
DefaultStatusCode: http.StatusNotFound,
},
{
Code: ErrorCodeManifestUnknown,
Value: "MANIFEST_UNKNOWN",
Message: "manifest unknown",
Description: `This error is returned when the manifest, identified by
name and tag is unknown to the repository.`,
DefaultStatusCode: http.StatusNotFound,
},
{
Code: ErrorCodeManifestInvalid,
Value: "MANIFEST_INVALID",
Message: "manifest invalid",
Description: `During upload, manifests undergo several checks ensuring
validity. If those checks fail, this error may be returned, unless a
more specific error is included.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeManifestUnverified,
Value: "MANIFEST_UNVERIFIED",
Message: "manifest failed signature verification",
Description: `During manifest upload, if the manifest fails signature
verification, this error will be returned.`,
DefaultStatusCode: http.StatusBadRequest,
},
{
Code: ErrorCodeBlobUnknown,
Value: "BLOB_UNKNOWN",
Message: "blob unknown to registry",
Description: `This error may be returned when a blob is unknown to the
registry in a specified repository. This can be returned with a
standard get or if a manifest references an unknown layer during
upload.`,
DefaultStatusCode: http.StatusNotFound,
},
{
Code: ErrorCodeBlobUploadUnknown,
Value: "BLOB_UPLOAD_UNKNOWN",
Message: "blob upload unknown to registry",
Description: `If a blob upload has been cancelled or was never
started, this error code may be returned.`,
DefaultStatusCode: http.StatusNotFound,
},
}
var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
var idToDescriptors map[string]ErrorDescriptor
func init() {
errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(descriptors))
idToDescriptors = make(map[string]ErrorDescriptor, len(descriptors))
for _, descriptor := range descriptors {
errorCodeToDescriptors[descriptor.Code] = descriptor
idToDescriptors[descriptor.Value] = descriptor
}
}

193
api/errors/errors.go Normal file
View file

@ -0,0 +1,193 @@
// Package errors describes the error codes that may be returned via the
// Docker Registry JSON HTTP API V2. In addition to declaractions,
// descriptions about the error codes and the conditions causing them are
// avialable in detail.
//
// Error definitions here are considered to be locked down for the V2 registry
// api. Any changes must be considered carefully and should not proceed
// without a change proposal in docker core.
package errors
import (
"fmt"
"strings"
)
// ErrorCode represents the error type. The errors are serialized via strings
// and the integer format may change and should *never* be exported.
type ErrorCode int
const (
// ErrorCodeUnknown is a catch-all for errors not defined below.
ErrorCodeUnknown ErrorCode = iota
// ErrorCodeDigestInvalid is returned when uploading a blob if the
// provided digest does not match the blob contents.
ErrorCodeDigestInvalid
// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
// size does not match the content length.
ErrorCodeSizeInvalid
// ErrorCodeNameInvalid is returned when the name in the manifest does not
// match the provided name.
ErrorCodeNameInvalid
// ErrorCodeTagInvalid is returned when the tag in the manifest does not
// match the provided tag.
ErrorCodeTagInvalid
// ErrorCodeNameUnknown when the repository name is not known.
ErrorCodeNameUnknown
// ErrorCodeManifestUnknown returned when image manifest is unknown.
ErrorCodeManifestUnknown
// ErrorCodeManifestInvalid returned when an image manifest is invalid,
// typically during a PUT operation. This error encompasses all errors
// encountered during manifest validation that aren't signature errors.
ErrorCodeManifestInvalid
// ErrorCodeManifestUnverified is returned when the manifest fails
// signature verfication.
ErrorCodeManifestUnverified
// ErrorCodeBlobUnknown is returned when a blob is unknown to the
// registry. This can happen when the manifest references a nonexistent
// layer or the result is not found by a blob fetch.
ErrorCodeBlobUnknown
// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
ErrorCodeBlobUploadUnknown
)
// ParseErrorCode attempts to parse the error code string, returning
// ErrorCodeUnknown if the error is not known.
func ParseErrorCode(s string) ErrorCode {
desc, ok := idToDescriptors[s]
if !ok {
return ErrorCodeUnknown
}
return desc.Code
}
// Descriptor returns the descriptor for the error code.
func (ec ErrorCode) Descriptor() ErrorDescriptor {
d, ok := errorCodeToDescriptors[ec]
if !ok {
return ErrorCodeUnknown.Descriptor()
}
return d
}
// String returns the canonical identifier for this error code.
func (ec ErrorCode) String() string {
return ec.Descriptor().Value
}
// Message returned the human-readable error message for this error code.
func (ec ErrorCode) Message() string {
return ec.Descriptor().Message
}
// MarshalText encodes the receiver into UTF-8-encoded text and returns the
// result.
func (ec ErrorCode) MarshalText() (text []byte, err error) {
return []byte(ec.String()), nil
}
// UnmarshalText decodes the form generated by MarshalText.
func (ec *ErrorCode) UnmarshalText(text []byte) error {
desc, ok := idToDescriptors[string(text)]
if !ok {
desc = ErrorCodeUnknown.Descriptor()
}
*ec = desc.Code
return nil
}
// Error provides a wrapper around ErrorCode with extra Details provided.
type Error struct {
Code ErrorCode `json:"code"`
Message string `json:"message,omitempty"`
Detail interface{} `json:"detail,omitempty"`
}
// Error returns a human readable representation of the error.
func (e Error) Error() string {
return fmt.Sprintf("%s: %s",
strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
e.Message)
}
// Errors provides the envelope for multiple errors and a few sugar methods
// for use within the application.
type Errors struct {
Errors []Error `json:"errors,omitempty"`
}
// Push pushes an error on to the error stack, with the optional detail
// argument. It is a programming error (ie panic) to push more than one
// detail at a time.
func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
if len(details) > 1 {
panic("please specify zero or one detail items for this error")
}
var detail interface{}
if len(details) > 0 {
detail = details[0]
}
if err, ok := detail.(error); ok {
detail = err.Error()
}
errs.PushErr(Error{
Code: code,
Message: code.Message(),
Detail: detail,
})
}
// PushErr pushes an error interface onto the error stack.
func (errs *Errors) PushErr(err error) {
switch err.(type) {
case Error:
errs.Errors = append(errs.Errors, err.(Error))
default:
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
}
}
func (errs *Errors) Error() string {
switch errs.Len() {
case 0:
return "<nil>"
case 1:
return errs.Errors[0].Error()
default:
msg := "errors:\n"
for _, err := range errs.Errors {
msg += err.Error() + "\n"
}
return msg
}
}
// Clear clears the errors.
func (errs *Errors) Clear() {
errs.Errors = errs.Errors[:0]
}
// Len returns the current number of errors.
func (errs *Errors) Len() int {
return len(errs.Errors)
}

165
api/errors/errors_test.go Normal file
View file

@ -0,0 +1,165 @@
package errors
import (
"encoding/json"
"reflect"
"testing"
"github.com/docker/docker-registry/digest"
)
// TestErrorCodes ensures that error code format, mappings and
// marshaling/unmarshaling. round trips are stable.
func TestErrorCodes(t *testing.T) {
for _, desc := range descriptors {
if desc.Code.String() != desc.Value {
t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
}
if desc.Code.Message() != desc.Message {
t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message)
}
// Serialize the error code using the json library to ensure that we
// get a string and it works round trip.
p, err := json.Marshal(desc.Code)
if err != nil {
t.Fatalf("error marshaling error code %v: %v", desc.Code, err)
}
if len(p) <= 0 {
t.Fatalf("expected content in marshaled before for error code %v", desc.Code)
}
// First, unmarshal to interface and ensure we have a string.
var ecUnspecified interface{}
if err := json.Unmarshal(p, &ecUnspecified); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
}
if _, ok := ecUnspecified.(string); !ok {
t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified)
}
// Now, unmarshal with the error code type and ensure they are equal
var ecUnmarshaled ErrorCode
if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
}
if ecUnmarshaled != desc.Code {
t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code)
}
}
}
// TestErrorsManagement does a quick check of the Errors type to ensure that
// members are properly pushed and marshaled.
func TestErrorsManagement(t *testing.T) {
var errs Errors
errs.Push(ErrorCodeDigestInvalid)
errs.Push(ErrorCodeBlobUnknown,
map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"})
p, err := json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}"
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
errs.Clear()
errs.Push(ErrorCodeUnknown)
expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
p, err = json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
}
// TestMarshalUnmarshal ensures that api errors can round trip through json
// without losing information.
func TestMarshalUnmarshal(t *testing.T) {
var errors Errors
for _, testcase := range []struct {
description string
err Error
}{
{
description: "unknown error",
err: Error{
Code: ErrorCodeUnknown,
Message: ErrorCodeUnknown.Descriptor().Message,
},
},
{
description: "unknown manifest",
err: Error{
Code: ErrorCodeManifestUnknown,
Message: ErrorCodeManifestUnknown.Descriptor().Message,
},
},
{
description: "unknown manifest",
err: Error{
Code: ErrorCodeBlobUnknown,
Message: ErrorCodeBlobUnknown.Descriptor().Message,
Detail: map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"},
},
},
} {
fatalf := func(format string, args ...interface{}) {
t.Fatalf(testcase.description+": "+format, args...)
}
unexpectedErr := func(err error) {
fatalf("unexpected error: %v", err)
}
p, err := json.Marshal(testcase.err)
if err != nil {
unexpectedErr(err)
}
var unmarshaled Error
if err := json.Unmarshal(p, &unmarshaled); err != nil {
unexpectedErr(err)
}
if !reflect.DeepEqual(unmarshaled, testcase.err) {
fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err)
}
// Roll everything up into an error response envelope.
errors.PushErr(testcase.err)
}
p, err := json.Marshal(errors)
if err != nil {
t.Fatalf("unexpected error marshaling error envelope: %v", err)
}
var unmarshaled Errors
if err := json.Unmarshal(p, &unmarshaled); err != nil {
t.Fatalf("unexpected error unmarshaling error envelope: %v", err)
}
if !reflect.DeepEqual(unmarshaled, errors) {
t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors)
}
}

View file

@ -12,16 +12,14 @@ import (
"os" "os"
"testing" "testing"
"github.com/docker/libtrust" "github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/storage"
_ "github.com/docker/docker-registry/storagedriver/inmemory"
"github.com/gorilla/handlers"
"github.com/docker/docker-registry/common/testutil" "github.com/docker/docker-registry/common/testutil"
"github.com/docker/docker-registry/configuration" "github.com/docker/docker-registry/configuration"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
_ "github.com/docker/docker-registry/storagedriver/inmemory"
"github.com/docker/libtrust"
"github.com/gorilla/handlers"
) )
// TestLayerAPI conducts a full of the of the layer api. // TestLayerAPI conducts a full of the of the layer api.
@ -133,6 +131,10 @@ func TestLayerAPI(t *testing.T) {
if !verifier.Verified() { if !verifier.Verified() {
t.Fatalf("response body did not pass verification") t.Fatalf("response body did not pass verification")
} }
// Missing tests:
// - Upload the same tarsum file under and different repository and
// ensure the content remains uncorrupted.
} }
func TestManifestAPI(t *testing.T) { func TestManifestAPI(t *testing.T) {
@ -180,9 +182,7 @@ func TestManifestAPI(t *testing.T) {
// } // }
dec := json.NewDecoder(resp.Body) dec := json.NewDecoder(resp.Body)
var respErrs struct { var respErrs errors.Errors
Errors []Error
}
if err := dec.Decode(&respErrs); err != nil { if err := dec.Decode(&respErrs); err != nil {
t.Fatalf("unexpected error decoding error response: %v", err) t.Fatalf("unexpected error decoding error response: %v", err)
} }
@ -191,7 +191,7 @@ func TestManifestAPI(t *testing.T) {
t.Fatalf("expected errors in response") t.Fatalf("expected errors in response")
} }
if respErrs.Errors[0].Code != ErrorCodeUnknownManifest { if respErrs.Errors[0].Code != errors.ErrorCodeManifestUnknown {
t.Fatalf("expected manifest unknown error: got %v", respErrs) t.Fatalf("expected manifest unknown error: got %v", respErrs)
} }
@ -217,7 +217,7 @@ func TestManifestAPI(t *testing.T) {
t.Fatalf("expected errors in response") t.Fatalf("expected errors in response")
} }
if respErrs.Errors[0].Code != ErrorCodeUnknownRepository { if respErrs.Errors[0].Code != errors.ErrorCodeNameUnknown {
t.Fatalf("expected respository unknown error: got %v", respErrs) t.Fatalf("expected respository unknown error: got %v", respErrs)
} }
@ -251,11 +251,11 @@ func TestManifestAPI(t *testing.T) {
for _, err := range respErrs.Errors { for _, err := range respErrs.Errors {
switch err.Code { switch err.Code {
case ErrorCodeUnverifiedManifest: case errors.ErrorCodeManifestUnverified:
unverified++ unverified++
case ErrorCodeUnknownLayer: case errors.ErrorCodeBlobUnknown:
missingLayers++ missingLayers++
case ErrorCodeInvalidDigest: case errors.ErrorCodeDigestInvalid:
// TODO(stevvooe): This error isn't quite descriptive enough -- // TODO(stevvooe): This error isn't quite descriptive enough --
// the layer with an invalid digest isn't identified. // the layer with an invalid digest isn't identified.
invalidDigests++ invalidDigests++

View file

@ -10,7 +10,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"github.com/docker/docker-registry" "github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
) )
@ -94,17 +94,18 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*storage.SignedManifest
case response.StatusCode == http.StatusOK: case response.StatusCode == http.StatusOK:
break break
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return nil, &registry.ImageManifestNotFoundError{Name: name, Tag: tag} return nil, &ImageManifestNotFoundError{Name: name, Tag: tag}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, errors return nil, &errs
default: default:
return nil, &registry.UnexpectedHTTPStatusError{Status: response.Status} return nil, &UnexpectedHTTPStatusError{Status: response.Status}
} }
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
@ -118,13 +119,8 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*storage.SignedManifest
} }
func (r *clientImpl) PutImageManifest(name, tag string, manifest *storage.SignedManifest) error { func (r *clientImpl) PutImageManifest(name, tag string, manifest *storage.SignedManifest) error {
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return err
}
putRequest, err := http.NewRequest("PUT", putRequest, err := http.NewRequest("PUT",
r.imageManifestURL(name, tag), bytes.NewReader(manifestBytes)) r.imageManifestURL(name, tag), bytes.NewReader(manifest.Raw))
if err != nil { if err != nil {
return err return err
} }
@ -140,15 +136,16 @@ func (r *clientImpl) PutImageManifest(name, tag string, manifest *storage.Signed
case response.StatusCode == http.StatusOK: case response.StatusCode == http.StatusOK:
return nil return nil
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errors errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errors)
if err != nil { if err != nil {
return err return err
} }
return errors
return &errors
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -170,17 +167,17 @@ func (r *clientImpl) DeleteImage(name, tag string) error {
case response.StatusCode == http.StatusNoContent: case response.StatusCode == http.StatusNoContent:
break break
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return &registry.ImageManifestNotFoundError{Name: name, Tag: tag} return &ImageManifestNotFoundError{Name: name, Tag: tag}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return err return err
} }
return errors return &errs
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
return nil return nil
@ -198,17 +195,17 @@ func (r *clientImpl) ListImageTags(name string) ([]string, error) {
case response.StatusCode == http.StatusOK: case response.StatusCode == http.StatusOK:
break break
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return nil, &registry.RepositoryNotFoundError{Name: name} return nil, &RepositoryNotFoundError{Name: name}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, errors return nil, &errs
default: default:
return nil, &registry.UnexpectedHTTPStatusError{Status: response.Status} return nil, &UnexpectedHTTPStatusError{Status: response.Status}
} }
tags := struct { tags := struct {
@ -235,7 +232,7 @@ func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) {
switch { switch {
case response.StatusCode == http.StatusOK: case response.StatusCode == http.StatusOK:
lengthHeader := response.Header.Get("Content-Length") lengthHeader := response.Header.Get("Content-Length")
length, err := strconv.ParseInt(lengthHeader, 10, 0) length, err := strconv.ParseInt(lengthHeader, 10, 64)
if err != nil { if err != nil {
return -1, err return -1, err
} }
@ -243,16 +240,16 @@ func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) {
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return -1, nil return -1, nil
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return -1, err return -1, err
} }
return -1, errors return -1, &errs
default: default:
response.Body.Close() response.Body.Close()
return -1, &registry.UnexpectedHTTPStatusError{Status: response.Status} return -1, &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -280,18 +277,18 @@ func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (i
return response.Body, int(length), nil return response.Body, int(length), nil
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
response.Body.Close() response.Body.Close()
return nil, 0, &registry.BlobNotFoundError{Name: name, Digest: dgst} return nil, 0, &BlobNotFoundError{Name: name, Digest: dgst}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
return nil, 0, errors return nil, 0, &errs
default: default:
response.Body.Close() response.Body.Close()
return nil, 0, &registry.UnexpectedHTTPStatusError{Status: response.Status} return nil, 0, &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -315,20 +312,20 @@ func (r *clientImpl) InitiateBlobUpload(name string) (string, error) {
// case response.StatusCode == http.StatusNotFound: // case response.StatusCode == http.StatusNotFound:
// return // return
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return "", err return "", err
} }
return "", errors return "", &errs
default: default:
return "", &registry.UnexpectedHTTPStatusError{Status: response.Status} return "", &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) { func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) {
response, err := http.Get(fmt.Sprintf("%s%s", r.Endpoint, location)) response, err := http.Get(location)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
@ -339,31 +336,30 @@ func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) {
case response.StatusCode == http.StatusNoContent: case response.StatusCode == http.StatusNoContent:
return parseRangeHeader(response.Header.Get("Range")) return parseRangeHeader(response.Header.Get("Range"))
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return 0, 0, &registry.BlobUploadNotFoundError{Location: location} return 0, 0, &BlobUploadNotFoundError{Location: location}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
return 0, 0, errors return 0, 0, &errs
default: default:
return 0, 0, &registry.UnexpectedHTTPStatusError{Status: response.Status} return 0, 0, &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error { func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error {
defer blob.Close() defer blob.Close()
putRequest, err := http.NewRequest("PUT", putRequest, err := http.NewRequest("PUT", location, blob)
fmt.Sprintf("%s%s", r.Endpoint, location), blob)
if err != nil { if err != nil {
return err return err
} }
queryValues := url.Values{} queryValues := url.Values{}
queryValues.Set("length", fmt.Sprint(length)) queryValues.Set("size", fmt.Sprint(length))
queryValues.Set("digest", dgst.String()) queryValues.Set("digest", dgst.String())
putRequest.URL.RawQuery = queryValues.Encode() putRequest.URL.RawQuery = queryValues.Encode()
@ -381,17 +377,17 @@ func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int,
case response.StatusCode == http.StatusCreated: case response.StatusCode == http.StatusCreated:
return nil return nil
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return &registry.BlobUploadNotFoundError{Location: location} return &BlobUploadNotFoundError{Location: location}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return err return err
} }
return errors return &errs
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -426,23 +422,23 @@ func (r *clientImpl) UploadBlobChunk(location string, blobChunk io.ReadCloser, l
if err != nil { if err != nil {
return err return err
} }
return &registry.BlobUploadInvalidRangeError{ return &BlobUploadInvalidRangeError{
Location: location, Location: location,
LastValidRange: lastValidRange, LastValidRange: lastValidRange,
BlobSize: blobSize, BlobSize: blobSize,
} }
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return &registry.BlobUploadNotFoundError{Location: location} return &BlobUploadNotFoundError{Location: location}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return err return err
} }
return errors return &errs
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -454,7 +450,7 @@ func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst d
} }
queryValues := new(url.Values) queryValues := new(url.Values)
queryValues.Set("length", fmt.Sprint(length)) queryValues.Set("size", fmt.Sprint(length))
queryValues.Set("digest", dgst.String()) queryValues.Set("digest", dgst.String())
putRequest.URL.RawQuery = queryValues.Encode() putRequest.URL.RawQuery = queryValues.Encode()
@ -474,17 +470,17 @@ func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst d
case response.StatusCode == http.StatusCreated: case response.StatusCode == http.StatusCreated:
return nil return nil
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return &registry.BlobUploadNotFoundError{Location: location} return &BlobUploadNotFoundError{Location: location}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return err return err
} }
return errors return &errs
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }
@ -506,17 +502,17 @@ func (r *clientImpl) CancelBlobUpload(location string) error {
case response.StatusCode == http.StatusNoContent: case response.StatusCode == http.StatusNoContent:
return nil return nil
case response.StatusCode == http.StatusNotFound: case response.StatusCode == http.StatusNotFound:
return &registry.BlobUploadNotFoundError{Location: location} return &BlobUploadNotFoundError{Location: location}
case response.StatusCode >= 400 && response.StatusCode < 500: case response.StatusCode >= 400 && response.StatusCode < 500:
errors := new(registry.Errors) var errs errors.Errors
decoder := json.NewDecoder(response.Body) decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&errors) err = decoder.Decode(&errs)
if err != nil { if err != nil {
return err return err
} }
return errors return &errs
default: default:
return &registry.UnexpectedHTTPStatusError{Status: response.Status} return &UnexpectedHTTPStatusError{Status: response.Status}
} }
} }

View file

@ -58,7 +58,8 @@ func TestPush(t *testing.T) {
}, },
}, },
} }
manifestBytes, err := json.Marshal(manifest) var err error
manifest.Raw, err = json.Marshal(manifest)
blobRequestResponseMappings := make([]testutil.RequestResponseMapping, 2*len(testBlobs)) blobRequestResponseMappings := make([]testutil.RequestResponseMapping, 2*len(testBlobs))
for i, blob := range testBlobs { for i, blob := range testBlobs {
@ -94,13 +95,25 @@ func TestPush(t *testing.T) {
Request: testutil.Request{ Request: testutil.Request{
Method: "PUT", Method: "PUT",
Route: "/v2/" + name + "/manifest/" + tag, Route: "/v2/" + name + "/manifest/" + tag,
Body: manifestBytes, Body: manifest.Raw,
}, },
Response: testutil.Response{ Response: testutil.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
}, },
})) }))
server := httptest.NewServer(handler) var server *httptest.Server
// HACK(stevvooe): Super hack to follow: the request response map approach
// above does not let us correctly format the location header to the
// server url. This handler intercepts and re-writes the location header
// to the server url.
hack := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w = &headerInterceptingResponseWriter{ResponseWriter: w, serverURL: server.URL}
handler.ServeHTTP(w, r)
})
server = httptest.NewServer(hack)
client := New(server.URL) client := New(server.URL)
objectStore := &memoryObjectStore{ objectStore := &memoryObjectStore{
mutex: new(sync.Mutex), mutex: new(sync.Mutex),
@ -370,3 +383,19 @@ func TestPullResume(t *testing.T) {
} }
} }
} }
// headerInterceptingResponseWriter is a hacky workaround to re-write the
// location header to have the server url.
type headerInterceptingResponseWriter struct {
http.ResponseWriter
serverURL string
}
func (hirw *headerInterceptingResponseWriter) WriteHeader(status int) {
location := hirw.Header().Get("Location")
if location != "" {
hirw.Header().Set("Location", hirw.serverURL+location)
}
hirw.ResponseWriter.WriteHeader(status)
}

79
client/errors.go Normal file
View file

@ -0,0 +1,79 @@
package client
import (
"fmt"
"github.com/docker/docker-registry/digest"
)
// RepositoryNotFoundError is returned when making an operation against a
// repository that does not exist in the registry.
type RepositoryNotFoundError struct {
Name string
}
func (e *RepositoryNotFoundError) Error() string {
return fmt.Sprintf("No repository found with Name: %s", e.Name)
}
// ImageManifestNotFoundError is returned when making an operation against a
// given image manifest that does not exist in the registry.
type ImageManifestNotFoundError struct {
Name string
Tag string
}
func (e *ImageManifestNotFoundError) Error() string {
return fmt.Sprintf("No manifest found with Name: %s, Tag: %s",
e.Name, e.Tag)
}
// BlobNotFoundError is returned when making an operation against a given image
// layer that does not exist in the registry.
type BlobNotFoundError struct {
Name string
Digest digest.Digest
}
func (e *BlobNotFoundError) Error() string {
return fmt.Sprintf("No blob found with Name: %s, Digest: %s",
e.Name, e.Digest)
}
// BlobUploadNotFoundError is returned when making a blob upload operation against an
// invalid blob upload location url.
// This may be the result of using a cancelled, completed, or stale upload
// location.
type BlobUploadNotFoundError struct {
Location string
}
func (e *BlobUploadNotFoundError) Error() string {
return fmt.Sprintf("No blob upload found at Location: %s", e.Location)
}
// BlobUploadInvalidRangeError is returned when attempting to upload an image
// blob chunk that is out of order.
// This provides the known BlobSize and LastValidRange which can be used to
// resume the upload.
type BlobUploadInvalidRangeError struct {
Location string
LastValidRange int
BlobSize int
}
func (e *BlobUploadInvalidRangeError) Error() string {
return fmt.Sprintf(
"Invalid range provided for upload at Location: %s. Last Valid Range: %d, Blob Size: %d",
e.Location, e.LastValidRange, e.BlobSize)
}
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
// returned when making a registry api call.
type UnexpectedHTTPStatusError struct {
Status string
}
func (e *UnexpectedHTTPStatusError) Error() string {
return fmt.Sprintf("Received unexpected HTTP status: %s", e.Status)
}

View file

@ -1,6 +1,9 @@
package registry package registry
import "github.com/Sirupsen/logrus" import (
"github.com/Sirupsen/logrus"
"github.com/docker/docker-registry/api/errors"
)
// Context should contain the request specific context for use in across // Context should contain the request specific context for use in across
// handlers. Resources that don't need to be shared across handlers should not // handlers. Resources that don't need to be shared across handlers should not
@ -16,7 +19,7 @@ type Context struct {
// Errors is a collection of errors encountered during the request to be // Errors is a collection of errors encountered during the request to be
// returned to the client API. If errors are added to the collection, the // returned to the client API. If errors are added to the collection, the
// handler *must not* start the response via http.ResponseWriter. // handler *must not* start the response via http.ResponseWriter.
Errors Errors Errors errors.Errors
// vars contains the extracted gorilla/mux variables that can be used for // vars contains the extracted gorilla/mux variables that can be used for
// assignment. // assignment.

311
errors.go
View file

@ -1,311 +0,0 @@
package registry
import (
"fmt"
"strings"
"github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
)
// ErrorCode represents the error type. The errors are serialized via strings
// and the integer format may change and should *never* be exported.
type ErrorCode int
const (
// ErrorCodeUnknown is a catch-all for errors not defined below.
ErrorCodeUnknown ErrorCode = iota
// The following errors can happen during a layer upload.
// ErrorCodeInvalidDigest is returned when uploading a layer if the
// provided digest does not match the layer contents.
ErrorCodeInvalidDigest
// ErrorCodeInvalidLength is returned when uploading a layer if the provided
// length does not match the content length.
ErrorCodeInvalidLength
// ErrorCodeInvalidName is returned when the name in the manifest does not
// match the provided name.
ErrorCodeInvalidName
// ErrorCodeInvalidTag is returned when the tag in the manifest does not
// match the provided tag.
ErrorCodeInvalidTag
// ErrorCodeUnknownRepository when the repository name is not known.
ErrorCodeUnknownRepository
// ErrorCodeUnknownManifest returned when image manifest name and tag is
// unknown, accompanied by a 404 status.
ErrorCodeUnknownManifest
// ErrorCodeInvalidManifest returned when an image manifest is invalid,
// typically during a PUT operation.
ErrorCodeInvalidManifest
// ErrorCodeUnverifiedManifest is returned when the manifest fails signature
// validation.
ErrorCodeUnverifiedManifest
// ErrorCodeUnknownLayer is returned when the manifest references a
// nonexistent layer.
ErrorCodeUnknownLayer
// ErrorCodeUnknownLayerUpload is returned when an upload is accessed.
ErrorCodeUnknownLayerUpload
// ErrorCodeUntrustedSignature is returned when the manifest is signed by an
// untrusted source.
ErrorCodeUntrustedSignature
)
var errorCodeStrings = map[ErrorCode]string{
ErrorCodeUnknown: "UNKNOWN",
ErrorCodeInvalidDigest: "INVALID_DIGEST",
ErrorCodeInvalidLength: "INVALID_LENGTH",
ErrorCodeInvalidName: "INVALID_NAME",
ErrorCodeInvalidTag: "INVALID_TAG",
ErrorCodeUnknownRepository: "UNKNOWN_REPOSITORY",
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD",
ErrorCodeUntrustedSignature: "UNTRUSTED_SIGNATURE",
}
var errorCodesMessages = map[ErrorCode]string{
ErrorCodeUnknown: "unknown error",
ErrorCodeInvalidDigest: "provided digest did not match uploaded content",
ErrorCodeInvalidLength: "provided length did not match content length",
ErrorCodeInvalidName: "manifest name did not match URI",
ErrorCodeInvalidTag: "manifest tag did not match URI",
ErrorCodeUnknownRepository: "repository not known to registry",
ErrorCodeUnknownManifest: "manifest not known",
ErrorCodeInvalidManifest: "manifest is invalid",
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
ErrorCodeUnknownLayer: "referenced layer not available",
ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload",
ErrorCodeUntrustedSignature: "manifest signed by untrusted source",
}
var stringToErrorCode map[string]ErrorCode
func init() {
stringToErrorCode = make(map[string]ErrorCode, len(errorCodeStrings))
// Build up reverse error code map
for k, v := range errorCodeStrings {
stringToErrorCode[v] = k
}
}
// ParseErrorCode attempts to parse the error code string, returning
// ErrorCodeUnknown if the error is not known.
func ParseErrorCode(s string) ErrorCode {
ec, ok := stringToErrorCode[s]
if !ok {
return ErrorCodeUnknown
}
return ec
}
// String returns the canonical identifier for this error code.
func (ec ErrorCode) String() string {
s, ok := errorCodeStrings[ec]
if !ok {
return errorCodeStrings[ErrorCodeUnknown]
}
return s
}
// Message returned the human-readable error message for this error code.
func (ec ErrorCode) Message() string {
m, ok := errorCodesMessages[ec]
if !ok {
return errorCodesMessages[ErrorCodeUnknown]
}
return m
}
// MarshalText encodes the receiver into UTF-8-encoded text and returns the
// result.
func (ec ErrorCode) MarshalText() (text []byte, err error) {
return []byte(ec.String()), nil
}
// UnmarshalText decodes the form generated by MarshalText.
func (ec *ErrorCode) UnmarshalText(text []byte) error {
*ec = stringToErrorCode[string(text)]
return nil
}
// Error provides a wrapper around ErrorCode with extra Details provided.
type Error struct {
Code ErrorCode `json:"code"`
Message string `json:"message,omitempty"`
Detail interface{} `json:"detail,omitempty"`
}
// Error returns a human readable representation of the error.
func (e Error) Error() string {
return fmt.Sprintf("%s: %s",
strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
e.Message)
}
// Errors provides the envelope for multiple errors and a few sugar methods
// for use within the application.
type Errors struct {
Errors []error `json:"errors,omitempty"`
}
// Push pushes an error on to the error stack, with the optional detail
// argument. It is a programming error (ie panic) to push more than one
// detail at a time.
func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
if len(details) > 1 {
panic("please specify zero or one detail items for this error")
}
var detail interface{}
if len(details) > 0 {
detail = details[0]
}
if err, ok := detail.(error); ok {
detail = err.Error()
}
errs.PushErr(Error{
Code: code,
Message: code.Message(),
Detail: detail,
})
}
// PushErr pushes an error interface onto the error stack.
func (errs *Errors) PushErr(err error) {
switch err.(type) {
case Error:
errs.Errors = append(errs.Errors, err)
default:
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
}
}
func (errs *Errors) Error() string {
switch errs.Len() {
case 0:
return "<nil>"
case 1:
return errs.Errors[0].Error()
default:
msg := "errors:\n"
for _, err := range errs.Errors {
msg += err.Error() + "\n"
}
return msg
}
}
// Clear clears the errors.
func (errs *Errors) Clear() {
errs.Errors = errs.Errors[:0]
}
// Len returns the current number of errors.
func (errs *Errors) Len() int {
return len(errs.Errors)
}
// DetailUnknownLayer provides detail for unknown layer errors, returned by
// image manifest push for layers that are not yet transferred. This intended
// to only be used on the backend to return detail for this specific error.
type DetailUnknownLayer struct {
// Unknown should contain the contents of a layer descriptor, which is a
// single FSLayer currently.
Unknown storage.FSLayer `json:"unknown"`
}
// RepositoryNotFoundError is returned when making an operation against a
// repository that does not exist in the registry.
type RepositoryNotFoundError struct {
Name string
}
func (e *RepositoryNotFoundError) Error() string {
return fmt.Sprintf("No repository found with Name: %s", e.Name)
}
// ImageManifestNotFoundError is returned when making an operation against a
// given image manifest that does not exist in the registry.
type ImageManifestNotFoundError struct {
Name string
Tag string
}
func (e *ImageManifestNotFoundError) Error() string {
return fmt.Sprintf("No manifest found with Name: %s, Tag: %s",
e.Name, e.Tag)
}
// BlobNotFoundError is returned when making an operation against a given image
// layer that does not exist in the registry.
type BlobNotFoundError struct {
Name string
Digest digest.Digest
}
func (e *BlobNotFoundError) Error() string {
return fmt.Sprintf("No blob found with Name: %s, Digest: %s",
e.Name, e.Digest)
}
// BlobUploadNotFoundError is returned when making a blob upload operation against an
// invalid blob upload location url.
// This may be the result of using a cancelled, completed, or stale upload
// location.
type BlobUploadNotFoundError struct {
Location string
}
func (e *BlobUploadNotFoundError) Error() string {
return fmt.Sprintf("No blob upload found at Location: %s", e.Location)
}
// BlobUploadInvalidRangeError is returned when attempting to upload an image
// blob chunk that is out of order.
// This provides the known BlobSize and LastValidRange which can be used to
// resume the upload.
type BlobUploadInvalidRangeError struct {
Location string
LastValidRange int
BlobSize int
}
func (e *BlobUploadInvalidRangeError) Error() string {
return fmt.Sprintf(
"Invalid range provided for upload at Location: %s. Last Valid Range: %d, Blob Size: %d",
e.Location, e.LastValidRange, e.BlobSize)
}
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
// returned when making a registry api call.
type UnexpectedHTTPStatusError struct {
Status string
}
func (e *UnexpectedHTTPStatusError) Error() string {
return fmt.Sprintf("Received unexpected HTTP status: %s", e.Status)
}

View file

@ -1,90 +0,0 @@
package registry
import (
"encoding/json"
"testing"
)
// TestErrorCodes ensures that error code format, mappings and
// marshaling/unmarshaling. round trips are stable.
func TestErrorCodes(t *testing.T) {
for ec := range errorCodeStrings {
if ec.String() != errorCodeStrings[ec] {
t.Fatalf("error code string incorrect: %q != %q", ec.String(), errorCodeStrings[ec])
}
if ec.Message() != errorCodesMessages[ec] {
t.Fatalf("incorrect message for error code %v: %q != %q", ec, ec.Message(), errorCodesMessages[ec])
}
// Serialize the error code using the json library to ensure that we
// get a string and it works round trip.
p, err := json.Marshal(ec)
if err != nil {
t.Fatalf("error marshaling error code %v: %v", ec, err)
}
if len(p) <= 0 {
t.Fatalf("expected content in marshaled before for error code %v", ec)
}
// First, unmarshal to interface and ensure we have a string.
var ecUnspecified interface{}
if err := json.Unmarshal(p, &ecUnspecified); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", ec, err)
}
if _, ok := ecUnspecified.(string); !ok {
t.Fatalf("expected a string for error code %v on unmarshal got a %T", ec, ecUnspecified)
}
// Now, unmarshal with the error code type and ensure they are equal
var ecUnmarshaled ErrorCode
if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", ec, err)
}
if ecUnmarshaled != ec {
t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, ec)
}
}
}
// TestErrorsManagement does a quick check of the Errors type to ensure that
// members are properly pushed and marshaled.
func TestErrorsManagement(t *testing.T) {
var errs Errors
errs.Push(ErrorCodeInvalidDigest)
var detail DetailUnknownLayer
detail.Unknown.BlobSum = "sometestblobsumdoesntmatter"
errs.Push(ErrorCodeUnknownLayer, detail)
p, err := json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}"
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
errs.Clear()
errs.Push(ErrorCodeUnknown)
expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
p, err = json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
}

View file

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
@ -41,7 +41,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
manifest, err := manifests.Get(imh.Name, imh.Tag) manifest, err := manifests.Get(imh.Name, imh.Tag)
if err != nil { if err != nil {
imh.Errors.Push(ErrorCodeUnknownManifest, err) imh.Errors.Push(errors.ErrorCodeManifestUnknown, err)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
@ -58,7 +58,7 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
var manifest storage.SignedManifest var manifest storage.SignedManifest
if err := dec.Decode(&manifest); err != nil { if err := dec.Decode(&manifest); err != nil {
imh.Errors.Push(ErrorCodeInvalidManifest, err) imh.Errors.Push(errors.ErrorCodeManifestInvalid, err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
@ -71,14 +71,14 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
for _, verificationError := range err { for _, verificationError := range err {
switch verificationError := verificationError.(type) { switch verificationError := verificationError.(type) {
case storage.ErrUnknownLayer: case storage.ErrUnknownLayer:
imh.Errors.Push(ErrorCodeUnknownLayer, verificationError.FSLayer) imh.Errors.Push(errors.ErrorCodeBlobUnknown, verificationError.FSLayer)
case storage.ErrManifestUnverified: case storage.ErrManifestUnverified:
imh.Errors.Push(ErrorCodeUnverifiedManifest) imh.Errors.Push(errors.ErrorCodeManifestUnverified)
default: default:
if verificationError == digest.ErrDigestInvalidFormat { if verificationError == digest.ErrDigestInvalidFormat {
// TODO(stevvooe): We need to really need to move all // TODO(stevvooe): We need to really need to move all
// errors to types. Its much more straightforward. // errors to types. Its much more straightforward.
imh.Errors.Push(ErrorCodeInvalidDigest) imh.Errors.Push(errors.ErrorCodeDigestInvalid)
} else { } else {
imh.Errors.PushErr(verificationError) imh.Errors.PushErr(verificationError)
} }
@ -99,10 +99,10 @@ func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *h
if err := manifests.Delete(imh.Name, imh.Tag); err != nil { if err := manifests.Delete(imh.Name, imh.Tag); err != nil {
switch err := err.(type) { switch err := err.(type) {
case storage.ErrUnknownManifest: case storage.ErrUnknownManifest:
imh.Errors.Push(ErrorCodeUnknownManifest, err) imh.Errors.Push(errors.ErrorCodeManifestUnknown, err)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
default: default:
imh.Errors.Push(ErrorCodeUnknown, err) imh.Errors.Push(errors.ErrorCodeUnknown, err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
} }
return return

View file

@ -3,6 +3,7 @@ package registry
import ( import (
"net/http" "net/http"
"github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -14,7 +15,7 @@ func layerDispatcher(ctx *Context, r *http.Request) http.Handler {
if err != nil { if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx.Errors.Push(ErrorCodeInvalidDigest, err) ctx.Errors.Push(errors.ErrorCodeDigestInvalid, err)
}) })
} }
@ -49,9 +50,9 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
switch err := err.(type) { switch err := err.(type) {
case storage.ErrUnknownLayer: case storage.ErrUnknownLayer:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
lh.Errors.Push(ErrorCodeUnknownLayer, err.FSLayer) lh.Errors.Push(errors.ErrorCodeBlobUnknown, err.FSLayer)
default: default:
lh.Errors.Push(ErrorCodeUnknown, err) lh.Errors.Push(errors.ErrorCodeUnknown, err)
} }
return return
} }

View file

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -38,7 +39,7 @@ func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logrus.Infof("error resolving upload: %v", err) logrus.Infof("error resolving upload: %v", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
}) })
} }
@ -66,7 +67,7 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R
upload, err := layers.Upload(luh.Name) upload, err := layers.Upload(luh.Name)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here? w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
return return
} }
@ -75,7 +76,7 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R
if err := luh.layerUploadResponse(w, r); err != nil { if err := luh.layerUploadResponse(w, r); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here? w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
return return
} }
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
@ -85,12 +86,12 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R
func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil { if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(ErrorCodeUnknownLayerUpload) luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown)
} }
if err := luh.layerUploadResponse(w, r); err != nil { if err := luh.layerUploadResponse(w, r); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here? w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
return return
} }
@ -102,7 +103,7 @@ func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Re
func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) { func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil { if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(ErrorCodeUnknownLayerUpload) luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown)
} }
var finished bool var finished bool
@ -119,14 +120,14 @@ func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Requ
if err := luh.maybeCompleteUpload(w, r); err != nil { if err := luh.maybeCompleteUpload(w, r); err != nil {
if err != errNotReadyToComplete { if err != errNotReadyToComplete {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
return return
} }
} }
if err := luh.layerUploadResponse(w, r); err != nil { if err := luh.layerUploadResponse(w, r); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here? w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
return return
} }
@ -141,7 +142,7 @@ func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Requ
func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) { func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil { if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(ErrorCodeUnknownLayerUpload) luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown)
} }
} }
@ -194,14 +195,14 @@ func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *htt
func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Request, size int64, dgst digest.Digest) { func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Request, size int64, dgst digest.Digest) {
layer, err := luh.Upload.Finish(size, dgst) layer, err := luh.Upload.Finish(size, dgst)
if err != nil { if err != nil {
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
layerURL, err := luh.urlBuilder.forLayer(layer) layerURL, err := luh.urlBuilder.forLayer(layer)
if err != nil { if err != nil {
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(errors.ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/docker/docker-registry/api/errors"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
@ -39,7 +40,7 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
switch err := err.(type) { switch err := err.(type) {
case storage.ErrUnknownRepository: case storage.ErrUnknownRepository:
w.WriteHeader(404) w.WriteHeader(404)
th.Errors.Push(ErrorCodeUnknownRepository, map[string]string{"name": th.Name}) th.Errors.Push(errors.ErrorCodeNameUnknown, map[string]string{"name": th.Name})
default: default:
th.Errors.PushErr(err) th.Errors.PushErr(err)
} }