From c9bb330b710b03b90b300bf4ad30e5b7c8d8eb20 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 6 Aug 2015 10:34:35 -0700 Subject: [PATCH 1/2] 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), } } From a601f923368125f3d3f7f52f9711272be386599c Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 6 Aug 2015 18:02:43 -0700 Subject: [PATCH 2/2] Add an "enabled" parameter under "readonly", and make it as if the mutable handlers don't exist when read-only mode is enabled Signed-off-by: Aaron Lehmann --- docs/configuration.md | 16 +++++++++------- registry/api/v2/errors.go | 10 ---------- registry/handlers/api_test.go | 4 ++-- registry/handlers/app.go | 10 ++++++++-- registry/handlers/blob.go | 13 +++++++++---- registry/handlers/blobupload.go | 21 ++++++++++++--------- registry/handlers/helpers.go | 14 -------------- registry/handlers/images.go | 13 +++++++++---- 8 files changed, 49 insertions(+), 52 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6d6d52a96..b076191fd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -118,7 +118,8 @@ information about each option that appears later in this page. age: 168h interval: 24h dryrun: false - readonly: false + readonly: + enabled: false auth: silly: realm: silly-realm @@ -667,12 +668,13 @@ Note: `age` and `interval` are strings containing a number with optional fractio ### 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. +If the `readonly` section under `maintenance` has `enabled` 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's `enabled` set to true. After the garbage collection +pass finishes, the registry may be restarted again, this time with `readonly` +removed from the configuration (or set to false). ### Openstack Swift diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go index 97cb03e28..ece52a2cd 100644 --- a/registry/api/v2/errors.go +++ b/registry/api/v2/errors.go @@ -133,14 +133,4 @@ 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 e85ae4348..0a0b264b9 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -658,7 +658,7 @@ func TestDeleteReadOnly(t *testing.T) { t.Fatalf("unexpected error deleting layer: %v", err) } - checkResponse(t, "deleting layer in read-only mode", resp, http.StatusServiceUnavailable) + checkResponse(t, "deleting layer in read-only mode", resp, http.StatusMethodNotAllowed) } func TestStartPushReadOnly(t *testing.T) { @@ -678,7 +678,7 @@ func TestStartPushReadOnly(t *testing.T) { } defer resp.Body.Close() - checkResponse(t, "starting push in read-only mode", resp, http.StatusServiceUnavailable) + checkResponse(t, "starting push in read-only mode", resp, http.StatusMethodNotAllowed) } func httpDelete(url string) (*http.Response, error) { diff --git a/registry/handlers/app.go b/registry/handlers/app.go index d851714ad..b11dc5b6d 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -109,9 +109,15 @@ func NewApp(ctx context.Context, configuration *configuration.Configuration) *Ap } } if v, ok := mc["readonly"]; ok { - app.readOnly, ok = v.(bool) + readOnly, ok := v.(map[interface{}]interface{}) if !ok { - panic("readonly config key must have a boolean value") + panic("readonly config key must contain additional keys") + } + if readOnlyEnabled, ok := readOnly["enabled"]; ok { + app.readOnly, ok = readOnlyEnabled.(bool) + if !ok { + panic("readonly's enabled config key must have a boolean value") + } } } } diff --git a/registry/handlers/blob.go b/registry/handlers/blob.go index 69c39841b..fb250acd2 100644 --- a/registry/handlers/blob.go +++ b/registry/handlers/blob.go @@ -32,11 +32,16 @@ func blobDispatcher(ctx *Context, r *http.Request) http.Handler { Digest: dgst, } - return handlers.MethodHandler{ - "GET": http.HandlerFunc(blobHandler.GetBlob), - "HEAD": http.HandlerFunc(blobHandler.GetBlob), - "DELETE": mutableHandler(blobHandler.DeleteBlob, ctx), + mhandler := handlers.MethodHandler{ + "GET": http.HandlerFunc(blobHandler.GetBlob), + "HEAD": http.HandlerFunc(blobHandler.GetBlob), } + + if !ctx.readOnly { + mhandler["DELETE"] = http.HandlerFunc(blobHandler.DeleteBlob) + } + + return mhandler } // blobHandler serves http blob requests. diff --git a/registry/handlers/blobupload.go b/registry/handlers/blobupload.go index 198a8f67f..1bd33d337 100644 --- a/registry/handlers/blobupload.go +++ b/registry/handlers/blobupload.go @@ -22,14 +22,17 @@ func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { UUID: getUploadUUID(ctx), } - handler := http.Handler(handlers.MethodHandler{ - "POST": mutableHandler(buh.StartBlobUpload, ctx), - "GET": http.HandlerFunc(buh.GetUploadStatus), - "HEAD": http.HandlerFunc(buh.GetUploadStatus), - "PATCH": mutableHandler(buh.PatchBlobData, ctx), - "PUT": mutableHandler(buh.PutBlobUploadComplete, ctx), - "DELETE": mutableHandler(buh.CancelBlobUpload, ctx), - }) + handler := handlers.MethodHandler{ + "GET": http.HandlerFunc(buh.GetUploadStatus), + "HEAD": http.HandlerFunc(buh.GetUploadStatus), + } + + if !ctx.readOnly { + handler["POST"] = http.HandlerFunc(buh.StartBlobUpload) + handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData) + handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete) + handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload) + } if buh.UUID != "" { state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state")) @@ -93,7 +96,7 @@ func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { } } - handler = closeResources(handler, buh.Upload) + return closeResources(handler, buh.Upload) } return handler diff --git a/registry/handlers/helpers.go b/registry/handlers/helpers.go index 9b462a192..5a3c99841 100644 --- a/registry/handlers/helpers.go +++ b/registry/handlers/helpers.go @@ -7,7 +7,6 @@ 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 @@ -61,16 +60,3 @@ 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 78e36a13b..0aeeb6f0f 100644 --- a/registry/handlers/images.go +++ b/registry/handlers/images.go @@ -32,11 +32,16 @@ func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { imageManifestHandler.Digest = dgst } - return handlers.MethodHandler{ - "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), - "PUT": mutableHandler(imageManifestHandler.PutImageManifest, ctx), - "DELETE": mutableHandler(imageManifestHandler.DeleteImageManifest, ctx), + mhandler := handlers.MethodHandler{ + "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), } + + if !ctx.readOnly { + mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest) + mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest) + } + + return mhandler } // imageManifestHandler handles http operations on image manifests.