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 }