Move registry api definitions under registry package
Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
54ae545ed3
commit
3822e685a0
18 changed files with 2593 additions and 8 deletions
1422
docs/api/v2/descriptors.go
Normal file
1422
docs/api/v2/descriptors.go
Normal file
File diff suppressed because it is too large
Load diff
9
docs/api/v2/doc.go
Normal file
9
docs/api/v2/doc.go
Normal file
|
@ -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
|
191
docs/api/v2/errors.go
Normal file
191
docs/api/v2/errors.go
Normal file
|
@ -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)
|
||||
}
|
165
docs/api/v2/errors_test.go
Normal file
165
docs/api/v2/errors_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
115
docs/api/v2/names.go
Normal file
115
docs/api/v2/names.go
Normal file
|
@ -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
|
||||
}
|
93
docs/api/v2/names_test.go
Normal file
93
docs/api/v2/names_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
docs/api/v2/routes.go
Normal file
36
docs/api/v2/routes.go
Normal file
|
@ -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
|
||||
}
|
198
docs/api/v2/routes_test.go
Normal file
198
docs/api/v2/routes_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
201
docs/api/v2/urls.go
Normal file
201
docs/api/v2/urls.go
Normal file
|
@ -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()
|
||||
}
|
155
docs/api/v2/urls_test.go
Normal file
155
docs/api/v2/urls_test.go
Normal file
|
@ -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"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/configuration"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"os"
|
||||
|
||||
"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/configuration"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
_ "github.com/docker/distribution/auth/silly"
|
||||
"github.com/docker/distribution/configuration"
|
||||
"github.com/docker/distribution/storage"
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/storage"
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
|
|
|
@ -3,7 +3,7 @@ package handlers
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/storage"
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/storage"
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/api/v2"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue