Initial implementation of Manifest HTTP API

Push, pull and delete of manifest files in the registry have been implemented
on top of the storage services. Basic workflows, including reporting of missing
manifests are tested, including various proposed response codes. Common testing
functionality has been collected into shared methods. A test suite may be
emerging but it might better to capture more edge cases (such as resumable
upload, range requests, etc.) before we commit to a full approach.

To support clearer test cases and simpler handler methods, an application aware
urlBuilder has been added. We may want to export the functionality for use in
the client, which could allow us to abstract away from gorilla/mux.

A few error codes have been added to fill in error conditions missing from the
proposal. Some use cases have identified some problems with the approach to
error reporting that requires more work to reconcile. To resolve this, the
mapping of Go errors into error types needs to pulled out of the handlers and
into the application. We also need to move to type-based errors, with rich
information, rather than value-based errors. ErrorHandlers will probably
replace the http.Handlers to make this work correctly.

Unrelated to the above, the "length" parameter has been migrated to "size" for
completing layer uploads. This change should have gone out before but these
diffs ending up being coupled with the parameter name change due to updates to
the layer unit tests.
This commit is contained in:
Stephen J Day 2014-11-26 12:16:58 -08:00
parent 6fead90736
commit e809796f59
10 changed files with 581 additions and 210 deletions

View file

@ -1,6 +1,8 @@
package registry package registry
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -10,7 +12,9 @@ import (
"os" "os"
"testing" "testing"
"github.com/Sirupsen/logrus" "github.com/docker/libtrust"
"github.com/docker/docker-registry/storage"
_ "github.com/docker/docker-registry/storagedriver/inmemory" _ "github.com/docker/docker-registry/storagedriver/inmemory"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -34,11 +38,10 @@ func TestLayerAPI(t *testing.T) {
app := NewApp(config) app := NewApp(config)
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
router := v2APIRouter() builder, err := newURLBuilderFromString(server.URL)
u, err := url.Parse(server.URL)
if err != nil { if err != nil {
t.Fatalf("error parsing server url: %v", err) t.Fatalf("error creating url builder: %v", err)
} }
imageName := "foo/bar" imageName := "foo/bar"
@ -52,154 +55,65 @@ func TestLayerAPI(t *testing.T) {
// ----------------------------------- // -----------------------------------
// Test fetch for non-existent content // Test fetch for non-existent content
r, err := router.GetRoute(routeNameBlob).Host(u.Host). layerURL, err := builder.buildLayerURL(imageName, layerDigest)
URL("name", imageName, if err != nil {
"digest", tarSumStr) t.Fatalf("error building url: %v", err)
}
resp, err := http.Get(r.String()) resp, err := http.Get(layerURL)
if err != nil { if err != nil {
t.Fatalf("unexpected error fetching non-existent layer: %v", err) t.Fatalf("unexpected error fetching non-existent layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound)
case http.StatusNotFound:
break // expected
default:
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status)
}
// ------------------------------------------ // ------------------------------------------
// Test head request for non-existent content // Test head request for non-existent content
resp, err = http.Head(r.String())
if err != nil {
t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
}
switch resp.StatusCode {
case http.StatusNotFound:
break // expected
default:
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status)
}
// ------------------------------------------
// Upload a layer
r, err = router.GetRoute(routeNameBlobUpload).Host(u.Host).
URL("name", imageName)
if err != nil {
t.Fatalf("error starting layer upload: %v", err)
}
resp, err = http.Post(r.String(), "", nil)
if err != nil {
t.Fatalf("error starting layer upload: %v", err)
}
if resp.StatusCode != http.StatusAccepted {
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status)
}
if resp.Header.Get("Location") == "" { // TODO(stevvooe): Need better check here.
t.Fatalf("unexpected Location: %q != %q", resp.Header.Get("Location"), "foo")
}
if resp.Header.Get("Content-Length") != "0" {
t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0")
}
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
layerFile.Seek(0, os.SEEK_SET)
uploadURLStr := resp.Header.Get("Location")
// TODO(sday): Cancel the layer upload here and restart.
query := url.Values{
"digest": []string{layerDigest.String()},
"length": []string{fmt.Sprint(layerLength)},
}
uploadURL, err := url.Parse(uploadURLStr)
if err != nil {
t.Fatalf("unexpected error parsing url: %v", err)
}
uploadURL.RawQuery = query.Encode()
// Just do a monolithic upload
req, err := http.NewRequest("PUT", uploadURL.String(), layerFile)
if err != nil {
t.Fatalf("unexpected error creating new request: %v", err)
}
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unexpected error doing put: %v", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusCreated:
break // expected
default:
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status)
}
if resp.Header.Get("Location") == "" {
t.Fatalf("unexpected Location: %q", resp.Header.Get("Location"))
}
if resp.Header.Get("Content-Length") != "0" {
t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0")
}
layerURL := resp.Header.Get("Location")
// ------------------------
// Use a head request to see if the layer exists.
resp, err = http.Head(layerURL) resp, err = http.Head(layerURL)
if err != nil { if err != nil {
t.Fatalf("unexpected error checking head on non-existent layer: %v", err) t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound)
case http.StatusOK:
break // expected // ------------------------------------------
default: // Upload a layer
d, err := httputil.DumpResponse(resp, true) layerUploadURL, err := builder.buildLayerUploadURL(imageName)
if err != nil { if err != nil {
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) t.Fatalf("error building upload url: %v", err)
} }
t.Logf("response:\n%s", string(d)) resp, err = http.Post(layerUploadURL, "", nil)
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) if err != nil {
t.Fatalf("error starting layer upload: %v", err)
} }
logrus.Infof("fetch the layer") checkResponse(t, "starting layer upload", resp, http.StatusAccepted)
checkHeaders(t, resp, http.Header{
"Location": []string{"*"},
"Content-Length": []string{"0"},
})
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
layerFile.Seek(0, os.SEEK_SET)
// TODO(sday): Cancel the layer upload here and restart.
uploadURLBase := startPushLayer(t, builder, imageName)
pushLayer(t, builder, imageName, layerDigest, uploadURLBase, layerFile)
// ------------------------
// Use a head request to see if the layer exists.
resp, err = http.Head(layerURL)
if err != nil {
t.Fatalf("unexpected error checking head on existing layer: %v", err)
}
checkResponse(t, "checking head on existing layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)},
})
// ---------------- // ----------------
// Fetch the layer! // Fetch the layer!
resp, err = http.Get(layerURL) resp, err = http.Get(layerURL)
@ -207,30 +121,299 @@ func TestLayerAPI(t *testing.T) {
t.Fatalf("unexpected error fetching layer: %v", err) t.Fatalf("unexpected error fetching layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "fetching layer", resp, http.StatusOK)
case http.StatusOK: checkHeaders(t, resp, http.Header{
break // expected "Content-Length": []string{fmt.Sprint(layerLength)},
default: })
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status)
}
// Verify the body // Verify the body
verifier := digest.NewDigestVerifier(layerDigest) verifier := digest.NewDigestVerifier(layerDigest)
io.Copy(verifier, resp.Body) io.Copy(verifier, resp.Body)
if !verifier.Verified() { if !verifier.Verified() {
d, err := httputil.DumpResponse(resp, true)
if err != nil {
t.Fatalf("unexpected status checking head on layer ayo!: %v, %v", resp.StatusCode, resp.Status)
}
t.Logf("response:\n%s", string(d))
t.Fatalf("response body did not pass verification") t.Fatalf("response body did not pass verification")
} }
} }
func TestManifestAPI(t *testing.T) {
pk, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatalf("unexpected error generating private key: %v", err)
}
config := configuration.Configuration{
Storage: configuration.Storage{
"inmemory": configuration.Parameters{},
},
}
app := NewApp(config)
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
builder, err := newURLBuilderFromString(server.URL)
if err != nil {
t.Fatalf("unexpected error creating url builder: %v", err)
}
imageName := "foo/bar"
tag := "thetag"
manifestURL, err := builder.buildManifestURL(imageName, tag)
if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err)
}
// -----------------------------
// Attempt to fetch the manifest
resp, err := http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error getting manifest: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound)
// TODO(stevvooe): Shoot. The error setup is not working out. The content-
// type headers are being set after writing the status code.
// if resp.Header.Get("Content-Type") != "application/json" {
// t.Fatalf("unexpected content type: %v != 'application/json'",
// resp.Header.Get("Content-Type"))
// }
dec := json.NewDecoder(resp.Body)
var respErrs struct {
Errors []Error
}
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 != ErrorCodeUnknownManifest {
t.Fatalf("expected manifest unknown error: got %v", respErrs)
}
// --------------------------------
// Attempt to push unsigned manifest with missing layers
unsignedManifest := &storage.Manifest{
Name: imageName,
Tag: tag,
FSLayers: []storage.FSLayer{
{
BlobSum: "asdf",
},
{
BlobSum: "qwer",
},
},
}
resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest)
defer resp.Body.Close()
checkResponse(t, "posting unsigned manifest", resp, http.StatusBadRequest)
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&respErrs); err != nil {
t.Fatalf("unexpected error decoding error response: %v", err)
}
var unverified int
var missingLayers int
var invalidDigests int
for _, err := range respErrs.Errors {
switch err.Code {
case ErrorCodeUnverifiedManifest:
unverified++
case ErrorCodeUnknownLayer:
missingLayers++
case ErrorCodeInvalidDigest:
// TODO(stevvooe): This error isn't quite descriptive enough --
// the layer with an invalid digest isn't identified.
invalidDigests++
default:
t.Fatalf("unexpected error: %v", err)
}
}
if unverified != 1 {
t.Fatalf("should have received one unverified manifest error: %v", respErrs)
}
if missingLayers != 2 {
t.Fatalf("should have received two missing layer errors: %v", respErrs)
}
if invalidDigests != 2 {
t.Fatalf("should have received two invalid digest errors: %v", respErrs)
}
// Push 2 random layers
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
for i := range unsignedManifest.FSLayers {
rs, dgstStr, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating random layer %d: %v", i, err)
}
dgst := digest.Digest(dgstStr)
expectedLayers[dgst] = rs
unsignedManifest.FSLayers[i].BlobSum = dgst
uploadURLBase := startPushLayer(t, builder, imageName)
pushLayer(t, builder, imageName, dgst, uploadURLBase, rs)
}
// -------------------
// Push the signed manifest with all layers pushed.
signedManifest, err := unsignedManifest.Sign(pk)
if err != nil {
t.Fatalf("unexpected error signing manifest: %v", err)
}
resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest)
checkResponse(t, "putting manifest", resp, http.StatusOK)
resp, err = http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error fetching manifest: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
var fetchedManifest storage.SignedManifest
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifest); err != nil {
t.Fatalf("error decoding fetched manifest: %v", err)
}
if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) {
t.Fatalf("manifests do not match")
}
}
func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
body, err := json.Marshal(v)
if err != nil {
t.Fatalf("unexpected error marshaling %v: %v", v, err)
}
req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
if err != nil {
t.Fatalf("error creating request for %s: %v", msg, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("error doing put request while %s: %v", msg, err)
}
return resp
}
func startPushLayer(t *testing.T, ub *urlBuilder, name string) string {
layerUploadURL, err := ub.buildLayerUploadURL(name)
if err != nil {
t.Fatalf("unexpected error building layer upload url: %v", err)
}
resp, err := http.Post(layerUploadURL, "", nil)
if err != nil {
t.Fatalf("unexpected error starting layer push: %v", err)
}
defer resp.Body.Close()
checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name), resp, http.StatusAccepted)
checkHeaders(t, resp, http.Header{
"Location": []string{"*"},
"Content-Length": []string{"0"},
})
return resp.Header.Get("Location")
}
// pushLayer pushes the layer content returning the url on success.
func pushLayer(t *testing.T, ub *urlBuilder, name string, dgst digest.Digest, uploadURLBase string, rs io.ReadSeeker) string {
rsLength, _ := rs.Seek(0, os.SEEK_END)
rs.Seek(0, os.SEEK_SET)
uploadURL := appendValues(uploadURLBase, url.Values{
"digest": []string{dgst.String()},
"size": []string{fmt.Sprint(rsLength)},
})
// Just do a monolithic upload
req, err := http.NewRequest("PUT", uploadURL, rs)
if err != nil {
t.Fatalf("unexpected error creating new request: %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unexpected error doing put: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated)
expectedLayerURL, err := ub.buildLayerURL(name, dgst)
if err != nil {
t.Fatalf("error building expected layer url: %v", err)
}
checkHeaders(t, resp, http.Header{
"Location": []string{expectedLayerURL},
"Content-Length": []string{"0"},
})
return resp.Header.Get("Location")
}
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
if resp.StatusCode != expectedStatus {
t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus)
maybeDumpResponse(t, resp)
t.FailNow()
}
}
func maybeDumpResponse(t *testing.T, resp *http.Response) {
if d, err := httputil.DumpResponse(resp, true); err != nil {
t.Logf("error dumping response: %v", err)
} else {
t.Logf("response:\n%s", string(d))
}
}
// matchHeaders checks that the response has at least the headers. If not, the
// test will fail. If a passed in header value is "*", any non-zero value will
// suffice as a match.
func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) {
for k, vs := range headers {
if resp.Header.Get(k) == "" {
t.Fatalf("response missing header %q", k)
}
for _, v := range vs {
if v == "*" {
// Just ensure there is some value.
if len(resp.Header[k]) > 0 {
continue
}
}
for _, hv := range resp.Header[k] {
if hv != v {
t.Fatalf("header value not matched in response: %q != %q", hv, v)
}
}
}
}
}

1
app.go
View file

@ -110,6 +110,7 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
context := &Context{ context := &Context{
App: app, App: app,
Name: vars["name"], Name: vars["name"],
urlBuilder: newURLBuilderFromRequest(r),
} }
// Store vars for underlying handlers. // Store vars for underlying handlers.

View file

@ -24,4 +24,6 @@ type Context struct {
// log provides a context specific logger. // log provides a context specific logger.
log *logrus.Entry log *logrus.Entry
urlBuilder *urlBuilder
} }

View file

@ -34,6 +34,14 @@ const (
// match the provided tag. // match the provided tag.
ErrorCodeInvalidTag ErrorCodeInvalidTag
// ErrorCodeUnknownManifest returned when image manifest name and tag is
// unknown, accompanied by a 404 status.
ErrorCodeUnknownManifest
// ErrorCodeInvalidManifest returned when an image manifest is invalid,
// typically during a PUT operation.
ErrorCodeInvalidManifest
// ErrorCodeUnverifiedManifest is returned when the manifest fails signature // ErrorCodeUnverifiedManifest is returned when the manifest fails signature
// validation. // validation.
ErrorCodeUnverifiedManifest ErrorCodeUnverifiedManifest
@ -56,6 +64,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrorCodeInvalidLength: "INVALID_LENGTH", ErrorCodeInvalidLength: "INVALID_LENGTH",
ErrorCodeInvalidName: "INVALID_NAME", ErrorCodeInvalidName: "INVALID_NAME",
ErrorCodeInvalidTag: "INVALID_TAG", ErrorCodeInvalidTag: "INVALID_TAG",
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST", ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
ErrorCodeUnknownLayer: "UNKNOWN_LAYER", ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD", ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD",
@ -66,12 +76,14 @@ var errorCodesMessages = map[ErrorCode]string{
ErrorCodeUnknown: "unknown error", ErrorCodeUnknown: "unknown error",
ErrorCodeInvalidDigest: "provided digest did not match uploaded content", ErrorCodeInvalidDigest: "provided digest did not match uploaded content",
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",
ErrorCodeUnverifiedManifest: "Manifest failed signature validation", ErrorCodeUnknownManifest: "manifest not known",
ErrorCodeUnknownLayer: "Referenced layer not available", ErrorCodeInvalidManifest: "manifest is invalid",
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
ErrorCodeUnknownLayer: "referenced layer not available",
ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload", ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload",
ErrorCodeUntrustedSignature: "Manifest signed by untrusted source", ErrorCodeUntrustedSignature: "manifest signed by untrusted source",
} }
var stringToErrorCode map[string]ErrorCode var stringToErrorCode map[string]ErrorCode
@ -178,7 +190,12 @@ func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
// PushErr pushes an error interface onto the error stack. // PushErr pushes an error interface onto the error stack.
func (errs *Errors) PushErr(err error) { func (errs *Errors) PushErr(err error) {
switch err.(type) {
case Error:
errs.Errors = append(errs.Errors, err) errs.Errors = append(errs.Errors, err)
default:
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
}
} }
func (errs *Errors) Error() string { func (errs *Errors) Error() string {

View file

@ -69,7 +69,7 @@ func TestErrorsManagement(t *testing.T) {
t.Fatalf("error marashaling errors: %v", err) t.Fatalf("error marashaling errors: %v", err)
} }
expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"Referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}" expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}"
if string(p) != expectedJSON { if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)

View file

@ -4,8 +4,6 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"github.com/gorilla/mux"
) )
// serveJSON marshals v and sets the content-type header to // serveJSON marshals v and sets the content-type header to
@ -32,10 +30,3 @@ func closeResources(handler http.Handler, closers ...io.Closer) http.Handler {
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
}) })
} }
// clondedRoute returns a clone of the named route from the router.
func clonedRoute(router *mux.Router, name string) *mux.Route {
route := new(mux.Route)
*route = *router.GetRoute(name) // clone the route
return route
}

View file

@ -1,8 +1,13 @@
package registry package registry
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
@ -32,15 +37,77 @@ type imageManifestHandler struct {
// GetImageManifest fetches the image manifest from the storage backend, if it exists. // GetImageManifest fetches the image manifest from the storage backend, if it exists.
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
manifests := imh.services.Manifests()
manifest, err := manifests.Get(imh.Name, imh.Tag)
if err != nil {
imh.Errors.Push(ErrorCodeUnknownManifest, err)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", fmt.Sprint(len(manifest.Raw)))
w.Write(manifest.Raw)
} }
// PutImageManifest validates and stores and image in the registry. // PutImageManifest validates and stores and image in the registry.
func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) {
manifests := imh.services.Manifests()
dec := json.NewDecoder(r.Body)
var manifest storage.SignedManifest
if err := dec.Decode(&manifest); err != nil {
imh.Errors.Push(ErrorCodeInvalidManifest, err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := manifests.Put(imh.Name, imh.Tag, &manifest); err != nil {
// TODO(stevvooe): These error handling switches really need to be
// handled by an app global mapper.
switch err := err.(type) {
case storage.ErrManifestVerification:
for _, verificationError := range err {
switch verificationError := verificationError.(type) {
case storage.ErrUnknownLayer:
imh.Errors.Push(ErrorCodeUnknownLayer, verificationError.FSLayer)
case storage.ErrManifestUnverified:
imh.Errors.Push(ErrorCodeUnverifiedManifest)
default:
if verificationError == digest.ErrDigestInvalidFormat {
// TODO(stevvooe): We need to really need to move all
// errors to types. Its much more straightforward.
imh.Errors.Push(ErrorCodeInvalidDigest)
} else {
imh.Errors.PushErr(verificationError)
}
}
}
default:
imh.Errors.PushErr(err)
}
w.WriteHeader(http.StatusBadRequest)
return
}
} }
// DeleteImageManifest removes the image with the given tag from the registry. // DeleteImageManifest removes the image with the given tag from the registry.
func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) {
manifests := imh.services.Manifests()
if err := manifests.Delete(imh.Name, imh.Tag); err != nil {
switch err := err.(type) {
case storage.ErrUnknownManifest:
imh.Errors.Push(ErrorCodeUnknownManifest, err)
w.WriteHeader(http.StatusNotFound)
default:
imh.Errors.Push(ErrorCodeUnknown, err)
w.WriteHeader(http.StatusBadRequest)
}
return
}
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusAccepted)
} }

View file

@ -6,7 +6,6 @@ import (
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
// layerDispatcher uses the request context to build a layerHandler. // layerDispatcher uses the request context to build a layerHandler.
@ -47,33 +46,16 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
layer, err := layers.Fetch(lh.Name, lh.Digest) layer, err := layers.Fetch(lh.Name, lh.Digest)
if err != nil { if err != nil {
switch err { switch err := err.(type) {
case storage.ErrLayerUnknown: case storage.ErrUnknownLayer:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
lh.Errors.Push(ErrorCodeUnknownLayer, lh.Errors.Push(ErrorCodeUnknownLayer, err.FSLayer)
map[string]interface{}{
"unknown": storage.FSLayer{BlobSum: lh.Digest},
})
return
default: default:
lh.Errors.Push(ErrorCodeUnknown, err) lh.Errors.Push(ErrorCodeUnknown, err)
return
} }
return
} }
defer layer.Close() defer layer.Close()
http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer) http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer)
} }
func buildLayerURL(router *mux.Router, r *http.Request, layer storage.Layer) (string, error) {
route := clonedRoute(router, routeNameBlob)
layerURL, err := route.Schemes(r.URL.Scheme).Host(r.Host).
URL("name", layer.Name(),
"digest", layer.Digest().String())
if err != nil {
return "", err
}
return layerURL.String(), nil
}

View file

@ -10,7 +10,6 @@ import (
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
// layerUploadDispatcher constructs and returns the layer upload handler for // layerUploadDispatcher constructs and returns the layer upload handler for
@ -151,7 +150,7 @@ func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.
// chunk responses. This sets the correct headers but the response status is // chunk responses. This sets the correct headers but the response status is
// left to the caller. // left to the caller.
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error { func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error {
uploadURL, err := buildLayerUploadURL(luh.router, r, luh.Upload) uploadURL, err := luh.urlBuilder.forLayerUpload(luh.Upload)
if err != nil { if err != nil {
logrus.Infof("error building upload url: %s", err) logrus.Infof("error building upload url: %s", err)
return err return err
@ -171,7 +170,7 @@ var errNotReadyToComplete = fmt.Errorf("not ready to complete upload")
func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *http.Request) error { func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *http.Request) error {
// If we get a digest and length, we can finish the upload. // If we get a digest and length, we can finish the upload.
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
sizeStr := r.FormValue("length") sizeStr := r.FormValue("size")
if dgstStr == "" || sizeStr == "" { if dgstStr == "" || sizeStr == "" {
return errNotReadyToComplete return errNotReadyToComplete
@ -200,7 +199,7 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req
return return
} }
layerURL, err := buildLayerURL(luh.router, r, layer) layerURL, err := luh.urlBuilder.forLayer(layer)
if err != nil { if err != nil {
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -211,15 +210,3 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
} }
func buildLayerUploadURL(router *mux.Router, r *http.Request, upload storage.LayerUpload) (string, error) {
route := clonedRoute(router, routeNameBlobUploadResume)
uploadURL, err := route.Schemes(r.URL.Scheme).Host(r.Host).
URL("name", upload.Name(), "uuid", upload.UUID())
if err != nil {
return "", err
}
return uploadURL.String(), nil
}

141
urls.go Normal file
View file

@ -0,0 +1,141 @@
package registry
import (
"net/http"
"net/url"
"github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
"github.com/gorilla/mux"
)
type urlBuilder struct {
url *url.URL // url root (ie http://localhost/)
router *mux.Router
}
func newURLBuilder(root *url.URL) *urlBuilder {
return &urlBuilder{
url: root,
router: v2APIRouter(),
}
}
func newURLBuilderFromRequest(r *http.Request) *urlBuilder {
u := &url.URL{
Scheme: r.URL.Scheme,
Host: r.Host,
}
return newURLBuilder(u)
}
func newURLBuilderFromString(root string) (*urlBuilder, error) {
u, err := url.Parse(root)
if err != nil {
return nil, err
}
return newURLBuilder(u), nil
}
func (ub *urlBuilder) forManifest(m *storage.Manifest) (string, error) {
return ub.buildManifestURL(m.Name, m.Tag)
}
func (ub *urlBuilder) buildManifestURL(name, tag string) (string, error) {
route := clonedRoute(ub.router, routeNameImageManifest)
manifestURL, err := route.
Schemes(ub.url.Scheme).
Host(ub.url.Host).
URL("name", name, "tag", tag)
if err != nil {
return "", err
}
return manifestURL.String(), nil
}
func (ub *urlBuilder) forLayer(l storage.Layer) (string, error) {
return ub.buildLayerURL(l.Name(), l.Digest())
}
func (ub *urlBuilder) buildLayerURL(name string, dgst digest.Digest) (string, error) {
route := clonedRoute(ub.router, routeNameBlob)
layerURL, err := route.
Schemes(ub.url.Scheme).
Host(ub.url.Host).
URL("name", name, "digest", dgst.String())
if err != nil {
return "", err
}
return layerURL.String(), nil
}
func (ub *urlBuilder) buildLayerUploadURL(name string) (string, error) {
route := clonedRoute(ub.router, routeNameBlobUpload)
uploadURL, err := route.
Schemes(ub.url.Scheme).
Host(ub.url.Host).
URL("name", name)
if err != nil {
return "", err
}
return uploadURL.String(), nil
}
func (ub *urlBuilder) forLayerUpload(layerUpload storage.LayerUpload) (string, error) {
return ub.buildLayerUploadResumeURL(layerUpload.Name(), layerUpload.UUID())
}
func (ub *urlBuilder) buildLayerUploadResumeURL(name, uuid string, values ...url.Values) (string, error) {
route := clonedRoute(ub.router, routeNameBlobUploadResume)
uploadURL, err := route.
Schemes(ub.url.Scheme).
Host(ub.url.Host).
URL("name", name, "uuid", uuid)
if err != nil {
return "", err
}
return appendValuesURL(uploadURL, values...).String(), 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()
}
// clondedRoute returns a clone of the named route from the router.
func clonedRoute(router *mux.Router, name string) *mux.Route {
route := new(mux.Route)
*route = *router.GetRoute(name) // clone the route
return route
}