zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/api/controller.go (about) 1 package api 2 3 import ( 4 "context" 5 "crypto/tls" 6 "crypto/x509" 7 "fmt" 8 "net" 9 "net/http" 10 "os" 11 "runtime" 12 "strconv" 13 "strings" 14 "syscall" 15 "time" 16 17 "github.com/gorilla/handlers" 18 "github.com/gorilla/mux" 19 "github.com/zitadel/oidc/pkg/client/rp" 20 21 "zotregistry.io/zot/errors" 22 "zotregistry.io/zot/pkg/api/config" 23 ext "zotregistry.io/zot/pkg/extensions" 24 extconf "zotregistry.io/zot/pkg/extensions/config" 25 "zotregistry.io/zot/pkg/extensions/monitoring" 26 "zotregistry.io/zot/pkg/log" 27 "zotregistry.io/zot/pkg/meta" 28 mTypes "zotregistry.io/zot/pkg/meta/types" 29 "zotregistry.io/zot/pkg/scheduler" 30 "zotregistry.io/zot/pkg/storage" 31 "zotregistry.io/zot/pkg/storage/gc" 32 ) 33 34 const ( 35 idleTimeout = 120 * time.Second 36 readHeaderTimeout = 5 * time.Second 37 ) 38 39 type Controller struct { 40 Config *config.Config 41 Router *mux.Router 42 MetaDB mTypes.MetaDB 43 StoreController storage.StoreController 44 Log log.Logger 45 Audit *log.Logger 46 Server *http.Server 47 Metrics monitoring.MetricServer 48 CveScanner ext.CveScanner 49 SyncOnDemand SyncOnDemand 50 RelyingParties map[string]rp.RelyingParty 51 CookieStore *CookieStore 52 taskScheduler *scheduler.Scheduler 53 // runtime params 54 chosenPort int // kernel-chosen port 55 } 56 57 func NewController(config *config.Config) *Controller { 58 var controller Controller 59 60 logger := log.NewLogger(config.Log.Level, config.Log.Output) 61 controller.Config = config 62 controller.Log = logger 63 64 if config.Log.Audit != "" { 65 audit := log.NewAuditLogger(config.Log.Level, config.Log.Audit) 66 controller.Audit = audit 67 } 68 69 return &controller 70 } 71 72 func DumpRuntimeParams(log log.Logger) { 73 var rLimit syscall.Rlimit 74 75 evt := log.Info().Int("cpus", runtime.NumCPU()) //nolint: zerologlint 76 77 err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) 78 if err == nil { 79 evt = evt.Uint64("max. open files", uint64(rLimit.Cur)) //nolint: unconvert // required for *BSD 80 } 81 82 if content, err := os.ReadFile("/proc/sys/net/core/somaxconn"); err == nil { 83 evt = evt.Str("listen backlog", strings.TrimSuffix(string(content), "\n")) 84 } 85 86 if content, err := os.ReadFile("/proc/sys/user/max_inotify_watches"); err == nil { 87 evt = evt.Str("max. inotify watches", strings.TrimSuffix(string(content), "\n")) 88 } 89 90 evt.Msg("runtime params") 91 } 92 93 func (c *Controller) GetPort() int { 94 return c.chosenPort 95 } 96 97 func (c *Controller) Run(reloadCtx context.Context) error { 98 if err := c.initCookieStore(); err != nil { 99 return err 100 } 101 102 c.StartBackgroundTasks(reloadCtx) 103 104 // setup HTTP API router 105 engine := mux.NewRouter() 106 107 // rate-limit HTTP requests if enabled 108 if c.Config.HTTP.Ratelimit != nil { 109 if c.Config.HTTP.Ratelimit.Rate != nil { 110 engine.Use(RateLimiter(c, *c.Config.HTTP.Ratelimit.Rate)) 111 } 112 113 for _, mrlim := range c.Config.HTTP.Ratelimit.Methods { 114 engine.Use(MethodRateLimiter(c, mrlim.Method, mrlim.Rate)) 115 } 116 } 117 118 engine.Use( 119 SessionLogger(c), 120 handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log), 121 handlers.PrintRecoveryStack(false))) 122 123 if c.Audit != nil { 124 engine.Use(SessionAuditLogger(c.Audit)) 125 } 126 127 c.Router = engine 128 c.Router.UseEncodedPath() 129 130 monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, 131 c.Config.DistSpecVersion) 132 133 //nolint: contextcheck 134 _ = NewRouteHandler(c) 135 136 addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port) 137 server := &http.Server{ 138 Addr: addr, 139 Handler: c.Router, 140 IdleTimeout: idleTimeout, 141 ReadHeaderTimeout: readHeaderTimeout, 142 } 143 c.Server = server 144 145 // Create the listener 146 listener, err := net.Listen("tcp", addr) 147 if err != nil { 148 return err 149 } 150 151 if c.Config.HTTP.Port == "0" || c.Config.HTTP.Port == "" { 152 chosenAddr, ok := listener.Addr().(*net.TCPAddr) 153 if !ok { 154 c.Log.Error().Str("port", c.Config.HTTP.Port).Msg("invalid addr type") 155 156 return errors.ErrBadType 157 } 158 159 c.chosenPort = chosenAddr.Port 160 161 c.Log.Info().Int("port", chosenAddr.Port).IPAddr("address", chosenAddr.IP).Msg( 162 "port is unspecified, listening on kernel chosen port", 163 ) 164 } else { 165 chosenPort, _ := strconv.ParseInt(c.Config.HTTP.Port, 10, 64) 166 167 c.chosenPort = int(chosenPort) 168 } 169 170 if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { 171 server.TLSConfig = &tls.Config{ 172 CipherSuites: []uint16{ 173 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 174 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 175 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 176 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 177 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 178 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 179 }, 180 CurvePreferences: []tls.CurveID{ 181 tls.CurveP256, 182 tls.X25519, 183 }, 184 PreferServerCipherSuites: true, 185 MinVersion: tls.VersionTLS12, 186 } 187 188 if c.Config.HTTP.TLS.CACert != "" { 189 clientAuth := tls.VerifyClientCertIfGiven 190 if c.Config.IsMTLSAuthEnabled() { 191 clientAuth = tls.RequireAndVerifyClientCert 192 } 193 194 caCert, err := os.ReadFile(c.Config.HTTP.TLS.CACert) 195 if err != nil { 196 c.Log.Error().Err(err).Str("caCert", c.Config.HTTP.TLS.CACert).Msg("failed to read file") 197 198 return err 199 } 200 201 caCertPool := x509.NewCertPool() 202 203 if !caCertPool.AppendCertsFromPEM(caCert) { 204 c.Log.Error().Err(errors.ErrBadCACert).Msg("failed to append certs from pem") 205 206 return errors.ErrBadCACert 207 } 208 209 server.TLSConfig.ClientAuth = clientAuth 210 server.TLSConfig.ClientCAs = caCertPool 211 } 212 213 return server.ServeTLS(listener, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key) 214 } 215 216 return server.Serve(listener) 217 } 218 219 func (c *Controller) Init(reloadCtx context.Context) error { 220 // print the current configuration, but strip secrets 221 c.Log.Info().Interface("params", c.Config.Sanitize()).Msg("configuration settings") 222 223 // print the current runtime environment 224 DumpRuntimeParams(c.Log) 225 226 var enabled bool 227 if c.Config != nil && 228 c.Config.Extensions != nil && 229 c.Config.Extensions.Metrics != nil && 230 *c.Config.Extensions.Metrics.Enable { 231 enabled = true 232 } 233 234 c.Metrics = monitoring.NewMetricsServer(enabled, c.Log) 235 236 if err := c.InitImageStore(); err != nil { //nolint:contextcheck 237 return err 238 } 239 240 if err := c.InitMetaDB(reloadCtx); err != nil { 241 return err 242 } 243 244 c.InitCVEInfo() 245 246 return nil 247 } 248 249 func (c *Controller) InitCVEInfo() { 250 // Enable CVE extension if extension config is provided 251 if c.Config != nil && c.Config.Extensions != nil { 252 c.CveScanner = ext.GetCveScanner(c.Config, c.StoreController, c.MetaDB, c.Log) 253 } 254 } 255 256 func (c *Controller) InitImageStore() error { 257 linter := ext.GetLinter(c.Config, c.Log) 258 259 storeController, err := storage.New(c.Config, linter, c.Metrics, c.Log) 260 if err != nil { 261 return err 262 } 263 264 c.StoreController = storeController 265 266 return nil 267 } 268 269 func (c *Controller) initCookieStore() error { 270 // setup sessions cookie store used to preserve logged in user in web sessions 271 if c.Config.IsBasicAuthnEnabled() { 272 cookieStore, err := NewCookieStore(c.StoreController) 273 if err != nil { 274 return err 275 } 276 277 c.CookieStore = cookieStore 278 } 279 280 return nil 281 } 282 283 func (c *Controller) InitMetaDB(reloadCtx context.Context) error { 284 // init metaDB if search is enabled or we need to store user profiles, api keys or signatures 285 if c.Config.IsSearchEnabled() || c.Config.IsBasicAuthnEnabled() || c.Config.IsImageTrustEnabled() || 286 c.Config.IsRetentionEnabled() { 287 driver, err := meta.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck 288 if err != nil { 289 return err 290 } 291 292 err = ext.SetupExtensions(c.Config, driver, c.Log) //nolint:contextcheck 293 if err != nil { 294 return err 295 } 296 297 err = driver.PatchDB() 298 if err != nil { 299 return err 300 } 301 302 err = meta.ParseStorage(driver, c.StoreController, c.Log) //nolint: contextcheck 303 if err != nil { 304 return err 305 } 306 307 c.MetaDB = driver 308 } 309 310 return nil 311 } 312 313 func (c *Controller) LoadNewConfig(reloadCtx context.Context, newConfig *config.Config) { 314 // reload access control config 315 c.Config.HTTP.AccessControl = newConfig.HTTP.AccessControl 316 317 // reload periodical gc config 318 c.Config.Storage.GC = newConfig.Storage.GC 319 c.Config.Storage.Dedupe = newConfig.Storage.Dedupe 320 c.Config.Storage.GCDelay = newConfig.Storage.GCDelay 321 c.Config.Storage.GCInterval = newConfig.Storage.GCInterval 322 // only if we have a metaDB already in place 323 if c.Config.IsRetentionEnabled() { 324 c.Config.Storage.Retention = newConfig.Storage.Retention 325 } 326 327 for subPath, storageConfig := range newConfig.Storage.SubPaths { 328 subPathConfig, ok := c.Config.Storage.SubPaths[subPath] 329 if ok { 330 subPathConfig.GC = storageConfig.GC 331 subPathConfig.Dedupe = storageConfig.Dedupe 332 subPathConfig.GCDelay = storageConfig.GCDelay 333 subPathConfig.GCInterval = storageConfig.GCInterval 334 // only if we have a metaDB already in place 335 if c.Config.IsRetentionEnabled() { 336 subPathConfig.Retention = storageConfig.Retention 337 } 338 339 c.Config.Storage.SubPaths[subPath] = subPathConfig 340 } 341 } 342 343 // reload background tasks 344 if newConfig.Extensions != nil { 345 if c.Config.Extensions == nil { 346 c.Config.Extensions = &extconf.ExtensionConfig{} 347 } 348 349 // reload sync extension 350 c.Config.Extensions.Sync = newConfig.Extensions.Sync 351 352 // reload only if search is enabled and reloaded config has search extension (can't setup routes at this stage) 353 if c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable { 354 if newConfig.Extensions.Search != nil { 355 c.Config.Extensions.Search.CVE = newConfig.Extensions.Search.CVE 356 } 357 } 358 359 // reload scrub extension 360 c.Config.Extensions.Scrub = newConfig.Extensions.Scrub 361 } else { 362 c.Config.Extensions = nil 363 } 364 365 c.InitCVEInfo() 366 367 c.StartBackgroundTasks(reloadCtx) 368 369 c.Log.Info().Interface("reloaded params", c.Config.Sanitize()). 370 Msg("loaded new configuration settings") 371 } 372 373 func (c *Controller) Shutdown() { 374 c.taskScheduler.Shutdown() 375 ctx := context.Background() 376 _ = c.Server.Shutdown(ctx) 377 } 378 379 func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { 380 c.taskScheduler = scheduler.NewScheduler(c.Config, c.Log) 381 c.taskScheduler.RunScheduler(reloadCtx) 382 383 // Enable running garbage-collect periodically for DefaultStore 384 if c.Config.Storage.GC { 385 gc := gc.NewGarbageCollect(c.StoreController.DefaultStore, c.MetaDB, gc.Options{ 386 Delay: c.Config.Storage.GCDelay, 387 ImageRetention: c.Config.Storage.Retention, 388 }, c.Audit, c.Log) 389 390 gc.CleanImageStorePeriodically(c.Config.Storage.GCInterval, c.taskScheduler) 391 } 392 393 // Enable running dedupe blobs both ways (dedupe or restore deduped blobs) 394 c.StoreController.DefaultStore.RunDedupeBlobs(time.Duration(0), c.taskScheduler) 395 396 // Enable extensions if extension config is provided for DefaultStore 397 if c.Config != nil && c.Config.Extensions != nil { 398 ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) 399 ext.EnableSearchExtension(c.Config, c.StoreController, c.MetaDB, c.taskScheduler, c.CveScanner, c.Log) 400 } 401 // runs once if metrics are enabled & imagestore is local 402 if c.Config.IsMetricsEnabled() && c.Config.Storage.StorageDriver == nil { 403 c.StoreController.DefaultStore.PopulateStorageMetrics(time.Duration(0), c.taskScheduler) 404 } 405 406 if c.Config.Storage.SubPaths != nil { 407 for route, storageConfig := range c.Config.Storage.SubPaths { 408 // Enable running garbage-collect periodically for subImageStore 409 if storageConfig.GC { 410 gc := gc.NewGarbageCollect(c.StoreController.SubStore[route], c.MetaDB, 411 gc.Options{ 412 Delay: storageConfig.GCDelay, 413 ImageRetention: storageConfig.Retention, 414 }, c.Audit, c.Log) 415 416 gc.CleanImageStorePeriodically(storageConfig.GCInterval, c.taskScheduler) 417 } 418 419 // Enable extensions if extension config is provided for subImageStore 420 if c.Config != nil && c.Config.Extensions != nil { 421 ext.EnableMetricsExtension(c.Config, c.Log, storageConfig.RootDirectory) 422 } 423 424 // Enable running dedupe blobs both ways (dedupe or restore deduped blobs) for subpaths 425 substore := c.StoreController.SubStore[route] 426 if substore != nil { 427 substore.RunDedupeBlobs(time.Duration(0), c.taskScheduler) 428 429 if c.Config.IsMetricsEnabled() && c.Config.Storage.StorageDriver == nil { 430 substore.PopulateStorageMetrics(time.Duration(0), c.taskScheduler) 431 } 432 } 433 } 434 } 435 436 if c.Config.Extensions != nil { 437 ext.EnableScrubExtension(c.Config, c.Log, c.StoreController, c.taskScheduler) 438 //nolint: contextcheck 439 syncOnDemand, err := ext.EnableSyncExtension(c.Config, c.MetaDB, c.StoreController, c.taskScheduler, c.Log) 440 if err != nil { 441 c.Log.Error().Err(err).Msg("unable to start sync extension") 442 } 443 444 c.SyncOnDemand = syncOnDemand 445 } 446 447 if c.CookieStore != nil { 448 c.CookieStore.RunSessionCleaner(c.taskScheduler) 449 } 450 451 // we can later move enabling the other scheduled tasks inside the call below 452 ext.EnableScheduledTasks(c.Config, c.taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck 453 } 454 455 type SyncOnDemand interface { 456 SyncImage(ctx context.Context, repo, reference string) error 457 SyncReference(ctx context.Context, repo string, subjectDigestStr string, referenceType string) error 458 }