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 5a8fabfee3
commit df9758ba39
7 changed files with 92 additions and 12 deletions

View file

@ -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,
})
)

View file

@ -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 {

View file

@ -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)

View file

@ -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),
}
}

View file

@ -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 != "" {

View file

@ -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)
}
}

View file

@ -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),
}
}