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>master
							parent
							
								
									f8109a78f9
								
							
						
					
					
						commit
						c9bb330b71
					
				|  | @ -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. | ||||
|  |  | |||
|  | @ -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, | ||||
| 	}) | ||||
| ) | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 != "" { | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue