From c9bb330b710b03b90b300bf4ad30e5b7c8d8eb20 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 6 Aug 2015 10:34:35 -0700 Subject: [PATCH] Add a read-only mode as a configuration option Add "readonly" under the storage/maintenance section. When this is set to true, uploads and deletions will return 503 Service Unavailable errors. Document the parameter and add some unit testing. Signed-off-by: Aaron Lehmann --- docs/configuration.md | 17 +++++++++--- registry/api/v2/errors.go | 10 +++++++ registry/handlers/api_test.go | 48 +++++++++++++++++++++++++++++++++ registry/handlers/app.go | 18 +++++++++---- registry/handlers/blob.go | 2 +- registry/handlers/blobupload.go | 8 +++--- registry/handlers/helpers.go | 14 ++++++++++ registry/handlers/images.go | 4 +-- 8 files changed, 106 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b4080cb07..6d6d52a96 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -118,6 +118,7 @@ information about each option that appears later in this page. age: 168h interval: 24h dryrun: false + readonly: false auth: silly: realm: silly-realm @@ -643,14 +644,15 @@ This storage backend uses Amazon's Simple Storage Service (S3). ### Maintenance -Currently the registry can perform one maintenance function: upload purging. This and future -maintenance functions which are related to storage can be configured under the maintenance section. +Currently upload purging and read-only mode are the only maintenance functions available. +These and future maintenance functions which are related to storage can be configured under +the maintenance section. ### Upload Purging Upload purging is a background process that periodically removes orphaned files from the upload directories of the registry. Upload purging is enabled by default. To - configure upload directory purging, the following parameters +configure upload directory purging, the following parameters must be set. @@ -663,6 +665,15 @@ must be set. Note: `age` and `interval` are strings containing a number with optional fraction and a unit suffix: e.g. 45m, 2h10m, 168h (1 week). +### Read-only mode + +If the `readonly` parameter in the `maintenance` section is set to true, clients +will not be allowed to write to the registry. This mode is useful to temporarily +prevent writes to the backend storage so a garbage collection pass can be run. +Before running garbage collection, the registry should be restarted with +`readonly` set to true. After the garbage collection pass finishes, the registry +may be restarted again, this time with `readonly` removed from the configuration. + ### Openstack Swift This storage backend uses Openstack Swift object storage. diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go index ece52a2cd..97cb03e28 100644 --- a/registry/api/v2/errors.go +++ b/registry/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/registry/handlers/api_test.go b/registry/handlers/api_test.go index 52a74a2b8..e85ae4348 100644 --- a/registry/handlers/api_test.go +++ b/registry/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/registry/handlers/app.go b/registry/handlers/app.go index 5103c5fbe..d851714ad 100644 --- a/registry/handlers/app.go +++ b/registry/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/registry/handlers/blob.go b/registry/handlers/blob.go index 4a923aa51..69c39841b 100644 --- a/registry/handlers/blob.go +++ b/registry/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/registry/handlers/blobupload.go b/registry/handlers/blobupload.go index bbb70b59d..198a8f67f 100644 --- a/registry/handlers/blobupload.go +++ b/registry/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/registry/handlers/helpers.go b/registry/handlers/helpers.go index 5a3c99841..9b462a192 100644 --- a/registry/handlers/helpers.go +++ b/registry/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/registry/handlers/images.go b/registry/handlers/images.go index e19317302..78e36a13b 100644 --- a/registry/handlers/images.go +++ b/registry/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), } }