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:
parent
6fead90736
commit
e809796f59
10 changed files with 581 additions and 210 deletions
491
api_test.go
491
api_test.go
|
@ -1,6 +1,8 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -10,7 +12,9 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
"github.com/docker/docker-registry/storage"
|
||||
_ "github.com/docker/docker-registry/storagedriver/inmemory"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
|
@ -34,11 +38,10 @@ func TestLayerAPI(t *testing.T) {
|
|||
|
||||
app := NewApp(config)
|
||||
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
|
||||
router := v2APIRouter()
|
||||
builder, err := newURLBuilderFromString(server.URL)
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing server url: %v", err)
|
||||
t.Fatalf("error creating url builder: %v", err)
|
||||
}
|
||||
|
||||
imageName := "foo/bar"
|
||||
|
@ -52,154 +55,65 @@ func TestLayerAPI(t *testing.T) {
|
|||
|
||||
// -----------------------------------
|
||||
// Test fetch for non-existent content
|
||||
r, err := router.GetRoute(routeNameBlob).Host(u.Host).
|
||||
URL("name", imageName,
|
||||
"digest", tarSumStr)
|
||||
layerURL, err := builder.buildLayerURL(imageName, layerDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("error building url: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(r.String())
|
||||
resp, err := http.Get(layerURL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error fetching 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 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)
|
||||
}
|
||||
checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound)
|
||||
|
||||
// ------------------------------------------
|
||||
// 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)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
break // expected
|
||||
default:
|
||||
d, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status)
|
||||
}
|
||||
checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound)
|
||||
|
||||
t.Logf("response:\n%s", string(d))
|
||||
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status)
|
||||
// ------------------------------------------
|
||||
// Upload a layer
|
||||
layerUploadURL, err := builder.buildLayerUploadURL(imageName)
|
||||
if err != nil {
|
||||
t.Fatalf("error building upload url: %v", err)
|
||||
}
|
||||
|
||||
logrus.Infof("fetch the layer")
|
||||
resp, err = http.Post(layerUploadURL, "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting layer upload: %v", err)
|
||||
}
|
||||
|
||||
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!
|
||||
resp, err = http.Get(layerURL)
|
||||
|
@ -207,30 +121,299 @@ func TestLayerAPI(t *testing.T) {
|
|||
t.Fatalf("unexpected error fetching layer: %v", err)
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
break // expected
|
||||
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)
|
||||
}
|
||||
checkResponse(t, "fetching layer", resp, http.StatusOK)
|
||||
checkHeaders(t, resp, http.Header{
|
||||
"Content-Length": []string{fmt.Sprint(layerLength)},
|
||||
})
|
||||
|
||||
// Verify the body
|
||||
verifier := digest.NewDigestVerifier(layerDigest)
|
||||
io.Copy(verifier, resp.Body)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
app.go
5
app.go
|
@ -108,8 +108,9 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
context := &Context{
|
||||
App: app,
|
||||
Name: vars["name"],
|
||||
App: app,
|
||||
Name: vars["name"],
|
||||
urlBuilder: newURLBuilderFromRequest(r),
|
||||
}
|
||||
|
||||
// Store vars for underlying handlers.
|
||||
|
|
|
@ -24,4 +24,6 @@ type Context struct {
|
|||
|
||||
// log provides a context specific logger.
|
||||
log *logrus.Entry
|
||||
|
||||
urlBuilder *urlBuilder
|
||||
}
|
||||
|
|
29
errors.go
29
errors.go
|
@ -34,6 +34,14 @@ const (
|
|||
// match the provided tag.
|
||||
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
|
||||
// validation.
|
||||
ErrorCodeUnverifiedManifest
|
||||
|
@ -56,6 +64,8 @@ var errorCodeStrings = map[ErrorCode]string{
|
|||
ErrorCodeInvalidLength: "INVALID_LENGTH",
|
||||
ErrorCodeInvalidName: "INVALID_NAME",
|
||||
ErrorCodeInvalidTag: "INVALID_TAG",
|
||||
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
|
||||
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
|
||||
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
|
||||
ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
|
||||
ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD",
|
||||
|
@ -66,12 +76,14 @@ var errorCodesMessages = map[ErrorCode]string{
|
|||
ErrorCodeUnknown: "unknown error",
|
||||
ErrorCodeInvalidDigest: "provided digest did not match uploaded content",
|
||||
ErrorCodeInvalidLength: "provided length did not match content length",
|
||||
ErrorCodeInvalidName: "Manifest name did not match URI",
|
||||
ErrorCodeInvalidTag: "Manifest tag did not match URI",
|
||||
ErrorCodeUnverifiedManifest: "Manifest failed signature validation",
|
||||
ErrorCodeUnknownLayer: "Referenced layer not available",
|
||||
ErrorCodeInvalidName: "manifest name did not match URI",
|
||||
ErrorCodeInvalidTag: "manifest tag did not match URI",
|
||||
ErrorCodeUnknownManifest: "manifest not known",
|
||||
ErrorCodeInvalidManifest: "manifest is invalid",
|
||||
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
|
||||
ErrorCodeUnknownLayer: "referenced layer not available",
|
||||
ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload",
|
||||
ErrorCodeUntrustedSignature: "Manifest signed by untrusted source",
|
||||
ErrorCodeUntrustedSignature: "manifest signed by untrusted source",
|
||||
}
|
||||
|
||||
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.
|
||||
func (errs *Errors) PushErr(err error) {
|
||||
errs.Errors = append(errs.Errors, err)
|
||||
switch err.(type) {
|
||||
case Error:
|
||||
errs.Errors = append(errs.Errors, err)
|
||||
default:
|
||||
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
func (errs *Errors) Error() string {
|
||||
|
|
|
@ -69,7 +69,7 @@ func TestErrorsManagement(t *testing.T) {
|
|||
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 {
|
||||
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
|
||||
|
|
|
@ -4,8 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
67
images.go
67
images.go
|
@ -1,8 +1,13 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
|
||||
"github.com/docker/docker-registry/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
|
@ -32,15 +37,77 @@ type imageManifestHandler struct {
|
|||
|
||||
// GetImageManifest fetches the image manifest from the storage backend, if it exists.
|
||||
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.
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
|
26
layer.go
26
layer.go
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
if err != nil {
|
||||
switch err {
|
||||
case storage.ErrLayerUnknown:
|
||||
switch err := err.(type) {
|
||||
case storage.ErrUnknownLayer:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
lh.Errors.Push(ErrorCodeUnknownLayer,
|
||||
map[string]interface{}{
|
||||
"unknown": storage.FSLayer{BlobSum: lh.Digest},
|
||||
})
|
||||
return
|
||||
lh.Errors.Push(ErrorCodeUnknownLayer, err.FSLayer)
|
||||
default:
|
||||
lh.Errors.Push(ErrorCodeUnknown, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
defer layer.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/docker/docker-registry/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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
|
||||
// left to the caller.
|
||||
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 {
|
||||
logrus.Infof("error building upload url: %s", 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 {
|
||||
// If we get a digest and length, we can finish the upload.
|
||||
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
|
||||
sizeStr := r.FormValue("length")
|
||||
sizeStr := r.FormValue("size")
|
||||
|
||||
if dgstStr == "" || sizeStr == "" {
|
||||
return errNotReadyToComplete
|
||||
|
@ -200,7 +199,7 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
layerURL, err := buildLayerURL(luh.router, r, layer)
|
||||
layerURL, err := luh.urlBuilder.forLayer(layer)
|
||||
if err != nil {
|
||||
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||
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.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
141
urls.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue