Move registry api definitions under registry package
Signed-off-by: Stephen J Day <stephen.day@docker.com>master
							parent
							
								
									54ae545ed3
								
							
						
					
					
						commit
						3822e685a0
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					// Package v2 describes routes, urls and the error codes used in the Docker
 | 
				
			||||||
 | 
					// Registry JSON HTTP API V2. In addition to declarations, descriptors are
 | 
				
			||||||
 | 
					// provided for routes and error codes that can be used for implementation and
 | 
				
			||||||
 | 
					// automatically generating documentation.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 v2
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,191 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrorCodeUnauthorized is returned if a request is not authorized.
 | 
				
			||||||
 | 
						ErrorCodeUnauthorized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrorCodeBlobUploadInvalid is returned when an upload is invalid.
 | 
				
			||||||
 | 
						ErrorCodeBlobUploadInvalid
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,165 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestErrorCodes ensures that error code format, mappings and
 | 
				
			||||||
 | 
					// marshaling/unmarshaling. round trips are stable.
 | 
				
			||||||
 | 
					func TestErrorCodes(t *testing.T) {
 | 
				
			||||||
 | 
						for _, desc := range errorDescriptors {
 | 
				
			||||||
 | 
							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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,115 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// RepositoryNameComponentMinLength is the minimum number of characters in a
 | 
				
			||||||
 | 
						// single repository name slash-delimited component
 | 
				
			||||||
 | 
						RepositoryNameComponentMinLength = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// RepositoryNameComponentMaxLength is the maximum number of characters in a
 | 
				
			||||||
 | 
						// single repository name slash-delimited component
 | 
				
			||||||
 | 
						RepositoryNameComponentMaxLength = 30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// RepositoryNameMinComponents is the minimum number of slash-delimited
 | 
				
			||||||
 | 
						// components that a repository name must have
 | 
				
			||||||
 | 
						RepositoryNameMinComponents = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// RepositoryNameMaxComponents is the maximum number of slash-delimited
 | 
				
			||||||
 | 
						// components that a repository name must have
 | 
				
			||||||
 | 
						RepositoryNameMaxComponents = 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// RepositoryNameTotalLengthMax is the maximum total number of characters in
 | 
				
			||||||
 | 
						// a repository name
 | 
				
			||||||
 | 
						RepositoryNameTotalLengthMax = 255
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RepositoryNameComponentRegexp restricts registtry path components names to
 | 
				
			||||||
 | 
					// start with at least two letters or numbers, with following parts able to
 | 
				
			||||||
 | 
					// separated by one period, dash or underscore.
 | 
				
			||||||
 | 
					var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RepositoryNameComponentAnchoredRegexp is the version of
 | 
				
			||||||
 | 
					// RepositoryNameComponentRegexp which must completely match the content
 | 
				
			||||||
 | 
					var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(stevvooe): RepositoryName needs to be limited to some fixed length.
 | 
				
			||||||
 | 
					// Looking path prefixes and s3 limitation of 1024, this should likely be
 | 
				
			||||||
 | 
					// around 512 bytes. 256 bytes might be more manageable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow 1 to
 | 
				
			||||||
 | 
					// 5 path components, separated by a forward slash.
 | 
				
			||||||
 | 
					var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/){0,4}` + RepositoryNameComponentRegexp.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
 | 
				
			||||||
 | 
					var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(stevvooe): Contribute these exports back to core, so they are shared.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// ErrRepositoryNameComponentShort is returned when a repository name
 | 
				
			||||||
 | 
						// contains a component which is shorter than
 | 
				
			||||||
 | 
						// RepositoryNameComponentMinLength
 | 
				
			||||||
 | 
						ErrRepositoryNameComponentShort = fmt.Errorf("respository name component must be %v or more characters", RepositoryNameComponentMinLength)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrRepositoryNameComponentLong is returned when a repository name
 | 
				
			||||||
 | 
						// contains a component which is longer than
 | 
				
			||||||
 | 
						// RepositoryNameComponentMaxLength
 | 
				
			||||||
 | 
						ErrRepositoryNameComponentLong = fmt.Errorf("respository name component must be %v characters or less", RepositoryNameComponentMaxLength)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrRepositoryNameMissingComponents is returned when a repository name
 | 
				
			||||||
 | 
						// contains fewer than RepositoryNameMinComponents components
 | 
				
			||||||
 | 
						ErrRepositoryNameMissingComponents = fmt.Errorf("repository name must have at least %v components", RepositoryNameMinComponents)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrRepositoryNameTooManyComponents is returned when a repository name
 | 
				
			||||||
 | 
						// contains more than RepositoryNameMaxComponents components
 | 
				
			||||||
 | 
						ErrRepositoryNameTooManyComponents = fmt.Errorf("repository name %v or less components", RepositoryNameMaxComponents)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrRepositoryNameLong is returned when a repository name is longer than
 | 
				
			||||||
 | 
						// RepositoryNameTotalLengthMax
 | 
				
			||||||
 | 
						ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ErrRepositoryNameComponentInvalid is returned when a repository name does
 | 
				
			||||||
 | 
						// not match RepositoryNameComponentRegexp
 | 
				
			||||||
 | 
						ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ValidateRespositoryName ensures the repository name is valid for use in the
 | 
				
			||||||
 | 
					// registry. This function accepts a superset of what might be accepted by
 | 
				
			||||||
 | 
					// docker core or docker hub. If the name does not pass validation, an error,
 | 
				
			||||||
 | 
					// describing the conditions, is returned.
 | 
				
			||||||
 | 
					func ValidateRespositoryName(name string) error {
 | 
				
			||||||
 | 
						if len(name) > RepositoryNameTotalLengthMax {
 | 
				
			||||||
 | 
							return ErrRepositoryNameLong
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components := strings.Split(name, "/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(components) < RepositoryNameMinComponents {
 | 
				
			||||||
 | 
							return ErrRepositoryNameMissingComponents
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(components) > RepositoryNameMaxComponents {
 | 
				
			||||||
 | 
							return ErrRepositoryNameTooManyComponents
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, component := range components {
 | 
				
			||||||
 | 
							if len(component) < RepositoryNameComponentMinLength {
 | 
				
			||||||
 | 
								return ErrRepositoryNameComponentShort
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(component) > RepositoryNameComponentMaxLength {
 | 
				
			||||||
 | 
								return ErrRepositoryNameComponentLong
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
 | 
				
			||||||
 | 
								return ErrRepositoryNameComponentInvalid
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepositoryNameRegexp(t *testing.T) {
 | 
				
			||||||
 | 
						for _, testcase := range []struct {
 | 
				
			||||||
 | 
							input string
 | 
				
			||||||
 | 
							err   error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "short",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "simple/name",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "library/ubuntu",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "docker/stevvooe/app",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameTooManyComponents,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "aa/aa/bb/bb/bb",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "a/a/a/b/b",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameComponentShort,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "a/a/a/a/",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameComponentShort,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "foo.com/bar/baz",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "blog.foo.com/bar/baz",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "asdf",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "asdf$$^/aa",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameComponentInvalid,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "aa-a/aa",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "aa/aa",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "a-a/a-a",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "a",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameComponentShort,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								input: "a-/a/a/a",
 | 
				
			||||||
 | 
								err:   ErrRepositoryNameComponentInvalid,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						} {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							failf := func(format string, v ...interface{}) {
 | 
				
			||||||
 | 
								t.Logf(testcase.input+": "+format, v...)
 | 
				
			||||||
 | 
								t.Fail()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := ValidateRespositoryName(testcase.input); err != testcase.err {
 | 
				
			||||||
 | 
								if testcase.err != nil {
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										failf("expected invalid repository: %v", testcase.err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										// Wrong error returned.
 | 
				
			||||||
 | 
										failf("unexpected error validating repository name: %v, expected %v", err, testcase.err)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										failf("unexpected error validating repository name: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "github.com/gorilla/mux"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The following are definitions of the name under which all V2 routes are
 | 
				
			||||||
 | 
					// registered. These symbols can be used to look up a route based on the name.
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						RouteNameBase            = "base"
 | 
				
			||||||
 | 
						RouteNameManifest        = "manifest"
 | 
				
			||||||
 | 
						RouteNameTags            = "tags"
 | 
				
			||||||
 | 
						RouteNameBlob            = "blob"
 | 
				
			||||||
 | 
						RouteNameBlobUpload      = "blob-upload"
 | 
				
			||||||
 | 
						RouteNameBlobUploadChunk = "blob-upload-chunk"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var allEndpoints = []string{
 | 
				
			||||||
 | 
						RouteNameManifest,
 | 
				
			||||||
 | 
						RouteNameTags,
 | 
				
			||||||
 | 
						RouteNameBlob,
 | 
				
			||||||
 | 
						RouteNameBlobUpload,
 | 
				
			||||||
 | 
						RouteNameBlobUploadChunk,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Router builds a gorilla router with named routes for the various API
 | 
				
			||||||
 | 
					// methods. This can be used directly by both server implementations and
 | 
				
			||||||
 | 
					// clients.
 | 
				
			||||||
 | 
					func Router() *mux.Router {
 | 
				
			||||||
 | 
						router := mux.NewRouter().
 | 
				
			||||||
 | 
							StrictSlash(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, descriptor := range routeDescriptors {
 | 
				
			||||||
 | 
							router.Path(descriptor.Path).Name(descriptor.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return router
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,198 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type routeTestCase struct {
 | 
				
			||||||
 | 
						RequestURI string
 | 
				
			||||||
 | 
						Vars       map[string]string
 | 
				
			||||||
 | 
						RouteName  string
 | 
				
			||||||
 | 
						StatusCode int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestRouter registers a test handler with all the routes and ensures that
 | 
				
			||||||
 | 
					// each route returns the expected path variables. Not method verification is
 | 
				
			||||||
 | 
					// present. This not meant to be exhaustive but as check to ensure that the
 | 
				
			||||||
 | 
					// expected variables are extracted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This may go away as the application structure comes together.
 | 
				
			||||||
 | 
					func TestRouter(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router := Router()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							testCase := routeTestCase{
 | 
				
			||||||
 | 
								RequestURI: r.RequestURI,
 | 
				
			||||||
 | 
								Vars:       mux.Vars(r),
 | 
				
			||||||
 | 
								RouteName:  mux.CurrentRoute(r).GetName(),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							enc := json.NewEncoder(w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := enc.Encode(testCase); err != nil {
 | 
				
			||||||
 | 
								http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Startup test server
 | 
				
			||||||
 | 
						server := httptest.NewServer(router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, testcase := range []routeTestCase{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBase,
 | 
				
			||||||
 | 
								RequestURI: "/v2/",
 | 
				
			||||||
 | 
								Vars:       map[string]string{},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameManifest,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/manifests/bar",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo",
 | 
				
			||||||
 | 
									"tag":  "bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameManifest,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/manifests/tag",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
									"tag":  "tag",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameTags,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/tags/list",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlob,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name":   "foo/bar",
 | 
				
			||||||
 | 
									"digest": "tarsum.dev+foo:abcdef0919234",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlob,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name":   "foo/bar",
 | 
				
			||||||
 | 
									"digest": "sha256:abcdef0919234",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlobUpload,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/uploads/",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlobUploadChunk,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
									"uuid": "uuid",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlobUploadChunk,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
									"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlobUploadChunk,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar",
 | 
				
			||||||
 | 
									"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// Check ambiguity: ensure we can distinguish between tags for
 | 
				
			||||||
 | 
								// "foo/bar/image/image" and image for "foo/bar/image" with tag
 | 
				
			||||||
 | 
								// "tags"
 | 
				
			||||||
 | 
								RouteName:  RouteNameManifest,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/manifests/manifests/tags",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar/manifests",
 | 
				
			||||||
 | 
									"tag":  "tags",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// This case presents an ambiguity between foo/bar with tag="tags"
 | 
				
			||||||
 | 
								// and list tags for "foo/bar/manifest"
 | 
				
			||||||
 | 
								RouteName:  RouteNameTags,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/bar/manifests/tags/list",
 | 
				
			||||||
 | 
								Vars: map[string]string{
 | 
				
			||||||
 | 
									"name": "foo/bar/manifests",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								RouteName:  RouteNameBlobUploadChunk,
 | 
				
			||||||
 | 
								RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
 | 
				
			||||||
 | 
								StatusCode: http.StatusNotFound,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						} {
 | 
				
			||||||
 | 
							// Register the endpoint
 | 
				
			||||||
 | 
							route := router.GetRoute(testcase.RouteName)
 | 
				
			||||||
 | 
							if route == nil {
 | 
				
			||||||
 | 
								t.Fatalf("route for name %q not found", testcase.RouteName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							route.Handler(testHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							u := server.URL + testcase.RequestURI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							resp, err := http.Get(u)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("error issuing get request: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if testcase.StatusCode == 0 {
 | 
				
			||||||
 | 
								// Override default, zero-value
 | 
				
			||||||
 | 
								testcase.StatusCode = http.StatusOK
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if resp.StatusCode != testcase.StatusCode {
 | 
				
			||||||
 | 
								t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if testcase.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
								// We don't care about json response.
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							dec := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var actualRouteInfo routeTestCase
 | 
				
			||||||
 | 
							if err := dec.Decode(&actualRouteInfo); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("error reading json response: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Needs to be set out of band
 | 
				
			||||||
 | 
							actualRouteInfo.StatusCode = resp.StatusCode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if actualRouteInfo.RouteName != testcase.RouteName {
 | 
				
			||||||
 | 
								t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !reflect.DeepEqual(actualRouteInfo, testcase) {
 | 
				
			||||||
 | 
								t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,201 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
 | 
						"github.com/gorilla/mux"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// URLBuilder creates registry API urls from a single base endpoint. It can be
 | 
				
			||||||
 | 
					// used to create urls for use in a registry client or server.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// All urls will be created from the given base, including the api version.
 | 
				
			||||||
 | 
					// For example, if a root of "/foo/" is provided, urls generated will be fall
 | 
				
			||||||
 | 
					// under "/foo/v2/...". Most application will only provide a schema, host and
 | 
				
			||||||
 | 
					// port, such as "https://localhost:5000/".
 | 
				
			||||||
 | 
					type URLBuilder struct {
 | 
				
			||||||
 | 
						root   *url.URL // url root (ie http://localhost/)
 | 
				
			||||||
 | 
						router *mux.Router
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewURLBuilder creates a URLBuilder with provided root url object.
 | 
				
			||||||
 | 
					func NewURLBuilder(root *url.URL) *URLBuilder {
 | 
				
			||||||
 | 
						return &URLBuilder{
 | 
				
			||||||
 | 
							root:   root,
 | 
				
			||||||
 | 
							router: Router(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
 | 
				
			||||||
 | 
					// a string argument for the root, returning an error if it is not a valid
 | 
				
			||||||
 | 
					// url.
 | 
				
			||||||
 | 
					func NewURLBuilderFromString(root string) (*URLBuilder, error) {
 | 
				
			||||||
 | 
						u, err := url.Parse(root)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return NewURLBuilder(u), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewURLBuilderFromRequest uses information from an *http.Request to
 | 
				
			||||||
 | 
					// construct the root url.
 | 
				
			||||||
 | 
					func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
 | 
				
			||||||
 | 
						var scheme string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						forwardedProto := r.Header.Get("X-Forwarded-Proto")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case len(forwardedProto) > 0:
 | 
				
			||||||
 | 
							scheme = forwardedProto
 | 
				
			||||||
 | 
						case r.TLS != nil:
 | 
				
			||||||
 | 
							scheme = "https"
 | 
				
			||||||
 | 
						case len(r.URL.Scheme) > 0:
 | 
				
			||||||
 | 
							scheme = r.URL.Scheme
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							scheme = "http"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						host := r.Host
 | 
				
			||||||
 | 
						forwardedHost := r.Header.Get("X-Forwarded-Host")
 | 
				
			||||||
 | 
						if len(forwardedHost) > 0 {
 | 
				
			||||||
 | 
							host = forwardedHost
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						u := &url.URL{
 | 
				
			||||||
 | 
							Scheme: scheme,
 | 
				
			||||||
 | 
							Host:   host,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return NewURLBuilder(u)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildBaseURL constructs a base url for the API, typically just "/v2/".
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildBaseURL() (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameBase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						baseURL, err := route.URL()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return baseURL.String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildTagsURL constructs a url to list the tags in the named repository.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameTags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tagsURL, err := route.URL("name", name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tagsURL.String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildManifestURL constructs a url for the manifest identified by name and tag.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameManifest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						manifestURL, err := route.URL("name", name, "tag", tag)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return manifestURL.String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildBlobURL constructs the url for the blob identified by name and dgst.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameBlob)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						layerURL, err := route.URL("name", name, "digest", dgst.String())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return layerURL.String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildBlobUploadURL constructs a url to begin a blob upload in the
 | 
				
			||||||
 | 
					// repository identified by name.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameBlobUpload)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uploadURL, err := route.URL("name", name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return appendValuesURL(uploadURL, values...).String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
 | 
				
			||||||
 | 
					// including any url values. This should generally not be used by clients, as
 | 
				
			||||||
 | 
					// this url is provided by server implementations during the blob upload
 | 
				
			||||||
 | 
					// process.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
 | 
				
			||||||
 | 
						route := ub.cloneRoute(RouteNameBlobUploadChunk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uploadURL, err := route.URL("name", name, "uuid", uuid)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return appendValuesURL(uploadURL, values...).String(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// clondedRoute returns a clone of the named route from the router. Routes
 | 
				
			||||||
 | 
					// must be cloned to avoid modifying them during url generation.
 | 
				
			||||||
 | 
					func (ub *URLBuilder) cloneRoute(name string) clonedRoute {
 | 
				
			||||||
 | 
						route := new(mux.Route)
 | 
				
			||||||
 | 
						root := new(url.URL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*route = *ub.router.GetRoute(name) // clone the route
 | 
				
			||||||
 | 
						*root = *ub.root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return clonedRoute{Route: route, root: root}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type clonedRoute struct {
 | 
				
			||||||
 | 
						*mux.Route
 | 
				
			||||||
 | 
						root *url.URL
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
 | 
				
			||||||
 | 
						routeURL, err := cr.Route.URL(pairs...)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return cr.root.ResolveReference(routeURL), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// appendValuesURL appends the parameters to the url.
 | 
				
			||||||
 | 
					func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
 | 
				
			||||||
 | 
						merged := u.Query()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, v := range values {
 | 
				
			||||||
 | 
							for k, vv := range v {
 | 
				
			||||||
 | 
								merged[k] = append(merged[k], vv...)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						u.RawQuery = merged.Encode()
 | 
				
			||||||
 | 
						return u
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// appendValues appends the parameters to the url. Panics if the string is not
 | 
				
			||||||
 | 
					// a url.
 | 
				
			||||||
 | 
					func appendValues(u string, values ...url.Values) string {
 | 
				
			||||||
 | 
						up, err := url.Parse(u)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err) // should never happen
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return appendValuesURL(up, values...).String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,155 @@
 | 
				
			||||||
 | 
					package v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type urlBuilderTestCase struct {
 | 
				
			||||||
 | 
						description  string
 | 
				
			||||||
 | 
						expectedPath string
 | 
				
			||||||
 | 
						build        func() (string, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
 | 
				
			||||||
 | 
						return []urlBuilderTestCase{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "test base url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/",
 | 
				
			||||||
 | 
								build:        urlBuilder.BuildBaseURL,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "test tags url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/tags/list",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildTagsURL("foo/bar")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "test manifest url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/manifests/tag",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildManifestURL("foo/bar", "tag")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "build blob url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "build blob upload url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/blobs/uploads/",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildBlobUploadURL("foo/bar")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "build blob upload url with digest and size",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
 | 
				
			||||||
 | 
										"size":   []string{"10000"},
 | 
				
			||||||
 | 
										"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "build blob upload chunk url",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								description:  "build blob upload chunk url with digest and size",
 | 
				
			||||||
 | 
								expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
 | 
				
			||||||
 | 
								build: func() (string, error) {
 | 
				
			||||||
 | 
									return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
 | 
				
			||||||
 | 
										"size":   []string{"10000"},
 | 
				
			||||||
 | 
										"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestURLBuilder tests the various url building functions, ensuring they are
 | 
				
			||||||
 | 
					// returning the expected values.
 | 
				
			||||||
 | 
					func TestURLBuilder(t *testing.T) {
 | 
				
			||||||
 | 
						roots := []string{
 | 
				
			||||||
 | 
							"http://example.com",
 | 
				
			||||||
 | 
							"https://example.com",
 | 
				
			||||||
 | 
							"http://localhost:5000",
 | 
				
			||||||
 | 
							"https://localhost:5443",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, root := range roots {
 | 
				
			||||||
 | 
							urlBuilder, err := NewURLBuilderFromString(root)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("unexpected error creating urlbuilder: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
 | 
				
			||||||
 | 
								url, err := testCase.build()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("%s: error building url: %v", testCase.description, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedURL := root + testCase.expectedPath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if url != expectedURL {
 | 
				
			||||||
 | 
									t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type builderFromRequestTestCase struct {
 | 
				
			||||||
 | 
						request *http.Request
 | 
				
			||||||
 | 
						base    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBuilderFromRequest(t *testing.T) {
 | 
				
			||||||
 | 
						u, err := url.Parse("http://example.com")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						forwardedProtoHeader := make(http.Header, 1)
 | 
				
			||||||
 | 
						forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testRequests := []struct {
 | 
				
			||||||
 | 
							request *http.Request
 | 
				
			||||||
 | 
							base    string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								request: &http.Request{URL: u, Host: u.Host},
 | 
				
			||||||
 | 
								base:    "http://example.com",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
 | 
				
			||||||
 | 
								base:    "https://example.com",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tr := range testRequests {
 | 
				
			||||||
 | 
							builder := NewURLBuilderFromRequest(tr.request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, testCase := range makeURLBuilderTestCases(builder) {
 | 
				
			||||||
 | 
								url, err := testCase.build()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("%s: error building url: %v", testCase.description, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectedURL := tr.base + testCase.expectedPath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if url != expectedURL {
 | 
				
			||||||
 | 
									t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ import (
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	"github.com/docker/distribution/configuration"
 | 
						"github.com/docker/distribution/configuration"
 | 
				
			||||||
	"github.com/docker/distribution/digest"
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
	"github.com/docker/distribution/manifest"
 | 
						"github.com/docker/distribution/manifest"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import (
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.google.com/p/go-uuid/uuid"
 | 
						"code.google.com/p/go-uuid/uuid"
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	"github.com/docker/distribution/auth"
 | 
						"github.com/docker/distribution/auth"
 | 
				
			||||||
	"github.com/docker/distribution/configuration"
 | 
						"github.com/docker/distribution/configuration"
 | 
				
			||||||
	ctxu "github.com/docker/distribution/context"
 | 
						ctxu "github.com/docker/distribution/context"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import (
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	_ "github.com/docker/distribution/auth/silly"
 | 
						_ "github.com/docker/distribution/auth/silly"
 | 
				
			||||||
	"github.com/docker/distribution/configuration"
 | 
						"github.com/docker/distribution/configuration"
 | 
				
			||||||
	"github.com/docker/distribution/storage"
 | 
						"github.com/docker/distribution/storage"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	ctxu "github.com/docker/distribution/context"
 | 
						ctxu "github.com/docker/distribution/context"
 | 
				
			||||||
	"github.com/docker/distribution/digest"
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
	"github.com/docker/distribution/storage"
 | 
						"github.com/docker/distribution/storage"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	ctxu "github.com/docker/distribution/context"
 | 
						ctxu "github.com/docker/distribution/context"
 | 
				
			||||||
	"github.com/docker/distribution/digest"
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
	"github.com/docker/distribution/manifest"
 | 
						"github.com/docker/distribution/manifest"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ package handlers
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	ctxu "github.com/docker/distribution/context"
 | 
						ctxu "github.com/docker/distribution/context"
 | 
				
			||||||
	"github.com/docker/distribution/digest"
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
	"github.com/docker/distribution/storage"
 | 
						"github.com/docker/distribution/storage"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import (
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	ctxu "github.com/docker/distribution/context"
 | 
						ctxu "github.com/docker/distribution/context"
 | 
				
			||||||
	"github.com/docker/distribution/digest"
 | 
						"github.com/docker/distribution/digest"
 | 
				
			||||||
	"github.com/docker/distribution/storage"
 | 
						"github.com/docker/distribution/storage"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/docker/distribution/api/v2"
 | 
						"github.com/docker/distribution/registry/api/v2"
 | 
				
			||||||
	"github.com/docker/distribution/storage"
 | 
						"github.com/docker/distribution/storage"
 | 
				
			||||||
	"github.com/gorilla/handlers"
 | 
						"github.com/gorilla/handlers"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue