zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/cli/server/root.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"os"
     9  	"path"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	glob "github.com/bmatcuk/doublestar/v4"
    16  	"github.com/mitchellh/mapstructure"
    17  	distspec "github.com/opencontainers/distribution-spec/specs-go"
    18  	"github.com/rs/zerolog/log"
    19  	"github.com/spf13/cobra"
    20  	"github.com/spf13/viper"
    21  
    22  	zerr "zotregistry.io/zot/errors"
    23  	"zotregistry.io/zot/pkg/api"
    24  	"zotregistry.io/zot/pkg/api/config"
    25  	"zotregistry.io/zot/pkg/api/constants"
    26  	extconf "zotregistry.io/zot/pkg/extensions/config"
    27  	"zotregistry.io/zot/pkg/extensions/monitoring"
    28  	zlog "zotregistry.io/zot/pkg/log"
    29  	storageConstants "zotregistry.io/zot/pkg/storage/constants"
    30  )
    31  
    32  // metadataConfig reports metadata after parsing, which we use to track
    33  // errors.
    34  func metadataConfig(md *mapstructure.Metadata) viper.DecoderConfigOption {
    35  	return func(c *mapstructure.DecoderConfig) {
    36  		c.Metadata = md
    37  	}
    38  }
    39  
    40  func newServeCmd(conf *config.Config) *cobra.Command {
    41  	// "serve"
    42  	serveCmd := &cobra.Command{
    43  		Use:     "serve <config>",
    44  		Aliases: []string{"serve"},
    45  		Short:   "`serve` stores and distributes OCI images",
    46  		Long:    "`serve` stores and distributes OCI images",
    47  		RunE: func(cmd *cobra.Command, args []string) error {
    48  			if len(args) > 0 {
    49  				if err := LoadConfiguration(conf, args[0]); err != nil {
    50  					return err
    51  				}
    52  			}
    53  
    54  			ctlr := api.NewController(conf)
    55  
    56  			// config reloader
    57  			hotReloader, err := NewHotReloader(ctlr, args[0])
    58  			if err != nil {
    59  				ctlr.Log.Error().Err(err).Msg("failed to create a new hot reloader")
    60  
    61  				return err
    62  			}
    63  
    64  			/* context used to cancel go routines so that
    65  			we can change their config on the fly (restart routines with different config) */
    66  			reloaderCtx := hotReloader.Start()
    67  
    68  			if err := ctlr.Init(reloaderCtx); err != nil {
    69  				ctlr.Log.Error().Err(err).Msg("failed to init controller")
    70  
    71  				return err
    72  			}
    73  
    74  			if err := ctlr.Run(reloaderCtx); err != nil {
    75  				log.Error().Err(err).Msg("unable to start controller, exiting")
    76  			}
    77  
    78  			return nil
    79  		},
    80  	}
    81  
    82  	return serveCmd
    83  }
    84  
    85  func newScrubCmd(conf *config.Config) *cobra.Command {
    86  	// "scrub"
    87  	scrubCmd := &cobra.Command{
    88  		Use:     "scrub <config>",
    89  		Aliases: []string{"scrub"},
    90  		Short:   "`scrub` checks manifest/blob integrity",
    91  		Long:    "`scrub` checks manifest/blob integrity",
    92  		RunE: func(cmd *cobra.Command, args []string) error {
    93  			if len(args) > 0 {
    94  				if err := LoadConfiguration(conf, args[0]); err != nil {
    95  					return err
    96  				}
    97  			} else {
    98  				if err := cmd.Usage(); err != nil {
    99  					return err
   100  				}
   101  
   102  				return nil
   103  			}
   104  
   105  			// checking if the server is  already running
   106  			req, err := http.NewRequestWithContext(context.Background(),
   107  				http.MethodGet,
   108  				fmt.Sprintf("http://%s/v2", net.JoinHostPort(conf.HTTP.Address, conf.HTTP.Port)),
   109  				nil)
   110  			if err != nil {
   111  				log.Error().Err(err).Msg("unable to create a new http request")
   112  
   113  				return err
   114  			}
   115  
   116  			response, err := http.DefaultClient.Do(req)
   117  			if err == nil {
   118  				response.Body.Close()
   119  				log.Warn().Msg("The server is running, in order to perform the scrub command the server should be shut down")
   120  
   121  				return zerr.ErrServerIsRunning
   122  			} else {
   123  				// server is down
   124  				ctlr := api.NewController(conf)
   125  				ctlr.Metrics = monitoring.NewMetricsServer(false, ctlr.Log)
   126  
   127  				if err := ctlr.InitImageStore(); err != nil {
   128  					return err
   129  				}
   130  
   131  				result, err := ctlr.StoreController.CheckAllBlobsIntegrity(cmd.Context())
   132  				if err != nil {
   133  					return err
   134  				}
   135  
   136  				result.PrintScrubResults(cmd.OutOrStdout())
   137  			}
   138  
   139  			return nil
   140  		},
   141  	}
   142  
   143  	return scrubCmd
   144  }
   145  
   146  func newVerifyCmd(conf *config.Config) *cobra.Command {
   147  	// verify
   148  	verifyCmd := &cobra.Command{
   149  		Use:     "verify <config>",
   150  		Aliases: []string{"verify"},
   151  		Short:   "`verify` validates a zot config file",
   152  		Long:    "`verify` validates a zot config file",
   153  		RunE: func(cmd *cobra.Command, args []string) error {
   154  			if len(args) > 0 {
   155  				if err := LoadConfiguration(conf, args[0]); err != nil {
   156  					log.Error().Str("config", args[0]).Msg("Config file is invalid")
   157  
   158  					return err
   159  				}
   160  
   161  				log.Info().Str("config", args[0]).Msg("Config file is valid")
   162  			}
   163  
   164  			return nil
   165  		},
   166  	}
   167  
   168  	return verifyCmd
   169  }
   170  
   171  // "zot" - registry server.
   172  func NewServerRootCmd() *cobra.Command {
   173  	showVersion := false
   174  	conf := config.New()
   175  
   176  	rootCmd := &cobra.Command{
   177  		Use:   "zot",
   178  		Short: "`zot`",
   179  		Long:  "`zot`",
   180  		RunE: func(cmd *cobra.Command, args []string) error {
   181  			if showVersion {
   182  				log.Info().Str("distribution-spec", distspec.Version).Str("commit", config.Commit).
   183  					Str("binary-type", config.BinaryType).Str("go version", config.GoVersion).Msg("version")
   184  			} else {
   185  				_ = cmd.Usage()
   186  				cmd.SilenceErrors = false
   187  			}
   188  
   189  			return nil
   190  		},
   191  	}
   192  
   193  	// "serve"
   194  	rootCmd.AddCommand(newServeCmd(conf))
   195  	// "verify"
   196  	rootCmd.AddCommand(newVerifyCmd(conf))
   197  	// "scrub"
   198  	rootCmd.AddCommand(newScrubCmd(conf))
   199  	// "version"
   200  	rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
   201  
   202  	return rootCmd
   203  }
   204  
   205  func validateStorageConfig(cfg *config.Config, log zlog.Logger) error {
   206  	expConfigMap := make(map[string]config.StorageConfig, 0)
   207  
   208  	defaultRootDir := cfg.Storage.RootDirectory
   209  
   210  	for _, storageConfig := range cfg.Storage.SubPaths {
   211  		if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) {
   212  			log.Error().Err(zerr.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory")
   213  
   214  			return zerr.ErrBadConfig
   215  		}
   216  
   217  		expConfig, ok := expConfigMap[storageConfig.RootDirectory]
   218  		if ok {
   219  			equal := expConfig.ParamsEqual(storageConfig)
   220  			if !equal {
   221  				log.Error().Err(zerr.ErrBadConfig).Msg("storage config with same root directory should have same parameters")
   222  
   223  				return zerr.ErrBadConfig
   224  			}
   225  		} else {
   226  			expConfigMap[storageConfig.RootDirectory] = storageConfig
   227  		}
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func validateCacheConfig(cfg *config.Config, log zlog.Logger) error {
   234  	// global
   235  	// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
   236  	//nolint: lll
   237  	if cfg.Storage.Dedupe && cfg.Storage.StorageDriver != nil && cfg.Storage.RemoteCache && cfg.Storage.CacheDriver == nil {
   238  		log.Error().Err(zerr.ErrBadConfig).Msg(
   239  			"dedupe set to true with remote storage and caching, but no remote cache configured!")
   240  
   241  		return zerr.ErrBadConfig
   242  	}
   243  
   244  	if cfg.Storage.CacheDriver != nil && cfg.Storage.RemoteCache {
   245  		// local storage with remote caching
   246  		if cfg.Storage.StorageDriver == nil {
   247  			log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
   248  
   249  			return zerr.ErrBadConfig
   250  		}
   251  
   252  		// unsupported cache driver
   253  		if cfg.Storage.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
   254  			log.Error().Err(zerr.ErrBadConfig).
   255  				Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).Msg("unsupported cache driver")
   256  
   257  			return zerr.ErrBadConfig
   258  		}
   259  	}
   260  
   261  	if !cfg.Storage.RemoteCache && cfg.Storage.CacheDriver != nil {
   262  		log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
   263  			Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory" +
   264  				"will ignore and use local caching")
   265  	}
   266  
   267  	// subpaths
   268  	for _, subPath := range cfg.Storage.SubPaths {
   269  		// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
   270  		//nolint: lll
   271  		if subPath.Dedupe && subPath.StorageDriver != nil && subPath.RemoteCache && subPath.CacheDriver == nil {
   272  			log.Error().Err(zerr.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!")
   273  
   274  			return zerr.ErrBadConfig
   275  		}
   276  
   277  		if subPath.CacheDriver != nil && subPath.RemoteCache {
   278  			// local storage with remote caching
   279  			if subPath.StorageDriver == nil {
   280  				log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
   281  
   282  				return zerr.ErrBadConfig
   283  			}
   284  
   285  			// unsupported cache driver
   286  			if subPath.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
   287  				log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).
   288  					Msg("unsupported cache driver")
   289  
   290  				return zerr.ErrBadConfig
   291  			}
   292  		}
   293  
   294  		if !subPath.RemoteCache && subPath.CacheDriver != nil {
   295  			log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
   296  				Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory," +
   297  					"will ignore and use local caching")
   298  		}
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  func validateExtensionsConfig(cfg *config.Config, log zlog.Logger) error {
   305  	if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
   306  		log.Warn().Msg("The mgmt extensions configuration option has been made redundant and will be ignored.")
   307  	}
   308  
   309  	if cfg.Extensions != nil && cfg.Extensions.APIKey != nil {
   310  		log.Warn().Msg("The apikey extension configuration will be ignored as API keys " +
   311  			"are now configurable in the HTTP settings.")
   312  	}
   313  
   314  	if cfg.Extensions != nil && cfg.Extensions.UI != nil && cfg.Extensions.UI.Enable != nil && *cfg.Extensions.UI.Enable {
   315  		// it would make sense to also check for mgmt and user prefs to be enabled,
   316  		// but those are both enabled by having the search and ui extensions enabled
   317  		if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
   318  			log.Error().Err(zerr.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
   319  
   320  			return zerr.ErrBadConfig
   321  		}
   322  	}
   323  
   324  	//nolint:lll
   325  	if cfg.Storage.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
   326  		cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
   327  		log.Error().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
   328  
   329  		return zerr.ErrBadConfig
   330  	}
   331  
   332  	for _, subPath := range cfg.Storage.SubPaths {
   333  		//nolint:lll
   334  		if subPath.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
   335  			cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
   336  			log.Error().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
   337  
   338  			return zerr.ErrBadConfig
   339  		}
   340  	}
   341  
   342  	return nil
   343  }
   344  
   345  func validateConfiguration(config *config.Config, log zlog.Logger) error {
   346  	if err := validateHTTP(config, log); err != nil {
   347  		return err
   348  	}
   349  
   350  	if err := validateGC(config, log); err != nil {
   351  		return err
   352  	}
   353  
   354  	if err := validateLDAP(config, log); err != nil {
   355  		return err
   356  	}
   357  
   358  	if err := validateOpenIDConfig(config, log); err != nil {
   359  		return err
   360  	}
   361  
   362  	if err := validateSync(config, log); err != nil {
   363  		return err
   364  	}
   365  
   366  	if err := validateStorageConfig(config, log); err != nil {
   367  		return err
   368  	}
   369  
   370  	if err := validateCacheConfig(config, log); err != nil {
   371  		return err
   372  	}
   373  
   374  	if err := validateExtensionsConfig(config, log); err != nil {
   375  		return err
   376  	}
   377  
   378  	// check authorization config, it should have basic auth enabled or ldap, api keys or OpenID
   379  	if config.HTTP.AccessControl != nil {
   380  		// checking for anonymous policy only authorization config: no users, no policies but anonymous policy
   381  		if err := validateAuthzPolicies(config, log); err != nil {
   382  			return err
   383  		}
   384  	}
   385  
   386  	if len(config.Storage.StorageDriver) != 0 {
   387  		// enforce s3 driver in case of using storage driver
   388  		if config.Storage.StorageDriver["name"] != storageConstants.S3StorageDriverName {
   389  			log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]).
   390  				Msg("unsupported storage driver")
   391  
   392  			return zerr.ErrBadConfig
   393  		}
   394  
   395  		// enforce filesystem storage in case sync feature is enabled
   396  		if config.Extensions != nil && config.Extensions.Sync != nil {
   397  			log.Error().Err(zerr.ErrBadConfig).Msg("sync supports only filesystem storage")
   398  
   399  			return zerr.ErrBadConfig
   400  		}
   401  	}
   402  
   403  	// enforce s3 driver on subpaths in case of using storage driver
   404  	if config.Storage.SubPaths != nil {
   405  		if len(config.Storage.SubPaths) > 0 {
   406  			subPaths := config.Storage.SubPaths
   407  
   408  			for route, storageConfig := range subPaths {
   409  				if len(storageConfig.StorageDriver) != 0 {
   410  					if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName {
   411  						log.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
   412  							storageConfig.StorageDriver["name"]).Msg("unsupported storage driver")
   413  
   414  						return zerr.ErrBadConfig
   415  					}
   416  				}
   417  			}
   418  		}
   419  	}
   420  
   421  	// check glob patterns in authz config are compilable
   422  	if config.HTTP.AccessControl != nil {
   423  		for pattern := range config.HTTP.AccessControl.Repositories {
   424  			ok := glob.ValidatePattern(pattern)
   425  			if !ok {
   426  				log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).Msg("authorization pattern could not be compiled")
   427  
   428  				return glob.ErrBadPattern
   429  			}
   430  		}
   431  	}
   432  
   433  	return nil
   434  }
   435  
   436  func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error {
   437  	if cfg.HTTP.Auth != nil && cfg.HTTP.Auth.OpenID != nil {
   438  		for provider, providerConfig := range cfg.HTTP.Auth.OpenID.Providers {
   439  			//nolint: gocritic
   440  			if config.IsOpenIDSupported(provider) {
   441  				if providerConfig.ClientID == "" || providerConfig.Issuer == "" ||
   442  					len(providerConfig.Scopes) == 0 {
   443  					log.Error().Err(zerr.ErrBadConfig).
   444  						Msg("OpenID provider config requires clientid, issuer and scopes parameters")
   445  
   446  					return zerr.ErrBadConfig
   447  				}
   448  			} else if config.IsOauth2Supported(provider) {
   449  				if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 {
   450  					log.Error().Err(zerr.ErrBadConfig).
   451  						Msg("OAuth2 provider config requires clientid and scopes parameters")
   452  
   453  					return zerr.ErrBadConfig
   454  				}
   455  			} else {
   456  				log.Error().Err(zerr.ErrBadConfig).
   457  					Msg("unsupported openid/oauth2 provider")
   458  
   459  				return zerr.ErrBadConfig
   460  			}
   461  		}
   462  	}
   463  
   464  	return nil
   465  }
   466  
   467  func validateAuthzPolicies(config *config.Config, log zlog.Logger) error {
   468  	if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil &&
   469  		config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) {
   470  		log.Error().Err(zerr.ErrBadConfig).
   471  			Msg("access control config requires one of httpasswd, ldap or openid authentication " +
   472  				"or using only 'anonymousPolicy' policies")
   473  
   474  		return zerr.ErrBadConfig
   475  	}
   476  
   477  	return nil
   478  }
   479  
   480  //nolint:gocyclo,cyclop,nestif
   481  func applyDefaultValues(config *config.Config, viperInstance *viper.Viper, log zlog.Logger) {
   482  	defaultVal := true
   483  
   484  	if config.Extensions == nil && viperInstance.Get("extensions") != nil {
   485  		config.Extensions = &extconf.ExtensionConfig{}
   486  
   487  		extMap := viperInstance.GetStringMap("extensions")
   488  		_, ok := extMap["metrics"]
   489  
   490  		if ok {
   491  			// we found a config like `"extensions": {"metrics": {}}`
   492  			// Note: In case metrics is not empty the config.Extensions will not be nil and we will not reach here
   493  			config.Extensions.Metrics = &extconf.MetricsConfig{}
   494  		}
   495  
   496  		_, ok = extMap["search"]
   497  		if ok {
   498  			// we found a config like `"extensions": {"search": {}}`
   499  			// Note: In case search is not empty the config.Extensions will not be nil and we will not reach here
   500  			config.Extensions.Search = &extconf.SearchConfig{}
   501  		}
   502  
   503  		_, ok = extMap["scrub"]
   504  		if ok {
   505  			// we found a config like `"extensions": {"scrub:": {}}`
   506  			// Note: In case scrub is not empty the config.Extensions will not be nil and we will not reach here
   507  			config.Extensions.Scrub = &extconf.ScrubConfig{}
   508  		}
   509  
   510  		_, ok = extMap["trust"]
   511  		if ok {
   512  			// we found a config like `"extensions": {"trust:": {}}`
   513  			// Note: In case trust is not empty the config.Extensions will not be nil and we will not reach here
   514  			config.Extensions.Trust = &extconf.ImageTrustConfig{}
   515  		}
   516  
   517  		_, ok = extMap["ui"]
   518  		if ok {
   519  			// we found a config like `"extensions": {"ui:": {}}`
   520  			// Note: In case UI is not empty the config.Extensions will not be nil and we will not reach here
   521  			config.Extensions.UI = &extconf.UIConfig{}
   522  		}
   523  	}
   524  
   525  	if config.Extensions != nil {
   526  		if config.Extensions.Sync != nil {
   527  			if config.Extensions.Sync.Enable == nil {
   528  				config.Extensions.Sync.Enable = &defaultVal
   529  			}
   530  
   531  			for id, regCfg := range config.Extensions.Sync.Registries {
   532  				if regCfg.TLSVerify == nil {
   533  					config.Extensions.Sync.Registries[id].TLSVerify = &defaultVal
   534  				}
   535  			}
   536  		}
   537  
   538  		if config.Extensions.Search != nil {
   539  			if config.Extensions.Search.Enable == nil {
   540  				config.Extensions.Search.Enable = &defaultVal
   541  			}
   542  
   543  			if *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil {
   544  				defaultUpdateInterval, _ := time.ParseDuration("2h")
   545  
   546  				if config.Extensions.Search.CVE.UpdateInterval < defaultUpdateInterval {
   547  					config.Extensions.Search.CVE.UpdateInterval = defaultUpdateInterval
   548  
   549  					log.Warn().Msg("CVE update interval set to too-short interval < 2h, " +
   550  						"changing update duration to 2 hours and continuing.")
   551  				}
   552  
   553  				if config.Extensions.Search.CVE.Trivy == nil {
   554  					config.Extensions.Search.CVE.Trivy = &extconf.TrivyConfig{}
   555  				}
   556  
   557  				if config.Extensions.Search.CVE.Trivy.DBRepository == "" {
   558  					defaultDBDownloadURL := "ghcr.io/aquasecurity/trivy-db"
   559  					log.Info().Str("trivyDownloadURL", defaultDBDownloadURL).
   560  						Msg("Config: using default Trivy DB download URL.")
   561  					config.Extensions.Search.CVE.Trivy.DBRepository = defaultDBDownloadURL
   562  				}
   563  
   564  				if config.Extensions.Search.CVE.Trivy.JavaDBRepository == "" {
   565  					defaultJavaDBDownloadURL := "ghcr.io/aquasecurity/trivy-java-db"
   566  					log.Info().Str("trivyJavaDownloadURL", defaultJavaDBDownloadURL).
   567  						Msg("Config: using default Trivy Java DB download URL.")
   568  					config.Extensions.Search.CVE.Trivy.JavaDBRepository = defaultJavaDBDownloadURL
   569  				}
   570  			}
   571  		}
   572  
   573  		if config.Extensions.Metrics != nil {
   574  			if config.Extensions.Metrics.Enable == nil {
   575  				config.Extensions.Metrics.Enable = &defaultVal
   576  			}
   577  
   578  			if config.Extensions.Metrics.Prometheus == nil {
   579  				config.Extensions.Metrics.Prometheus = &extconf.PrometheusConfig{Path: constants.DefaultMetricsExtensionRoute}
   580  			}
   581  		}
   582  
   583  		if config.Extensions.Scrub != nil {
   584  			if config.Extensions.Scrub.Enable == nil {
   585  				config.Extensions.Scrub.Enable = &defaultVal
   586  			}
   587  
   588  			if config.Extensions.Scrub.Interval == 0 {
   589  				config.Extensions.Scrub.Interval = 24 * time.Hour //nolint: gomnd
   590  			}
   591  		}
   592  
   593  		if config.Extensions.UI != nil {
   594  			if config.Extensions.UI.Enable == nil {
   595  				config.Extensions.UI.Enable = &defaultVal
   596  			}
   597  		}
   598  
   599  		if config.Extensions.Trust != nil {
   600  			if config.Extensions.Trust.Enable == nil {
   601  				config.Extensions.Trust.Enable = &defaultVal
   602  			}
   603  		}
   604  	}
   605  
   606  	if !config.Storage.GC {
   607  		if viperInstance.Get("storage::gcdelay") == nil {
   608  			config.Storage.GCDelay = 0
   609  		}
   610  
   611  		if viperInstance.Get("storage::retention::delay") == nil {
   612  			config.Storage.Retention.Delay = 0
   613  		}
   614  
   615  		if viperInstance.Get("storage::gcinterval") == nil {
   616  			config.Storage.GCInterval = 0
   617  		}
   618  	}
   619  
   620  	// apply deleteUntagged default
   621  	for idx := range config.Storage.Retention.Policies {
   622  		if !viperInstance.IsSet("storage::retention::policies::" + fmt.Sprint(idx) + "::deleteUntagged") {
   623  			config.Storage.Retention.Policies[idx].DeleteUntagged = &defaultVal
   624  		}
   625  	}
   626  
   627  	// cache settings
   628  
   629  	// global storage
   630  
   631  	// if dedupe is true but remoteCache bool not set in config file
   632  	// for cloud based storage, remoteCache defaults to true
   633  	if config.Storage.Dedupe && !viperInstance.IsSet("storage::remotecache") && config.Storage.StorageDriver != nil {
   634  		config.Storage.RemoteCache = true
   635  	}
   636  
   637  	// s3 dedup=false, check for previous dedupe usage and set to true if cachedb found
   638  	if !config.Storage.Dedupe && config.Storage.StorageDriver != nil {
   639  		cacheDir, _ := config.Storage.StorageDriver["rootdirectory"].(string)
   640  		cachePath := path.Join(cacheDir, storageConstants.BoltdbName+storageConstants.DBExtensionName)
   641  
   642  		if _, err := os.Stat(cachePath); err == nil {
   643  			log.Info().Msg("Config: dedupe set to false for s3 driver but used to be true.")
   644  			log.Info().Str("cache path", cachePath).Msg("found cache database")
   645  
   646  			config.Storage.RemoteCache = false
   647  		}
   648  	}
   649  
   650  	// subpaths
   651  	for name, storageConfig := range config.Storage.SubPaths {
   652  		// if dedupe is true but remoteCache bool not set in config file
   653  		// for cloud based storage, remoteCache defaults to true
   654  		if storageConfig.Dedupe && !viperInstance.IsSet("storage::subpaths::"+name+"::remotecache") && storageConfig.StorageDriver != nil { //nolint:lll
   655  			storageConfig.RemoteCache = true
   656  		}
   657  
   658  		// s3 dedup=false, check for previous dedupe usage and set to true if cachedb found
   659  		if !storageConfig.Dedupe && storageConfig.StorageDriver != nil {
   660  			subpathCacheDir, _ := storageConfig.StorageDriver["rootdirectory"].(string)
   661  			subpathCachePath := path.Join(subpathCacheDir, storageConstants.BoltdbName+storageConstants.DBExtensionName)
   662  
   663  			if _, err := os.Stat(subpathCachePath); err == nil {
   664  				log.Info().Msg("Config: dedupe set to false for s3 driver but used to be true. ")
   665  				log.Info().Str("cache path", subpathCachePath).Msg("found cache database")
   666  
   667  				storageConfig.RemoteCache = false
   668  			}
   669  		}
   670  
   671  		// if gc is enabled
   672  		if storageConfig.GC {
   673  			// and gcDelay is not set, it is set to default value
   674  			if !viperInstance.IsSet("storage::subpaths::" + name + "::gcdelay") {
   675  				storageConfig.GCDelay = storageConstants.DefaultGCDelay
   676  			}
   677  
   678  			// and retentionDelay is not set, it is set to default value
   679  			if !viperInstance.IsSet("storage::subpaths::" + name + "::retention::delay") {
   680  				storageConfig.Retention.Delay = storageConstants.DefaultRetentionDelay
   681  			}
   682  
   683  			// and gcInterval is not set, it is set to default value
   684  			if !viperInstance.IsSet("storage::subpaths::" + name + "::gcinterval") {
   685  				storageConfig.GCInterval = storageConstants.DefaultGCInterval
   686  			}
   687  		}
   688  
   689  		// apply deleteUntagged default
   690  		for idx := range storageConfig.Retention.Policies {
   691  			deleteUntaggedKey := "storage::subpaths::" + name + "::retention::policies::" + fmt.Sprint(idx) + "::deleteUntagged"
   692  			if !viperInstance.IsSet(deleteUntaggedKey) {
   693  				storageConfig.Retention.Policies[idx].DeleteUntagged = &defaultVal
   694  			}
   695  		}
   696  
   697  		config.Storage.SubPaths[name] = storageConfig
   698  	}
   699  
   700  	// if OpenID authentication is enabled,
   701  	// API Keys are also enabled in order to provide data path authentication
   702  	if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
   703  		config.HTTP.Auth.APIKey = true
   704  	}
   705  }
   706  
   707  func updateDistSpecVersion(config *config.Config, log zlog.Logger) {
   708  	if config.DistSpecVersion == distspec.Version {
   709  		return
   710  	}
   711  
   712  	log.Warn().Str("config version", config.DistSpecVersion).Str("supported version", distspec.Version).
   713  		Msg("config dist-spec version differs from version actually used")
   714  
   715  	config.DistSpecVersion = distspec.Version
   716  }
   717  
   718  func LoadConfiguration(config *config.Config, configPath string) error {
   719  	// Default is dot (.) but because we allow glob patterns in authz
   720  	// we need another key delimiter.
   721  	viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::"))
   722  
   723  	viperInstance.SetConfigFile(configPath)
   724  
   725  	if err := viperInstance.ReadInConfig(); err != nil {
   726  		log.Error().Err(err).Msg("error while reading configuration")
   727  
   728  		return err
   729  	}
   730  
   731  	metaData := &mapstructure.Metadata{}
   732  	if err := viperInstance.UnmarshalExact(&config, metadataConfig(metaData)); err != nil {
   733  		log.Error().Err(err).Msg("error while unmarshaling new config")
   734  
   735  		return err
   736  	}
   737  
   738  	log := zlog.NewLogger(config.Log.Level, config.Log.Output)
   739  
   740  	if len(metaData.Keys) == 0 {
   741  		log.Error().Err(zerr.ErrBadConfig).Msg("config doesn't contain any key:value pair")
   742  
   743  		return zerr.ErrBadConfig
   744  	}
   745  
   746  	if len(metaData.Unused) > 0 {
   747  		log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys")
   748  
   749  		return zerr.ErrBadConfig
   750  	}
   751  
   752  	if err := updateLDAPConfig(config); err != nil {
   753  		return err
   754  	}
   755  
   756  	// defaults
   757  	applyDefaultValues(config, viperInstance, log)
   758  
   759  	// various config checks
   760  	if err := validateConfiguration(config, log); err != nil {
   761  		return err
   762  	}
   763  
   764  	// update distSpecVersion
   765  	updateDistSpecVersion(config, log)
   766  
   767  	return nil
   768  }
   769  
   770  func updateLDAPConfig(conf *config.Config) error {
   771  	if conf.HTTP.Auth == nil || conf.HTTP.Auth.LDAP == nil {
   772  		return nil
   773  	}
   774  
   775  	if conf.HTTP.Auth.LDAP.CredentialsFile == "" {
   776  		conf.HTTP.Auth.LDAP.SetBindDN("anonym-user")
   777  
   778  		return nil
   779  	}
   780  
   781  	newLDAPCredentials, err := readLDAPCredentials(conf.HTTP.Auth.LDAP.CredentialsFile)
   782  	if err != nil {
   783  		return err
   784  	}
   785  
   786  	conf.HTTP.Auth.LDAP.SetBindDN(newLDAPCredentials.BindDN)
   787  	conf.HTTP.Auth.LDAP.SetBindPassword(newLDAPCredentials.BindPassword)
   788  
   789  	return nil
   790  }
   791  
   792  func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) {
   793  	viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::"))
   794  
   795  	viperInstance.SetConfigFile(ldapConfigPath)
   796  
   797  	if err := viperInstance.ReadInConfig(); err != nil {
   798  		log.Error().Err(err).Msg("error while reading configuration")
   799  
   800  		return config.LDAPCredentials{}, err
   801  	}
   802  
   803  	var ldapCredentials config.LDAPCredentials
   804  
   805  	if err := viperInstance.Unmarshal(&ldapCredentials); err != nil {
   806  		log.Error().Err(err).Msg("error while unmarshaling new config")
   807  
   808  		return config.LDAPCredentials{}, err
   809  	}
   810  
   811  	return ldapCredentials, nil
   812  }
   813  
   814  func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool {
   815  	adminPolicy := cfg.HTTP.AccessControl.AdminPolicy
   816  	anonymousPolicyPresent := false
   817  
   818  	log.Info().Msg("checking if anonymous authorization is the only type of authorization policy configured")
   819  
   820  	if len(adminPolicy.Actions)+len(adminPolicy.Users) > 0 {
   821  		log.Info().Msg("admin policy detected, anonymous authorization is not the only authorization policy configured")
   822  
   823  		return false
   824  	}
   825  
   826  	for _, repository := range cfg.HTTP.AccessControl.Repositories {
   827  		if len(repository.DefaultPolicy) > 0 {
   828  			log.Info().Interface("repository", repository).
   829  				Msg("default policy detected, anonymous authorization is not the only authorization policy configured")
   830  
   831  			return false
   832  		}
   833  
   834  		if len(repository.AnonymousPolicy) > 0 {
   835  			log.Info().Msg("anonymous authorization detected")
   836  
   837  			anonymousPolicyPresent = true
   838  		}
   839  
   840  		for _, policy := range repository.Policies {
   841  			if len(policy.Actions)+len(policy.Users) > 0 {
   842  				log.Info().Interface("repository", repository).
   843  					Msg("repository with non-empty policy detected, " +
   844  						"anonymous authorization is not the only authorization policy configured")
   845  
   846  				return false
   847  			}
   848  		}
   849  	}
   850  
   851  	return anonymousPolicyPresent
   852  }
   853  
   854  func validateLDAP(config *config.Config, log zlog.Logger) error {
   855  	// LDAP mandatory configuration
   856  	if config.HTTP.Auth != nil && config.HTTP.Auth.LDAP != nil {
   857  		ldap := config.HTTP.Auth.LDAP
   858  		if ldap.UserAttribute == "" {
   859  			log.Error().Str("userAttribute", ldap.UserAttribute).
   860  				Msg("invalid LDAP configuration, missing mandatory key: userAttribute")
   861  
   862  			return zerr.ErrLDAPConfig
   863  		}
   864  
   865  		if ldap.Address == "" {
   866  			log.Error().Str("address", ldap.Address).
   867  				Msg("invalid LDAP configuration, missing mandatory key: address")
   868  
   869  			return zerr.ErrLDAPConfig
   870  		}
   871  
   872  		if ldap.BaseDN == "" {
   873  			log.Error().Str("basedn", ldap.BaseDN).
   874  				Msg("invalid LDAP configuration, missing mandatory key: basedn")
   875  
   876  			return zerr.ErrLDAPConfig
   877  		}
   878  	}
   879  
   880  	return nil
   881  }
   882  
   883  func validateHTTP(config *config.Config, log zlog.Logger) error {
   884  	if config.HTTP.Port != "" {
   885  		port, err := strconv.ParseInt(config.HTTP.Port, 10, 64)
   886  		if err != nil || (port < 0 || port > 65535) {
   887  			log.Error().Str("port", config.HTTP.Port).Msg("invalid port")
   888  
   889  			return zerr.ErrBadConfig
   890  		}
   891  	}
   892  
   893  	return nil
   894  }
   895  
   896  func validateGC(config *config.Config, log zlog.Logger) error {
   897  	// enforce GC params
   898  	if config.Storage.GCDelay < 0 {
   899  		log.Error().Err(zerr.ErrBadConfig).Dur("delay", config.Storage.GCDelay).
   900  			Msg("invalid garbage-collect delay specified")
   901  
   902  		return zerr.ErrBadConfig
   903  	}
   904  
   905  	if config.Storage.GCInterval < 0 {
   906  		log.Error().Err(zerr.ErrBadConfig).Dur("interval", config.Storage.GCInterval).
   907  			Msg("invalid garbage-collect interval specified")
   908  
   909  		return zerr.ErrBadConfig
   910  	}
   911  
   912  	if !config.Storage.GC {
   913  		if config.Storage.GCDelay != 0 {
   914  			log.Warn().Err(zerr.ErrBadConfig).
   915  				Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored")
   916  		}
   917  
   918  		if config.Storage.GCInterval != 0 {
   919  			log.Warn().Err(zerr.ErrBadConfig).
   920  				Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored")
   921  		}
   922  	}
   923  
   924  	if err := validateGCRules(config.Storage.Retention, log); err != nil {
   925  		return err
   926  	}
   927  
   928  	// subpaths
   929  	for name, subPath := range config.Storage.SubPaths {
   930  		if subPath.GC && subPath.GCDelay <= 0 {
   931  			log.Error().Err(zerr.ErrBadConfig).
   932  				Str("subPath", name).
   933  				Interface("gcDelay", subPath.GCDelay).
   934  				Msg("invalid GC delay configuration - cannot be negative or zero")
   935  
   936  			return zerr.ErrBadConfig
   937  		}
   938  
   939  		if err := validateGCRules(subPath.Retention, log); err != nil {
   940  			return err
   941  		}
   942  	}
   943  
   944  	return nil
   945  }
   946  
   947  func validateGCRules(retention config.ImageRetention, log zlog.Logger) error {
   948  	for _, policy := range retention.Policies {
   949  		for _, pattern := range policy.Repositories {
   950  			if ok := glob.ValidatePattern(pattern); !ok {
   951  				log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).
   952  					Msg("retention repo glob pattern could not be compiled")
   953  
   954  				return zerr.ErrBadConfig
   955  			}
   956  		}
   957  
   958  		for _, tagRule := range policy.KeepTags {
   959  			for _, regex := range tagRule.Patterns {
   960  				_, err := regexp.Compile(regex)
   961  				if err != nil {
   962  					log.Error().Err(glob.ErrBadPattern).Str("regex", regex).
   963  						Msg("retention tag regex could not be compiled")
   964  
   965  					return zerr.ErrBadConfig
   966  				}
   967  			}
   968  		}
   969  	}
   970  
   971  	return nil
   972  }
   973  
   974  func validateSync(config *config.Config, log zlog.Logger) error {
   975  	// check glob patterns in sync config are compilable
   976  	if config.Extensions != nil && config.Extensions.Sync != nil {
   977  		for id, regCfg := range config.Extensions.Sync.Registries {
   978  			// check retry options are configured for sync
   979  			if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil {
   980  				log.Error().Err(zerr.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]",
   981  					config.Extensions.Sync.Registries[id]).Msg("retryDelay is required when using maxRetries")
   982  
   983  				return zerr.ErrBadConfig
   984  			}
   985  
   986  			if regCfg.Content != nil {
   987  				for _, content := range regCfg.Content {
   988  					ok := glob.ValidatePattern(content.Prefix)
   989  					if !ok {
   990  						log.Error().Err(glob.ErrBadPattern).Str("prefix", content.Prefix).
   991  							Msg("sync prefix could not be compiled")
   992  
   993  						return zerr.ErrBadConfig
   994  					}
   995  
   996  					if content.Tags != nil && content.Tags.Regex != nil {
   997  						_, err := regexp.Compile(*content.Tags.Regex)
   998  						if err != nil {
   999  							log.Error().Err(glob.ErrBadPattern).Str("regex", *content.Tags.Regex).
  1000  								Msg("sync content regex could not be compiled")
  1001  
  1002  							return zerr.ErrBadConfig
  1003  						}
  1004  					}
  1005  
  1006  					if content.StripPrefix && !strings.Contains(content.Prefix, "/*") && content.Destination == "/" {
  1007  						log.Error().Err(zerr.ErrBadConfig).
  1008  							Interface("sync content", content).
  1009  							Msg("sync config: can not use stripPrefix true and destination '/' without using glob patterns in prefix")
  1010  
  1011  						return zerr.ErrBadConfig
  1012  					}
  1013  
  1014  					// check sync config doesn't overlap with retention config
  1015  					validateRetentionSyncOverlaps(config, content, regCfg.URLs, log)
  1016  				}
  1017  			}
  1018  		}
  1019  	}
  1020  
  1021  	return nil
  1022  }