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:
parent
f8109a78f9
commit
c9bb330b71
8 changed files with 106 additions and 15 deletions
|
@ -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,14 +644,15 @@ 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
|
||||||
|
|
||||||
Upload purging is a background process that periodically removes orphaned files from the upload
|
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
|
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.
|
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).
|
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.
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 != "" {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue