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 <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2015-08-06 10:34:35 -07:00
parent f8109a78f9
commit c9bb330b71
8 changed files with 106 additions and 15 deletions

View file

@ -118,6 +118,7 @@ information about each option that appears later in this page.
age: 168h age: 168h
interval: 24h interval: 24h
dryrun: false dryrun: false
readonly: false
auth: auth:
silly: silly:
realm: silly-realm realm: silly-realm
@ -643,8 +644,9 @@ This storage backend uses Amazon's Simple Storage Service (S3).
### Maintenance ### Maintenance
Currently the registry can perform one maintenance function: upload purging. This and future Currently upload purging and read-only mode are the only maintenance functions available.
maintenance functions which are related to storage can be configured under the maintenance section. These and future maintenance functions which are related to storage can be configured under
the maintenance section.
### Upload Purging ### Upload Purging
@ -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). 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 ### Openstack Swift
This storage backend uses Openstack Swift object storage. This storage backend uses Openstack Swift object storage.

View file

@ -133,4 +133,14 @@ var (
longer proceed.`, longer proceed.`,
HTTPStatusCode: http.StatusNotFound, 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,
})
) )

View file

@ -633,6 +633,54 @@ func TestDeleteDisabled(t *testing.T) {
checkResponse(t, "deleting layer with delete disabled", resp, http.StatusMethodNotAllowed) 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) { func httpDelete(url string) (*http.Response, error) {
req, err := http.NewRequest("DELETE", url, nil) req, err := http.NewRequest("DELETE", url, nil)
if err != nil { if err != nil {

View file

@ -64,6 +64,9 @@ type App struct {
// true if this registry is configured as a pull through cache // true if this registry is configured as a pull through cache
isCache bool 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 // 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() purgeConfig := uploadPurgeDefaultConfig()
if mc, ok := configuration.Storage["maintenance"]; ok { if mc, ok := configuration.Storage["maintenance"]; ok {
for k, v := range mc { if v, ok := mc["uploadpurging"]; ok {
switch k { purgeConfig, ok = v.(map[interface{}]interface{})
case "uploadpurging": if !ok {
purgeConfig = v.(map[interface{}]interface{}) 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) startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig)

View file

@ -35,7 +35,7 @@ func blobDispatcher(ctx *Context, r *http.Request) http.Handler {
return handlers.MethodHandler{ return handlers.MethodHandler{
"GET": http.HandlerFunc(blobHandler.GetBlob), "GET": http.HandlerFunc(blobHandler.GetBlob),
"HEAD": http.HandlerFunc(blobHandler.GetBlob), "HEAD": http.HandlerFunc(blobHandler.GetBlob),
"DELETE": http.HandlerFunc(blobHandler.DeleteBlob), "DELETE": mutableHandler(blobHandler.DeleteBlob, ctx),
} }
} }

View file

@ -23,12 +23,12 @@ func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
} }
handler := http.Handler(handlers.MethodHandler{ handler := http.Handler(handlers.MethodHandler{
"POST": http.HandlerFunc(buh.StartBlobUpload), "POST": mutableHandler(buh.StartBlobUpload, ctx),
"GET": http.HandlerFunc(buh.GetUploadStatus), "GET": http.HandlerFunc(buh.GetUploadStatus),
"HEAD": http.HandlerFunc(buh.GetUploadStatus), "HEAD": http.HandlerFunc(buh.GetUploadStatus),
"PATCH": http.HandlerFunc(buh.PatchBlobData), "PATCH": mutableHandler(buh.PatchBlobData, ctx),
"PUT": http.HandlerFunc(buh.PutBlobUploadComplete), "PUT": mutableHandler(buh.PutBlobUploadComplete, ctx),
"DELETE": http.HandlerFunc(buh.CancelBlobUpload), "DELETE": mutableHandler(buh.CancelBlobUpload, ctx),
}) })
if buh.UUID != "" { if buh.UUID != "" {

View file

@ -7,6 +7,7 @@ import (
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
) )
// closeResources closes all the provided resources after running the target // 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 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)
}
}

View file

@ -34,8 +34,8 @@ func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
return handlers.MethodHandler{ return handlers.MethodHandler{
"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest),
"PUT": http.HandlerFunc(imageManifestHandler.PutImageManifest), "PUT": mutableHandler(imageManifestHandler.PutImageManifest, ctx),
"DELETE": http.HandlerFunc(imageManifestHandler.DeleteImageManifest), "DELETE": mutableHandler(imageManifestHandler.DeleteImageManifest, ctx),
} }
} }