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  }