diff --git a/docs/api/v2/errors.go b/docs/api/v2/errors.go index ece52a2cd..97cb03e28 100644 --- a/docs/api/v2/errors.go +++ b/docs/api/v2/errors.go @@ -133,4 +133,14 @@ var ( longer proceed.`, HTTPStatusCode: http.StatusNotFound, }) + + // ErrorCodeMaintenanceMode is returned when an upload can't be + // accepted because the registry is in maintenance mode. + ErrorCodeMaintenanceMode = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "MAINTENANCE_MODE", + Message: "registry in maintenance mode", + Description: `The upload cannot be accepted because the registry + is running read-only in maintenance mode.`, + HTTPStatusCode: http.StatusServiceUnavailable, + }) ) diff --git a/docs/handlers/api_test.go b/docs/handlers/api_test.go index 52a74a2b8..e85ae4348 100644 --- a/docs/handlers/api_test.go +++ b/docs/handlers/api_test.go @@ -633,6 +633,54 @@ func TestDeleteDisabled(t *testing.T) { checkResponse(t, "deleting layer with delete disabled", resp, http.StatusMethodNotAllowed) } +func TestDeleteReadOnly(t *testing.T) { + env := newTestEnv(t, true) + + imageName := "foo/bar" + // "build" our layer file + layerFile, tarSumStr, err := testutil.CreateRandomTarFile() + if err != nil { + t.Fatalf("error creating random layer file: %v", err) + } + + layerDigest := digest.Digest(tarSumStr) + layerURL, err := env.builder.BuildBlobURL(imageName, layerDigest) + if err != nil { + t.Fatalf("Error building blob URL") + } + uploadURLBase, _ := startPushLayer(t, env.builder, imageName) + pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) + + env.app.readOnly = true + + resp, err := httpDelete(layerURL) + if err != nil { + t.Fatalf("unexpected error deleting layer: %v", err) + } + + checkResponse(t, "deleting layer in read-only mode", resp, http.StatusServiceUnavailable) +} + +func TestStartPushReadOnly(t *testing.T) { + env := newTestEnv(t, true) + env.app.readOnly = true + + imageName := "foo/bar" + + layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) + 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, "starting push in read-only mode", resp, http.StatusServiceUnavailable) +} + func httpDelete(url string) (*http.Response, error) { req, err := http.NewRequest("DELETE", url, nil) if err != nil { diff --git a/docs/handlers/app.go b/docs/handlers/app.go index 5103c5fbe..d851714ad 100644 --- a/docs/handlers/app.go +++ b/docs/handlers/app.go @@ -64,6 +64,9 @@ type App struct { // true if this registry is configured as a pull through cache isCache bool + + // true if the registry is in a read-only maintenance mode + readOnly bool } // NewApp takes a configuration and returns a configured app, ready to serve @@ -99,13 +102,18 @@ func NewApp(ctx context.Context, configuration *configuration.Configuration) *Ap purgeConfig := uploadPurgeDefaultConfig() if mc, ok := configuration.Storage["maintenance"]; ok { - for k, v := range mc { - switch k { - case "uploadpurging": - purgeConfig = v.(map[interface{}]interface{}) + if v, ok := mc["uploadpurging"]; ok { + purgeConfig, ok = v.(map[interface{}]interface{}) + if !ok { + panic("uploadpurging config key must contain additional keys") + } + } + if v, ok := mc["readonly"]; ok { + app.readOnly, ok = v.(bool) + if !ok { + panic("readonly config key must have a boolean value") } } - } startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig) diff --git a/docs/handlers/blob.go b/docs/handlers/blob.go index 4a923aa51..69c39841b 100644 --- a/docs/handlers/blob.go +++ b/docs/handlers/blob.go @@ -35,7 +35,7 @@ func blobDispatcher(ctx *Context, r *http.Request) http.Handler { return handlers.MethodHandler{ "GET": http.HandlerFunc(blobHandler.GetBlob), "HEAD": http.HandlerFunc(blobHandler.GetBlob), - "DELETE": http.HandlerFunc(blobHandler.DeleteBlob), + "DELETE": mutableHandler(blobHandler.DeleteBlob, ctx), } } diff --git a/docs/handlers/blobupload.go b/docs/handlers/blobupload.go index bbb70b59d..198a8f67f 100644 --- a/docs/handlers/blobupload.go +++ b/docs/handlers/blobupload.go @@ -23,12 +23,12 @@ func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { } handler := http.Handler(handlers.MethodHandler{ - "POST": http.HandlerFunc(buh.StartBlobUpload), + "POST": mutableHandler(buh.StartBlobUpload, ctx), "GET": http.HandlerFunc(buh.GetUploadStatus), "HEAD": http.HandlerFunc(buh.GetUploadStatus), - "PATCH": http.HandlerFunc(buh.PatchBlobData), - "PUT": http.HandlerFunc(buh.PutBlobUploadComplete), - "DELETE": http.HandlerFunc(buh.CancelBlobUpload), + "PATCH": mutableHandler(buh.PatchBlobData, ctx), + "PUT": mutableHandler(buh.PutBlobUploadComplete, ctx), + "DELETE": mutableHandler(buh.CancelBlobUpload, ctx), }) if buh.UUID != "" { diff --git a/docs/handlers/helpers.go b/docs/handlers/helpers.go index 5a3c99841..9b462a192 100644 --- a/docs/handlers/helpers.go +++ b/docs/handlers/helpers.go @@ -7,6 +7,7 @@ import ( ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/distribution/registry/api/v2" ) // closeResources closes all the provided resources after running the target @@ -60,3 +61,16 @@ func copyFullPayload(responseWriter http.ResponseWriter, r *http.Request, destWr return nil } + +// mutableHandler wraps a http.HandlerFunc with a check that the registry is +// not in read-only mode. If it is in read-only mode, the wrapper returns +// v2.ErrorCodeMaintenanceMode to the client. +func mutableHandler(handler http.HandlerFunc, ctx *Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if ctx.App.readOnly { + ctx.Errors = append(ctx.Errors, v2.ErrorCodeMaintenanceMode) + return + } + handler(w, r) + } +} diff --git a/docs/handlers/images.go b/docs/handlers/images.go index e19317302..78e36a13b 100644 --- a/docs/handlers/images.go +++ b/docs/handlers/images.go @@ -34,8 +34,8 @@ func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { return handlers.MethodHandler{ "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), - "PUT": http.HandlerFunc(imageManifestHandler.PutImageManifest), - "DELETE": http.HandlerFunc(imageManifestHandler.DeleteImageManifest), + "PUT": mutableHandler(imageManifestHandler.PutImageManifest, ctx), + "DELETE": mutableHandler(imageManifestHandler.DeleteImageManifest, ctx), } }