mirror of
https://github.com/project-zot/zot.git
synced 2026-06-15 11:37:56 +08:00
934b22d124
* fix(security): enhance timeout configurations and body size limits for HTTP requests Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(tests): refactor backend result handling in proxyHTTPRequest test Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): preserve ContentLength in proxied requests to prevent server hang Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): preserve explicit zero-length request bodies in proxyHTTPRequest fix(tests): add test for normalizedTimeout function to ensure default fallback Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): prevent default HTTP timeout values from being set unless explicitly configured Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): refactor timeout handling to use explicit checks for nil and non-positive values Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(tests): add wait_for_event_count function to ensure expected event generation Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): improve timeout handling and update error responses for large requests Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): enhance HTTP timeout handling with explicit accessors and default values Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): increase default API key body size and timeout values for improved performance Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): unify timeout handling by replacing specific read/write timeouts with a single default timeout Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): consolidate HTTP timeout accessors and enhance timeout handling Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(security): simplify HTTP timeout accessors and set default values for read/write timeouts Co-authored-by: Copilot <copilot@github.com> Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> --------- Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> Co-authored-by: Copilot <copilot@github.com>
639 lines
20 KiB
Go
639 lines
20 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/fips140"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
goerrors "errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/securecookie"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
|
|
"zotregistry.dev/zot/v2/errors"
|
|
"zotregistry.dev/zot/v2/pkg/api/config"
|
|
"zotregistry.dev/zot/v2/pkg/common"
|
|
ext "zotregistry.dev/zot/v2/pkg/extensions"
|
|
events "zotregistry.dev/zot/v2/pkg/extensions/events"
|
|
monitoring "zotregistry.dev/zot/v2/pkg/extensions/monitoring"
|
|
log "zotregistry.dev/zot/v2/pkg/log"
|
|
meta "zotregistry.dev/zot/v2/pkg/meta"
|
|
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
|
scheduler "zotregistry.dev/zot/v2/pkg/scheduler"
|
|
storage "zotregistry.dev/zot/v2/pkg/storage"
|
|
gc "zotregistry.dev/zot/v2/pkg/storage/gc"
|
|
)
|
|
|
|
const (
|
|
idleTimeout = 120 * time.Second
|
|
readHeaderTimeout = 5 * time.Second
|
|
)
|
|
|
|
type Controller struct {
|
|
Config *config.Config
|
|
Router *mux.Router
|
|
MetaDB mTypes.MetaDB
|
|
StoreController storage.StoreController
|
|
Log log.Logger
|
|
Audit *log.Logger
|
|
Server *http.Server
|
|
Metrics monitoring.MetricServer
|
|
EventRecorder events.Recorder
|
|
CveScanner ext.CveScanner
|
|
SyncOnDemand SyncOnDemand
|
|
RelyingParties map[string]rp.RelyingParty
|
|
CookieStore *CookieStore
|
|
HTPasswd *HTPasswd
|
|
HTPasswdWatcher *HTPasswdWatcher
|
|
LDAPClient *LDAPClient
|
|
taskScheduler *scheduler.Scheduler
|
|
Healthz *common.Healthz
|
|
// runtime params (atomic: Run may set the port concurrently with GetPort readers, e.g. tests)
|
|
chosenPort atomic.Int64
|
|
// TLS certificate management
|
|
TlsWatcher atomic.Pointer[TlsConfigWatcher]
|
|
}
|
|
|
|
func NewController(appConfig *config.Config) *Controller {
|
|
var controller Controller
|
|
|
|
logger := log.NewLogger(appConfig.Log.Level, appConfig.Log.Output)
|
|
controller.Healthz = common.NewHealthzServer(appConfig, logger)
|
|
|
|
if appConfig.Cluster != nil {
|
|
// we need the set of local sockets (IP address:port) for identifying
|
|
// the local member cluster socket for logging and lookup.
|
|
localSockets, err := common.GetLocalSockets(appConfig.HTTP.Port)
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("failed to get local sockets")
|
|
panic("failed to get local sockets")
|
|
}
|
|
|
|
// memberSocket is the local member's socket
|
|
// the index is also fetched for quick lookups during proxying
|
|
memberSocketIdx, memberSocket, err := GetLocalMemberClusterSocket(appConfig.Cluster.Members, localSockets)
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("failed to get member socket")
|
|
panic("failed to get member socket")
|
|
}
|
|
|
|
if memberSocketIdx < 0 || memberSocket == "" {
|
|
// there is a misconfiguration if the memberSocket cannot be identified
|
|
logger.Error().
|
|
Str("members", strings.Join(appConfig.Cluster.Members, ",")).
|
|
Str("localSockets", strings.Join(localSockets, ",")).
|
|
Msg("failed to determine the local cluster socket")
|
|
panic("failed to determine the local cluster socket")
|
|
}
|
|
|
|
internalProxyConfig := &config.ClusterRequestProxyConfig{
|
|
LocalMemberClusterSocket: memberSocket,
|
|
LocalMemberClusterSocketIndex: uint64(memberSocketIdx),
|
|
}
|
|
appConfig.Cluster.Proxy = internalProxyConfig
|
|
|
|
logger = logger.With().
|
|
Str("clusterMember", memberSocket).
|
|
Str("clusterMemberIndex", strconv.Itoa(memberSocketIdx)).Logger()
|
|
}
|
|
|
|
htp := NewHTPasswd(logger)
|
|
|
|
htw, err := NewHTPasswdWatcher(htp, "")
|
|
if err != nil {
|
|
logger.Panic().Err(err).Msg("failed to create htpasswd watcher")
|
|
}
|
|
|
|
controller.Config = appConfig
|
|
controller.Log = logger
|
|
controller.HTPasswd = htp
|
|
controller.HTPasswdWatcher = htw
|
|
|
|
if appConfig.Log.Audit != "" {
|
|
audit := log.NewAuditLogger(appConfig.Log.Level, appConfig.Log.Audit)
|
|
controller.Audit = audit
|
|
}
|
|
|
|
return &controller
|
|
}
|
|
|
|
func (c *Controller) GetPort() int {
|
|
return int(c.chosenPort.Load())
|
|
}
|
|
|
|
func (c *Controller) Run() error {
|
|
if err := c.initCookieStore(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.StartBackgroundTasks()
|
|
|
|
// setup HTTP API router
|
|
engine := mux.NewRouter()
|
|
|
|
// rate-limit HTTP requests if enabled
|
|
ratelimitConfig := c.Config.CopyRatelimit()
|
|
if ratelimitConfig != nil {
|
|
if ratelimitConfig.Rate != nil {
|
|
engine.Use(RateLimiter(c, *ratelimitConfig.Rate))
|
|
}
|
|
|
|
for _, mrlim := range ratelimitConfig.Methods {
|
|
engine.Use(MethodRateLimiter(c, mrlim.Method, mrlim.Rate))
|
|
}
|
|
}
|
|
|
|
engine.Use(
|
|
SessionLogger(c),
|
|
RecoveryHandler(c.Log),
|
|
)
|
|
|
|
if c.Audit != nil {
|
|
engine.Use(SessionAuditLogger(c.Audit))
|
|
}
|
|
|
|
c.Router = engine
|
|
c.Router.UseEncodedPath()
|
|
|
|
commit, binaryType, goVersion, distSpecVersion := c.Config.GetVersionInfo()
|
|
monitoring.SetServerInfo(c.Metrics, commit, binaryType, goVersion, distSpecVersion)
|
|
|
|
//nolint: contextcheck
|
|
_ = NewRouteHandler(c)
|
|
|
|
port := c.Config.GetHTTPPort()
|
|
addr := fmt.Sprintf("%s:%s", c.Config.GetHTTPAddress(), port)
|
|
|
|
server := &http.Server{
|
|
Addr: addr,
|
|
Handler: c.Router,
|
|
ReadTimeout: c.Config.GetHTTPReadTimeout(),
|
|
WriteTimeout: c.Config.GetHTTPWriteTimeout(),
|
|
IdleTimeout: idleTimeout,
|
|
ReadHeaderTimeout: readHeaderTimeout,
|
|
}
|
|
c.Server = server
|
|
|
|
// Create the listener
|
|
listener, err := net.Listen("tcp", addr) //nolint: noctx
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Always derive the bound port from the listener so GetPort matches the actual socket (numeric
|
|
// config, kernel-chosen :0, or service names like "http" resolved by net.Listen).
|
|
chosenAddr, ok := listener.Addr().(*net.TCPAddr)
|
|
if !ok {
|
|
c.Log.Error().Str("port", port).Msg("invalid addr type")
|
|
|
|
return errors.ErrBadType
|
|
}
|
|
|
|
c.chosenPort.Store(int64(chosenAddr.Port))
|
|
|
|
if port == "0" || port == "" {
|
|
c.Log.Info().Int("port", chosenAddr.Port).IPAddr("address", chosenAddr.IP).Msg(
|
|
"port is unspecified, listening on kernel chosen port",
|
|
)
|
|
}
|
|
|
|
tlsConfig := c.Config.CopyTLSConfig()
|
|
if tlsConfig != nil && tlsConfig.Key != "" && tlsConfig.Cert != "" {
|
|
// These are the same as the cipher suites in defaultCipherSuitesFIPS for TLS 1.2
|
|
// see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=123
|
|
// Note: Order doesn't matter - Go 1.17+ automatically orders cipher suites based on
|
|
// hardware capabilities and security properties. See https://go.dev/blog/tls-cipher-suites
|
|
cipherSuites := []uint16{
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
}
|
|
if !fips140.Enabled() {
|
|
// CHACHA20_POLY1305 is not FIPS-compliant
|
|
cipherSuites = append(cipherSuites,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
)
|
|
}
|
|
|
|
// This is a subset of the default curve preferences in defaultCurvePreferencesFIPS for TLS 1.2
|
|
// see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=106
|
|
// P-256, P-384, and P-521 are all FIPS-compliant NIST curves
|
|
curvePreferences := []tls.CurveID{
|
|
tls.CurveP256,
|
|
tls.CurveP384,
|
|
tls.CurveP521,
|
|
}
|
|
if !fips140.Enabled() {
|
|
// X25519 is not FIPS-compliant
|
|
curvePreferences = append(curvePreferences, tls.X25519)
|
|
}
|
|
|
|
server.TLSConfig = &tls.Config{
|
|
CipherSuites: cipherSuites,
|
|
CurvePreferences: curvePreferences,
|
|
// PreferServerCipherSuites is ignored in Go 1.17+ - Go automatically orders cipher suites
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
// Load CA certificate for mTLS client verification
|
|
// Note: CA certificate is loaded statically. Unlike the server certificate which is reloaded
|
|
// dynamically through the file watcher, CA certificate changes require a server restart.
|
|
// If dynamic CA certificate rotation is needed in the future (e.g., for rotating mTLS client certificates),
|
|
// the TlsConfigWatcher can be extended to also monitor and reload the CA certificate and update
|
|
// server.TLSConfig.ClientCAs accordingly.
|
|
if tlsConfig.CACert != "" {
|
|
caCert, err := os.ReadFile(tlsConfig.CACert)
|
|
if err != nil {
|
|
c.Log.Error().Err(err).Str("caCert", tlsConfig.CACert).Msg("failed to read file")
|
|
|
|
return err
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
c.Log.Error().Err(errors.ErrBadCACert).Msg("failed to append certs from pem")
|
|
|
|
return errors.ErrBadCACert
|
|
}
|
|
|
|
// Use VerifyClientCertIfGiven even if mTLS is enabled: clients without cert will be treated as anonymous
|
|
// You can control permissions for mTLS anonymous requests via accessControl policies
|
|
server.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
|
server.TLSConfig.ClientCAs = caCertPool
|
|
}
|
|
|
|
// Store TLS config paths in watcher for dynamic reloading
|
|
tlsCertPath := tlsConfig.Cert
|
|
tlsKeyPath := tlsConfig.Key
|
|
|
|
// Create and start certificate watcher
|
|
// Note: The watcher is stored in c.TlsWatcher before calling Start() so it can be properly
|
|
// cleaned up during Shutdown even if Start fails. The Stop() method gracefully handles
|
|
// the case where Start() was never called or failed by checking if the done channel is nil.
|
|
watcher := NewTlsConfigWatcher(tlsCertPath, tlsKeyPath, c.Log)
|
|
c.TlsWatcher.Store(watcher)
|
|
defer watcher.Stop()
|
|
|
|
// Load initial certificate
|
|
if err := watcher.ReloadCertificate(); err != nil {
|
|
c.Log.Error().Err(err).Msg("failed to load initial certificate")
|
|
|
|
return err
|
|
}
|
|
|
|
// Start file watching for certificate reloading
|
|
if err := watcher.Start(); err != nil {
|
|
c.Log.Warn().Err(err).Msg("failed to start certificate watcher, will use fallback polling")
|
|
}
|
|
|
|
// Set GetCertificate callback for dynamic certificate reloading
|
|
server.TLSConfig.GetCertificate = watcher.GetCertificate
|
|
|
|
c.Healthz.Ready()
|
|
|
|
// Pass empty strings to ServeTLS since GetCertificate handles certificate loading
|
|
return server.ServeTLS(listener, "", "")
|
|
}
|
|
|
|
c.Healthz.Ready()
|
|
|
|
return server.Serve(listener)
|
|
}
|
|
|
|
func (c *Controller) Init() error {
|
|
// report if fips140 mode is enabled
|
|
if fips140.Enabled() {
|
|
c.Log.Info().Msg("fips140 is currently enabled")
|
|
}
|
|
|
|
// print the current configuration, but strip secrets
|
|
c.Log.Info().Interface("params", c.Config.Sanitize()).Msg("configuration settings")
|
|
|
|
// log authentication methods status
|
|
authConfig := c.Config.CopyAuthConfig()
|
|
c.Log.Info().Bool("enabled", authConfig.IsTraditionalBearerAuthEnabled()).Msg("jwt bearer authentication")
|
|
c.Log.Info().Bool("enabled", authConfig.IsOIDCBearerAuthEnabled()).Msg("oidc bearer authentication")
|
|
c.Log.Info().Bool("enabled", authConfig.IsHtpasswdAuthEnabled()).Msg("basic authentication (htpasswd)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsLdapAuthEnabled()).Msg("basic authentication (LDAP)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsAPIKeyEnabled()).Msg("basic authentication (API key)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsOpenIDAuthEnabled()).Msg("OpenID authentication")
|
|
c.Log.Info().Bool("enabled", c.Config.IsMTLSAuthEnabled()).Msg("mutual TLS authentication")
|
|
|
|
// print the current runtime environment
|
|
DumpRuntimeParams(c.Log)
|
|
|
|
var enabled bool
|
|
extensionsConfig := c.Config.CopyExtensionsConfig()
|
|
|
|
if extensionsConfig.IsMetricsEnabled() {
|
|
enabled = true
|
|
}
|
|
|
|
c.Metrics = monitoring.NewMetricsServer(enabled, c.Log)
|
|
|
|
if err := c.InitEventRecorder(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.InitImageStore(); err != nil { //nolint:contextcheck
|
|
return err
|
|
}
|
|
|
|
if err := c.InitMetaDB(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.InitCVEInfo()
|
|
c.Healthz.Started()
|
|
|
|
if authConfig.IsHtpasswdAuthEnabled() {
|
|
err := c.HTPasswdWatcher.ChangeFile(authConfig.HTPasswd.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) InitCVEInfo() {
|
|
// Enable CVE extension if extension config is provided
|
|
c.CveScanner = ext.GetCveScanner(c.Config, c.StoreController, c.MetaDB, c.Log)
|
|
}
|
|
|
|
func (c *Controller) InitImageStore() error {
|
|
linter := ext.GetLinter(c.Config, c.Log)
|
|
|
|
storeController, err := storage.New(c.Config, linter, c.Metrics, c.Log, c.EventRecorder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.StoreController = storeController
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) initCookieStore() error {
|
|
// setup sessions cookie store used to preserve logged in user in web sessions
|
|
if c.Config.HTTP.Auth.IsBasicAuthnEnabled() {
|
|
if c.Config.HTTP.Auth.SessionHashKey == nil {
|
|
c.Log.Warn().Msg("hashKey is not set in config, generating a random one")
|
|
|
|
key := securecookie.GenerateRandomKey(64) //nolint: gomnd
|
|
c.Config.HTTP.Auth.SessionHashKey = key
|
|
}
|
|
|
|
cookieStore, err := NewCookieStore(c.Config.HTTP.Auth, c.StoreController, c.Log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.CookieStore = cookieStore
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) InitMetaDB() error {
|
|
// init metaDB if search is enabled or we need to store user profiles, api keys or signatures
|
|
// Get auth config safely
|
|
authConfig := c.Config.CopyAuthConfig()
|
|
extensionsConfig := c.Config.CopyExtensionsConfig()
|
|
|
|
if extensionsConfig.IsSearchEnabled() || authConfig.IsBasicAuthnEnabled() || extensionsConfig.IsImageTrustEnabled() ||
|
|
c.Config.IsRetentionEnabled() || c.Config.IsQuotaEnabled() {
|
|
// Get storage config safely
|
|
storageConfig := c.Config.CopyStorageConfig()
|
|
|
|
driver, err := meta.New(storageConfig.StorageConfig, c.Log) //nolint:contextcheck
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ext.SetupExtensions(c.Config, driver, c.Log) //nolint:contextcheck
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = driver.PatchDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = meta.ParseStorage(driver, c.StoreController, c.Log) //nolint: contextcheck
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.MetaDB = driver
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) InitEventRecorder() error {
|
|
eventRecorder, err := ext.NewEventRecorder(c.Config, c.Log)
|
|
if err != nil && !goerrors.Is(err, errors.ErrExtensionNotEnabled) {
|
|
return err
|
|
}
|
|
|
|
c.EventRecorder = eventRecorder
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) LoadNewConfig(newConfig *config.Config) {
|
|
// Update only reloadable config fields atomically
|
|
c.Config.UpdateReloadableConfig(newConfig)
|
|
|
|
// Operations that need to happen after config update
|
|
authConfig := c.Config.CopyAuthConfig()
|
|
if authConfig.IsHtpasswdAuthEnabled() {
|
|
err := c.HTPasswdWatcher.ChangeFile(authConfig.HTPasswd.Path)
|
|
if err != nil {
|
|
c.Log.Error().Err(err).Msg("failed to change watched htpasswd file")
|
|
}
|
|
} else {
|
|
_ = c.HTPasswdWatcher.ChangeFile("")
|
|
}
|
|
|
|
if c.LDAPClient != nil && authConfig.IsLdapAuthEnabled() {
|
|
c.LDAPClient.lock.Lock()
|
|
c.LDAPClient.BindDN = authConfig.LDAP.BindDN()
|
|
c.LDAPClient.BindPassword = authConfig.LDAP.BindPassword()
|
|
c.LDAPClient.lock.Unlock()
|
|
}
|
|
|
|
c.InitCVEInfo()
|
|
|
|
c.Log.Info().Interface("reloaded params", c.Config.Sanitize()).
|
|
Msg("loaded new configuration settings")
|
|
|
|
// log authentication methods status
|
|
c.Log.Info().Bool("enabled", authConfig.IsTraditionalBearerAuthEnabled()).Msg("jwt bearer authentication")
|
|
c.Log.Info().Bool("enabled", authConfig.IsOIDCBearerAuthEnabled()).Msg("oidc bearer authentication")
|
|
c.Log.Info().Bool("enabled", authConfig.IsHtpasswdAuthEnabled()).Msg("basic authentication (htpasswd)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsLdapAuthEnabled()).Msg("basic authentication (LDAP)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsAPIKeyEnabled()).Msg("basic authentication (API key)")
|
|
c.Log.Info().Bool("enabled", authConfig.IsOpenIDAuthEnabled()).Msg("OpenID authentication")
|
|
c.Log.Info().Bool("enabled", c.Config.IsMTLSAuthEnabled()).Msg("mutual TLS authentication")
|
|
}
|
|
|
|
func (c *Controller) Shutdown() {
|
|
// Stop certificate watcher if it's running
|
|
watcher := c.TlsWatcher.Load()
|
|
if watcher != nil {
|
|
watcher.Stop()
|
|
}
|
|
|
|
// stop all background tasks
|
|
c.StopBackgroundTasks()
|
|
|
|
// Stop metrics server to prevent resource leaks (only during full shutdown)
|
|
if c.Metrics != nil {
|
|
c.Metrics.Stop()
|
|
}
|
|
|
|
if c.Server != nil {
|
|
ctx := context.Background()
|
|
_ = c.Server.Shutdown(ctx)
|
|
}
|
|
|
|
// close metadb
|
|
if c.MetaDB != nil {
|
|
c.MetaDB.Close()
|
|
}
|
|
}
|
|
|
|
// StopBackgroundTasks will stop scheduler and wait for all tasks to finish their work.
|
|
func (c *Controller) StopBackgroundTasks() {
|
|
if c.taskScheduler != nil {
|
|
c.taskScheduler.Shutdown()
|
|
}
|
|
|
|
// Close HTPasswdWatcher to prevent resource leaks
|
|
if c.HTPasswdWatcher != nil {
|
|
_ = c.HTPasswdWatcher.Close()
|
|
}
|
|
}
|
|
|
|
func (c *Controller) StartBackgroundTasks() {
|
|
c.taskScheduler = scheduler.NewScheduler(c.Config, c.Metrics, c.Log)
|
|
c.taskScheduler.RunScheduler()
|
|
|
|
// Start HTPasswdWatcher goroutine
|
|
if c.HTPasswdWatcher != nil {
|
|
c.HTPasswdWatcher.Run()
|
|
}
|
|
|
|
// Run GC and retention tasks
|
|
RunGCTasks(c.Config, c.StoreController, c.MetaDB, c.taskScheduler, c.Log, c.Audit)
|
|
|
|
// Enable running dedupe blobs both ways (dedupe or restore deduped blobs)
|
|
c.StoreController.DefaultStore.RunDedupeBlobs(time.Duration(0), c.taskScheduler)
|
|
|
|
// Always call EnableSearchExtension to ensure proper logging, even when search is disabled
|
|
ext.EnableSearchExtension(c.Config, c.StoreController, c.MetaDB, c.taskScheduler, c.CveScanner, c.Log)
|
|
|
|
// Always call EnableMetricsExtension to ensure proper logging, even when metrics is disabled
|
|
storageConfig := c.Config.CopyStorageConfig()
|
|
ext.EnableMetricsExtension(c.Config, c.Log, storageConfig.RootDirectory)
|
|
|
|
// runs once if metrics are enabled & imagestore is local
|
|
extensionsConfig := c.Config.CopyExtensionsConfig()
|
|
if extensionsConfig.IsMetricsEnabled() && storageConfig.StorageDriver == nil {
|
|
c.StoreController.DefaultStore.PopulateStorageMetrics(time.Duration(0), c.taskScheduler)
|
|
}
|
|
|
|
if storageConfig.SubPaths != nil {
|
|
for route, subStorageConfig := range storageConfig.SubPaths {
|
|
// Enable extensions if extension config is provided for subImageStore
|
|
ext.EnableMetricsExtension(c.Config, c.Log, subStorageConfig.RootDirectory)
|
|
|
|
// Enable running dedupe blobs both ways (dedupe or restore deduped blobs) for subpaths
|
|
substore := c.StoreController.SubStore[route]
|
|
if substore != nil {
|
|
substore.RunDedupeBlobs(time.Duration(0), c.taskScheduler)
|
|
|
|
if extensionsConfig.IsMetricsEnabled() && storageConfig.StorageDriver == nil {
|
|
substore.PopulateStorageMetrics(time.Duration(0), c.taskScheduler)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always call EnableScrubExtension to ensure proper logging, even when scrub is disabled
|
|
ext.EnableScrubExtension(c.Config, c.Log, c.StoreController, c.taskScheduler)
|
|
|
|
// Always call EnableSyncExtension to ensure proper logging, even when sync is disabled
|
|
//nolint: contextcheck
|
|
syncOnDemand, err := ext.EnableSyncExtension(c.Config, c.MetaDB, c.StoreController, c.taskScheduler, c.Log)
|
|
if err != nil {
|
|
c.Log.Error().Err(err).Msg("failed to start sync extension")
|
|
}
|
|
|
|
// Only set SyncOnDemand if sync is actually enabled
|
|
if extensionsConfig.IsSyncEnabled() {
|
|
c.SyncOnDemand = syncOnDemand
|
|
}
|
|
|
|
if c.CookieStore != nil {
|
|
c.CookieStore.RunSessionCleaner(c.taskScheduler)
|
|
}
|
|
|
|
// we can later move enabling the other scheduled tasks inside the call below
|
|
ext.EnableScheduledTasks(c.Config, c.taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
|
|
}
|
|
|
|
// RunGCTasks runs minimal GC and retention tasks without full controller.
|
|
func RunGCTasks(conf *config.Config, storeController storage.StoreController, metaDB mTypes.MetaDB,
|
|
taskScheduler *scheduler.Scheduler, logger log.Logger, audit *log.Logger,
|
|
) {
|
|
// Enable running garbage-collect periodically for DefaultStore
|
|
storageConfig := conf.CopyStorageConfig()
|
|
if storageConfig.GC {
|
|
gc := gc.NewGarbageCollect(storeController.DefaultStore, metaDB, gc.Options{
|
|
Delay: storageConfig.GCDelay,
|
|
ImageRetention: storageConfig.Retention,
|
|
MaxSchedulerDelay: storageConfig.GCMaxSchedulerDelay,
|
|
}, audit, logger)
|
|
|
|
gc.CleanImageStorePeriodically(storageConfig.GCInterval, taskScheduler)
|
|
}
|
|
|
|
// Handle subpaths
|
|
if storageConfig.SubPaths != nil {
|
|
for route, subStorageConfig := range storageConfig.SubPaths {
|
|
// Enable running garbage-collect periodically for subImageStore
|
|
if subStorageConfig.GC {
|
|
gc := gc.NewGarbageCollect(storeController.SubStore[route], metaDB,
|
|
gc.Options{
|
|
Delay: subStorageConfig.GCDelay,
|
|
ImageRetention: subStorageConfig.Retention,
|
|
MaxSchedulerDelay: subStorageConfig.GCMaxSchedulerDelay,
|
|
}, audit, logger)
|
|
|
|
gc.CleanImageStorePeriodically(subStorageConfig.GCInterval, taskScheduler)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type SyncOnDemand interface {
|
|
SyncImage(ctx context.Context, repo, reference string) error
|
|
SyncReferrers(ctx context.Context, repo string, subjectDigestStr string, referenceTypes []string) error
|
|
}
|