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
+5 -4
View File
@@ -54,7 +54,8 @@ func newReloaderCommand() *cobra.Command {
}
func run(cmd *cobra.Command, args []string) error {
if err := config.ApplyFlags(cfg); err != nil {
scopeWarnings, err := config.ApplyFlags(cfg)
if err != nil {
return fmt.Errorf("applying flags: %w", err)
}
@@ -81,9 +82,9 @@ func run(cmd *cobra.Command, args []string) error {
log.Info("Starting Reloader")
// Enforce master-parity scope semantics before reconcilers/manager read the
// config: selector and ignore lists are only honored in global mode.
for _, w := range cfg.ApplyNamespaceScope() {
// Namespace-scope semantics are enforced in ApplyFlags; surface any warnings
// it produced now that logging is configured.
for _, w := range scopeWarnings {
log.Info(w)
}
@@ -89,13 +89,12 @@ Create the namespace selector if it does not watch globally
{{- end -}}
{{/*
Effective set of namespaces to watch in scoped mode: the release namespace
(always included so the meta-info ConfigMap, HA leases and events keep working)
plus the user-supplied reloader.namespaces, deduped and sorted.
Namespaces to watch in scoped mode: exactly the user-supplied reloader.namespaces,
trimmed, de-duped and sorted. The release namespace is intentionally NOT added here
Reloader watches only what the user asked for (an empty result means global mode).
Returns a JSON-encoded list; consumers use mustFromJson to iterate.
*/}}
{{- define "reloader-watchNamespaces" -}}
{{- $relNs := .Values.namespace | default .Release.Namespace -}}
{{- $ns := .Values.reloader.namespaces | default list -}}
{{- if kindIs "string" $ns -}}
{{- $ns = splitList "," $ns -}}
@@ -107,8 +106,7 @@ Returns a JSON-encoded list; consumers use mustFromJson to iterate.
{{- $clean = append $clean $t -}}
{{- end -}}
{{- end -}}
{{- $all := concat (list $relNs) $clean | uniq | sortAlpha -}}
{{- $all | toJson -}}
{{- $clean | uniq | sortAlpha | toJson -}}
{{- end -}}
{{/*
@@ -118,6 +116,18 @@ Comma-joined form of reloader-watchNamespaces, for the --namespaces CLI flag.
{{- include "reloader-watchNamespaces" . | mustFromJson | join "," -}}
{{- end -}}
{{/*
Namespaces that need namespaced RBAC in scoped mode: the watched namespaces plus
the release namespace, so leader-election leases, the meta-info ConfigMap and
events keep working there even though it is not watched for reloads.
Returns a JSON-encoded list; consumers use mustFromJson to iterate.
*/}}
{{- define "reloader-rbacNamespaces" -}}
{{- $relNs := .Values.namespace | default .Release.Namespace -}}
{{- $watch := include "reloader-watchNamespaces" . | mustFromJson -}}
{{- concat (list $relNs) $watch | uniq | sortAlpha | toJson -}}
{{- end -}}
{{/*
The namespaced RBAC rules granted to Reloader in every watched namespace.
Shared between the single-namespace Role and the per-namespace scoped Roles so
@@ -56,7 +56,6 @@ rules:
{{- if and (.Capabilities.APIVersions.Has "argoproj.io/v1alpha1") (.Values.reloader.isArgoRollouts) }}
- apiGroups:
- "argoproj.io"
- ""
resources:
- rollouts
verbs:
@@ -241,7 +241,7 @@ spec:
- "--namespaces-to-ignore={{ .Values.reloader.ignoreNamespaces }}"
{{- end }}
{{- if (include "reloader-namespaceSelector" .) }}
- "--namespace-selector=\"{{ include "reloader-namespaceSelector" . }}\""
- "--namespace-selector={{ include "reloader-namespaceSelector" . }}"
{{- end }}
{{- if .Values.reloader.resourceLabelSelector }}
- "--resource-label-selector={{ .Values.reloader.resourceLabelSelector }}"
@@ -275,6 +275,26 @@ spec:
{{- if .Values.reloader.custom_annotations.configmap_auto }}
- "--configmap-auto-annotation"
- "{{ .Values.reloader.custom_annotations.configmap_auto }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.configmap_exclude }}
- "--configmap-exclude-annotation"
- "{{ .Values.reloader.custom_annotations.configmap_exclude }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.secret_exclude }}
- "--secret-exclude-annotation"
- "{{ .Values.reloader.custom_annotations.secret_exclude }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.secretproviderclass }}
- "--secretproviderclass-annotation"
- "{{ .Values.reloader.custom_annotations.secretproviderclass }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.secretproviderclass_auto }}
- "--secretproviderclass-auto-annotation"
- "{{ .Values.reloader.custom_annotations.secretproviderclass_auto }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.secretproviderclass_exclude }}
- "--secretproviderclass-exclude-annotation"
- "{{ .Values.reloader.custom_annotations.secretproviderclass_exclude }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.search }}
- "--auto-search-annotation"
@@ -7,7 +7,7 @@
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
{{- end }}
{{- if .Values.reloader.namespaces }}
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
{{- range $ns := (include "reloader-rbacNamespaces" . | mustFromJson) }}
apiVersion: {{ $apiVersion }}
kind: Role
metadata:
@@ -4,7 +4,7 @@
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
{{- end }}
{{- if .Values.reloader.namespaces }}
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
{{- range $ns := (include "reloader-rbacNamespaces" . | mustFromJson) }}
apiVersion: {{ $apiVersion }}
kind: RoleBinding
metadata:
@@ -223,6 +223,11 @@ reloader:
# custom_annotations:
# configmap: "my.company.com/configmap"
# secret: "my.company.com/secret"
# configmap_exclude: "my.company.com/configmap-exclude"
# secret_exclude: "my.company.com/secret-exclude"
# secretproviderclass: "my.company.com/secretproviderclass"
# secretproviderclass_auto: "my.company.com/secretproviderclass-auto"
# secretproviderclass_exclude: "my.company.com/secretproviderclass-exclude"
# ignore: "my.company.com/reloader-ignore"
custom_annotations: {}
+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(