Merge pull request #834 from stevvooe/next-generation
Implement tags HTTP API handler
This commit is contained in:
commit
12326f7090
9 changed files with 205 additions and 39 deletions
55
api_test.go
55
api_test.go
|
@ -195,6 +195,32 @@ func TestManifestAPI(t *testing.T) {
|
||||||
t.Fatalf("expected manifest unknown error: got %v", respErrs)
|
t.Fatalf("expected manifest unknown error: got %v", respErrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagsURL, err := builder.buildTagsURL(imageName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error building tags url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = http.Get(tagsURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check that we get an unknown repository error when asking for tags
|
||||||
|
checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound)
|
||||||
|
dec = json.NewDecoder(resp.Body)
|
||||||
|
if err := dec.Decode(&respErrs); err != nil {
|
||||||
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respErrs.Errors) == 0 {
|
||||||
|
t.Fatalf("expected errors in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if respErrs.Errors[0].Code != ErrorCodeUnknownRepository {
|
||||||
|
t.Fatalf("expected respository unknown error: got %v", respErrs)
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
// Attempt to push unsigned manifest with missing layers
|
// Attempt to push unsigned manifest with missing layers
|
||||||
unsignedManifest := &storage.Manifest{
|
unsignedManifest := &storage.Manifest{
|
||||||
|
@ -300,6 +326,35 @@ func TestManifestAPI(t *testing.T) {
|
||||||
if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) {
|
if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) {
|
||||||
t.Fatalf("manifests do not match")
|
t.Fatalf("manifests do not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the tag is listed.
|
||||||
|
resp, err = http.Get(tagsURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check that we get an unknown repository error when asking for tags
|
||||||
|
checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK)
|
||||||
|
dec = json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var tagsResponse tagsAPIResponse
|
||||||
|
|
||||||
|
if err := dec.Decode(&tagsResponse); err != nil {
|
||||||
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagsResponse.Name != imageName {
|
||||||
|
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tagsResponse.Tags) != 1 {
|
||||||
|
t.Fatalf("expected some tags in response: %v", tagsResponse.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagsResponse.Tags[0] != tag {
|
||||||
|
t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
|
func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
|
||||||
|
|
|
@ -34,6 +34,9 @@ const (
|
||||||
// match the provided tag.
|
// match the provided tag.
|
||||||
ErrorCodeInvalidTag
|
ErrorCodeInvalidTag
|
||||||
|
|
||||||
|
// ErrorCodeUnknownRepository when the repository name is not known.
|
||||||
|
ErrorCodeUnknownRepository
|
||||||
|
|
||||||
// ErrorCodeUnknownManifest returned when image manifest name and tag is
|
// ErrorCodeUnknownManifest returned when image manifest name and tag is
|
||||||
// unknown, accompanied by a 404 status.
|
// unknown, accompanied by a 404 status.
|
||||||
ErrorCodeUnknownManifest
|
ErrorCodeUnknownManifest
|
||||||
|
@ -64,6 +67,7 @@ var errorCodeStrings = map[ErrorCode]string{
|
||||||
ErrorCodeInvalidLength: "INVALID_LENGTH",
|
ErrorCodeInvalidLength: "INVALID_LENGTH",
|
||||||
ErrorCodeInvalidName: "INVALID_NAME",
|
ErrorCodeInvalidName: "INVALID_NAME",
|
||||||
ErrorCodeInvalidTag: "INVALID_TAG",
|
ErrorCodeInvalidTag: "INVALID_TAG",
|
||||||
|
ErrorCodeUnknownRepository: "UNKNOWN_REPOSITORY",
|
||||||
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
|
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
|
||||||
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
|
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
|
||||||
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
|
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
|
||||||
|
@ -78,6 +82,7 @@ var errorCodesMessages = map[ErrorCode]string{
|
||||||
ErrorCodeInvalidLength: "provided length did not match content length",
|
ErrorCodeInvalidLength: "provided length did not match content length",
|
||||||
ErrorCodeInvalidName: "manifest name did not match URI",
|
ErrorCodeInvalidName: "manifest name did not match URI",
|
||||||
ErrorCodeInvalidTag: "manifest tag did not match URI",
|
ErrorCodeInvalidTag: "manifest tag did not match URI",
|
||||||
|
ErrorCodeUnknownRepository: "repository not known to registry",
|
||||||
ErrorCodeUnknownManifest: "manifest not known",
|
ErrorCodeUnknownManifest: "manifest not known",
|
||||||
ErrorCodeInvalidManifest: "manifest is invalid",
|
ErrorCodeInvalidManifest: "manifest is invalid",
|
||||||
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
|
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
|
||||||
|
|
|
@ -3,49 +3,12 @@ package storage
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
|
|
||||||
"github.com/docker/docker-registry/digest"
|
"github.com/docker/docker-registry/digest"
|
||||||
|
"github.com/docker/libtrust"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrUnknownManifest is returned if the manifest is not known by the
|
|
||||||
// registry.
|
|
||||||
type ErrUnknownManifest struct {
|
|
||||||
Name string
|
|
||||||
Tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrUnknownManifest) Error() string {
|
|
||||||
return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrManifestUnverified is returned when the registry is unable to verify
|
|
||||||
// the manifest.
|
|
||||||
type ErrManifestUnverified struct{}
|
|
||||||
|
|
||||||
func (ErrManifestUnverified) Error() string {
|
|
||||||
return fmt.Sprintf("unverified manifest")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrManifestVerification provides a type to collect errors encountered
|
|
||||||
// during manifest verification. Currently, it accepts errors of all types,
|
|
||||||
// but it may be narrowed to those involving manifest verification.
|
|
||||||
type ErrManifestVerification []error
|
|
||||||
|
|
||||||
func (errs ErrManifestVerification) Error() string {
|
|
||||||
var parts []string
|
|
||||||
for _, err := range errs {
|
|
||||||
parts = append(parts, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
||||||
// content with unknown schema version can be decoded against this struct to
|
// content with unknown schema version can be decoded against this struct to
|
||||||
// check the version.
|
// check the version.
|
||||||
|
|
|
@ -99,6 +99,20 @@ func TestManifestStorage(t *testing.T) {
|
||||||
if !reflect.DeepEqual(fetchedManifest, sm) {
|
if !reflect.DeepEqual(fetchedManifest, sm) {
|
||||||
t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm)
|
t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grabs the tags and check that this tagged manifest is present
|
||||||
|
tags, err := ms.Tags(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error fetching tags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) != 1 {
|
||||||
|
t.Fatalf("unexpected tags returned: %v", tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags[0] != tag {
|
||||||
|
t.Fatalf("unexpected tag found in tags: %v != %v", tags, []string{tag})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type layerKey struct {
|
type layerKey struct {
|
||||||
|
|
|
@ -3,11 +3,56 @@ package storage
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/storagedriver"
|
"github.com/docker/docker-registry/storagedriver"
|
||||||
"github.com/docker/libtrust"
|
"github.com/docker/libtrust"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnknownRepository is returned if the named repository is not known by
|
||||||
|
// the registry.
|
||||||
|
type ErrUnknownRepository struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUnknownRepository) Error() string {
|
||||||
|
return fmt.Sprintf("unknown respository name=%s", err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnknownManifest is returned if the manifest is not known by the
|
||||||
|
// registry.
|
||||||
|
type ErrUnknownManifest struct {
|
||||||
|
Name string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUnknownManifest) Error() string {
|
||||||
|
return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrManifestUnverified is returned when the registry is unable to verify
|
||||||
|
// the manifest.
|
||||||
|
type ErrManifestUnverified struct{}
|
||||||
|
|
||||||
|
func (ErrManifestUnverified) Error() string {
|
||||||
|
return fmt.Sprintf("unverified manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrManifestVerification provides a type to collect errors encountered
|
||||||
|
// during manifest verification. Currently, it accepts errors of all types,
|
||||||
|
// but it may be narrowed to those involving manifest verification.
|
||||||
|
type ErrManifestVerification []error
|
||||||
|
|
||||||
|
func (errs ErrManifestVerification) Error() string {
|
||||||
|
var parts []string
|
||||||
|
for _, err := range errs {
|
||||||
|
parts = append(parts, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
type manifestStore struct {
|
type manifestStore struct {
|
||||||
driver storagedriver.StorageDriver
|
driver storagedriver.StorageDriver
|
||||||
pathMapper *pathMapper
|
pathMapper *pathMapper
|
||||||
|
@ -16,6 +61,34 @@ type manifestStore struct {
|
||||||
|
|
||||||
var _ ManifestService = &manifestStore{}
|
var _ ManifestService = &manifestStore{}
|
||||||
|
|
||||||
|
func (ms *manifestStore) Tags(name string) ([]string, error) {
|
||||||
|
p, err := ms.pathMapper.path(manifestTagsPath{
|
||||||
|
name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []string
|
||||||
|
entries, err := ms.driver.List(p)
|
||||||
|
if err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case storagedriver.PathNotFoundError:
|
||||||
|
return nil, ErrUnknownRepository{Name: name}
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
_, filename := path.Split(entry)
|
||||||
|
|
||||||
|
tags = append(tags, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ms *manifestStore) Exists(name, tag string) (bool, error) {
|
func (ms *manifestStore) Exists(name, tag string) (bool, error) {
|
||||||
p, err := ms.path(name, tag)
|
p, err := ms.path(name, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,6 +64,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
|
||||||
repoPrefix := append(rootPrefix, "repositories")
|
repoPrefix := append(rootPrefix, "repositories")
|
||||||
|
|
||||||
switch v := spec.(type) {
|
switch v := spec.(type) {
|
||||||
|
case manifestTagsPath:
|
||||||
|
return path.Join(append(repoPrefix, v.name, "manifests")...), nil
|
||||||
case manifestPathSpec:
|
case manifestPathSpec:
|
||||||
// TODO(sday): May need to store manifest by architecture.
|
// TODO(sday): May need to store manifest by architecture.
|
||||||
return path.Join(append(repoPrefix, v.name, "manifests", v.tag)...), nil
|
return path.Join(append(repoPrefix, v.name, "manifests", v.tag)...), nil
|
||||||
|
@ -109,6 +111,14 @@ type pathSpec interface {
|
||||||
pathSpec()
|
pathSpec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manifestTagsPath describes the path elements required to point to the
|
||||||
|
// directory with all manifest tags under the repository.
|
||||||
|
type manifestTagsPath struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manifestTagsPath) pathSpec() {}
|
||||||
|
|
||||||
// manifestPathSpec describes the path elements used to build a manifest path.
|
// manifestPathSpec describes the path elements used to build a manifest path.
|
||||||
// The contents should be a signed manifest json file.
|
// The contents should be a signed manifest json file.
|
||||||
type manifestPathSpec struct {
|
type manifestPathSpec struct {
|
||||||
|
|
|
@ -52,6 +52,9 @@ func (ss *Services) Manifests() ManifestService {
|
||||||
|
|
||||||
// ManifestService provides operations on image manifests.
|
// ManifestService provides operations on image manifests.
|
||||||
type ManifestService interface {
|
type ManifestService interface {
|
||||||
|
// Tags lists the tags under the named repository.
|
||||||
|
Tags(name string) ([]string, error)
|
||||||
|
|
||||||
// Exists returns true if the layer exists.
|
// Exists returns true if the layer exists.
|
||||||
Exists(name, tag string) (bool, error)
|
Exists(name, tag string) (bool, error)
|
||||||
|
|
||||||
|
|
31
tags.go
31
tags.go
|
@ -1,8 +1,10 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/storage"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +24,34 @@ type tagsHandler struct {
|
||||||
*Context
|
*Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tagsAPIResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetTags returns a json list of tags for a specific image name.
|
// GetTags returns a json list of tags for a specific image name.
|
||||||
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO(stevvooe): Implement this method.
|
defer r.Body.Close()
|
||||||
|
manifests := th.services.Manifests()
|
||||||
|
|
||||||
|
tags, err := manifests.Tags(th.Name)
|
||||||
|
if err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case storage.ErrUnknownRepository:
|
||||||
|
w.WriteHeader(404)
|
||||||
|
th.Errors.Push(ErrorCodeUnknownRepository, map[string]string{"name": th.Name})
|
||||||
|
default:
|
||||||
|
th.Errors.PushErr(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
if err := enc.Encode(tagsAPIResponse{
|
||||||
|
Name: th.Name,
|
||||||
|
Tags: tags,
|
||||||
|
}); err != nil {
|
||||||
|
th.Errors.PushErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
14
urls.go
14
urls.go
|
@ -39,6 +39,20 @@ func newURLBuilderFromString(root string) (*urlBuilder, error) {
|
||||||
return newURLBuilder(u), nil
|
return newURLBuilder(u), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ub *urlBuilder) buildTagsURL(name string) (string, error) {
|
||||||
|
route := clonedRoute(ub.router, routeNameTags)
|
||||||
|
|
||||||
|
tagsURL, err := route.
|
||||||
|
Schemes(ub.url.Scheme).
|
||||||
|
Host(ub.url.Host).
|
||||||
|
URL("name", name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ub *urlBuilder) forManifest(m *storage.Manifest) (string, error) {
|
func (ub *urlBuilder) forManifest(m *storage.Manifest) (string, error) {
|
||||||
return ub.buildManifestURL(m.Name, m.Tag)
|
return ub.buildManifestURL(m.Name, m.Tag)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue