From 4bc98135fbd8dc83d824c32d0765366eea8ef1d7 Mon Sep 17 00:00:00 2001 From: Petu Eusebiu Date: Thu, 21 Jul 2022 17:48:18 +0300 Subject: [PATCH] Added support for changing config via API closes #268, closes #698 Signed-off-by: Petu Eusebiu --- Makefile | 6 +- examples/config-allextensions.json | 23 +- examples/config-anonymous-authz.json | 46 +- examples/config-cfg-extension.json | 36 ++ examples/config-policy.json | 228 +++++----- examples/config-tls.json | 4 +- go.mod | 2 +- pkg/api/authn.go | 4 +- pkg/api/authz.go | 14 +- pkg/api/config/config.go | 60 +-- pkg/api/config/loader.go | 164 +++++++ pkg/api/config/validator.go | 387 ++++++++++++++++ pkg/api/constants/extensions.go | 1 + pkg/api/controller.go | 161 ++++--- pkg/api/controller_test.go | 122 +++--- pkg/api/routes.go | 3 +- pkg/cli/config_reloader.go | 121 +++-- pkg/cli/config_reloader_test.go | 340 ++++++++++++--- pkg/cli/extensions_test.go | 31 +- pkg/cli/root.go | 437 ++----------------- pkg/cli/root_test.go | 247 ++++++++--- pkg/extensions/config/config.go | 15 +- pkg/extensions/extension_search.go | 28 +- pkg/extensions/extension_search_disabled.go | 4 +- pkg/extensions/extensions-config-disabled.go | 18 + pkg/extensions/extensions-config.go | 90 ++++ pkg/extensions/extensions_test.go | 292 ++++++++++++- pkg/extensions/sync/on_demand.go | 2 +- pkg/extensions/sync/sync.go | 3 +- pkg/extensions/sync/sync_internal_test.go | 6 +- pkg/extensions/sync/sync_test.go | 121 ----- pkg/extensions/sync/utils.go | 2 +- test/blackbox/anonymous_policiy.bats | 30 +- 33 files changed, 1965 insertions(+), 1083 deletions(-) create mode 100644 examples/config-cfg-extension.json create mode 100644 pkg/api/config/loader.go create mode 100644 pkg/api/config/validator.go create mode 100644 pkg/extensions/extensions-config-disabled.go create mode 100644 pkg/extensions/extensions-config.go diff --git a/Makefile b/Makefile index 26cd26468c..7061b2ad3e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ TESTDATA := $(TOP_LEVEL)/test/data OS ?= linux ARCH ?= amd64 BENCH_OUTPUT ?= stdout -EXTENSIONS ?= sync,search,scrub,metrics,ui_base,lint +EXTENSIONS ?= sync,search,scrub,metrics,ui_base,lint,config comma:= , hyphen:= - extended-name:= @@ -84,7 +84,7 @@ privileged-test: check-skopeo $(TESTDATA) $(NOTATION) go test -failfast -tags needprivileges,$(EXTENSIONS),containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-dev-needprivileges.txt -covermode=atomic ./pkg/storage/... ./pkg/cli/... -run ^TestElevatedPrivileges $(TESTDATA): check-skopeo - $(shell mkdir -p ${TESTDATA}; cd ${TESTDATA}; mkdir -p noidentity; ../scripts/gen_certs.sh; cd ${TESTDATA}/noidentity; ../../scripts/gen_nameless_certs.sh; cd ${TOP_LEVEL}; skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:7 oci:${TESTDATA}/zot-test:0.0.1;skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1) + $(shell mkdir -p ${TESTDATA}; cd ${TESTDATA}; touch htpasswd; mkdir -p noidentity; ../scripts/gen_certs.sh; cd ${TESTDATA}/noidentity; ../../scripts/gen_nameless_certs.sh; cd ${TOP_LEVEL}; skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:7 oci:${TESTDATA}/zot-test:0.0.1;skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1) $(shell chmod -R a=rwx ${TESTDATA}) .PHONY: run-bench @@ -196,7 +196,7 @@ run: binary test verify-config: _verify-config verify-config-warnings verify-config-commited .PHONY: _verify-config -_verify-config: binary +_verify-config: binary $(TESTDATA) rm -f output.txt $(foreach file, $(wildcard examples/config-*), ./bin/zot-$(OS)-$(ARCH) verify $(file) 2>&1 | tee -a output.txt || exit 1;) diff --git a/examples/config-allextensions.json b/examples/config-allextensions.json index 8a4de7c7e5..606ddfeaf9 100644 --- a/examples/config-allextensions.json +++ b/examples/config-allextensions.json @@ -5,12 +5,33 @@ }, "http": { "address": "127.0.0.1", - "port": "8080" + "port": "8080", + "auth": { + "htpasswd": { + "path": "test/data/htpasswd" + } + }, + "accesscontrol": { + "adminpolicy": { + "actions": [ + "read", + "create", + "update", + "delete" + ], + "users": [ + "admin" + ] + } + } }, "log": { "level": "debug" }, "extensions": { + "sysconfig": { + "enable": true + }, "metrics": {}, "sync": { "credentialsFile": "./examples/sync-auth-filepath.json", diff --git a/examples/config-anonymous-authz.json b/examples/config-anonymous-authz.json index f530e3cdf6..42ea6af4cb 100644 --- a/examples/config-anonymous-authz.json +++ b/examples/config-anonymous-authz.json @@ -8,28 +8,30 @@ "port": "8080", "realm": "zot", "accessControl": { - "**": { - "anonymousPolicy": [ - "read", - "create" - ] - }, - "tmp/**": { - "anonymousPolicy": [ - "read", - "create", - "update" - ] - }, - "infra/**": { - "anonymousPolicy": [ - "read" - ] - }, - "repos2/repo": { - "anonymousPolicy": [ - "read" - ] + "repositories": { + "**": { + "anonymousPolicy": [ + "read", + "create" + ] + }, + "tmp/**": { + "anonymousPolicy": [ + "read", + "create", + "update" + ] + }, + "infra/**": { + "anonymousPolicy": [ + "read" + ] + }, + "repos2/repo": { + "anonymousPolicy": [ + "read" + ] + } } } }, diff --git a/examples/config-cfg-extension.json b/examples/config-cfg-extension.json new file mode 100644 index 0000000000..90f04c1be1 --- /dev/null +++ b/examples/config-cfg-extension.json @@ -0,0 +1,36 @@ +{ + "distspecversion": "1.0.1-dev", + "extensions": { + "sysconfig": { + "enable": true + } + }, + "http": { + "accesscontrol": { + "adminpolicy": { + "actions": [ + "read", + "create", + "update", + "delete" + ], + "users": [ + "admin" + ] + } + }, + "address": "127.0.0.1", + "auth": { + "htpasswd": { + "path": "test/data/htpasswd" + } + }, + "port": "5000" + }, + "log": { + "level": "debug" + }, + "storage": { + "rootdirectory": "/tmp/zot" + } +} diff --git a/examples/config-policy.json b/examples/config-policy.json index a23931770b..b9f254ac48 100644 --- a/examples/config-policy.json +++ b/examples/config-policy.json @@ -1,113 +1,115 @@ -{ - "distSpecVersion": "1.0.1-dev", - "storage": { - "rootDirectory": "/tmp/zot" - }, - "http": { - "address": "127.0.0.1", - "port": "8080", - "realm": "zot", - "auth": { - "htpasswd": { - "path": "test/data/htpasswd" - }, - "failDelay": 1 - }, - "accessControl": { - "**": { - "anonymousPolicy": ["read"], - "policies": [ - { - "users": [ - "charlie" - ], - "actions": [ - "read", - "create", - "update" - ] - } - ], - "defaultPolicy": [ - "read", - "create" - ] - }, - "tmp/**": { - "defaultPolicy": [ - "read", - "create", - "update" - ] - }, - "infra/**": { - "policies": [ - { - "users": [ - "alice", - "bob" - ], - "actions": [ - "create", - "read", - "update", - "delete" - ] - }, - { - "users": [ - "mallory" - ], - "actions": [ - "create", - "read" - ] - } - ], - "defaultPolicy": [ - "read" - ] - }, - "repos2/repo": { - "policies": [ - { - "users": [ - "charlie" - ], - "actions": [ - "read", - "create" - ] - }, - { - "users": [ - "mallory" - ], - "actions": [ - "create", - "read" - ] - } - ], - "defaultPolicy": [ - "read" - ] - }, - "adminPolicy": { - "users": [ - "admin" - ], - "actions": [ - "read", - "create", - "update", - "delete" - ] - } - } - }, - "log": { - "level": "debug", - "output": "/tmp/zot.log" - } -} +{ + "distSpecVersion": "1.0.1-dev", + "storage": { + "rootDirectory": "/tmp/zot" + }, + "http": { + "address": "127.0.0.1", + "port": "8080", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "test/data/htpasswd" + }, + "failDelay": 1 + }, + "accessControl": { + "repositories": { + "**": { + "anonymousPolicy": ["read"], + "policies": [ + { + "users": [ + "charlie" + ], + "actions": [ + "read", + "create", + "update" + ] + } + ], + "defaultPolicy": [ + "read", + "create" + ] + }, + "tmp/**": { + "defaultPolicy": [ + "read", + "create", + "update" + ] + }, + "infra/**": { + "policies": [ + { + "users": [ + "alice", + "bob" + ], + "actions": [ + "create", + "read", + "update", + "delete" + ] + }, + { + "users": [ + "mallory" + ], + "actions": [ + "create", + "read" + ] + } + ], + "defaultPolicy": [ + "read" + ] + }, + "repos2/repo": { + "policies": [ + { + "users": [ + "charlie" + ], + "actions": [ + "read", + "create" + ] + }, + { + "users": [ + "mallory" + ], + "actions": [ + "create", + "read" + ] + } + ], + "defaultPolicy": [ + "read" + ] + } + }, + "adminPolicy": { + "users": [ + "admin" + ], + "actions": [ + "read", + "create", + "update", + "delete" + ] + } + } + }, + "log": { + "level": "debug", + "output": "/tmp/zot.log" + } +} diff --git a/examples/config-tls.json b/examples/config-tls.json index 901a166b10..6ebfa697c5 100644 --- a/examples/config-tls.json +++ b/examples/config-tls.json @@ -8,8 +8,8 @@ "port": "8080", "realm": "zot", "tls": { - "cert": "../../test/data/server.cert", - "key": "../../test/data/server.key" + "cert": "test/data/server.cert", + "key": "test/data/server.key" } }, "log": { diff --git a/go.mod b/go.mod index 5c0ff4bd96..cbf155b4f1 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/gofrs/uuid v4.2.0+incompatible github.com/google/go-containerregistry v0.11.0 github.com/google/uuid v1.3.0 - github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/json-iterator/go v1.1.12 github.com/minio/sha256-simd v1.0.0 @@ -55,6 +54,7 @@ require ( require github.com/open-policy-agent/opa v0.43.0 // indirect require ( + github.com/gorilla/handlers v1.5.1 github.com/notaryproject/notation-go v0.10.0-alpha.3 github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 github.com/sigstore/cosign v1.11.1 diff --git a/pkg/api/authn.go b/pkg/api/authn.go index aa9a0a2622..f21897593e 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -180,7 +180,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { return } - if request.Header.Get("Authorization") == "" && anonymousPolicyExists(ctlr.Config.AccessControl) { + if request.Header.Get("Authorization") == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) { // Process request next.ServeHTTP(response, request) @@ -197,7 +197,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { // some client tools might send Authorization: Basic Og== (decoded into ":") // empty username and password - if username == "" && passphrase == "" && anonymousPolicyExists(ctlr.Config.AccessControl) { + if username == "" && passphrase == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) { // Process request next.ServeHTTP(response, request) diff --git a/pkg/api/authz.go b/pkg/api/authz.go index aa8b90113f..e892342e2c 100644 --- a/pkg/api/authz.go +++ b/pkg/api/authz.go @@ -32,7 +32,7 @@ type AccessController struct { func NewAccessController(config *config.Config) *AccessController { return &AccessController{ - Config: config.AccessControl, + Config: config.HTTP.AccessControl, Log: log.NewLogger(config.Log.Level, config.Log.Output), } } @@ -228,6 +228,18 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { return } + if request.RequestURI == constants.ExtConfigPrefix { + if acCtrlr.isAdmin(identity) { + next.ServeHTTP(response, request) + + return + } + + authzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) + + return + } + var action string if request.Method == http.MethodGet || request.Method == http.MethodHead { action = READ diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 79b5ad876a..5b1b4782fc 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -1,13 +1,11 @@ package config import ( - "fmt" "os" "time" "github.com/getlantern/deepcopy" distspec "github.com/opencontainers/distribution-spec/specs-go" - "github.com/spf13/viper" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/storage" ) @@ -62,14 +60,16 @@ type RatelimitConfig struct { } type HTTPConfig struct { - Address string - Port string - AllowOrigin string // comma separated - TLS *TLSConfig - Auth *AuthConfig - RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"` - Realm string - Ratelimit *RatelimitConfig `mapstructure:",omitempty"` + Address string + Port string + AllowOrigin string // comma separated + TLS *TLSConfig + Auth *AuthConfig + AccessControl *AccessControlConfig + Realm string + AllowReadAccess bool `mapstructure:",omitempty"` + ReadOnly bool `mapstructure:",omitempty"` + Ratelimit *RatelimitConfig `mapstructure:",omitempty"` } type LDAPConfig struct { @@ -126,7 +126,6 @@ type Config struct { GoVersion string Commit string BinaryType string - AccessControl *AccessControlConfig Storage GlobalStorageConfig HTTP HTTPConfig Log *LogConfig @@ -185,42 +184,3 @@ func (c *Config) Sanitize() *Config { return sanitizedConfig } - -// LoadAccessControlConfig populates config.AccessControl struct with values from config. -func (c *Config) LoadAccessControlConfig(viperInstance *viper.Viper) error { - if c.HTTP.RawAccessControl == nil { - return nil - } - - c.AccessControl = &AccessControlConfig{} - c.AccessControl.Repositories = make(map[string]PolicyGroup) - - for policy := range c.HTTP.RawAccessControl { - var policies []Policy - - var policyGroup PolicyGroup - - if policy == "adminpolicy" { - adminPolicy := viperInstance.GetStringMapStringSlice("http::accessControl::adminPolicy") - c.AccessControl.AdminPolicy.Actions = adminPolicy["actions"] - c.AccessControl.AdminPolicy.Users = adminPolicy["users"] - - continue - } - - err := viperInstance.UnmarshalKey(fmt.Sprintf("http::accessControl::%s::policies", policy), &policies) - if err != nil { - return err - } - - defaultPolicy := viperInstance.GetStringSlice(fmt.Sprintf("http::accessControl::%s::defaultPolicy", policy)) - policyGroup.DefaultPolicy = defaultPolicy - - anonymousPolicy := viperInstance.GetStringSlice(fmt.Sprintf("http::accessControl::%s::anonymousPolicy", policy)) - policyGroup.Policies = policies - policyGroup.AnonymousPolicy = anonymousPolicy - c.AccessControl.Repositories[policy] = policyGroup - } - - return nil -} diff --git a/pkg/api/config/loader.go b/pkg/api/config/loader.go new file mode 100644 index 0000000000..036985922b --- /dev/null +++ b/pkg/api/config/loader.go @@ -0,0 +1,164 @@ +package config + +import ( + "io" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/api/constants" + extconf "zotregistry.io/zot/pkg/extensions/config" +) + +func LoadFromBufferWithWriter(configPath string, in io.Reader, config *Config) (func() error, error) { + viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::")) + viperInstance.SetConfigFile(configPath) + + if err := viperInstance.ReadConfig(in); err != nil { + log.Error().Err(err).Msg("error while reading configuration") + + return nil, err + } + + if err := unmarshal(viperInstance, config); err != nil { + return nil, err + } + + // defaults + applyDefaultValues(config, viperInstance) + + if err := Validate(config); err != nil { + log.Error().Err(err).Msg("config is not valid") + + return nil, err + } + + writer := viperInstance.WriteConfig + + return writer, nil +} + +func LoadFromFile(configPath string, config *Config) error { + viper.SetConfigFile(configPath) + + viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::")) + viperInstance.SetConfigFile(configPath) + + return loadFromFile(viperInstance, config) +} + +func loadFromFile(viperInstance *viper.Viper, config *Config) error { + if err := viperInstance.ReadInConfig(); err != nil { + log.Error().Err(err).Msg("error while reading configuration") + + return err + } + + if err := unmarshal(viperInstance, config); err != nil { + return err + } + + // defaults + applyDefaultValues(config, viperInstance) + + if err := Validate(config); err != nil { + log.Error().Err(err).Msg("config is not valid") + + return err + } + + return nil +} + +func unmarshal(viperInstance *viper.Viper, config *Config) error { + metaData := &mapstructure.Metadata{} + if err := viperInstance.Unmarshal(&config, metadataConfig(metaData)); err != nil { + log.Error().Err(err).Msg("error while unmarshalling new config") + + return err + } + + if len(metaData.Keys) == 0 { + log.Error().Err(errors.ErrBadConfig).Interface("config", config).Msgf("config doesn't contain any key:value pair") + + return errors.ErrBadConfig + } + + if len(metaData.Unused) > 0 { + log.Error().Err(errors.ErrBadConfig).Msgf("unknown keys: %v", metaData.Unused) + + return errors.ErrBadConfig + } + + return nil +} + +func metadataConfig(md *mapstructure.Metadata) viper.DecoderConfigOption { + return func(c *mapstructure.DecoderConfig) { + c.Metadata = md + } +} + +func applyDefaultValues(config *Config, viperInstance *viper.Viper) { + defaultVal := true + + if config.Extensions == nil && viperInstance.Get("extensions") != nil { + config.Extensions = &extconf.ExtensionConfig{} + + extMap := viperInstance.GetStringMap("extensions") + _, ok := extMap["metrics"] + + if ok { + // we found a config like `"extensions": {"metrics": {}}` + // Note: In case metrics is not empty the config.Extensions will not be nil and we will not reach here + config.Extensions.Metrics = &extconf.MetricsConfig{} + } + + _, ok = extMap["search"] + if ok { + // we found a config like `"extensions": {"search": {}}` + // Note: In case search is not empty the config.Extensions will not be nil and we will not reach here + config.Extensions.Search = &extconf.SearchConfig{} + } + } + + if config.Extensions != nil { + if config.Extensions.Sync != nil { + if config.Extensions.Sync.Enable == nil { + config.Extensions.Sync.Enable = &defaultVal + } + + for id, regCfg := range config.Extensions.Sync.Registries { + if regCfg.TLSVerify == nil { + config.Extensions.Sync.Registries[id].TLSVerify = &defaultVal + } + } + } + + if config.Extensions.Search != nil { + if config.Extensions.Search.Enable == nil { + config.Extensions.Search.Enable = &defaultVal + } + + if config.Extensions.Search.CVE == nil { + config.Extensions.Search.CVE = &extconf.CVEConfig{UpdateInterval: 24 * time.Hour} // nolint: gomnd + } + } + + if config.Extensions.Metrics != nil { + if config.Extensions.Metrics.Enable == nil { + config.Extensions.Metrics.Enable = &defaultVal + } + + if config.Extensions.Metrics.Prometheus == nil { + config.Extensions.Metrics.Prometheus = &extconf.PrometheusConfig{Path: constants.DefaultMetricsExtensionRoute} + } + } + } + + if !config.Storage.GC && viperInstance.Get("storage::gcdelay") == nil { + config.Storage.GCDelay = 0 + } +} diff --git a/pkg/api/config/validator.go b/pkg/api/config/validator.go new file mode 100644 index 0000000000..90ca714de3 --- /dev/null +++ b/pkg/api/config/validator.go @@ -0,0 +1,387 @@ +package config + +import ( + "os" + "strings" + + glob "github.com/bmatcuk/doublestar/v4" + distspec "github.com/opencontainers/distribution-spec/specs-go" + "github.com/rs/zerolog/log" + "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/extensions/sync" + "zotregistry.io/zot/pkg/storage" +) + +func Validate(config *Config) error { + if config.HTTP.Address == "" || config.HTTP.Port == "" { + log.Error().Err(errors.ErrBadConfig).Msgf("config needs valid address or port") + + return errors.ErrBadConfig + } + + if err := validateGC(config); err != nil { + return err + } + + if err := validateLDAP(config); err != nil { + return err + } + + if err := validateSync(config); err != nil { + return err + } + + if err := validateStorageConfig(config); err != nil { + return err + } + + if err := validateLocalStorage(config.Storage.RootDirectory); err != nil { + return err + } + + if config.Storage.SubPaths != nil { + for _, subPath := range config.Storage.SubPaths { + if err := validateLocalStorage(subPath.RootDirectory); err != nil { + return err + } + } + } + + if err := validateHtpasswd(config); err != nil { + return err + } + + if err := validateTLSCerts(config); err != nil { + return err + } + + // check authorization config, it should have basic auth enabled or ldap + if config.HTTP.AccessControl != nil { + // checking for anonymous policy only authorization config: no users, no policies but anonymous policy + if err := validateAuthzPolicies(config); err != nil { + return err + } + } + + if err := validateConfigExtensions(config); err != nil { + return err + } + + if err := validateRemoteStorage(config); err != nil { + return err + } + + // check glob patterns in authz config are compilable + if config.HTTP.AccessControl != nil { + for pattern := range config.HTTP.AccessControl.Repositories { + ok := glob.ValidatePattern(pattern) + if !ok { + log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).Msg("authorization pattern could not be compiled") + + return glob.ErrBadPattern + } + } + } + + updateDistSpecVersion(config) + + return nil +} + +func updateDistSpecVersion(config *Config) { + if config.DistSpecVersion == distspec.Version { + return + } + + log.Warn().Msgf("config dist-spec version: %s differs from version actually used: %s", + config.DistSpecVersion, distspec.Version) + + config.DistSpecVersion = distspec.Version +} + +func validateLDAP(config *Config) error { + // LDAP mandatory configuration + if config.HTTP.Auth != nil && config.HTTP.Auth.LDAP != nil { + ldap := config.HTTP.Auth.LDAP + if ldap.UserAttribute == "" { + log.Error().Str("userAttribute", ldap.UserAttribute). + Msg("invalid LDAP configuration, missing mandatory key: userAttribute") + + return errors.ErrLDAPConfig + } + + if ldap.Address == "" { + log.Error().Str("address", ldap.Address). + Msg("invalid LDAP configuration, missing mandatory key: address") + + return errors.ErrLDAPConfig + } + + if ldap.BaseDN == "" { + log.Error().Str("basedn", ldap.BaseDN). + Msg("invalid LDAP configuration, missing mandatory key: basedn") + + return errors.ErrLDAPConfig + } + } + + return nil +} + +func validateGC(config *Config) error { + // enforce GC params + if config.Storage.GCDelay < 0 { + log.Error().Err(errors.ErrBadConfig). + Msgf("invalid garbage-collect delay %v specified", config.Storage.GCDelay) + + return errors.ErrBadConfig + } + + if config.Storage.GCInterval < 0 { + log.Error().Err(errors.ErrBadConfig). + Msgf("invalid garbage-collect interval %v specified", config.Storage.GCInterval) + + return errors.ErrBadConfig + } + + if !config.Storage.GC { + if config.Storage.GCDelay != 0 { + log.Warn().Err(errors.ErrBadConfig). + Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored") + } + + if config.Storage.GCInterval != 0 { + log.Warn().Err(errors.ErrBadConfig). + Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored") + } + } + + return nil +} + +func validateSync(config *Config) error { + // check glob patterns in sync config are compilable + if config.Extensions != nil && config.Extensions.Sync != nil { + if config.Extensions.Sync.CredentialsFile != "" { + _, err := sync.GetFileCredentials(config.Extensions.Sync.CredentialsFile) + if err != nil { + log.Error().Err(err).Msg("sync: couldn't read credentials file") + + return err + } + } + + for id, regCfg := range config.Extensions.Sync.Registries { + // check retry options are configured for sync + if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil { + log.Error().Err(errors.ErrBadConfig).Msgf("extensions.sync.registries[%d].retryDelay"+ + " is required when using extensions.sync.registries[%d].maxRetries", id, id) + + return errors.ErrBadConfig + } + + if regCfg.Content != nil { + for _, content := range regCfg.Content { + ok := glob.ValidatePattern(content.Prefix) + if !ok { + log.Error().Err(glob.ErrBadPattern).Str("pattern", content.Prefix).Msg("sync pattern could not be compiled") + + return glob.ErrBadPattern + } + } + } + } + } + + return nil +} + +func validateAuthzPolicies(config *Config) error { + if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil)) && + !authzContainsOnlyAnonymousPolicy(config) { + log.Error().Err(errors.ErrBadConfig). + Msg("access control config requires httpasswd, ldap authentication " + + "or using only 'anonymousPolicy' policies") + + return errors.ErrBadConfig + } + + return nil +} + +func authzContainsOnlyAnonymousPolicy(cfg *Config) bool { + adminPolicy := cfg.HTTP.AccessControl.AdminPolicy + anonymousPolicyPresent := false + + log.Info().Msg("checking if anonymous authorization is the only type of authorization policy configured") + + if len(adminPolicy.Actions)+len(adminPolicy.Users) > 0 { + log.Info().Msg("admin policy detected, anonymous authorization is not the only authorization policy configured") + + return false + } + + for _, repository := range cfg.HTTP.AccessControl.Repositories { + if len(repository.DefaultPolicy) > 0 { + log.Info().Interface("repository", repository). + Msg("default policy detected, anonymous authorization is not the only authorization policy configured") + + return false + } + + if len(repository.AnonymousPolicy) > 0 { + log.Info().Msg("anonymous authorization detected") + + anonymousPolicyPresent = true + } + + for _, policy := range repository.Policies { + if len(policy.Actions)+len(policy.Users) > 0 { + log.Info().Interface("repository", repository). + Msg("repository with non-empty policy detected, " + + "anonymous authorization is not the only authorization policy configured") + + return false + } + } + } + + return anonymousPolicyPresent +} + +func validateLocalStorage(rootDir string) error { + if _, err := os.Stat(rootDir); os.IsNotExist(err) { + if err := os.MkdirAll(rootDir, storage.DefaultDirPerms); err != nil { + log.Error().Err(err).Str("rootDir", rootDir).Msg("unable to create root dir") + + return err + } + } + + return nil +} + +func validateRemoteStorage(config *Config) error { + if len(config.Storage.StorageDriver) != 0 { + // enforce s3 driver in case of using storage driver + if config.Storage.StorageDriver["name"] != storage.S3StorageDriverName { + log.Error().Err(errors.ErrBadConfig).Msgf("unsupported storage driver: %s", config.Storage.StorageDriver["name"]) + + return errors.ErrBadConfig + } + + // enforce filesystem storage in case sync feature is enabled + if config.Extensions != nil && config.Extensions.Sync != nil { + log.Error().Err(errors.ErrBadConfig).Msg("sync supports only filesystem storage") + + return errors.ErrBadConfig + } + } + + // enforce s3 driver on subpaths in case of using storage driver + if config.Storage.SubPaths != nil { + if len(config.Storage.SubPaths) > 0 { + subPaths := config.Storage.SubPaths + + for route, storageConfig := range subPaths { + if len(storageConfig.StorageDriver) != 0 { + if storageConfig.StorageDriver["name"] != storage.S3StorageDriverName { + log.Error().Err(errors.ErrBadConfig).Str("subpath", + route).Msgf("unsupported storage driver: %s", storageConfig.StorageDriver["name"]) + + return errors.ErrBadConfig + } + } + } + } + } + + return nil +} + +func validateHtpasswd(config *Config) error { + if config.HTTP.Auth != nil && config.HTTP.Auth.HTPasswd.Path != "" { + _, err := os.Open(config.HTTP.Auth.HTPasswd.Path) + if err != nil { + log.Error().Err(err).Str("path", config.HTTP.Auth.HTPasswd.Path).Msg("authn: couldn't read htpasswd file") + + return err + } + } + + return nil +} + +func validateTLSCerts(config *Config) error { + if config.HTTP.TLS != nil { + if config.HTTP.TLS.CACert != "" { + _, err := os.Open(config.HTTP.TLS.CACert) + if err != nil { + log.Error().Err(err).Str("path", config.HTTP.TLS.CACert).Msg("authn: couldn't read TLS cacert file") + + return err + } + } + + if config.HTTP.TLS.Cert != "" { + _, err := os.Open(config.HTTP.TLS.Cert) + if err != nil { + log.Error().Err(err).Str("path", config.HTTP.TLS.Cert).Msg("authn: couldn't read TLS cert file") + + return err + } + } + + if config.HTTP.TLS.Key != "" { + _, err := os.Open(config.HTTP.TLS.Key) + if err != nil { + log.Error().Err(err).Str("path", config.HTTP.TLS.Key).Msg("authn: couldn't read TLS key file") + + return err + } + } + } + + return nil +} + +func validateConfigExtensions(config *Config) error { + if config.Extensions != nil && config.Extensions.SysConfig != nil { + if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil)) || + config.HTTP.AccessControl == nil { + log.Error().Err(errors.ErrBadConfig).Msgf("config extensions needs auth and authorization enabled") + + return errors.ErrBadConfig + } + } + + return nil +} + +func validateStorageConfig(cfg *Config) error { + expConfigMap := make(map[string]StorageConfig, 0) + + defaultRootDir := cfg.Storage.RootDirectory + + for _, storageConfig := range cfg.Storage.SubPaths { + if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) { + log.Error().Err(errors.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory") + + return errors.ErrBadConfig + } + + expConfig, ok := expConfigMap[storageConfig.RootDirectory] + if ok { + equal := expConfig.ParamsEqual(storageConfig) + if !equal { + log.Error().Err(errors.ErrBadConfig).Msg("storage config with same root directory should have same parameters") + + return errors.ErrBadConfig + } + } else { + expConfigMap[storageConfig.RootDirectory] = storageConfig + } + } + + return nil +} diff --git a/pkg/api/constants/extensions.go b/pkg/api/constants/extensions.go index 6e3105eea7..9f64cca96c 100644 --- a/pkg/api/constants/extensions.go +++ b/pkg/api/constants/extensions.go @@ -6,4 +6,5 @@ const ( ExtOciDiscoverPrefix = "/_oci/ext/discover" // zot specific extensions. ExtSearchPrefix = RoutePrefix + "/_zot/ext/search" + ExtConfigPrefix = RoutePrefix + "/_zot/ext/config" ) diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 257770689a..239cc47bc0 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -56,6 +56,14 @@ func NewController(config *config.Config) *Controller { controller.Audit = audit } + addr := fmt.Sprintf("%s:%s", controller.Config.HTTP.Address, controller.Config.HTTP.Port) + server := &http.Server{ + Addr: addr, + Handler: controller.Router, + IdleTimeout: idleTimeout, + } + controller.Server = server + return &controller } @@ -135,6 +143,7 @@ func (c *Controller) Run(reloadCtx context.Context) error { } c.Router = engine + c.Server.Handler = engine c.Router.UseEncodedPath() var enabled bool @@ -147,32 +156,26 @@ func (c *Controller) Run(reloadCtx context.Context) error { c.Metrics = monitoring.NewMetricsServer(enabled, c.Log) - if err := c.InitImageStore(reloadCtx); err != nil { + if err := c.InitImageStore(); err != nil { return err } - monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, - c.Config.DistSpecVersion) - // nolint: contextcheck _ = NewRouteHandler(c) - addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port) - server := &http.Server{ - Addr: addr, - Handler: c.Router, - IdleTimeout: idleTimeout, - } - c.Server = server + c.StartBackgroundTasks(reloadCtx) + + monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, + c.Config.DistSpecVersion) // Create the listener - listener, err := net.Listen("tcp", addr) + listener, err := net.Listen("tcp", c.Server.Addr) if err != nil { return err } if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { - server.TLSConfig = &tls.Config{ + c.Server.TLSConfig = &tls.Config{ CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, @@ -192,7 +195,7 @@ func (c *Controller) Run(reloadCtx context.Context) error { if c.Config.HTTP.TLS.CACert != "" { clientAuth := tls.VerifyClientCertIfGiven if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") && - !anonymousPolicyExists(c.Config.AccessControl) { + !anonymousPolicyExists(c.Config.HTTP.AccessControl) { clientAuth = tls.RequireAndVerifyClientCert } @@ -207,17 +210,17 @@ func (c *Controller) Run(reloadCtx context.Context) error { panic(errors.ErrBadCACert) } - server.TLSConfig.ClientAuth = clientAuth - server.TLSConfig.ClientCAs = caCertPool + c.Server.TLSConfig.ClientAuth = clientAuth + c.Server.TLSConfig.ClientCAs = caCertPool } - return server.ServeTLS(listener, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key) + return c.Server.ServeTLS(listener, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key) } - return server.Serve(listener) + return c.Server.Serve(listener) } -func (c *Controller) InitImageStore(reloadCtx context.Context) error { +func (c *Controller) InitImageStore() error { c.StoreController = storage.StoreController{} linter := ext.GetLinter(c.Config, c.Log) @@ -293,8 +296,6 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error { } } - c.StartBackgroundTasks(reloadCtx) - return nil } @@ -392,23 +393,6 @@ func compareImageStore(root1, root2 string) bool { return isSameFile } -func (c *Controller) LoadNewConfig(reloadCtx context.Context, config *config.Config) { - // reload access control config - c.Config.AccessControl = config.AccessControl - c.Config.HTTP.RawAccessControl = config.HTTP.RawAccessControl - - // Enable extensions if extension config is provided - if config.Extensions != nil && config.Extensions.Sync != nil { - // reload sync config - c.Config.Extensions.Sync = config.Extensions.Sync - ext.EnableSyncExtension(reloadCtx, c.Config, c.wgShutDown, c.StoreController, c.Log) - } else if c.Config.Extensions != nil { - c.Config.Extensions.Sync = nil - } - - c.Log.Info().Interface("reloaded params", c.Config.Sanitize()).Msg("new configuration settings") -} - func (c *Controller) Shutdown() { // wait gracefully c.wgShutDown.Wait() @@ -417,11 +401,11 @@ func (c *Controller) Shutdown() { _ = c.Server.Shutdown(ctx) } -func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { +func (c *Controller) StartBackgroundTasks(ctx context.Context) { // Enable extensions if extension config is provided for DefaultStore if c.Config != nil && c.Config.Extensions != nil { ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) - ext.EnableSearchExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) + ext.EnableSearchExtension(ctx, c.Config, c.Log, c.Config.Storage.RootDirectory) } if c.Config.Storage.SubPaths != nil { @@ -429,7 +413,7 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { // Enable extensions if extension config is provided for subImageStore if c.Config != nil && c.Config.Extensions != nil { ext.EnableMetricsExtension(c.Config, c.Log, storageConfig.RootDirectory) - ext.EnableSearchExtension(c.Config, c.Log, storageConfig.RootDirectory) + ext.EnableSearchExtension(ctx, c.Config, c.Log, storageConfig.RootDirectory) } } } @@ -437,7 +421,7 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { // Enable extensions if extension config is provided for storeController if c.Config.Extensions != nil { if c.Config.Extensions.Sync != nil { - ext.EnableSyncExtension(reloadCtx, c.Config, c.wgShutDown, c.StoreController, c.Log) + ext.EnableSyncExtension(ctx, c.Config, c.wgShutDown, c.StoreController, c.Log) } } @@ -445,25 +429,25 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { ext.EnableScrubExtension(c.Config, c.Log, false, nil, "") } - go StartPeriodicTasks(c.StoreController.DefaultStore, c.StoreController.SubStore, c.Config.Storage.SubPaths, + go StartPeriodicTasks(ctx, c.StoreController.DefaultStore, c.StoreController.SubStore, c.Config.Storage.SubPaths, c.Config.Storage.GC, c.Config.Storage.GCInterval, c.Config.Extensions, c.Log) } -func StartPeriodicTasks(defaultStore storage.ImageStore, subStore map[string]storage.ImageStore, +func StartPeriodicTasks(ctx context.Context, defaultStore storage.ImageStore, subStore map[string]storage.ImageStore, subPaths map[string]config.StorageConfig, gcEnabled bool, gcInterval time.Duration, extensions *extconf.ExtensionConfig, log log.Logger, ) { // start periodic gc and/or scrub for DefaultStore - StartPeriodicTasksForImageStore(defaultStore, gcEnabled, gcInterval, extensions, log) + StartPeriodicTasksForImageStore(ctx, defaultStore, gcEnabled, gcInterval, extensions, log) for route, storageConfig := range subPaths { // Enable running garbage-collect or/and scrub periodically for subImageStore - StartPeriodicTasksForImageStore(subStore[route], storageConfig.GC, storageConfig.GCInterval, extensions, log) + StartPeriodicTasksForImageStore(ctx, subStore[route], storageConfig.GC, storageConfig.GCInterval, extensions, log) } } -func StartPeriodicTasksForImageStore(imageStore storage.ImageStore, configGC bool, configGCInterval time.Duration, - extensions *extconf.ExtensionConfig, log log.Logger, +func StartPeriodicTasksForImageStore(ctx context.Context, imageStore storage.ImageStore, configGC bool, + configGCInterval time.Duration, extensions *extconf.ExtensionConfig, log log.Logger, ) { scrubInterval := time.Duration(0) gcInterval := time.Duration(0) @@ -486,39 +470,47 @@ func StartPeriodicTasksForImageStore(imageStore storage.ImageStore, configGC boo return } - log.Info().Msg(fmt.Sprintf("Periodic interval for %s set to %s", imageStore.RootDir(), interval)) + log.Info().Msg(fmt.Sprintf("periodic interval for %s set to %s", imageStore.RootDir(), interval)) var lastGC, lastScrub time.Time for { - log.Info().Msg(fmt.Sprintf("Starting periodic background tasks for %s", imageStore.RootDir())) + select { + case <-ctx.Done(): + log.Info().Msgf("periodic background task for %s will exit, config reloaded", + imageStore.RootDir()) - // Enable running garbage-collect or/and scrub periodically for imageStore - RunBackgroundTasks(imageStore, gc, scrub, log) + return + default: + log.Info().Msg(fmt.Sprintf("starting periodic background tasks for %s", imageStore.RootDir())) - log.Info().Msg(fmt.Sprintf("Finishing periodic background tasks for %s", imageStore.RootDir())) + // Enable running garbage-collect or/and scrub periodically for imageStore + RunBackgroundTasks(ctx, imageStore, gc, scrub, log) - if gc { - lastGC = time.Now() - } + log.Info().Msg(fmt.Sprintf("finishing periodic background tasks for %s", imageStore.RootDir())) - if scrub { - lastScrub = time.Now() - } + if gc { + lastGC = time.Now() + } - time.Sleep(interval) + if scrub { + lastScrub = time.Now() + } - if !lastGC.IsZero() && time.Since(lastGC) >= gcInterval { - gc = true - } + time.Sleep(interval) + + if !lastGC.IsZero() && time.Since(lastGC) >= gcInterval { + gc = true + } - if !lastScrub.IsZero() && time.Since(lastScrub) >= scrubInterval { - scrub = true + if !lastScrub.IsZero() && time.Since(lastScrub) >= scrubInterval { + scrub = true + } } } } -func RunBackgroundTasks(imgStore storage.ImageStore, gc, scrub bool, log log.Logger) { +func RunBackgroundTasks(ctx context.Context, imgStore storage.ImageStore, gc, scrub bool, log log.Logger) { repos, err := imgStore.GetRepositories() if err != nil { log.Error().Err(err).Msg(fmt.Sprintf("error while running background task for %s", imgStore.RootDir())) @@ -527,26 +519,33 @@ func RunBackgroundTasks(imgStore storage.ImageStore, gc, scrub bool, log log.Log } for _, repo := range repos { - if gc { - start := time.Now() + select { + case <-ctx.Done(): + log.Info().Msgf("periodic background task for %s will exit, config reloaded", imgStore.RootDir()) - // run gc for this repo - imgStore.RunGCRepo(repo) + return + default: + if gc { + start := time.Now() - elapsed := time.Since(start) - log.Info().Msg(fmt.Sprintf("gc for %s executed in %s", repo, elapsed)) - time.Sleep(1 * time.Minute) - } + // run gc for this repo + imgStore.RunGCRepo(repo) + + elapsed := time.Since(start) + log.Info().Msg(fmt.Sprintf("gc for %s executed in %s", repo, elapsed)) + time.Sleep(1 * time.Minute) + } - if scrub { - start := time.Now() + if scrub { + start := time.Now() - // run scrub for this repo - ext.EnableScrubExtension(nil, log, true, imgStore, repo) + // run scrub for this repo + ext.EnableScrubExtension(nil, log, true, imgStore, repo) - elapsed := time.Since(start) - log.Info().Msg(fmt.Sprintf("scrub for %s executed in %s", repo, elapsed)) - time.Sleep(1 * time.Minute) + elapsed := time.Since(start) + log.Info().Msg(fmt.Sprintf("scrub for %s executed in %s", repo, elapsed)) + time.Sleep(1 * time.Minute) + } } } } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index f9034ad762..2204a292d0 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -991,7 +991,7 @@ func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) { Key: ServerKey, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1057,7 +1057,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -1082,7 +1082,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] // setup TLS mutual auth cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key") @@ -1109,7 +1109,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { // empty default authorization and give user the permission to create repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) @@ -1140,7 +1140,7 @@ func TestMutualTLSAuthWithoutCN(t *testing.T) { CACert: "../../test/data/noidentity/ca.crt", } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -1261,7 +1261,7 @@ func TestTLSMutualAuthAllowReadAccess(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1427,7 +1427,7 @@ func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1846,7 +1846,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) { ctlr := api.NewController(conf) ctlr.Config.Storage.RootDirectory = t.TempDir() - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -2062,7 +2062,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { Path: htpasswdPath, }, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -2127,9 +2127,9 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // first let's use global based policies // add test user to global policy with create perm - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // now it should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2165,7 +2165,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // get tags with read access should get 200 - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2195,7 +2195,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm on repo - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll // delete blob should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2207,7 +2207,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // now let's use only repository based policies // add test user to repo's policy with create perm // longest path matching should match the repo and not **/* - conf.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ Policies: []config.Policy{ { Users: []string{}, @@ -2217,8 +2217,8 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { DefaultPolicy: []string{}, } - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // now it should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2254,7 +2254,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // get tags with read access should get 200 - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2290,7 +2290,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm on repo - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll // delete blob should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2300,10 +2300,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // remove permissions on **/* so it will not interfere with zot-test namespace - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // get manifest should get 403, we don't have perm at all on this repo resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2313,7 +2313,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read perm on repo - conf.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ + conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ { Users: []string{"test"}, Actions: []string{"read"}, @@ -2340,7 +2340,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add create perm on repo - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // should get 201 with create perm resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2426,7 +2426,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.Body(), ShouldResemble, manifestBlob) // add update perm on repo - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll // update manifest should get 201 with update perm resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2446,10 +2446,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.Body(), ShouldResemble, updatedManifestBlob) // now use default repo policy - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} - repoPolicy = conf.AccessControl.Repositories["zot-test"] + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] repoPolicy.DefaultPolicy = []string{"update"} - conf.AccessControl.Repositories["zot-test"] = repoPolicy + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy // update manifest should get 201 with update perm on repo's default policy resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2461,10 +2461,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusCreated) // with default read on repo should still get 200 - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} - repoPolicy = conf.AccessControl.Repositories[AuthorizationNamespace] + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] repoPolicy.DefaultPolicy = []string{"read"} - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2474,7 +2474,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // upload blob without user create but with default create should get 200 repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create") - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2483,15 +2483,15 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // remove per repo policy - repoPolicy = conf.AccessControl.Repositories[AuthorizationNamespace] + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy - repoPolicy = conf.AccessControl.Repositories["zot-test"] + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories["zot-test"] = repoPolicy + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2507,8 +2507,8 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read perm - conf.AccessControl.AdminPolicy.Users = append(conf.AccessControl.AdminPolicy.Users, "test") - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "read") + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "test") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "read") // with read perm should get 200 resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2524,7 +2524,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add create perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "create") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") // with create perm should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2552,7 +2552,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "delete") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete") // with delete perm should get http.StatusAccepted resp, err = resty.R().SetBasicAuth(username, passphrase). Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) @@ -2568,7 +2568,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add update perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "update") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update") // update manifest should get 201 with update perm resp, err = resty.R().SetBasicAuth(username, passphrase). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). @@ -2578,7 +2578,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - conf.AccessControl = &config.AccessControlConfig{} + conf.HTTP.AccessControl = &config.AccessControlConfig{} resp, err = resty.R().SetBasicAuth(username, passphrase). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). @@ -2652,7 +2652,7 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { conf := config.New() conf.HTTP.Port = port conf.HTTP.Auth = &config.AuthConfig{} - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ TestRepo: config.PolicyGroup{ AnonymousPolicy: []string{}, @@ -2690,9 +2690,9 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - if entry, ok := conf.AccessControl.Repositories[TestRepo]; ok { + if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok { entry.AnonymousPolicy = []string{"create", "read"} - conf.AccessControl.Repositories[TestRepo] = entry + conf.HTTP.AccessControl.Repositories[TestRepo] = entry } // now it should get 202 @@ -2809,9 +2809,9 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(resp.Body(), ShouldResemble, manifestBlob) // add update perm on repo - if entry, ok := conf.AccessControl.Repositories[TestRepo]; ok { + if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok { entry.AnonymousPolicy = []string{"create", "read", "update"} - conf.AccessControl.Repositories[TestRepo] = entry + conf.HTTP.AccessControl.Repositories[TestRepo] = entry } // update manifest should get 201 with update perm @@ -2851,7 +2851,7 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { }, } // config with all policy types, to test that the correct one is applied in each case - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -2891,9 +2891,9 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // should have access to /v2/, anonymous policy is applied, "read" allowed resp, err = resty.R().Get(baseURL + "/v2/") @@ -2939,7 +2939,7 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "read") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // with read permission should get 200, because default policy allows reading now resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2975,8 +2975,8 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read permission to user "bob" - conf.AccessControl.AdminPolicy.Users = append(conf.AccessControl.AdminPolicy.Users, "bob") - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "create") + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "bob") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") // added create permission to user "bob", should be allowed now resp, err = resty.R().SetBasicAuth("bob", passphrase). @@ -3060,7 +3060,7 @@ func TestHTTPReadOnly(t *testing.T) { conf := config.New() conf.HTTP.Port = port // enable read-only mode - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ DefaultPolicy: []string{"read"}, @@ -5798,7 +5798,7 @@ func TestPeriodicGC(t *testing.T) { So(string(data), ShouldContainSubstring, "\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000") So(string(data), ShouldContainSubstring, - fmt.Sprintf("Starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll + fmt.Sprintf("starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll So(string(data), ShouldNotContainSubstring, fmt.Sprintf("error while running background task for %s", ctlr.StoreController.DefaultStore.RootDir())) So(string(data), ShouldContainSubstring, @@ -5842,7 +5842,7 @@ func TestPeriodicGC(t *testing.T) { So(string(data), ShouldContainSubstring, fmt.Sprintf("\"SubPaths\":{\"/a\":{\"RootDirectory\":\"%s\",\"GC\":true,\"Dedupe\":false,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":86400000000000", subDir)) //nolint:lll // gofumpt conflicts with lll So(string(data), ShouldContainSubstring, - fmt.Sprintf("Starting periodic background tasks for %s", ctlr.StoreController.SubStore["/a"].RootDir())) //nolint:lll + fmt.Sprintf("starting periodic background tasks for %s", ctlr.StoreController.SubStore["/a"].RootDir())) //nolint:lll }) } @@ -5873,13 +5873,13 @@ func TestPeriodicTasks(t *testing.T) { data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) So(string(data), ShouldContainSubstring, - fmt.Sprintf("Starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll + fmt.Sprintf("starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll So(string(data), ShouldNotContainSubstring, fmt.Sprintf("error while running background task for %s", ctlr.StoreController.DefaultStore.RootDir())) So(string(data), ShouldContainSubstring, - fmt.Sprintf("Finishing periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll + fmt.Sprintf("finishing periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll So(string(data), ShouldContainSubstring, - fmt.Sprintf("Periodic interval for %s set to %s", + fmt.Sprintf("periodic interval for %s set to %s", ctlr.StoreController.DefaultStore.RootDir(), ctlr.Config.Extensions.Scrub.Interval)) }) @@ -5909,13 +5909,13 @@ func TestPeriodicTasks(t *testing.T) { data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) So(string(data), ShouldContainSubstring, - fmt.Sprintf("Starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll + fmt.Sprintf("starting periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll So(string(data), ShouldNotContainSubstring, fmt.Sprintf("error while running background task for %s", ctlr.StoreController.DefaultStore.RootDir())) So(string(data), ShouldContainSubstring, - fmt.Sprintf("Finishing periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll + fmt.Sprintf("finishing periodic background tasks for %s", ctlr.StoreController.DefaultStore.RootDir())) //nolint:lll So(string(data), ShouldContainSubstring, - fmt.Sprintf("Periodic interval for %s set to %s", + fmt.Sprintf("periodic interval for %s set to %s", ctlr.StoreController.DefaultStore.RootDir(), ctlr.Config.Storage.GCInterval)) }) } @@ -5993,7 +5993,7 @@ func TestSearchRoutes(t *testing.T) { Search: searchConfig, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ repoName: config.PolicyGroup{ Policies: []config.Policy{ diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 8fcf66d432..302e86267d 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -61,7 +61,7 @@ func (rh *RouteHandler) SetupRoutes() { rh.c.Router.Use(AuthHandler(rh.c)) // authz is being enabled if AccessControl is specified // if Authn is not present AccessControl will have only default policies - if rh.c.Config.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) { + if rh.c.Config.HTTP.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) { if isAuthnEnabled(rh.c.Config) { rh.c.Log.Info().Msg("access control is being enabled") } else { @@ -123,6 +123,7 @@ func (rh *RouteHandler) SetupRoutes() { // extended build ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) ext.SetupSearchRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) + ext.SetupConfigRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log) } } } diff --git a/pkg/cli/config_reloader.go b/pkg/cli/config_reloader.go index 62fb907ba8..b12dc82766 100644 --- a/pkg/cli/config_reloader.go +++ b/pkg/cli/config_reloader.go @@ -2,6 +2,9 @@ package cli import ( "context" + "errors" + "net/http" + "time" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" @@ -9,13 +12,14 @@ import ( "zotregistry.io/zot/pkg/api/config" ) +const fsnotifyRateLimit = 500 * time.Millisecond + type HotReloader struct { - watcher *fsnotify.Watcher - filePath string - ctlr *api.Controller + watcher *fsnotify.Watcher + configPath string } -func NewHotReloader(ctlr *api.Controller, filePath string) (*HotReloader, error) { +func NewHotReloader(configPath string) (*HotReloader, error) { // creates a new file watcher watcher, err := fsnotify.NewWatcher() if err != nil { @@ -23,60 +27,97 @@ func NewHotReloader(ctlr *api.Controller, filePath string) (*HotReloader, error) } hotReloader := &HotReloader{ - watcher: watcher, - filePath: filePath, - ctlr: ctlr, + watcher: watcher, + configPath: configPath, } return hotReloader, nil } -func (hr *HotReloader) Start() context.Context { +func (hr *HotReloader) Start() { + defer hr.watcher.Close() done := make(chan bool) + cfg := config.New() + if err := config.LoadFromFile(hr.configPath, cfg); err != nil { + panic(err) + } + + ctlr := api.NewController(cfg) + reloadCtx, cancelOnReloadFunc := context.WithCancel(context.Background()) - // run watcher + + // start server go func() { - defer hr.watcher.Close() + if err := ctlr.Run(reloadCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() - go func() { - for { - select { - // watch for events - case event := <-hr.watcher.Events: - if event.Op == fsnotify.Write { - log.Info().Msg("config file changed, trying to reload config") + go func() { + var ( + timer *time.Timer + lastEvent fsnotify.Event + ) + + // this is an workaround for fsnotify firing 2 write events instead of 1 + timer = time.NewTimer(time.Millisecond) + <-timer.C // timer should be expired at first + + for { + select { + // watch for events + case event := <-hr.watcher.Events: + lastEvent = event + + timer.Reset(fsnotifyRateLimit) + case <-timer.C: + if lastEvent.Op == fsnotify.Write { + log.Info().Msg("reloader: config file changed, trying to hot reload config") + + newConfig := &config.Config{} + if err := config.LoadFromFile(hr.configPath, newConfig); err != nil { + log.Error().Err(err).Msg("reloader: couldn't hot reload config, retry writing it.") + + continue + } - newConfig := config.New() + // create new context + reloadCtx, cancelOnReloadFunc = context.WithCancel(context.Background()) - err := LoadConfiguration(newConfig, hr.filePath) - if err != nil { - log.Error().Err(err).Msg("couldn't reload config, retry writing it.") + shutdownFunc := ctlr.Shutdown + ctlr = api.NewController(newConfig) - continue - } - // if valid config then reload - cancelOnReloadFunc() + // if valid config then reload + // stop go routines + cancelOnReloadFunc() + // stop server + shutdownFunc() - // create new context - reloadCtx, cancelOnReloadFunc = context.WithCancel(context.Background()) - hr.ctlr.LoadNewConfig(reloadCtx, newConfig) + // wait for server to shutdown + for isServerRunning(cfg.HTTP.Address, cfg.HTTP.Port) { + continue } - // watch for errors - case err := <-hr.watcher.Errors: - log.Error().Err(err).Msgf("fsnotfy error while watching config %s", hr.filePath) - panic(err) + + // start new server + go func() { + if err := ctlr.Run(reloadCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() } + // watch for errors + case err := <-hr.watcher.Errors: + log.Error().Err(err).Msgf("reloader: fsnotfy error while watching config %s", hr.configPath) + panic(err) } - }() - - if err := hr.watcher.Add(hr.filePath); err != nil { - log.Error().Err(err).Msgf("error adding config file %s to FsNotify watcher", hr.filePath) - panic(err) } - - <-done }() - return reloadCtx + if err := hr.watcher.Add(hr.configPath); err != nil { + log.Error().Err(err).Msgf("reloader: error adding config file %s to FsNotify watcher", hr.configPath) + panic(err) + } + + <-done } diff --git a/pkg/cli/config_reloader_test.go b/pkg/cli/config_reloader_test.go index 30d3ad44a6..1d6c08da49 100644 --- a/pkg/cli/config_reloader_test.go +++ b/pkg/cli/config_reloader_test.go @@ -13,6 +13,11 @@ import ( "zotregistry.io/zot/pkg/test" ) +const ( + username = "test" + password = "test" +) + func TestConfigReloader(t *testing.T) { oldArgs := os.Args @@ -25,9 +30,6 @@ func TestConfigReloader(t *testing.T) { logFile, err := os.CreateTemp("", "zot-log*.txt") So(err, ShouldBeNil) - username := "alice" - password := "alice" - hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { panic(err) @@ -43,7 +45,7 @@ func TestConfigReloader(t *testing.T) { content := fmt.Sprintf(`{ "distSpecVersion": "0.1.0-dev", "storage": { - "rootDirectory": "/tmp/zot" + "rootDirectory": "%s" }, "http": { "address": "127.0.0.1", @@ -56,14 +58,16 @@ func TestConfigReloader(t *testing.T) { "failDelay": 1 }, "accessControl": { - "**": { - "policies": [ - { - "users": ["charlie"], - "actions": ["read"] + "repositories": { + "**": { + "policies": [ + { + "users": ["other"], + "actions": ["read"] + } + ], + "defaultPolicy": ["read", "create"] } - ], - "defaultPolicy": ["read", "create"] }, "adminPolicy": { "users": ["admin"], @@ -75,7 +79,7 @@ func TestConfigReloader(t *testing.T) { "level": "debug", "output": "%s" } - }`, port, htpasswdPath, logFile.Name()) + }`, t.TempDir(), port, htpasswdPath, logFile.Name()) cfgfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) @@ -99,7 +103,7 @@ func TestConfigReloader(t *testing.T) { content = fmt.Sprintf(`{ "distSpecVersion": "0.1.0-dev", "storage": { - "rootDirectory": "/tmp/zot" + "rootDirectory": "%s" }, "http": { "address": "127.0.0.1", @@ -112,14 +116,16 @@ func TestConfigReloader(t *testing.T) { "failDelay": 1 }, "accessControl": { - "**": { - "policies": [ - { - "users": ["alice"], - "actions": ["read", "create", "update", "delete"] + "repositories": { + "**": { + "policies": [ + { + "users": ["test"], + "actions": ["read", "create", "update", "delete"] + } + ], + "defaultPolicy": ["read"] } - ], - "defaultPolicy": ["read"] }, "adminPolicy": { "users": ["admin"], @@ -131,7 +137,7 @@ func TestConfigReloader(t *testing.T) { "level": "debug", "output": "%s" } - }`, port, htpasswdPath, logFile.Name()) + }`, t.TempDir(), port, htpasswdPath, logFile.Name()) err = cfgfile.Truncate(0) So(err, ShouldBeNil) @@ -146,13 +152,11 @@ func TestConfigReloader(t *testing.T) { So(err, ShouldBeNil) // wait for config reload - time.Sleep(2 * time.Second) + time.Sleep(3 * time.Second) data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) - So(string(data), ShouldContainSubstring, "reloaded params") - So(string(data), ShouldContainSubstring, "new configuration settings") - So(string(data), ShouldContainSubstring, "\"Users\":[\"alice\"]") + So(string(data), ShouldContainSubstring, "\"Users\":[\"test\"]") So(string(data), ShouldContainSubstring, "\"Actions\":[\"read\",\"create\",\"update\",\"delete\"]") }) @@ -168,7 +172,7 @@ func TestConfigReloader(t *testing.T) { content := fmt.Sprintf(`{ "distSpecVersion": "0.1.0-dev", "storage": { - "rootDirectory": "/tmp/zot" + "rootDirectory": "%s" }, "http": { "address": "127.0.0.1", @@ -184,9 +188,7 @@ func TestConfigReloader(t *testing.T) { "urls": ["http://localhost:8080"], "tlsVerify": false, "onDemand": true, - "maxRetries": 3, - "retryDelay": "15m", - "certDir": "", + "pollInterval": "1m", "content":[ { "prefix": "zot-test", @@ -199,7 +201,7 @@ func TestConfigReloader(t *testing.T) { }] } } - }`, port, logFile.Name()) + }`, t.TempDir(), port, logFile.Name()) cfgfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) @@ -223,7 +225,7 @@ func TestConfigReloader(t *testing.T) { content = fmt.Sprintf(`{ "distSpecVersion": "0.1.0-dev", "storage": { - "rootDirectory": "/tmp/zot" + "rootDirectory": "%s" }, "http": { "address": "127.0.0.1", @@ -236,12 +238,10 @@ func TestConfigReloader(t *testing.T) { "extensions": { "sync": { "registries": [{ - "urls": ["http://localhost:9999"], + "urls": ["localhost:9999"], "tlsVerify": true, "onDemand": false, - "maxRetries": 10, - "retryDelay": "5m", - "certDir": "certs", + "pollInterval": "1m", "content":[ { "prefix": "zot-cve-test", @@ -254,7 +254,7 @@ func TestConfigReloader(t *testing.T) { }] } } - }`, port, logFile.Name()) + }`, t.TempDir(), port, logFile.Name()) err = cfgfile.Truncate(0) So(err, ShouldBeNil) @@ -269,18 +269,13 @@ func TestConfigReloader(t *testing.T) { So(err, ShouldBeNil) // wait for config reload - time.Sleep(2 * time.Second) + time.Sleep(3 * time.Second) data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) - So(string(data), ShouldContainSubstring, "reloaded params") - So(string(data), ShouldContainSubstring, "new configuration settings") - So(string(data), ShouldContainSubstring, "\"URLs\":[\"http://localhost:9999\"]") + So(string(data), ShouldContainSubstring, "\"URLs\":[\"localhost:9999\"]") So(string(data), ShouldContainSubstring, "\"TLSVerify\":true") So(string(data), ShouldContainSubstring, "\"OnDemand\":false") - So(string(data), ShouldContainSubstring, "\"MaxRetries\":10") - So(string(data), ShouldContainSubstring, "\"RetryDelay\":300000000000") - So(string(data), ShouldContainSubstring, "\"CertDir\":\"certs\"") So(string(data), ShouldContainSubstring, "\"Prefix\":\"zot-cve-test\"") So(string(data), ShouldContainSubstring, "\"Regex\":\"tag\"") So(string(data), ShouldContainSubstring, "\"Semver\":false") @@ -298,7 +293,7 @@ func TestConfigReloader(t *testing.T) { content := fmt.Sprintf(`{ "distSpecVersion": "0.1.0-dev", "storage": { - "rootDirectory": "/tmp/zot" + "rootDirectory": "%s" }, "http": { "address": "127.0.0.1", @@ -329,7 +324,224 @@ func TestConfigReloader(t *testing.T) { }] } } - }`, port, logFile.Name()) + }`, t.TempDir(), port, logFile.Name()) + + cfgfile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + + defer os.Remove(cfgfile.Name()) // clean up + + _, err = cfgfile.Write([]byte(content)) + So(err, ShouldBeNil) + + // err = cfgfile.Close() + // So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "serve", cfgfile.Name()} + go func() { + err = cli.NewServerRootCmd().Execute() + So(err, ShouldBeNil) + }() + + test.WaitTillServerReady(baseURL) + + Convey("reload with unparsable config", func(c C) { + content = "[]" + + err = cfgfile.Truncate(0) + So(err, ShouldBeNil) + + _, err = cfgfile.Seek(0, io.SeekStart) + So(err, ShouldBeNil) + + _, err = cfgfile.WriteString(content) + So(err, ShouldBeNil) + + err = cfgfile.Close() + So(err, ShouldBeNil) + + // wait for config reload + time.Sleep(3 * time.Second) + + data, err := os.ReadFile(logFile.Name()) + So(err, ShouldBeNil) + So(string(data), ShouldContainSubstring, "\"URLs\":[\"http://localhost:8080\"]") + So(string(data), ShouldContainSubstring, "\"TLSVerify\":false") + So(string(data), ShouldContainSubstring, "\"OnDemand\":true") + So(string(data), ShouldContainSubstring, "\"MaxRetries\":3") + So(string(data), ShouldContainSubstring, "\"CertDir\":\"\"") + So(string(data), ShouldContainSubstring, "\"Prefix\":\"zot-test\"") + So(string(data), ShouldContainSubstring, "\"Regex\":\".*\"") + So(string(data), ShouldContainSubstring, "\"Semver\":true") + }) + + Convey("reload with invalid config", func(c C) { + // config with sysconfig enabled but no auth and autz + content := fmt.Sprintf(` + { + "distSpecVersion": "1.0.1-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s" + }, + "log": { + "level": "debug", + "output": "%s" + }, + "extensions": { + "sysconfig": { + "enable": true + } + } + }`, t.TempDir(), port, logFile.Name()) + + err = cfgfile.Truncate(0) + So(err, ShouldBeNil) + + _, err = cfgfile.Seek(0, io.SeekStart) + So(err, ShouldBeNil) + + _, err = cfgfile.WriteString(content) + So(err, ShouldBeNil) + + err = cfgfile.Close() + So(err, ShouldBeNil) + + // wait for config reload + time.Sleep(3 * time.Second) + + data, err := os.ReadFile(logFile.Name()) + So(err, ShouldBeNil) + So(string(data), ShouldContainSubstring, "\"URLs\":[\"http://localhost:8080\"]") + So(string(data), ShouldContainSubstring, "\"TLSVerify\":false") + So(string(data), ShouldContainSubstring, "\"OnDemand\":true") + So(string(data), ShouldContainSubstring, "\"MaxRetries\":3") + So(string(data), ShouldContainSubstring, "\"CertDir\":\"\"") + So(string(data), ShouldContainSubstring, "\"Prefix\":\"zot-test\"") + So(string(data), ShouldContainSubstring, "\"Regex\":\".*\"") + So(string(data), ShouldContainSubstring, "\"Semver\":true") + }) + + Convey("reload with valid config but wrong values", func(c C) { + // config with sysconfig enabled but no auth and autz + content := fmt.Sprintf(` + { + "distSpecVersion": "1.0.1-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "auth": { + "htpasswd": { + "path": "/wrong/path/htpasswd" + } + } + }, + "log": { + "level": "debug", + "output": "%s" + } + }`, t.TempDir(), port, logFile.Name()) + + err = cfgfile.Truncate(0) + So(err, ShouldBeNil) + + _, err = cfgfile.Seek(0, io.SeekStart) + So(err, ShouldBeNil) + + _, err = cfgfile.WriteString(content) + So(err, ShouldBeNil) + + err = cfgfile.Close() + So(err, ShouldBeNil) + + // wait for config reload + time.Sleep(3 * time.Second) + + data, err := os.ReadFile(logFile.Name()) + So(err, ShouldBeNil) + So(string(data), ShouldContainSubstring, "\"URLs\":[\"http://localhost:8080\"]") + So(string(data), ShouldContainSubstring, "\"TLSVerify\":false") + So(string(data), ShouldContainSubstring, "\"OnDemand\":true") + So(string(data), ShouldContainSubstring, "\"MaxRetries\":3") + So(string(data), ShouldContainSubstring, "\"CertDir\":\"\"") + So(string(data), ShouldContainSubstring, "\"Prefix\":\"zot-test\"") + So(string(data), ShouldContainSubstring, "\"Regex\":\".*\"") + So(string(data), ShouldContainSubstring, "\"Semver\":true") + }) + }) +} + +func TestConfigReloaderAllExtensions(t *testing.T) { + oldArgs := os.Args + + defer func() { os.Args = oldArgs }() + + Convey("reload access control config", t, func(c C) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + logFile, err := os.CreateTemp("", "zot-log*.txt") + So(err, ShouldBeNil) + + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash)) + + htpasswdPath := test.MakeHtpasswdFileFromString(usernameAndHash) + defer os.Remove(htpasswdPath) + + defer os.Remove(logFile.Name()) // clean up + + content := fmt.Sprintf(`{ + "distSpecVersion": "0.1.0-dev", + "storage": { + "rootDirectory": "%s", + "gc": true, + "gcDelay": "1h", + "gcInterval": "24h" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "%s" + }, + "failDelay": 1 + }, + "accessControl": { + "adminPolicy": { + "users": ["admin"], + "actions": ["read", "create", "update", "delete"] + } + } + }, + "extensions": { + "metrics": {}, + "search": { + "cve": { + "updateInterval": "2h" + } + }, + "scrub": { + "interval": "24h" + } + }, + "log": { + "level": "debug", + "output": "%s" + } + }`, t.TempDir(), port, htpasswdPath, logFile.Name()) cfgfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) @@ -350,7 +562,30 @@ func TestConfigReloader(t *testing.T) { test.WaitTillServerReady(baseURL) - content = "[]" + // wait for cve db to be downloaded + time.Sleep(30 * time.Second) + + content = fmt.Sprintf(`{ + "distSpecVersion": "0.1.0-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "%s" + }, + "failDelay": 1 + } + }, + "log": { + "level": "debug", + "output": "%s" + } + }`, t.TempDir(), port, htpasswdPath, logFile.Name()) err = cfgfile.Truncate(0) So(err, ShouldBeNil) @@ -365,19 +600,10 @@ func TestConfigReloader(t *testing.T) { So(err, ShouldBeNil) // wait for config reload - time.Sleep(2 * time.Second) + time.Sleep(3 * time.Second) data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) - So(string(data), ShouldNotContainSubstring, "reloaded params") - So(string(data), ShouldNotContainSubstring, "new configuration settings") - So(string(data), ShouldContainSubstring, "\"URLs\":[\"http://localhost:8080\"]") - So(string(data), ShouldContainSubstring, "\"TLSVerify\":false") - So(string(data), ShouldContainSubstring, "\"OnDemand\":true") - So(string(data), ShouldContainSubstring, "\"MaxRetries\":3") - So(string(data), ShouldContainSubstring, "\"CertDir\":\"\"") - So(string(data), ShouldContainSubstring, "\"Prefix\":\"zot-test\"") - So(string(data), ShouldContainSubstring, "\"Regex\":\".*\"") - So(string(data), ShouldContainSubstring, "\"Semver\":true") + So(string(data), ShouldContainSubstring, "\"Extensions\":null") }) } diff --git a/pkg/cli/extensions_test.go b/pkg/cli/extensions_test.go index c398ae8bf0..a894724e85 100644 --- a/pkg/cli/extensions_test.go +++ b/pkg/cli/extensions_test.go @@ -147,7 +147,7 @@ func testWithMetricsEnabled(cfgContentFormat string) { data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) So(string(data), ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":{\"Enable\":true,\"Prometheus\":{\"Path\":\"/metrics\"}},\"Scrub\":null,\"Lint\":null}") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":{\"Enable\":true,\"Prometheus\":{\"Path\":\"/metrics\"}},\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}") //nolint:lll // gofumpt conflicts with lll } func TestServeMetricsExtension(t *testing.T) { @@ -271,7 +271,7 @@ func TestServeMetricsExtension(t *testing.T) { data, err := os.ReadFile(logFile.Name()) So(err, ShouldBeNil) So(string(data), ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":{\"Enable\":false,\"Prometheus\":{\"Path\":\"/metrics\"}},\"Scrub\":null,\"Lint\":null}}") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":{\"Enable\":false,\"Prometheus\":{\"Path\":\"/metrics\"}},\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}}") //nolint:lll // gofumpt conflicts with lll }) } @@ -441,11 +441,11 @@ func TestServeScrubExtension(t *testing.T) { // Even if in config we specified scrub interval=1h, the minimum interval is 2h dataStr := string(data) So(dataStr, ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Interval\":3600000000000},\"Lint\":null") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Interval\":3600000000000},\"Lint\":null,\"SysConfig\":null") //nolint:lll // gofumpt conflicts with lll So(dataStr, ShouldContainSubstring, "Scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.") - So(dataStr, ShouldContainSubstring, "Starting periodic background tasks for") - So(dataStr, ShouldContainSubstring, "Finishing periodic background tasks for") + So(dataStr, ShouldContainSubstring, "starting periodic background tasks for") + So(dataStr, ShouldContainSubstring, "finishing periodic background tasks for") }) Convey("scrub not enabled - scrub interval param not set", t, func(c C) { @@ -471,10 +471,9 @@ func TestServeScrubExtension(t *testing.T) { So(err, ShouldBeNil) data, err := os.ReadFile(logPath) So(err, ShouldBeNil) - defer os.Remove(logPath) // clean up dataStr := string(data) So(dataStr, ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}") + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}") So(dataStr, ShouldContainSubstring, "Scrub config not provided, skipping scrub") So(dataStr, ShouldNotContainSubstring, "Scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.") @@ -509,14 +508,14 @@ func TestServeLintExtension(t *testing.T) { logPath, err := runCLIWithConfig(t.TempDir(), content) So(err, ShouldBeNil) + data, err := os.ReadFile(logPath) So(err, ShouldBeNil) - defer os.Remove(logPath) // clean up So(string(data), ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enabled\":true,\"MandatoryAnnotations\":") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enabled\":true,\"MandatoryAnnotations\":[\"annot1\"]},\"SysConfig\":null") //nolint:lll // gofumpt conflicts with lll }) - Convey("lint enabled", t, func(c C) { + Convey("lint disabled", t, func(c C) { content := `{ "storage": { "rootDirectory": "%s" @@ -540,9 +539,8 @@ func TestServeLintExtension(t *testing.T) { So(err, ShouldBeNil) data, err := os.ReadFile(logPath) So(err, ShouldBeNil) - defer os.Remove(logPath) // clean up So(string(data), ShouldContainSubstring, - "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enabled\":false,\"MandatoryAnnotations\":null}") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enabled\":false,\"MandatoryAnnotations\":null},\"SysConfig\":null") //nolint:lll // gofumpt conflicts with lll }) } @@ -577,7 +575,7 @@ func TestServeSearchEnabled(t *testing.T) { WaitTillTrivyDBDownloadStarted(tempDir) defer os.Remove(logPath) // clean up - substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":86400000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll + substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":86400000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}" //nolint:lll // gofumpt conflicts with lll found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout) So(found, ShouldBeTrue) So(err, ShouldBeNil) @@ -622,7 +620,7 @@ func TestServeSearchEnabledCVE(t *testing.T) { // to avoid data race when multiple go routines write to trivy DB instance. WaitTillTrivyDBDownloadStarted(tempDir) - substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll + substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}" //nolint:lll // gofumpt conflicts with lll found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout) So(found, ShouldBeTrue) So(err, ShouldBeNil) @@ -670,7 +668,7 @@ func TestServeSearchEnabledNoCVE(t *testing.T) { // to avoid data race when multiple go routines write to trivy DB instance. WaitTillTrivyDBDownloadStarted(tempDir) - substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":86400000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll + substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":86400000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}" //nolint:lll // gofumpt conflicts with lll found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout) So(found, ShouldBeTrue) So(err, ShouldBeNil) @@ -713,10 +711,9 @@ func TestServeSearchDisabled(t *testing.T) { So(err, ShouldBeNil) data, err := os.ReadFile(logPath) So(err, ShouldBeNil) - defer os.Remove(logPath) // clean up dataStr := string(data) So(dataStr, ShouldContainSubstring, - "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":10800000000000},\"Enable\":false},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}") //nolint:lll // gofumpt conflicts with lll + "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":10800000000000},\"Enable\":false},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"SysConfig\":null}") //nolint:lll // gofumpt conflicts with lll So(dataStr, ShouldContainSubstring, "CVE config not provided, skipping CVE update") So(dataStr, ShouldNotContainSubstring, "CVE update interval set to too-short interval < 2h, changing update duration to 2 hours and continuing.") diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 19f8948c8f..ba57b83914 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -5,32 +5,15 @@ import ( "fmt" "net" "net/http" - "strings" - "time" - glob "github.com/bmatcuk/doublestar/v4" - "github.com/mitchellh/mapstructure" distspec "github.com/opencontainers/distribution-spec/specs-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/spf13/viper" - "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" - "zotregistry.io/zot/pkg/api/constants" - extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" - "zotregistry.io/zot/pkg/storage" ) -// metadataConfig reports metadata after parsing, which we use to track -// errors. -func metadataConfig(md *mapstructure.Metadata) viper.DecoderConfigOption { - return func(c *mapstructure.DecoderConfig) { - c.Metadata = md - } -} - func newServeCmd(conf *config.Config) *cobra.Command { // "serve" serveCmd := &cobra.Command{ @@ -40,25 +23,18 @@ func newServeCmd(conf *config.Config) *cobra.Command { Long: "`serve` stores and distributes OCI images", Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { - if err := LoadConfiguration(conf, args[0]); err != nil { + hotReloader, err := NewHotReloader(args[0]) + if err != nil { panic(err) } - } - ctlr := api.NewController(conf) - - // config reloader - hotReloader, err := NewHotReloader(ctlr, args[0]) - if err != nil { - panic(err) - } - - /* context used to cancel go routines so that - we can change their config on the fly (restart routines with different config) */ - reloaderCtx := hotReloader.Start() + hotReloader.Start() + } else { + if err := cmd.Usage(); err != nil { + panic(err) + } - if err := ctlr.Run(reloaderCtx); err != nil { - panic(err) + return } }, } @@ -75,7 +51,7 @@ func newScrubCmd(conf *config.Config) *cobra.Command { Long: "`scrub` checks manifest/blob integrity", Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { - if err := LoadConfiguration(conf, args[0]); err != nil { + if err := config.LoadFromFile(args[0], conf); err != nil { panic(err) } } else { @@ -87,18 +63,8 @@ func newScrubCmd(conf *config.Config) *cobra.Command { } // checking if the server is already running - req, err := http.NewRequestWithContext(context.Background(), - http.MethodGet, - fmt.Sprintf("http://%s/v2", net.JoinHostPort(conf.HTTP.Address, conf.HTTP.Port)), - nil) - if err != nil { - log.Error().Err(err).Msg("unable to create a new http request") - panic(err) - } - - response, err := http.DefaultClient.Do(req) - if err == nil { - response.Body.Close() + ok := isServerRunning(conf.HTTP.Address, conf.HTTP.Port) + if ok { log.Warn().Msg("The server is running, in order to perform the scrub command the server should be shut down") panic("Error: server is running") } else { @@ -106,7 +72,7 @@ func newScrubCmd(conf *config.Config) *cobra.Command { ctlr := api.NewController(conf) ctlr.Metrics = monitoring.NewMetricsServer(false, ctlr.Log) - if err := ctlr.InitImageStore(context.Background()); err != nil { + if err := ctlr.InitImageStore(); err != nil { panic(err) } @@ -132,11 +98,13 @@ func newVerifyCmd(conf *config.Config) *cobra.Command { Long: "`verify` validates a zot config file", Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { - if err := LoadConfiguration(conf, args[0]); err != nil { + if err := config.LoadFromFile(args[0], conf); err != nil { + log.Error().Err(err).Str("path", args[0]).Msgf("invalid config file %s", args[0]) + panic(err) } - log.Info().Msgf("Config file %s is valid", args[0]) + log.Info().Msgf("config file %s is valid", args[0]) } }, } @@ -203,372 +171,23 @@ func NewCliRootCmd() *cobra.Command { return rootCmd } -func validateStorageConfig(cfg *config.Config) error { - expConfigMap := make(map[string]config.StorageConfig, 0) - - defaultRootDir := cfg.Storage.RootDirectory - - for _, storageConfig := range cfg.Storage.SubPaths { - if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) { - log.Error().Err(errors.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory") - - return errors.ErrBadConfig - } - - expConfig, ok := expConfigMap[storageConfig.RootDirectory] - if ok { - equal := expConfig.ParamsEqual(storageConfig) - if !equal { - log.Error().Err(errors.ErrBadConfig).Msg("storage config with same root directory should have same parameters") - - return errors.ErrBadConfig - } - } else { - expConfigMap[storageConfig.RootDirectory] = storageConfig - } - } - - return nil -} - -func validateConfiguration(config *config.Config) error { - if err := validateGC(config); err != nil { - return err - } - - if err := validateLDAP(config); err != nil { - return err - } - - if err := validateSync(config); err != nil { - return err - } - - if err := validateStorageConfig(config); err != nil { - return err - } - - // check authorization config, it should have basic auth enabled or ldap - if config.HTTP.RawAccessControl != nil { - // checking for anonymous policy only authorization config: no users, no policies but anonymous policy - if err := validateAuthzPolicies(config); err != nil { - return err - } - } - - if len(config.Storage.StorageDriver) != 0 { - // enforce s3 driver in case of using storage driver - if config.Storage.StorageDriver["name"] != storage.S3StorageDriverName { - log.Error().Err(errors.ErrBadConfig).Msgf("unsupported storage driver: %s", config.Storage.StorageDriver["name"]) - - return errors.ErrBadConfig - } - - // enforce filesystem storage in case sync feature is enabled - if config.Extensions != nil && config.Extensions.Sync != nil { - log.Error().Err(errors.ErrBadConfig).Msg("sync supports only filesystem storage") - - return errors.ErrBadConfig - } - } - - // enforce s3 driver on subpaths in case of using storage driver - if config.Storage.SubPaths != nil { - if len(config.Storage.SubPaths) > 0 { - subPaths := config.Storage.SubPaths - - for route, storageConfig := range subPaths { - if len(storageConfig.StorageDriver) != 0 { - if storageConfig.StorageDriver["name"] != storage.S3StorageDriverName { - log.Error().Err(errors.ErrBadConfig).Str("subpath", - route).Msgf("unsupported storage driver: %s", storageConfig.StorageDriver["name"]) - - return errors.ErrBadConfig - } - } - } - } - } - - // check glob patterns in authz config are compilable - if config.AccessControl != nil { - for pattern := range config.AccessControl.Repositories { - ok := glob.ValidatePattern(pattern) - if !ok { - log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).Msg("authorization pattern could not be compiled") - - return glob.ErrBadPattern - } - } - } - - return nil -} - -func validateAuthzPolicies(config *config.Config) error { - if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil)) && - !authzContainsOnlyAnonymousPolicy(config) { - log.Error().Err(errors.ErrBadConfig). - Msg("access control config requires httpasswd, ldap authentication " + - "or using only 'anonymousPolicy' policies") - - return errors.ErrBadConfig - } - - return nil -} - -func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) { - defaultVal := true - - if config.Extensions == nil && viperInstance.Get("extensions") != nil { - config.Extensions = &extconf.ExtensionConfig{} - - extMap := viperInstance.GetStringMap("extensions") - _, ok := extMap["metrics"] - - if ok { - // we found a config like `"extensions": {"metrics": {}}` - // Note: In case metrics is not empty the config.Extensions will not be nil and we will not reach here - config.Extensions.Metrics = &extconf.MetricsConfig{} - } - - _, ok = extMap["search"] - if ok { - // we found a config like `"extensions": {"search": {}}` - // Note: In case search is not empty the config.Extensions will not be nil and we will not reach here - config.Extensions.Search = &extconf.SearchConfig{} - } - } - - if config.Extensions != nil { - if config.Extensions.Sync != nil { - if config.Extensions.Sync.Enable == nil { - config.Extensions.Sync.Enable = &defaultVal - } - - for id, regCfg := range config.Extensions.Sync.Registries { - if regCfg.TLSVerify == nil { - config.Extensions.Sync.Registries[id].TLSVerify = &defaultVal - } - } - } - - if config.Extensions.Search != nil { - if config.Extensions.Search.Enable == nil { - config.Extensions.Search.Enable = &defaultVal - } - - if config.Extensions.Search.CVE == nil { - config.Extensions.Search.CVE = &extconf.CVEConfig{UpdateInterval: 24 * time.Hour} // nolint: gomnd - } - } - - if config.Extensions.Metrics != nil { - if config.Extensions.Metrics.Enable == nil { - config.Extensions.Metrics.Enable = &defaultVal - } - - if config.Extensions.Metrics.Prometheus == nil { - config.Extensions.Metrics.Prometheus = &extconf.PrometheusConfig{Path: constants.DefaultMetricsExtensionRoute} - } - } - } - - if !config.Storage.GC && viperInstance.Get("storage::gcdelay") == nil { - config.Storage.GCDelay = 0 - } -} - -func updateDistSpecVersion(config *config.Config) { - if config.DistSpecVersion == distspec.Version { - return - } - - log.Warn(). - Msgf("config dist-spec version: %s differs from version actually used: %s", - config.DistSpecVersion, distspec.Version) - - config.DistSpecVersion = distspec.Version -} - -func LoadConfiguration(config *config.Config, configPath string) error { - // Default is dot (.) but because we allow glob patterns in authz - // we need another key delimiter. - viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::")) - - viperInstance.SetConfigFile(configPath) - - if err := viperInstance.ReadInConfig(); err != nil { - log.Error().Err(err).Msg("error while reading configuration") - - return err - } - - metaData := &mapstructure.Metadata{} - if err := viperInstance.Unmarshal(&config, metadataConfig(metaData)); err != nil { - log.Error().Err(err).Msg("error while unmarshalling new config") - - return err - } - - if len(metaData.Keys) == 0 { - log.Error().Err(errors.ErrBadConfig).Msgf("config doesn't contain any key:value pair") - - return errors.ErrBadConfig - } - - if len(metaData.Unused) > 0 { - log.Error().Err(errors.ErrBadConfig).Msgf("unknown keys: %v", metaData.Unused) - - return errors.ErrBadConfig - } - - err := config.LoadAccessControlConfig(viperInstance) +func isServerRunning(address, port string) bool { + // checking if the server is already running + req, err := http.NewRequestWithContext(context.Background(), + http.MethodGet, + fmt.Sprintf("http://%s/v2", net.JoinHostPort(address, port)), + nil) if err != nil { - log.Error().Err(err).Msg("unable to unmarshal config's accessControl") - - return err - } - - // defaults - applyDefaultValues(config, viperInstance) - - // various config checks - if err := validateConfiguration(config); err != nil { - return err + log.Error().Err(err).Msg("unable to create a new http request") + panic(err) } - // update distSpecVersion - updateDistSpecVersion(config) - - return nil -} - -func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool { - adminPolicy := cfg.AccessControl.AdminPolicy - anonymousPolicyPresent := false - - log.Info().Msg("checking if anonymous authorization is the only type of authorization policy configured") - - if len(adminPolicy.Actions)+len(adminPolicy.Users) > 0 { - log.Info().Msg("admin policy detected, anonymous authorization is not the only authorization policy configured") - + response, err := http.DefaultClient.Do(req) + if err != nil { return false } - for _, repository := range cfg.AccessControl.Repositories { - if len(repository.DefaultPolicy) > 0 { - log.Info().Interface("repository", repository). - Msg("default policy detected, anonymous authorization is not the only authorization policy configured") - - return false - } - - if len(repository.AnonymousPolicy) > 0 { - log.Info().Msg("anonymous authorization detected") - - anonymousPolicyPresent = true - } - - for _, policy := range repository.Policies { - if len(policy.Actions)+len(policy.Users) > 0 { - log.Info().Interface("repository", repository). - Msg("repository with non-empty policy detected, " + - "anonymous authorization is not the only authorization policy configured") - - return false - } - } - } - - return anonymousPolicyPresent -} - -func validateLDAP(config *config.Config) error { - // LDAP mandatory configuration - if config.HTTP.Auth != nil && config.HTTP.Auth.LDAP != nil { - ldap := config.HTTP.Auth.LDAP - if ldap.UserAttribute == "" { - log.Error().Str("userAttribute", ldap.UserAttribute). - Msg("invalid LDAP configuration, missing mandatory key: userAttribute") - - return errors.ErrLDAPConfig - } - - if ldap.Address == "" { - log.Error().Str("address", ldap.Address). - Msg("invalid LDAP configuration, missing mandatory key: address") - - return errors.ErrLDAPConfig - } - - if ldap.BaseDN == "" { - log.Error().Str("basedn", ldap.BaseDN). - Msg("invalid LDAP configuration, missing mandatory key: basedn") - - return errors.ErrLDAPConfig - } - } - - return nil -} - -func validateGC(config *config.Config) error { - // enforce GC params - if config.Storage.GCDelay < 0 { - log.Error().Err(errors.ErrBadConfig). - Msgf("invalid garbage-collect delay %v specified", config.Storage.GCDelay) - - return errors.ErrBadConfig - } - - if config.Storage.GCInterval < 0 { - log.Error().Err(errors.ErrBadConfig). - Msgf("invalid garbage-collect interval %v specified", config.Storage.GCInterval) - - return errors.ErrBadConfig - } - - if !config.Storage.GC { - if config.Storage.GCDelay != 0 { - log.Warn().Err(errors.ErrBadConfig). - Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored") - } - - if config.Storage.GCInterval != 0 { - log.Warn().Err(errors.ErrBadConfig). - Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored") - } - } - - return nil -} - -func validateSync(config *config.Config) error { - // check glob patterns in sync config are compilable - if config.Extensions != nil && config.Extensions.Sync != nil { - for id, regCfg := range config.Extensions.Sync.Registries { - // check retry options are configured for sync - if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil { - log.Error().Err(errors.ErrBadConfig).Msgf("extensions.sync.registries[%d].retryDelay"+ - " is required when using extensions.sync.registries[%d].maxRetries", id, id) - - return errors.ErrBadConfig - } - - if regCfg.Content != nil { - for _, content := range regCfg.Content { - ok := glob.ValidatePattern(content.Prefix) - if !ok { - log.Error().Err(glob.ErrBadPattern).Str("pattern", content.Prefix).Msg("sync pattern could not be compiled") - - return glob.ErrBadPattern - } - } - } - } - } + response.Body.Close() - return nil + return true } diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 64ad47e94a..f6311f7870 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "os" "path" "testing" @@ -88,6 +89,22 @@ func TestServe(t *testing.T) { os.Args = []string{"cli_test", "serve", tmpfile.Name()} So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) }) + + Convey("wrong path to htpasswd", func(c C) { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"distSpecVersion":"1.0.1-dev", + "http":{"address":"127.0.0.1","port":"8080", + "auth":{"htpasswd":{"path":"/A/B/C/htpasswd"}}}, + "log":{"level":"debug"}}`) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "serve", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) }) } @@ -109,13 +126,67 @@ func TestVerify(t *testing.T) { So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) }) + Convey("Test verify unwritable rootDir", t, func() { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"storage":{"rootDirectory":"/a/b/c/d"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + + content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", + "subPaths": {"/a": {"rootDirectory": "/a/b/c/d"}}}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) + + Convey("Test verify invalid certs", t, func() { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"distSpecVersion":"1.0.1-dev","storage":{"rootDirectory":"/tmp/zot"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "tls":{"cert":"server.cert"}}, + "log":{"level":"debug"}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + + content = []byte(`{"distSpecVersion":"1.0.1-dev","storage":{"rootDirectory":"/tmp/zot"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "tls":{"cacert":"ca.crt"}}, + "log":{"level":"debug"}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + + content = []byte(`{"distSpecVersion":"1.0.1-dev","storage":{"rootDirectory":"/tmp/zot"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "tls":{"key":"server.key"}}, + "log":{"level":"debug"}}`) + err = os.WriteFile(tmpfile.Name(), content, 0o0600) + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) + Convey("Test verify storage driver different than s3", t, func(c C) { tmpfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", "storageDriver": {"name": "gcs"}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) err = tmpfile.Close() @@ -129,9 +200,9 @@ func TestVerify(t *testing.T) { So(err, ShouldBeNil) defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", "storageDriver": {"name": "s3"}, - "subPaths": {"/a": {"rootDirectory": "/zot-a","storageDriver": {"name": "gcs"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","storageDriver": {"name": "gcs"}}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) err = tmpfile.Close() @@ -145,9 +216,9 @@ func TestVerify(t *testing.T) { So(err, ShouldBeNil) defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a"},"/b": {"rootDirectory": "/zot-a"}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a"},"/b": {"rootDirectory": "/tmp/zot-a"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} @@ -156,10 +227,10 @@ func TestVerify(t *testing.T) { // sub paths that point to same directory should have same storage config. content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, - "/b": {{"rootDirectory": "/zot-a","dedupe":"false"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"false"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} @@ -167,39 +238,39 @@ func TestVerify(t *testing.T) { // sub paths that point to default root directory should not be allowed. content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true"},"/b": {{"rootDirectory": "/zot-a"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true"},"/b": {"rootDirectory": "/tmp/zot-a"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"false"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"false"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} @@ -207,9 +278,9 @@ func TestVerify(t *testing.T) { content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", "subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) os.Args = []string{"cli_test", "verify", tmpfile.Name()} @@ -238,7 +309,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}, "accessControl":{"adminPolicy":{"users":["admin"], "actions":["read","create","update","delete"]}}}}`) _, err = tmpfile.Write(content) @@ -255,8 +326,8 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "accessControl":{"**":{"anonymousPolicy": ["read", "create"]}, - "/repo":{"anonymousPolicy": ["read", "create"]} + "accessControl":{"repositories":{"**":{"anonymousPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]}} }}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -272,8 +343,8 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "accessControl":{"**":{"defaultPolicy": ["read", "create"]}, - "/repo":{"anonymousPolicy": ["read", "create"]}, + "accessControl":{"repositories":{"**":{"defaultPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]}}, "adminPolicy":{"users":["admin"], "actions":["read","create","update","delete"]} }}}`) @@ -291,11 +362,11 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "accessControl":{"**":{"defaultPolicy": ["read", "create"]}, + "accessControl":{"repositories":{ "/repo":{"anonymousPolicy": ["read", "create"]}, "/repo2":{"policies": [{ "users": ["charlie"], - "actions": ["read", "create", "update"]}]} + "actions": ["read", "create", "update"]}]}} }}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -305,13 +376,30 @@ func TestVerify(t *testing.T) { So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) }) + Convey("Test verify anonymous-only authorization fail", t, func(c C) { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "accessControl":{"repositories":{"**":{"defaultPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]} + }}}}`) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) + Convey("Test verify w/ sync and w/o filesystem storage", t, func(c C) { tmpfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", "storageDriver": {"name": "s3"}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, "extensions":{"sync": {"registries": [{"urls":["localhost:9999"], "maxRetries": 1, "retryDelay": "10s"}]}}}`) _, err = tmpfile.Write(content) @@ -328,7 +416,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, "extensions":{"sync": {"registries": [{"urls":["localhost:9999"], "maxRetries": 1, "retryDelay": "10s"}]}}}`) _, err = tmpfile.Write(content) @@ -345,7 +433,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, "extensions":{"sync": {"registries": [{"urls":["localhost:9999"], "maxRetries": 1, "retryDelay": "10s", "content": [{"prefix":"[repo%^&"}]}]}}}`) @@ -357,14 +445,32 @@ func TestVerify(t *testing.T) { So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) }) + Convey("Test verify with bad sync credentials file", t, func(c C) { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, + "extensions":{"sync": {"credentialsFile": "credentials.json", "registries": [{"urls":["localhost:9999"], + "maxRetries": 1, "retryDelay": "10s", + "content": [{"prefix":"**"}]}]}}}`) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) + Convey("Test verify with bad authorization repo patterns", t, func(c C) { tmpfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}, - "accessControl":{"[":{"policies":[],"anonymousPolicy":[]}}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}, + "accessControl":{"repositories":{"[":{"policies":[],"anonymousPolicy":[]}}}}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) err = tmpfile.Close() @@ -379,7 +485,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, "extensions":{"sync": {"registries": [{"urls":["localhost:9999"], "maxRetries": 1, "retryDelay": "10s", "content": [{"prefix":"repo**"}]}]}}}`) @@ -398,7 +504,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}, + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, "extensions":{"sync": {"registries": [{"urls":["localhost:9999"], "maxRetries": 10, "content": [{"prefix":"repo**"}]}]}}}`) _, err = tmpfile.Write(content) @@ -472,6 +578,22 @@ func TestVerify(t *testing.T) { So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) }) + Convey("Test verify config extension w/o authz", t, func(c C) { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", "storageDriver": {"name": "s3"}}, + "http":{"address":"127.0.0.1","port":"8080","realm":"zot", + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}, + "extensions":{"sysconfig":{"enable": true}}}`) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "verify", tmpfile.Name()} + So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic) + }) + Convey("Test verify good config", t, func(c C) { tmpfile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) @@ -489,12 +611,11 @@ func TestVerify(t *testing.T) { }) } +func loadConfiguration(cfg *config.Config, configPath string) error { + return config.LoadFromFile(configPath, cfg) +} + func TestLoadConfig(t *testing.T) { - Convey("Test viper load config", t, func(c C) { - config := config.New() - err := cli.LoadConfiguration(config, "../../examples/config-policy.json") - So(err, ShouldBeNil) - }) Convey("Test subpath config combination", t, func(c C) { config := config.New() tmpfile, err := os.CreateTemp("", "zot-test*.json") @@ -502,42 +623,42 @@ func TestLoadConfig(t *testing.T) { defer os.Remove(tmpfile.Name()) content := []byte(`{"storage":{"rootDirectory":"/tmp/zot", "subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, tmpfile.Name()) + err = loadConfiguration(config, tmpfile.Name()) So(err, ShouldNotBeNil) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, tmpfile.Name()) + err = loadConfiguration(config, tmpfile.Name()) So(err, ShouldNotBeNil) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"false"}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"false"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, tmpfile.Name()) + err = loadConfiguration(config, tmpfile.Name()) So(err, ShouldNotBeNil) content = []byte(`{"storage":{"rootDirectory":"/tmp/zot", - "subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"}, - "/b": {"rootDirectory": "/zot-a","dedupe":"true"}}}, + "subPaths": {"/a": {"rootDirectory": "/tmp/zot-a","dedupe":"true"}, + "/b": {"rootDirectory": "/tmp/zot-a","dedupe":"true"}}}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`) + "auth":{"htpasswd":{"path":"../../test/data/htpasswd"},"failDelay":1}}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, tmpfile.Name()) + err = loadConfiguration(config, tmpfile.Name()) So(err, ShouldBeNil) }) } @@ -545,13 +666,13 @@ func TestLoadConfig(t *testing.T) { func TestGC(t *testing.T) { Convey("Test GC config", t, func(c C) { config := config.New() - err := cli.LoadConfiguration(config, "../../examples/config-multiple.json") + err := loadConfiguration(config, "../../examples/config-multiple.json") So(err, ShouldBeNil) So(config.Storage.GCDelay, ShouldEqual, storage.DefaultGCDelay) - err = cli.LoadConfiguration(config, "../../examples/config-gc.json") + err = loadConfiguration(config, "../../examples/config-gc.json") So(err, ShouldBeNil) So(config.Storage.GCDelay, ShouldNotEqual, storage.DefaultGCDelay) - err = cli.LoadConfiguration(config, "../../examples/config-gc-periodic.json") + err = loadConfiguration(config, "../../examples/config-gc-periodic.json") So(err, ShouldBeNil) }) @@ -573,7 +694,7 @@ func TestGC(t *testing.T) { err = os.WriteFile(file.Name(), contents, 0o600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, file.Name()) + err = loadConfiguration(config, file.Name()) So(err, ShouldBeNil) }) @@ -593,7 +714,7 @@ func TestGC(t *testing.T) { err = os.WriteFile(file.Name(), contents, 0o600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, file.Name()) + err = loadConfiguration(config, file.Name()) So(err, ShouldBeNil) }) @@ -611,7 +732,7 @@ func TestGC(t *testing.T) { err = os.WriteFile(file.Name(), contents, 0o600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, file.Name()) + err = loadConfiguration(config, file.Name()) So(err, ShouldNotBeNil) }) @@ -628,7 +749,7 @@ func TestGC(t *testing.T) { err = os.WriteFile(file.Name(), content, 0o600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, file.Name()) + err = loadConfiguration(config, file.Name()) So(err, ShouldBeNil) So(config.Storage.GCDelay, ShouldEqual, 0) }) @@ -647,7 +768,7 @@ func TestGC(t *testing.T) { err = os.WriteFile(file.Name(), contents, 0o600) So(err, ShouldBeNil) - err = cli.LoadConfiguration(config, file.Name()) + err = loadConfiguration(config, file.Name()) So(err, ShouldNotBeNil) }) }) diff --git a/pkg/extensions/config/config.go b/pkg/extensions/config/config.go index 4855ac77ca..931a290c4f 100644 --- a/pkg/extensions/config/config.go +++ b/pkg/extensions/config/config.go @@ -7,11 +7,12 @@ import ( ) type ExtensionConfig struct { - Search *SearchConfig - Sync *sync.Config - Metrics *MetricsConfig - Scrub *ScrubConfig - Lint *LintConfig + Search *SearchConfig + Sync *sync.Config + Metrics *MetricsConfig + Scrub *ScrubConfig + Lint *LintConfig + SysConfig *SysConfig } type LintConfig struct { @@ -19,6 +20,10 @@ type LintConfig struct { MandatoryAnnotations []string } +type SysConfig struct { + Enable *bool +} + type SearchConfig struct { // CVE search CVE *CVEConfig diff --git a/pkg/extensions/extension_search.go b/pkg/extensions/extension_search.go index 6aa0005b0a..ddfde38466 100644 --- a/pkg/extensions/extension_search.go +++ b/pkg/extensions/extension_search.go @@ -4,6 +4,7 @@ package extensions import ( + "context" "time" gqlHandler "github.com/99designs/gqlgen/graphql/handler" @@ -18,7 +19,7 @@ import ( "zotregistry.io/zot/pkg/storage" ) -func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) { +func EnableSearchExtension(ctx context.Context, config *config.Config, log log.Logger, rootDir string) { if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil { defaultUpdateInterval, _ := time.ParseDuration("2h") @@ -29,7 +30,7 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string } go func() { - err := downloadTrivyDB(rootDir, log, + err := downloadTrivyDB(ctx, rootDir, log, config.Extensions.Search.CVE.UpdateInterval) if err != nil { log.Error().Err(err).Msg("error while downloading TrivyDB") @@ -40,18 +41,25 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string } } -func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error { +func downloadTrivyDB(ctx context.Context, dbDir string, log log.Logger, updateInterval time.Duration) error { for { - log.Info().Msg("updating the CVE database") + select { + case <-ctx.Done(): + log.Info().Msgf("updating CVE database routine will exit, config reloaded") - err := cveinfo.UpdateCVEDb(dbDir, log) - if err != nil { - return err - } + return nil + default: + log.Info().Msg("updating the CVE database") - log.Info().Str("DB update completed, next update scheduled after", updateInterval.String()).Msg("") + err := cveinfo.UpdateCVEDb(dbDir, log) + if err != nil { + return err + } - time.Sleep(updateInterval) + log.Info().Str("DB update completed, next update scheduled after", updateInterval.String()).Msg("") + + time.Sleep(updateInterval) + } } } diff --git a/pkg/extensions/extension_search_disabled.go b/pkg/extensions/extension_search_disabled.go index ce757b8180..7da09a9c2c 100644 --- a/pkg/extensions/extension_search_disabled.go +++ b/pkg/extensions/extension_search_disabled.go @@ -4,6 +4,8 @@ package extensions import ( + "context" + "github.com/gorilla/mux" distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions" "zotregistry.io/zot/pkg/api/config" @@ -12,7 +14,7 @@ import ( ) // EnableSearchExtension ... -func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) { +func EnableSearchExtension(ctx context.Context, config *config.Config, log log.Logger, rootDir string) { log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," + "please build a binary that does so") } diff --git a/pkg/extensions/extensions-config-disabled.go b/pkg/extensions/extensions-config-disabled.go new file mode 100644 index 0000000000..2a635af8ee --- /dev/null +++ b/pkg/extensions/extensions-config-disabled.go @@ -0,0 +1,18 @@ +//go:build !config +// +build !config + +package extensions + +import ( + "github.com/gorilla/mux" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +func SetupConfigRoutes(config *config.Config, router *mux.Router, + storeController storage.StoreController, log log.Logger, +) { + log.Warn().Msg("skipping enabling config extension because given zot binary doesn't include this feature," + + "please build a binary that does so") +} diff --git a/pkg/extensions/extensions-config.go b/pkg/extensions/extensions-config.go new file mode 100644 index 0000000000..c1d1cf6296 --- /dev/null +++ b/pkg/extensions/extensions-config.go @@ -0,0 +1,90 @@ +//go:build config +// +build config + +package extensions + +import ( + "net/http" + "os" + "sync" + + "github.com/gorilla/mux" + "github.com/spf13/viper" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +type ConfigHandler struct { + configPath string + lock *sync.RWMutex + log log.Logger +} + +func NewConfigHandler(configPath string, log log.Logger) ConfigHandler { + return ConfigHandler{ + configPath: configPath, + lock: &sync.RWMutex{}, + log: log, + } +} + +func (handler *ConfigHandler) Handler(response http.ResponseWriter, request *http.Request) { + switch request.Method { + case http.MethodGet: + handler.lock.RLock() + defer handler.lock.RUnlock() + + config, err := os.ReadFile(handler.configPath) + if err != nil { + handler.log.Error().Err(err).Msg("config ext: couldn't read config file") + response.WriteHeader(http.StatusInternalServerError) + + return + } + + _, _ = response.Write(config) + + return + case http.MethodPost: + handler.lock.Lock() + defer handler.lock.Unlock() + + cfg := &config.Config{} + + writeConfig, err := config.LoadFromBufferWithWriter(handler.configPath, request.Body, cfg) + if err != nil { + handler.log.Error().Err(err).Msg("config ext: invalid config") + http.Error(response, "Invalid config", http.StatusBadRequest) + + return + } + + if err := writeConfig(); err != nil { + handler.log.Error().Err(err).Msg("config ext: couldn't write config") + http.Error(response, "Invalid config", http.StatusInternalServerError) + + return + } + + response.WriteHeader(http.StatusAccepted) + } +} + +func SetupConfigRoutes(config *config.Config, router *mux.Router, + storeController storage.StoreController, l log.Logger, +) { + if config.Extensions.SysConfig != nil && *config.Extensions.SysConfig.Enable { + log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()} + log.Info().Msg("setting up extensions routes") + + configPath := viper.GetViper().ConfigFileUsed() + + handler := NewConfigHandler(configPath, log) + + if config.Extensions.SysConfig != nil && *config.Extensions.SysConfig.Enable { + router.PathPrefix(constants.ExtConfigPrefix).Methods("GET", "POST").HandlerFunc(handler.Handler) + } + } +} diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index e78169b73d..d6cbf605b4 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -1,21 +1,33 @@ -//go:build sync || metrics -// +build sync metrics +//go:build sync || metrics || config +// +build sync metrics config package extensions_test import ( "context" + "fmt" + "io/ioutil" + "net/http" "os" "testing" . "github.com/smartystreets/goconvey/convey" + "golang.org/x/crypto/bcrypt" + "gopkg.in/resty.v1" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/cli" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/sync" "zotregistry.io/zot/pkg/test" ) +const ( + username = "test" + password = "test" +) + func TestEnableExtension(t *testing.T) { Convey("Verify log if sync disabled in config", t, func() { globalDir := t.TempDir() @@ -106,3 +118,279 @@ func TestMetricsExtension(t *testing.T) { "Prometheus instrumentation Path not set, changing to '/metrics'.") }) } + +func TestConfigExtensionAPI(t *testing.T) { + testCases := []struct { + configContent string + getStatus int + postStatus int + putStatus int + }{ + { + configContent: `{ + "distSpecVersion": "0.1.0-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "%s" + }, + "failDelay": 1 + }, + "accessControl": { + "adminPolicy": { + "users": ["test"], + "actions": ["read", "create", "update", "delete"] + } + } + }, + "extensions":{ + "sysconfig": { + "enable": true + } + }, + "log": { + "level": "debug" + } + }`, + getStatus: http.StatusOK, + postStatus: http.StatusAccepted, + putStatus: http.StatusMethodNotAllowed, + }, + { + configContent: `{ + "distSpecVersion": "0.1.0-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "%s" + }, + "failDelay": 1 + }, + "accessControl": { + "adminPolicy": { + "users": ["other"] + } + } + }, + "extensions":{ + "sysconfig": { + "enable": true + } + }, + "log": { + "level": "debug" + } + }`, + getStatus: http.StatusForbidden, + postStatus: http.StatusForbidden, + putStatus: http.StatusMethodNotAllowed, + }, + } + + Convey("Verify config http handler", t, func() { + for _, testCase := range testCases { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + logFile, err := ioutil.TempFile("", "zot-log*.txt") + So(err, ShouldBeNil) + + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + usernameAndHash := fmt.Sprintf("%s:%s\n%s:%s", username, string(hash), "nonadmin", string(hash)) + + htpasswdPath := test.MakeHtpasswdFileFromString(usernameAndHash) + defer os.Remove(htpasswdPath) + + defer os.Remove(logFile.Name()) // clean up + + content := fmt.Sprintf(testCase.configContent, t.TempDir(), port, htpasswdPath) + cfgfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + + defer os.Remove(cfgfile.Name()) // clean up + + _, err = cfgfile.Write([]byte(content)) + So(err, ShouldBeNil) + + err = cfgfile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "serve", cfgfile.Name()} + go func() { + err = cli.NewServerRootCmd().Execute() + So(err, ShouldBeNil) + }() + + test.WaitTillServerReady(baseURL) + + // get config + + resp, err := resty.R().SetBasicAuth("nonadmin", password). + Get(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get config + resp, err = resty.R().SetBasicAuth(username, password). + Get(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, testCase.getStatus) + + // post config + resp, err = resty.R().SetBasicAuth(username, password). + SetHeader("Content-Type", "application/json"). + SetBody([]byte(content)). + Post(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, testCase.postStatus) + + // put config should fail + resp, err = resty.R().SetBasicAuth(username, password). + Put(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, testCase.putStatus) + } + }) +} + +func TestConfigExtensionAPIErrors(t *testing.T) { + Convey("Verify config http handler", t, func() { + username := "test" + password := "test" + + configContent := `{ + "distSpecVersion": "0.1.0-dev", + "storage": { + "rootDirectory": "%s" + }, + "http": { + "address": "127.0.0.1", + "port": "%s", + "realm": "zot", + "auth": { + "htpasswd": { + "path": "%s" + }, + "failDelay": 1 + }, + "accessControl": { + "adminPolicy": { + "users": ["test"] + } + } + }, + "extensions":{ + "sysconfig": { + "enable": true + } + }, + "log": { + "level": "debug" + } + }` + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + logFile, err := ioutil.TempFile("", "zot-log*.txt") + So(err, ShouldBeNil) + + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash)) + + htpasswdPath := test.MakeHtpasswdFileFromString(usernameAndHash) + defer os.Remove(htpasswdPath) + + defer os.Remove(logFile.Name()) // clean up + + content := fmt.Sprintf(configContent, t.TempDir(), port, htpasswdPath) + cfgfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + + defer os.Remove(cfgfile.Name()) // clean up + + _, err = cfgfile.Write([]byte(content)) + So(err, ShouldBeNil) + + err = cfgfile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "serve", cfgfile.Name()} + go func() { + err = cli.NewServerRootCmd().Execute() + So(err, ShouldBeNil) + }() + + test.WaitTillServerReady(baseURL) + + Convey("GET read config error", func() { + // trigger permission denied on reading config file when GET + err = os.Chmod(cfgfile.Name(), 0o000) + So(err, ShouldBeNil) + + // get config + resp, err := resty.R().SetBasicAuth(username, password). + Get(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) + }) + Convey("POST errors", func() { + // trigger unmarshall error + resp, err := resty.R().SetBasicAuth(username, password). + SetHeader("Content-Type", "application/json"). + SetBody([]byte("{}")). + Post(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + + // trigger invalid config error + badConfig := ` + {"log": {"level":"debug"}} + ` + resp, err = resty.R().SetBasicAuth(username, password). + SetHeader("Content-Type", "application/json"). + SetBody([]byte(badConfig)). + Post(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + + // trigger write config error + err = os.Chmod(cfgfile.Name(), 0o000) + So(err, ShouldBeNil) + + resp, err = resty.R().SetBasicAuth(username, password). + SetHeader("Content-Type", "application/json"). + SetBody([]byte(content)). + Post(baseURL + constants.ExtConfigPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) + }) + }) +} diff --git a/pkg/extensions/sync/on_demand.go b/pkg/extensions/sync/on_demand.go index 29eb250c25..be46f32da5 100644 --- a/pkg/extensions/sync/on_demand.go +++ b/pkg/extensions/sync/on_demand.go @@ -96,7 +96,7 @@ func syncOneImage(imageChannel chan error, cfg Config, storeController storage.S if cfg.CredentialsFile != "" { var err error - credentialsFile, err = getFileCredentials(cfg.CredentialsFile) + credentialsFile, err = GetFileCredentials(cfg.CredentialsFile) if err != nil { log.Error().Str("errorType", TypeOf(err)). Err(err).Msgf("couldn't get registry credentials from %s", cfg.CredentialsFile) diff --git a/pkg/extensions/sync/sync.go b/pkg/extensions/sync/sync.go index 42a7d9063c..ea33d4ca09 100644 --- a/pkg/extensions/sync/sync.go +++ b/pkg/extensions/sync/sync.go @@ -582,7 +582,7 @@ func Run(ctx context.Context, cfg Config, var err error if cfg.CredentialsFile != "" { - credentialsFile, err = getFileCredentials(cfg.CredentialsFile) + credentialsFile, err = GetFileCredentials(cfg.CredentialsFile) if err != nil { logger.Error().Str("errortype", TypeOf(err)). Err(err).Msgf("couldn't get registry credentials from %s", cfg.CredentialsFile) @@ -651,6 +651,7 @@ func Run(ctx context.Context, cfg Config, select { case <-ctx.Done(): ticker.Stop() + logger.Info().Msg("sync routine will exit, config reloaded") return case <-ticker.C: diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 5c9349b73b..c5e5cb5830 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -93,7 +93,7 @@ func TestSyncInternal(t *testing.T) { _, err = parseRepositoryReference(repositoryReference) So(err, ShouldNotBeNil) - _, err = getFileCredentials("/path/to/inexistent/file") + _, err = GetFileCredentials("/path/to/inexistent/file") So(err, ShouldNotBeNil) tempFile, err := os.CreateTemp("", "sync-credentials-") @@ -106,7 +106,7 @@ func TestSyncInternal(t *testing.T) { panic(err) } - _, err = getFileCredentials(tempFile.Name()) + _, err = GetFileCredentials(tempFile.Name()) So(err, ShouldNotBeNil) srcCtx := &types.SystemContext{} @@ -151,7 +151,7 @@ func TestSyncInternal(t *testing.T) { So(Run(ctx, cfg, storage.StoreController{}, new(goSync.WaitGroup), log.NewLogger("debug", "")), ShouldNotBeNil) - _, err = getFileCredentials("/invalid/path/to/file") + _, err = GetFileCredentials("/invalid/path/to/file") So(err, ShouldNotBeNil) }) diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index d7ff94a9bb..4a0734320b 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -38,7 +38,6 @@ import ( "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" - "zotregistry.io/zot/pkg/cli" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/sync" "zotregistry.io/zot/pkg/storage" @@ -739,126 +738,6 @@ func TestOnDemandPermsDenied(t *testing.T) { }) } -func TestConfigReloader(t *testing.T) { - Convey("Verify periodically sync config reloader works", t, func() { - duration, _ := time.ParseDuration("3s") - - sctlr, srcBaseURL, srcDir, _, _ := startUpstreamServer(t, false, false) - defer os.RemoveAll(srcDir) - - defer func() { - sctlr.Shutdown() - }() - - var tlsVerify bool - - syncRegistryConfig := sync.RegistryConfig{ - Content: []sync.Content{ - { - Prefix: testImage, - }, - }, - URLs: []string{srcBaseURL}, - PollInterval: duration, - TLSVerify: &tlsVerify, - CertDir: "", - OnDemand: true, - } - - defaultVal := true - syncConfig := &sync.Config{ - Enable: &defaultVal, - Registries: []sync.RegistryConfig{syncRegistryConfig}, - } - - destPort := test.GetFreePort() - destConfig := config.New() - destBaseURL := test.GetBaseURL(destPort) - - destConfig.HTTP.Port = destPort - - destDir, err := os.MkdirTemp("", "oci-dest-repo-test") - if err != nil { - panic(err) - } - - defer os.RemoveAll(destDir) - - destConfig.Storage.RootDirectory = destDir - - destConfig.Extensions = &extconf.ExtensionConfig{} - destConfig.Extensions.Search = nil - destConfig.Extensions.Sync = syncConfig - - logFile, err := os.CreateTemp("", "zot-log*.txt") - So(err, ShouldBeNil) - - defer os.Remove(logFile.Name()) // clean up - - destConfig.Log.Output = logFile.Name() - - dctlr := api.NewController(destConfig) - - defer func() { - dctlr.Shutdown() - }() - - content := fmt.Sprintf(`{"distSpecVersion": "0.1.0-dev", "storage": {"rootDirectory": "%s"}, - "http": {"address": "127.0.0.1", "port": "%s"}, - "log": {"level": "debug", "output": "%s"}}`, destDir, destPort, logFile.Name()) - - cfgfile, err := os.CreateTemp("", "zot-test*.json") - So(err, ShouldBeNil) - - defer os.Remove(cfgfile.Name()) // clean up - - _, err = cfgfile.Write([]byte(content)) - So(err, ShouldBeNil) - - hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name()) - So(err, ShouldBeNil) - - reloadCtx := hotReloader.Start() - - go func() { - // this blocks - if err := dctlr.Run(reloadCtx); err != nil { - return - } - }() - - // wait till ready - for { - _, err := resty.R().Get(destBaseURL) - if err == nil { - break - } - - time.Sleep(100 * time.Millisecond) - } - - // let it sync - time.Sleep(3 * time.Second) - - // modify config - _, err = cfgfile.WriteString(" ") - So(err, ShouldBeNil) - - err = cfgfile.Close() - So(err, ShouldBeNil) - - time.Sleep(2 * time.Second) - - data, err := os.ReadFile(logFile.Name()) - t.Logf("downstream log: %s", string(data)) - So(err, ShouldBeNil) - So(string(data), ShouldContainSubstring, "reloaded params") - So(string(data), ShouldContainSubstring, "new configuration settings") - So(string(data), ShouldContainSubstring, "\"Sync\":null") - So(string(data), ShouldNotContainSubstring, "sync:") - }) -} - func TestMandatoryAnnotations(t *testing.T) { Convey("Verify mandatory annotations failing - on demand disabled", t, func() { updateDuration, _ := time.ParseDuration("30m") diff --git a/pkg/extensions/sync/utils.go b/pkg/extensions/sync/utils.go index b82d81437d..8015b378db 100644 --- a/pkg/extensions/sync/utils.go +++ b/pkg/extensions/sync/utils.go @@ -185,7 +185,7 @@ func getRepoDestination(remoteRepo string, content Content) string { } // Get sync.FileCredentials from file. -func getFileCredentials(filepath string) (CredentialsFile, error) { +func GetFileCredentials(filepath string) (CredentialsFile, error) { credsFile, err := os.ReadFile(filepath) if err != nil { return nil, err diff --git a/test/blackbox/anonymous_policiy.bats b/test/blackbox/anonymous_policiy.bats index ccbfb4c885..bce91ecb84 100644 --- a/test/blackbox/anonymous_policiy.bats +++ b/test/blackbox/anonymous_policiy.bats @@ -31,20 +31,22 @@ function setup_file() { } }, "accessControl": { - "**": { - "anonymousPolicy": ["read"], - "policies": [ - { - "users": [ - "test" - ], - "actions": [ - "read", - "create", - "update" - ] - } - ] + "repositories": { + "**": { + "anonymousPolicy": ["read"], + "policies": [ + { + "users": [ + "test" + ], + "actions": [ + "read", + "create", + "update" + ] + } + ] + } } } },