github.com/lusis/distribution@v2.0.1+incompatible/registry/handlers/app.go (about)

     1  package handlers
     2  
     3  import (
     4  	"expvar"
     5  	"fmt"
     6  	"math/rand"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/docker/distribution"
    13  	"github.com/docker/distribution/configuration"
    14  	ctxu "github.com/docker/distribution/context"
    15  	"github.com/docker/distribution/notifications"
    16  	"github.com/docker/distribution/registry/api/v2"
    17  	"github.com/docker/distribution/registry/auth"
    18  	registrymiddleware "github.com/docker/distribution/registry/middleware/registry"
    19  	repositorymiddleware "github.com/docker/distribution/registry/middleware/repository"
    20  	"github.com/docker/distribution/registry/storage"
    21  	"github.com/docker/distribution/registry/storage/cache"
    22  	storagedriver "github.com/docker/distribution/registry/storage/driver"
    23  	"github.com/docker/distribution/registry/storage/driver/factory"
    24  	storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware"
    25  	"github.com/garyburd/redigo/redis"
    26  	"github.com/gorilla/mux"
    27  	"golang.org/x/net/context"
    28  )
    29  
    30  // App is a global registry application object. Shared resources can be placed
    31  // on this object that will be accessible from all requests. Any writable
    32  // fields should be protected.
    33  type App struct {
    34  	context.Context
    35  
    36  	Config configuration.Configuration
    37  
    38  	router           *mux.Router                 // main application router, configured with dispatchers
    39  	driver           storagedriver.StorageDriver // driver maintains the app global storage driver instance.
    40  	registry         distribution.Namespace      // registry is the primary registry backend for the app instance.
    41  	accessController auth.AccessController       // main access controller for application
    42  
    43  	// events contains notification related configuration.
    44  	events struct {
    45  		sink   notifications.Sink
    46  		source notifications.SourceRecord
    47  	}
    48  
    49  	redis *redis.Pool
    50  }
    51  
    52  // NewApp takes a configuration and returns a configured app, ready to serve
    53  // requests. The app only implements ServeHTTP and can be wrapped in other
    54  // handlers accordingly.
    55  func NewApp(ctx context.Context, configuration configuration.Configuration) *App {
    56  	app := &App{
    57  		Config:  configuration,
    58  		Context: ctx,
    59  		router:  v2.RouterWithPrefix(configuration.HTTP.Prefix),
    60  	}
    61  
    62  	app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "instance.id"))
    63  
    64  	// Register the handler dispatchers.
    65  	app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
    66  		return http.HandlerFunc(apiBase)
    67  	})
    68  	app.register(v2.RouteNameManifest, imageManifestDispatcher)
    69  	app.register(v2.RouteNameTags, tagsDispatcher)
    70  	app.register(v2.RouteNameBlob, layerDispatcher)
    71  	app.register(v2.RouteNameBlobUpload, layerUploadDispatcher)
    72  	app.register(v2.RouteNameBlobUploadChunk, layerUploadDispatcher)
    73  
    74  	var err error
    75  	app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters())
    76  
    77  	if err != nil {
    78  		// TODO(stevvooe): Move the creation of a service into a protected
    79  		// method, where this is created lazily. Its status can be queried via
    80  		// a health check.
    81  		panic(err)
    82  	}
    83  
    84  	purgeConfig := uploadPurgeDefaultConfig()
    85  	if mc, ok := configuration.Storage["maintenance"]; ok {
    86  		for k, v := range mc {
    87  			switch k {
    88  			case "uploadpurging":
    89  				purgeConfig = v.(map[interface{}]interface{})
    90  			}
    91  		}
    92  
    93  	}
    94  
    95  	startUploadPurger(app.driver, ctxu.GetLogger(app), purgeConfig)
    96  
    97  	app.driver, err = applyStorageMiddleware(app.driver, configuration.Middleware["storage"])
    98  	if err != nil {
    99  		panic(err)
   100  	}
   101  
   102  	app.configureEvents(&configuration)
   103  	app.configureRedis(&configuration)
   104  
   105  	// configure storage caches
   106  	if cc, ok := configuration.Storage["cache"]; ok {
   107  		switch cc["layerinfo"] {
   108  		case "redis":
   109  			if app.redis == nil {
   110  				panic("redis configuration required to use for layerinfo cache")
   111  			}
   112  			app.registry = storage.NewRegistryWithDriver(app.driver, cache.NewRedisLayerInfoCache(app.redis))
   113  			ctxu.GetLogger(app).Infof("using redis layerinfo cache")
   114  		case "inmemory":
   115  			app.registry = storage.NewRegistryWithDriver(app.driver, cache.NewInMemoryLayerInfoCache())
   116  			ctxu.GetLogger(app).Infof("using inmemory layerinfo cache")
   117  		default:
   118  			if cc["layerinfo"] != "" {
   119  				ctxu.GetLogger(app).Warnf("unkown cache type %q, caching disabled", configuration.Storage["cache"])
   120  			}
   121  		}
   122  	}
   123  
   124  	if app.registry == nil {
   125  		// configure the registry if no cache section is available.
   126  		app.registry = storage.NewRegistryWithDriver(app.driver, nil)
   127  	}
   128  
   129  	app.registry, err = applyRegistryMiddleware(app.registry, configuration.Middleware["registry"])
   130  	if err != nil {
   131  		panic(err)
   132  	}
   133  
   134  	authType := configuration.Auth.Type()
   135  
   136  	if authType != "" {
   137  		accessController, err := auth.GetAccessController(configuration.Auth.Type(), configuration.Auth.Parameters())
   138  		if err != nil {
   139  			panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err))
   140  		}
   141  		app.accessController = accessController
   142  	}
   143  
   144  	return app
   145  }
   146  
   147  // register a handler with the application, by route name. The handler will be
   148  // passed through the application filters and context will be constructed at
   149  // request time.
   150  func (app *App) register(routeName string, dispatch dispatchFunc) {
   151  
   152  	// TODO(stevvooe): This odd dispatcher/route registration is by-product of
   153  	// some limitations in the gorilla/mux router. We are using it to keep
   154  	// routing consistent between the client and server, but we may want to
   155  	// replace it with manual routing and structure-based dispatch for better
   156  	// control over the request execution.
   157  
   158  	app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch))
   159  }
   160  
   161  // configureEvents prepares the event sink for action.
   162  func (app *App) configureEvents(configuration *configuration.Configuration) {
   163  	// Configure all of the endpoint sinks.
   164  	var sinks []notifications.Sink
   165  	for _, endpoint := range configuration.Notifications.Endpoints {
   166  		if endpoint.Disabled {
   167  			ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name)
   168  			continue
   169  		}
   170  
   171  		ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers)
   172  		endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{
   173  			Timeout:   endpoint.Timeout,
   174  			Threshold: endpoint.Threshold,
   175  			Backoff:   endpoint.Backoff,
   176  			Headers:   endpoint.Headers,
   177  		})
   178  
   179  		sinks = append(sinks, endpoint)
   180  	}
   181  
   182  	// NOTE(stevvooe): Moving to a new queueing implementation is as easy as
   183  	// replacing broadcaster with a rabbitmq implementation. It's recommended
   184  	// that the registry instances also act as the workers to keep deployment
   185  	// simple.
   186  	app.events.sink = notifications.NewBroadcaster(sinks...)
   187  
   188  	// Populate registry event source
   189  	hostname, err := os.Hostname()
   190  	if err != nil {
   191  		hostname = configuration.HTTP.Addr
   192  	} else {
   193  		// try to pick the port off the config
   194  		_, port, err := net.SplitHostPort(configuration.HTTP.Addr)
   195  		if err == nil {
   196  			hostname = net.JoinHostPort(hostname, port)
   197  		}
   198  	}
   199  
   200  	app.events.source = notifications.SourceRecord{
   201  		Addr:       hostname,
   202  		InstanceID: ctxu.GetStringValue(app, "instance.id"),
   203  	}
   204  }
   205  
   206  func (app *App) configureRedis(configuration *configuration.Configuration) {
   207  	if configuration.Redis.Addr == "" {
   208  		ctxu.GetLogger(app).Infof("redis not configured")
   209  		return
   210  	}
   211  
   212  	pool := &redis.Pool{
   213  		Dial: func() (redis.Conn, error) {
   214  			// TODO(stevvooe): Yet another use case for contextual timing.
   215  			ctx := context.WithValue(app, "redis.connect.startedat", time.Now())
   216  
   217  			done := func(err error) {
   218  				logger := ctxu.GetLoggerWithField(ctx, "redis.connect.duration",
   219  					ctxu.Since(ctx, "redis.connect.startedat"))
   220  				if err != nil {
   221  					logger.Errorf("redis: error connecting: %v", err)
   222  				} else {
   223  					logger.Infof("redis: connect %v", configuration.Redis.Addr)
   224  				}
   225  			}
   226  
   227  			conn, err := redis.DialTimeout("tcp",
   228  				configuration.Redis.Addr,
   229  				configuration.Redis.DialTimeout,
   230  				configuration.Redis.ReadTimeout,
   231  				configuration.Redis.WriteTimeout)
   232  			if err != nil {
   233  				ctxu.GetLogger(app).Errorf("error connecting to redis instance %s: %v",
   234  					configuration.Redis.Addr, err)
   235  				done(err)
   236  				return nil, err
   237  			}
   238  
   239  			// authorize the connection
   240  			if configuration.Redis.Password != "" {
   241  				if _, err = conn.Do("AUTH", configuration.Redis.Password); err != nil {
   242  					defer conn.Close()
   243  					done(err)
   244  					return nil, err
   245  				}
   246  			}
   247  
   248  			// select the database to use
   249  			if configuration.Redis.DB != 0 {
   250  				if _, err = conn.Do("SELECT", configuration.Redis.DB); err != nil {
   251  					defer conn.Close()
   252  					done(err)
   253  					return nil, err
   254  				}
   255  			}
   256  
   257  			done(nil)
   258  			return conn, nil
   259  		},
   260  		MaxIdle:     configuration.Redis.Pool.MaxIdle,
   261  		MaxActive:   configuration.Redis.Pool.MaxActive,
   262  		IdleTimeout: configuration.Redis.Pool.IdleTimeout,
   263  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
   264  			// TODO(stevvooe): We can probably do something more interesting
   265  			// here with the health package.
   266  			_, err := c.Do("PING")
   267  			return err
   268  		},
   269  		Wait: false, // if a connection is not avialable, proceed without cache.
   270  	}
   271  
   272  	app.redis = pool
   273  
   274  	// setup expvar
   275  	registry := expvar.Get("registry")
   276  	if registry == nil {
   277  		registry = expvar.NewMap("registry")
   278  	}
   279  
   280  	registry.(*expvar.Map).Set("redis", expvar.Func(func() interface{} {
   281  		return map[string]interface{}{
   282  			"Config": configuration.Redis,
   283  			"Active": app.redis.ActiveCount(),
   284  		}
   285  	}))
   286  }
   287  
   288  func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   289  	defer r.Body.Close() // ensure that request body is always closed.
   290  
   291  	// Instantiate an http context here so we can track the error codes
   292  	// returned by the request router.
   293  	ctx := defaultContextManager.context(app, w, r)
   294  	defer func() {
   295  		ctxu.GetResponseLogger(ctx).Infof("response completed")
   296  	}()
   297  	defer defaultContextManager.release(ctx)
   298  
   299  	// NOTE(stevvooe): Total hack to get instrumented responsewriter from context.
   300  	var err error
   301  	w, err = ctxu.GetResponseWriter(ctx)
   302  	if err != nil {
   303  		ctxu.GetLogger(ctx).Warnf("response writer not found in context")
   304  	}
   305  
   306  	// Set a header with the Docker Distribution API Version for all responses.
   307  	w.Header().Add("Docker-Distribution-API-Version", "registry/2.0")
   308  	app.router.ServeHTTP(w, r)
   309  }
   310  
   311  // dispatchFunc takes a context and request and returns a constructed handler
   312  // for the route. The dispatcher will use this to dynamically create request
   313  // specific handlers for each endpoint without creating a new router for each
   314  // request.
   315  type dispatchFunc func(ctx *Context, r *http.Request) http.Handler
   316  
   317  // TODO(stevvooe): dispatchers should probably have some validation error
   318  // chain with proper error reporting.
   319  
   320  // dispatcher returns a handler that constructs a request specific context and
   321  // handler, using the dispatch factory function.
   322  func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
   323  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   324  		context := app.context(w, r)
   325  
   326  		if err := app.authorized(w, r, context); err != nil {
   327  			ctxu.GetLogger(context).Errorf("error authorizing context: %v", err)
   328  			return
   329  		}
   330  
   331  		// Add username to request logging
   332  		context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, "auth.user.name"))
   333  
   334  		if app.nameRequired(r) {
   335  			repository, err := app.registry.Repository(context, getName(context))
   336  
   337  			if err != nil {
   338  				ctxu.GetLogger(context).Errorf("error resolving repository: %v", err)
   339  
   340  				switch err := err.(type) {
   341  				case distribution.ErrRepositoryUnknown:
   342  					context.Errors.Push(v2.ErrorCodeNameUnknown, err)
   343  				case distribution.ErrRepositoryNameInvalid:
   344  					context.Errors.Push(v2.ErrorCodeNameInvalid, err)
   345  				}
   346  
   347  				w.WriteHeader(http.StatusBadRequest)
   348  				serveJSON(w, context.Errors)
   349  				return
   350  			}
   351  
   352  			// assign and decorate the authorized repository with an event bridge.
   353  			context.Repository = notifications.Listen(
   354  				repository,
   355  				app.eventBridge(context, r))
   356  
   357  			context.Repository, err = applyRepoMiddleware(context.Repository, app.Config.Middleware["repository"])
   358  			if err != nil {
   359  				ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err)
   360  				context.Errors.Push(v2.ErrorCodeUnknown, err)
   361  				w.WriteHeader(http.StatusInternalServerError)
   362  				serveJSON(w, context.Errors)
   363  				return
   364  			}
   365  		}
   366  
   367  		dispatch(context, r).ServeHTTP(w, r)
   368  
   369  		// Automated error response handling here. Handlers may return their
   370  		// own errors if they need different behavior (such as range errors
   371  		// for layer upload).
   372  		if context.Errors.Len() > 0 {
   373  			if context.Value("http.response.status") == 0 {
   374  				// TODO(stevvooe): Getting this value from the context is a
   375  				// bit of a hack. We can further address with some of our
   376  				// future refactoring.
   377  				w.WriteHeader(http.StatusBadRequest)
   378  			}
   379  			app.logError(context, context.Errors)
   380  			serveJSON(w, context.Errors)
   381  		}
   382  	})
   383  }
   384  
   385  func (app *App) logError(context context.Context, errors v2.Errors) {
   386  	for _, e := range errors.Errors {
   387  		c := ctxu.WithValue(context, "err.code", e.Code)
   388  		c = ctxu.WithValue(c, "err.message", e.Message)
   389  		c = ctxu.WithValue(c, "err.detail", e.Detail)
   390  		c = ctxu.WithLogger(c, ctxu.GetLogger(c,
   391  			"err.code",
   392  			"err.message",
   393  			"err.detail"))
   394  		ctxu.GetLogger(c).Errorf("An error occured")
   395  	}
   396  }
   397  
   398  // context constructs the context object for the application. This only be
   399  // called once per request.
   400  func (app *App) context(w http.ResponseWriter, r *http.Request) *Context {
   401  	ctx := defaultContextManager.context(app, w, r)
   402  	ctx = ctxu.WithVars(ctx, r)
   403  	ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx,
   404  		"vars.name",
   405  		"vars.reference",
   406  		"vars.digest",
   407  		"vars.uuid"))
   408  
   409  	context := &Context{
   410  		App:        app,
   411  		Context:    ctx,
   412  		urlBuilder: v2.NewURLBuilderFromRequest(r),
   413  	}
   414  
   415  	return context
   416  }
   417  
   418  // authorized checks if the request can proceed with access to the requested
   419  // repository. If it succeeds, the context may access the requested
   420  // repository. An error will be returned if access is not available.
   421  func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error {
   422  	ctxu.GetLogger(context).Debug("authorizing request")
   423  	repo := getName(context)
   424  
   425  	if app.accessController == nil {
   426  		return nil // access controller is not enabled.
   427  	}
   428  
   429  	var accessRecords []auth.Access
   430  
   431  	if repo != "" {
   432  		accessRecords = appendAccessRecords(accessRecords, r.Method, repo)
   433  	} else {
   434  		// Only allow the name not to be set on the base route.
   435  		if app.nameRequired(r) {
   436  			// For this to be properly secured, repo must always be set for a
   437  			// resource that may make a modification. The only condition under
   438  			// which name is not set and we still allow access is when the
   439  			// base route is accessed. This section prevents us from making
   440  			// that mistake elsewhere in the code, allowing any operation to
   441  			// proceed.
   442  			w.Header().Set("Content-Type", "application/json; charset=utf-8")
   443  			w.WriteHeader(http.StatusForbidden)
   444  
   445  			var errs v2.Errors
   446  			errs.Push(v2.ErrorCodeUnauthorized)
   447  			serveJSON(w, errs)
   448  			return fmt.Errorf("forbidden: no repository name")
   449  		}
   450  	}
   451  
   452  	ctx, err := app.accessController.Authorized(context.Context, accessRecords...)
   453  	if err != nil {
   454  		switch err := err.(type) {
   455  		case auth.Challenge:
   456  			w.Header().Set("Content-Type", "application/json; charset=utf-8")
   457  			err.ServeHTTP(w, r)
   458  
   459  			var errs v2.Errors
   460  			errs.Push(v2.ErrorCodeUnauthorized, accessRecords)
   461  			serveJSON(w, errs)
   462  		default:
   463  			// This condition is a potential security problem either in
   464  			// the configuration or whatever is backing the access
   465  			// controller. Just return a bad request with no information
   466  			// to avoid exposure. The request should not proceed.
   467  			ctxu.GetLogger(context).Errorf("error checking authorization: %v", err)
   468  			w.WriteHeader(http.StatusBadRequest)
   469  		}
   470  
   471  		return err
   472  	}
   473  
   474  	// TODO(stevvooe): This pattern needs to be cleaned up a bit. One context
   475  	// should be replaced by another, rather than replacing the context on a
   476  	// mutable object.
   477  	context.Context = ctx
   478  	return nil
   479  }
   480  
   481  // eventBridge returns a bridge for the current request, configured with the
   482  // correct actor and source.
   483  func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listener {
   484  	actor := notifications.ActorRecord{
   485  		Name: getUserName(ctx, r),
   486  	}
   487  	request := notifications.NewRequestRecord(ctxu.GetRequestID(ctx), r)
   488  
   489  	return notifications.NewBridge(ctx.urlBuilder, app.events.source, actor, request, app.events.sink)
   490  }
   491  
   492  // nameRequired returns true if the route requires a name.
   493  func (app *App) nameRequired(r *http.Request) bool {
   494  	route := mux.CurrentRoute(r)
   495  	return route == nil || route.GetName() != v2.RouteNameBase
   496  }
   497  
   498  // apiBase implements a simple yes-man for doing overall checks against the
   499  // api. This can support auth roundtrips to support docker login.
   500  func apiBase(w http.ResponseWriter, r *http.Request) {
   501  	const emptyJSON = "{}"
   502  	// Provide a simple /v2/ 200 OK response with empty json response.
   503  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
   504  	w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
   505  
   506  	fmt.Fprint(w, emptyJSON)
   507  }
   508  
   509  // appendAccessRecords checks the method and adds the appropriate Access records to the records list.
   510  func appendAccessRecords(records []auth.Access, method string, repo string) []auth.Access {
   511  	resource := auth.Resource{
   512  		Type: "repository",
   513  		Name: repo,
   514  	}
   515  
   516  	switch method {
   517  	case "GET", "HEAD":
   518  		records = append(records,
   519  			auth.Access{
   520  				Resource: resource,
   521  				Action:   "pull",
   522  			})
   523  	case "POST", "PUT", "PATCH":
   524  		records = append(records,
   525  			auth.Access{
   526  				Resource: resource,
   527  				Action:   "pull",
   528  			},
   529  			auth.Access{
   530  				Resource: resource,
   531  				Action:   "push",
   532  			})
   533  	case "DELETE":
   534  		// DELETE access requires full admin rights, which is represented
   535  		// as "*". This may not be ideal.
   536  		records = append(records,
   537  			auth.Access{
   538  				Resource: resource,
   539  				Action:   "*",
   540  			})
   541  	}
   542  	return records
   543  }
   544  
   545  // applyRegistryMiddleware wraps a registry instance with the configured middlewares
   546  func applyRegistryMiddleware(registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) {
   547  	for _, mw := range middlewares {
   548  		rmw, err := registrymiddleware.Get(mw.Name, mw.Options, registry)
   549  		if err != nil {
   550  			return nil, fmt.Errorf("unable to configure registry middleware (%s): %s", mw.Name, err)
   551  		}
   552  		registry = rmw
   553  	}
   554  	return registry, nil
   555  
   556  }
   557  
   558  // applyRepoMiddleware wraps a repository with the configured middlewares
   559  func applyRepoMiddleware(repository distribution.Repository, middlewares []configuration.Middleware) (distribution.Repository, error) {
   560  	for _, mw := range middlewares {
   561  		rmw, err := repositorymiddleware.Get(mw.Name, mw.Options, repository)
   562  		if err != nil {
   563  			return nil, err
   564  		}
   565  		repository = rmw
   566  	}
   567  	return repository, nil
   568  }
   569  
   570  // applyStorageMiddleware wraps a storage driver with the configured middlewares
   571  func applyStorageMiddleware(driver storagedriver.StorageDriver, middlewares []configuration.Middleware) (storagedriver.StorageDriver, error) {
   572  	for _, mw := range middlewares {
   573  		smw, err := storagemiddleware.Get(mw.Name, mw.Options, driver)
   574  		if err != nil {
   575  			return nil, fmt.Errorf("unable to configure storage middleware (%s): %v", mw.Name, err)
   576  		}
   577  		driver = smw
   578  	}
   579  	return driver, nil
   580  }
   581  
   582  // uploadPurgeDefaultConfig provides a default configuration for upload
   583  // purging to be used in the absence of configuration in the
   584  // confifuration file
   585  func uploadPurgeDefaultConfig() map[interface{}]interface{} {
   586  	config := map[interface{}]interface{}{}
   587  	config["enabled"] = true
   588  	config["age"] = "168h"
   589  	config["interval"] = "24h"
   590  	config["dryrun"] = false
   591  	return config
   592  }
   593  
   594  func badPurgeUploadConfig(reason string) {
   595  	panic(fmt.Sprintf("Unable to parse upload purge configuration: %s", reason))
   596  }
   597  
   598  // startUploadPurger schedules a goroutine which will periodically
   599  // check upload directories for old files and delete them
   600  func startUploadPurger(storageDriver storagedriver.StorageDriver, log ctxu.Logger, config map[interface{}]interface{}) {
   601  	if config["enabled"] == false {
   602  		return
   603  	}
   604  
   605  	var purgeAgeDuration time.Duration
   606  	var err error
   607  	purgeAge, ok := config["age"]
   608  	if ok {
   609  		ageStr, ok := purgeAge.(string)
   610  		if !ok {
   611  			badPurgeUploadConfig("age is not a string")
   612  		}
   613  		purgeAgeDuration, err = time.ParseDuration(ageStr)
   614  		if err != nil {
   615  			badPurgeUploadConfig(fmt.Sprintf("Cannot parse duration: %s", err.Error()))
   616  		}
   617  	} else {
   618  		badPurgeUploadConfig("age missing")
   619  	}
   620  
   621  	var intervalDuration time.Duration
   622  	interval, ok := config["interval"]
   623  	if ok {
   624  		intervalStr, ok := interval.(string)
   625  		if !ok {
   626  			badPurgeUploadConfig("interval is not a string")
   627  		}
   628  
   629  		intervalDuration, err = time.ParseDuration(intervalStr)
   630  		if err != nil {
   631  			badPurgeUploadConfig(fmt.Sprintf("Cannot parse interval: %s", err.Error()))
   632  		}
   633  	} else {
   634  		badPurgeUploadConfig("interval missing")
   635  	}
   636  
   637  	var dryRunBool bool
   638  	dryRun, ok := config["dryrun"]
   639  	if ok {
   640  		dryRunBool, ok = dryRun.(bool)
   641  		if !ok {
   642  			badPurgeUploadConfig("cannot parse dryrun")
   643  		}
   644  	} else {
   645  		badPurgeUploadConfig("dryrun missing")
   646  	}
   647  
   648  	go func() {
   649  		rand.Seed(time.Now().Unix())
   650  		jitter := time.Duration(rand.Int()%60) * time.Minute
   651  		log.Infof("Starting upload purge in %s", jitter)
   652  		time.Sleep(jitter)
   653  
   654  		for {
   655  			storage.PurgeUploads(storageDriver, time.Now().Add(-purgeAgeDuration), !dryRunBool)
   656  			log.Infof("Starting upload purge in %s", intervalDuration)
   657  			time.Sleep(intervalDuration)
   658  		}
   659  	}()
   660  }