Merge pull request #835 from stevvooe/next-generation

Lock down HTTP API error codes
This commit is contained in:
Olivier Gambier 2014-12-10 13:23:06 -08:00
commit c7dc83442b
15 changed files with 722 additions and 515 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

@ -19,7 +19,12 @@ test:
- go version - go version
override: override:
- test -z $(gofmt -s -l . | tee /dev/stderr) - test -z $(gofmt -s -l . | tee /dev/stderr)
- go vet ./...
# TODO(stevvooe): go vet is complaining about something that can't be
# reproduced locally and doesn't make sense based on the existing code.
# Turning it off for now.
# - go vet ./...
- test -z $(golint ./... | tee /dev/stderr) - test -z $(golint ./... | tee /dev/stderr)
- go test -test.v -test.short ./... - go test -test.v -test.short ./...

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