commit
						0120d1c7db
					
				| 
						 | 
				
			
			@ -11,8 +11,8 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/Sirupsen/logrus"
 | 
			
		||||
	"github.com/docker/distribution/registry/api/v2"
 | 
			
		||||
	"github.com/docker/docker/pkg/requestdecorator"
 | 
			
		||||
	"github.com/docker/docker/registry/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// for mocking in unit tests
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/Sirupsen/logrus"
 | 
			
		||||
	"github.com/docker/distribution/digest"
 | 
			
		||||
	"github.com/docker/docker/registry/v2"
 | 
			
		||||
	"github.com/docker/distribution/registry/api/v2"
 | 
			
		||||
	"github.com/docker/docker/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,8 +109,8 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
 | 
			
		|||
// - Succeeded to head image blob (already exists)
 | 
			
		||||
// - Failed with no error (continue to Push the Blob)
 | 
			
		||||
// - Failed with error
 | 
			
		||||
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
 | 
			
		||||
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (bool, error) {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -141,11 +141,11 @@ func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string,
 | 
			
		|||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s:%s", res.StatusCode, imageName, sumType, sum), res)
 | 
			
		||||
	return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s", res.StatusCode, imageName, dgst), res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
 | 
			
		||||
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobWrtr io.Writer, auth *RequestAuthorization) error {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -175,8 +175,8 @@ func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, b
 | 
			
		|||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
 | 
			
		||||
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
 | 
			
		||||
	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, 0, err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +198,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
 | 
			
		|||
		if res.StatusCode == 401 {
 | 
			
		||||
			return nil, 0, errLoginRequired
 | 
			
		||||
		}
 | 
			
		||||
		return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s:%s", res.StatusCode, imageName, sumType, sum), res)
 | 
			
		||||
		return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s", res.StatusCode, imageName, dgst), res)
 | 
			
		||||
	}
 | 
			
		||||
	lenStr := res.Header.Get("Content-Length")
 | 
			
		||||
	l, err := strconv.ParseInt(lenStr, 10, 64)
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +212,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
 | 
			
		|||
// Push the image to the server for storage.
 | 
			
		||||
// 'layer' is an uncompressed reader of the blob to be pushed.
 | 
			
		||||
// The server will generate it's own checksum calculation.
 | 
			
		||||
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error {
 | 
			
		||||
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobRdr io.Reader, auth *RequestAuthorization) error {
 | 
			
		||||
	location, err := r.initiateBlobUpload(ep, imageName, auth)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +225,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
 | 
			
		|||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	queryParams := req.URL.Query()
 | 
			
		||||
	queryParams.Add("digest", sumType+":"+sumStr)
 | 
			
		||||
	queryParams.Add("digest", dgst.String())
 | 
			
		||||
	req.URL.RawQuery = queryParams.Encode()
 | 
			
		||||
	if err := auth.Authorize(req); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			@ -245,7 +245,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
 | 
			
		|||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
 | 
			
		||||
		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s:%s", res.StatusCode, imageName, sumType, sumStr), res)
 | 
			
		||||
		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s", res.StatusCode, imageName, dgst), res)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,144 +0,0 @@
 | 
			
		|||
package v2
 | 
			
		||||
 | 
			
		||||
import "net/http"
 | 
			
		||||
 | 
			
		||||
// TODO(stevvooe): Add route descriptors for each named route, along with
 | 
			
		||||
// accepted methods, parameters, returned status codes and error codes.
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 | 
			
		||||
	// HTTPStatusCodes provides a list of status under which this error
 | 
			
		||||
	// condition may arise. If it is empty, the error condition may be seen
 | 
			
		||||
	// for any status code.
 | 
			
		||||
	HTTPStatusCodes []int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrorDescriptors provides a list of HTTP API Error codes that may be
 | 
			
		||||
// encountered when interacting with the registry API.
 | 
			
		||||
var ErrorDescriptors = []ErrorDescriptor{
 | 
			
		||||
	{
 | 
			
		||||
		Code:    ErrorCodeUnknown,
 | 
			
		||||
		Value:   "UNKNOWN",
 | 
			
		||||
		Message: "unknown error",
 | 
			
		||||
		Description: `Generic error returned when the error does not have an
 | 
			
		||||
		API classification.`,
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{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. The detail will contain information
 | 
			
		||||
		the failed validation.`,
 | 
			
		||||
		HTTPStatusCodes: []int{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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{http.StatusBadRequest, 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.`,
 | 
			
		||||
		HTTPStatusCodes: []int{http.StatusNotFound},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
 | 
			
		||||
var idToDescriptors map[string]ErrorDescriptor
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
 | 
			
		||||
	idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
 | 
			
		||||
 | 
			
		||||
	for _, descriptor := range ErrorDescriptors {
 | 
			
		||||
		errorCodeToDescriptors[descriptor.Code] = descriptor
 | 
			
		||||
		idToDescriptors[descriptor.Value] = descriptor
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
// 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.
 | 
			
		||||
//
 | 
			
		||||
// Currently, while the HTTP API definitions are considered stable, the Go API
 | 
			
		||||
// exports are considered unstable. Go API consumers should take care when
 | 
			
		||||
// relying on these definitions until this message is deleted.
 | 
			
		||||
package v2
 | 
			
		||||
| 
						 | 
				
			
			@ -1,185 +0,0 @@
 | 
			
		|||
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
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,163 +0,0 @@
 | 
			
		|||
package v2
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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]string{"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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +0,0 @@
 | 
			
		|||
package v2
 | 
			
		||||
 | 
			
		||||
import "regexp"
 | 
			
		||||
 | 
			
		||||
// This file defines regular expressions for use in route definition. These
 | 
			
		||||
// are also defined in the registry code base. Until they are in a common,
 | 
			
		||||
// shared location, and exported, they must be repeated here.
 | 
			
		||||
 | 
			
		||||
// 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]+)*`)
 | 
			
		||||
 | 
			
		||||
// 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}`)
 | 
			
		||||
 | 
			
		||||
// DigestRegexp matches valid digest types.
 | 
			
		||||
var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+`)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,66 +0,0 @@
 | 
			
		|||
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)
 | 
			
		||||
 | 
			
		||||
	// GET /v2/	Check	Check that the registry implements API version 2(.1)
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/").
 | 
			
		||||
		Name(RouteNameBase)
 | 
			
		||||
 | 
			
		||||
	// GET      /v2/<name>/manifest/<reference>	Image Manifest	Fetch the image manifest identified by name and reference where reference can be a tag or digest.
 | 
			
		||||
	// PUT      /v2/<name>/manifest/<reference>	Image Manifest	Upload the image manifest identified by name and reference where reference can be a tag or digest.
 | 
			
		||||
	// DELETE   /v2/<name>/manifest/<reference>	Image Manifest	Delete the image identified by name and reference where reference can be a tag or digest.
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + DigestRegexp.String() + "}").
 | 
			
		||||
		Name(RouteNameManifest)
 | 
			
		||||
 | 
			
		||||
	// GET	/v2/<name>/tags/list	Tags	Fetch the tags under the repository identified by name.
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list").
 | 
			
		||||
		Name(RouteNameTags)
 | 
			
		||||
 | 
			
		||||
	// GET	/v2/<name>/blob/<digest>	Layer	Fetch the blob identified by digest.
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
 | 
			
		||||
		Name(RouteNameBlob)
 | 
			
		||||
 | 
			
		||||
	// POST	/v2/<name>/blob/upload/	Layer Upload	Initiate an upload of the layer identified by tarsum.
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/").
 | 
			
		||||
		Name(RouteNameBlobUpload)
 | 
			
		||||
 | 
			
		||||
	// GET	/v2/<name>/blob/upload/<uuid>	Layer Upload	Get the status of the upload identified by tarsum and uuid.
 | 
			
		||||
	// PUT	/v2/<name>/blob/upload/<uuid>	Layer Upload	Upload all or a chunk of the upload identified by tarsum and uuid.
 | 
			
		||||
	// DELETE	/v2/<name>/blob/upload/<uuid>	Layer Upload	Cancel the upload identified by layer and uuid
 | 
			
		||||
	router.
 | 
			
		||||
		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
 | 
			
		||||
		Name(RouteNameBlobUploadChunk)
 | 
			
		||||
 | 
			
		||||
	return router
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,192 +0,0 @@
 | 
			
		|||
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",
 | 
			
		||||
				"reference": "bar",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			RouteName:  RouteNameManifest,
 | 
			
		||||
			RequestURI: "/v2/foo/bar/manifests/tag",
 | 
			
		||||
			Vars: map[string]string{
 | 
			
		||||
				"name":      "foo/bar",
 | 
			
		||||
				"reference": "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",
 | 
			
		||||
				"reference": "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
 | 
			
		||||
		router.GetRoute(testcase.RouteName).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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										179
									
								
								docs/v2/urls.go
								
								
								
								
							
							
						
						
									
										179
									
								
								docs/v2/urls.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,179 +0,0 @@
 | 
			
		|||
package v2
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"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 {
 | 
			
		||||
	u := &url.URL{
 | 
			
		||||
		Scheme: r.URL.Scheme,
 | 
			
		||||
		Host:   r.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 reference.
 | 
			
		||||
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
 | 
			
		||||
	route := ub.cloneRoute(RouteNameManifest)
 | 
			
		||||
 | 
			
		||||
	manifestURL, err := route.URL("name", name, "reference", reference)
 | 
			
		||||
	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 string) (string, error) {
 | 
			
		||||
	route := ub.cloneRoute(RouteNameBlob)
 | 
			
		||||
 | 
			
		||||
	layerURL, err := route.URL("name", name, "digest", dgst)
 | 
			
		||||
	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()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,113 +0,0 @@
 | 
			
		|||
package v2
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type urlBuilderTestCase struct {
 | 
			
		||||
	description  string
 | 
			
		||||
	expectedPath string
 | 
			
		||||
	build        func() (string, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestURLBuilder tests the various url building functions, ensuring they are
 | 
			
		||||
// returning the expected values.
 | 
			
		||||
func TestURLBuilder(t *testing.T) {
 | 
			
		||||
	var (
 | 
			
		||||
		urlBuilder *URLBuilder
 | 
			
		||||
		err        error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	testCases := []urlBuilderTestCase{
 | 
			
		||||
		{
 | 
			
		||||
			description:  "test base url",
 | 
			
		||||
			expectedPath: "/v2/",
 | 
			
		||||
			build: func() (string, error) {
 | 
			
		||||
				return 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"},
 | 
			
		||||
				})
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 testCases {
 | 
			
		||||
			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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue