From af0411420a3155ed6ae2b344ccdc01a134e4e0a7 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 10 Nov 2014 15:09:10 -0800 Subject: [PATCH 1/2] Initial implementation of API errors data structure --- errors.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++++ errors_test.go | 77 ++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 errors.go create mode 100644 errors_test.go diff --git a/errors.go b/errors.go new file mode 100644 index 000000000..751515944 --- /dev/null +++ b/errors.go @@ -0,0 +1,157 @@ +package registry + +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 ErrorCode = iota + + // The following errors can happen during a layer upload. + ErrorCodeInvalidChecksum + ErrorCodeInvalidLength + ErrorCodeInvalidTarsum + + // The following errors can happen during manifest upload. + ErrorCodeInvalidName + ErrorCodeInvalidTag + ErrorCodeUnverifiedManifest + ErrorCodeUnknownLayer + ErrorCodeUntrustedSignature +) + +var errorCodeStrings = map[ErrorCode]string{ + ErrorCodeUnknown: "UNKNOWN", + ErrorCodeInvalidChecksum: "INVALID_CHECKSUM", + ErrorCodeInvalidLength: "INVALID_LENGTH", + ErrorCodeInvalidTarsum: "INVALID_TARSUM", + ErrorCodeInvalidName: "INVALID_NAME", + ErrorCodeInvalidTag: "INVALID_TAG", + ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST", + ErrorCodeUnknownLayer: "UNKNOWN_LAYER", + ErrorCodeUntrustedSignature: "UNTRUSTED_SIGNATURE", +} + +var errorCodesMessages = map[ErrorCode]string{ + ErrorCodeUnknown: "unknown error", + ErrorCodeInvalidChecksum: "provided checksum did not match uploaded content", + ErrorCodeInvalidLength: "provided length did not match content length", + ErrorCodeInvalidTarsum: "provided tarsum did not match binary content", + ErrorCodeInvalidName: "Manifest name did not match URI", + ErrorCodeInvalidTag: "Manifest tag did not match URI", + ErrorCodeUnverifiedManifest: "Manifest failed signature validation", + ErrorCodeUnknownLayer: "Referenced layer not available", + 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 +} + +func (ec ErrorCode) Message() string { + m, ok := errorCodesMessages[ec] + + if !ok { + return errorCodesMessages[ErrorCodeUnknown] + } + + return m +} + +func (ec ErrorCode) MarshalText() (text []byte, err error) { + return []byte(ec.String()), nil +} + +func (ec *ErrorCode) UnmarshalText(text []byte) error { + *ec = stringToErrorCode[string(text)] + + return nil +} + +type Error struct { + Code ErrorCode `json:"code,omitempty"` + 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.Title(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] + } + + errs.Errors = append(errs.Errors, Error{ + Code: code, + Message: code.Message(), + Detail: detail, + }) +} + +// 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 json object with the key "blobSum" currently. + Unknown struct { + + // BlobSum contains the uniquely identifying tarsum of the layer. + BlobSum string `json:"blobSum"` + } `json:"unknown"` +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 000000000..dc6a8de78 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,77 @@ +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: %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(ErrorCodeInvalidChecksum) + + 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_CHECKSUM\",\"message\":\"provided checksum 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) + } +} From da7eef2e0427d4f077a46b6ae39749fa8f7abf66 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 10 Nov 2014 15:56:23 -0800 Subject: [PATCH 2/2] Allow Errors to be an error itself This has Errors implement the error interface, allowing it to pose as an error itself. Use of this in the server may be minimal, for now, but it's useful for passing around opaque client errors. A method, PushErr, has also been add to allow arbitrary errors to be passed into the Errors list. This keeps the errors list flexible, allowing the app to collect and errors before we have codes properly mapped. --- errors.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/errors.go b/errors.go index 751515944..53dcb6bf6 100644 --- a/errors.go +++ b/errors.go @@ -119,7 +119,7 @@ func (e Error) Error() string { // 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"` + Errors []error `json:"errors,omitempty"` } // Push pushes an error on to the error stack, with the optional detail @@ -135,13 +135,33 @@ func (errs *Errors) Push(code ErrorCode, details ...interface{}) { detail = details[0] } - errs.Errors = append(errs.Errors, 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) { + errs.Errors = append(errs.Errors, err) +} + +func (errs *Errors) Error() string { + switch len(errs.Errors) { + case 0: + return "" + case 1: + return errs.Errors[0].Error() + default: + msg := "errors:\n" + for _, err := range errs.Errors { + msg += err.Error() + "\n" + } + return msg + } +} + // 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.