added support for multiple watch namespaces

This commit is contained in:
Safwan
2026-07-14 14:14:55 +05:00
parent 0ef47cdc4d
commit faf488fcd7
10 changed files with 154 additions and 80 deletions
+12 -6
View File
@@ -264,9 +264,11 @@ func BindFlags(fs *pflag.FlagSet, cfg *Config) {
_ = v.BindEnv("alert-proxy", "ALERT_PROXY", "ALERT_WEBHOOK_PROXY")
}
// ApplyFlags applies flag values from viper to the config struct.
// Call this after parsing flags.
func ApplyFlags(cfg *Config) error {
// ApplyFlags applies flag values from viper to the config struct. Call this
// after parsing flags. It returns any human-readable warnings produced while
// finalizing namespace scope (see ApplyNamespaceScope) so the caller can log
// them once a logger is available.
func ApplyFlags(cfg *Config) ([]string, error) {
// Boolean flags
cfg.AutoReloadAll = v.GetBool("auto-reload-all")
cfg.SyncAfterRestart = v.GetBool("sync-after-restart")
@@ -365,7 +367,7 @@ func ApplyFlags(cfg *Config) error {
joinedNS := strings.Join(nsSelectors, ",")
selector, err := labels.Parse(joinedNS)
if err != nil {
return fmt.Errorf("invalid selector %q: %w", joinedNS, err)
return nil, fmt.Errorf("invalid selector %q: %w", joinedNS, err)
}
cfg.NamespaceSelectors = []labels.Selector{selector}
}
@@ -373,7 +375,7 @@ func ApplyFlags(cfg *Config) error {
joinedRes := strings.Join(resSelectors, ",")
selector, err := labels.Parse(joinedRes)
if err != nil {
return fmt.Errorf("invalid selector %q: %w", joinedRes, err)
return nil, fmt.Errorf("invalid selector %q: %w", joinedRes, err)
}
cfg.ResourceSelectors = []labels.Selector{selector}
}
@@ -389,7 +391,11 @@ func ApplyFlags(cfg *Config) error {
cfg.LeaderElection.RetryPeriod = 2 * time.Second
}
return nil
// Enforce namespace-scope semantics here so the finalized config is
// self-consistent for every caller: selector/ignore lists are only honored
// in global mode. Warnings are returned for the caller to log once logging
// is set up.
return cfg.ApplyNamespaceScope(), nil
}
// parseBoolString parses a string as a boolean, defaulting to false.
+81 -20
View File
@@ -92,7 +92,7 @@ func TestBindFlags_DefaultValues(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -126,7 +126,7 @@ func TestBindFlags_CustomValues(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -168,7 +168,7 @@ func TestApplyFlags_SecretProviderClassAnnotations(t *testing.T) {
if err := fs.Parse(nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
defaults := DefaultAnnotations()
@@ -195,7 +195,7 @@ func TestApplyFlags_SecretProviderClassAnnotations(t *testing.T) {
if err := fs.Parse(args); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Annotations.SecretProviderClassAuto != "spc.example.com/auto" {
@@ -218,7 +218,7 @@ func TestApplyFlags_ExcludeAnnotations(t *testing.T) {
if err := fs.Parse(nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
defaults := DefaultAnnotations()
@@ -241,7 +241,7 @@ func TestApplyFlags_ExcludeAnnotations(t *testing.T) {
if err := fs.Parse(args); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Annotations.ConfigmapExclude != "cm.example.com/exclude" {
@@ -261,7 +261,7 @@ func TestApplyFlags_IgnoreAnnotation(t *testing.T) {
if err := fs.Parse(nil); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Annotations.Ignore != DefaultAnnotations().Ignore {
@@ -276,7 +276,7 @@ func TestApplyFlags_IgnoreAnnotation(t *testing.T) {
if err := fs.Parse([]string{"--ignore-annotation=my.company.com/reloader-ignore"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if cfg.Annotations.Ignore != "my.company.com/reloader-ignore" {
@@ -313,7 +313,7 @@ func TestApplyFlags_BooleanStrings(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
err := ApplyFlags(cfg)
_, err := ApplyFlags(cfg)
if (err != nil) != tt.wantErr {
t.Errorf("ApplyFlags() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -343,7 +343,7 @@ func TestApplyFlags_CommaSeparatedLists(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -378,7 +378,7 @@ func TestApplyFlags_Selectors(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -409,7 +409,7 @@ func TestApplyFlags_InvalidSelector(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
err := ApplyFlags(cfg)
_, err := ApplyFlags(cfg)
if err == nil {
t.Error("ApplyFlags() should return error for invalid selector")
}
@@ -461,7 +461,7 @@ func TestApplyFlags_AlertingEnvVars(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -494,7 +494,7 @@ func TestApplyFlags_LegacyProxyEnvVar(t *testing.T) {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -511,7 +511,7 @@ func TestApplyFlagsCSIIntegration(t *testing.T) {
if err := fs.Parse([]string{"--enable-csi-integration=true"}); err != nil {
t.Fatal(err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatal(err)
}
if !cfg.CSIIntegrationEnabled {
@@ -592,7 +592,7 @@ func TestApplyFlags_NamespacesScoped(t *testing.T) {
if err := fs.Parse([]string{"--namespaces=team-a,team-b"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -617,7 +617,7 @@ func TestApplyFlags_NamespacesFromEnv(t *testing.T) {
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -636,7 +636,7 @@ func TestApplyFlags_NamespacesGlobal(t *testing.T) {
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -657,7 +657,7 @@ func TestApplyFlags_NamespacesTrimsEmptyEntries(t *testing.T) {
if err := fs.Parse([]string{"--namespaces=team-a, ,team-b,"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -682,7 +682,7 @@ func TestApplyFlags_NamespacesAllEmptyIsGlobal(t *testing.T) {
if err := fs.Parse([]string{"--namespaces=, ,"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
if _, err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
@@ -693,3 +693,64 @@ func TestApplyFlags_NamespacesAllEmptyIsGlobal(t *testing.T) {
t.Errorf("all-empty namespaces should be global mode")
}
}
// ApplyFlags must finalize a self-consistent config: in scoped mode it enforces
// namespace-scope semantics (clears selector/ignore lists) and returns warnings,
// without the caller having to invoke ApplyNamespaceScope separately.
func TestApplyFlags_ScopedClearsSelectorsAndIgnores(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{
"--namespaces=team-a",
"--namespace-selector=env=prod",
"--namespaces-to-ignore=kube-system",
}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
warnings, err := ApplyFlags(cfg)
if err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.NamespaceSelectors) != 0 || len(cfg.NamespaceSelectorStrings) != 0 {
t.Errorf("scoped mode should clear namespace selectors, got %v", cfg.NamespaceSelectorStrings)
}
if len(cfg.IgnoredNamespaces) != 0 {
t.Errorf("scoped mode should clear ignored namespaces, got %v", cfg.IgnoredNamespaces)
}
if len(warnings) != 2 {
t.Errorf("expected 2 scope warnings, got %v", warnings)
}
}
func TestApplyFlags_GlobalKeepsSelectorsNoWarnings(t *testing.T) {
resetViper()
t.Setenv("KUBERNETES_NAMESPACE", "")
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{
"--namespace-selector=env=prod",
"--namespaces-to-ignore=kube-system",
}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
warnings, err := ApplyFlags(cfg)
if err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if !cfg.IsGlobalMode() {
t.Fatalf("no --namespaces should be global mode")
}
if len(cfg.NamespaceSelectors) != 1 || len(cfg.IgnoredNamespaces) != 1 {
t.Errorf("global mode should keep selectors and ignored namespaces")
}
if len(warnings) != 0 {
t.Errorf("global mode should produce no warnings, got %v", warnings)
}
}
+12 -40
View File
@@ -66,9 +66,18 @@ type ManagerOptions struct {
Collectors *metrics.Collectors
}
// NewManager creates a new controller-runtime manager with the given options.
// This follows controller-runtime and operator-sdk conventions for leader election.
// NewManager creates a new controller-runtime manager using the ambient cluster
// configuration (in-cluster or kubeconfig). It follows controller-runtime and
// operator-sdk conventions for leader election.
func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
return NewManagerWithRestConfig(opts, ctrl.GetConfigOrDie())
}
// NewManagerWithRestConfig creates a new controller-runtime manager with the
// given rest.Config. NewManager delegates here; tests can call it directly with
// a pre-existing cluster configuration (set cfg.MetricsAddr/HealthAddr to "0" to
// disable those servers and avoid port conflicts).
func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctrl.Manager, error) {
cfg := opts.Config
le := cfg.LeaderElection
@@ -100,7 +109,7 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
opts.Log.Info("namespace filtering enabled", "namespaces", cfg.WatchedNamespaces)
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts)
mgr, err := ctrl.NewManager(restConfig, mgrOpts)
if err != nil {
return nil, fmt.Errorf("creating manager: %w", err)
}
@@ -119,43 +128,6 @@ func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
return mgr, nil
}
// NewManagerWithRestConfig creates a new controller-runtime manager with the given rest.Config.
// This is useful for testing where you have a pre-existing cluster configuration.
func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctrl.Manager, error) {
cfg := opts.Config
le := cfg.LeaderElection
mgrOpts := ctrl.Options{
Scheme: runtimeScheme,
Metrics: ctrlmetrics.Options{
BindAddress: "0", // Disable metrics server in tests
},
HealthProbeBindAddress: "0", // Disable health probes in tests
// Leader election configuration
LeaderElection: cfg.EnableHA,
LeaderElectionID: le.LockName,
LeaderElectionNamespace: le.Namespace,
LeaderElectionReleaseOnCancel: le.ReleaseOnCancel,
LeaseDuration: &le.LeaseDuration,
RenewDeadline: &le.RenewDeadline,
RetryPeriod: &le.RetryPeriod,
}
if nsScope := buildDefaultNamespaces(cfg.WatchedNamespaces); nsScope != nil {
mgrOpts.Cache = cache.Options{
DefaultNamespaces: nsScope,
}
}
mgr, err := ctrl.NewManager(restConfig, mgrOpts)
if err != nil {
return nil, fmt.Errorf("creating manager: %w", err)
}
return mgr, nil
}
// SetupReconcilers sets up all reconcilers with the manager.
func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, collectors *metrics.Collectors) error {
registry := workload.NewRegistry(