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 }