added watch namespaces support

This commit is contained in:
Safwan
2026-07-14 13:09:14 +05:00
parent 0cfbe10125
commit 0ef47cdc4d
7 changed files with 270 additions and 26 deletions
+34 -6
View File
@@ -54,10 +54,10 @@ type Config struct {
EnablePProf bool `json:"enablePProf"`
PProfAddr string `json:"pprofAddr,omitempty"`
Alerting AlertingConfig `json:"alerting"`
LeaderElection LeaderElectionConfig `json:"leaderElection"`
WatchedNamespace string `json:"watchedNamespace,omitempty"`
SyncPeriod time.Duration `json:"syncPeriod"`
Alerting AlertingConfig `json:"alerting"`
LeaderElection LeaderElectionConfig `json:"leaderElection"`
WatchedNamespaces []string `json:"watchedNamespaces,omitempty"`
SyncPeriod time.Duration `json:"syncPeriod"`
}
// AnnotationConfig holds customizable annotation keys.
@@ -137,8 +137,8 @@ func NewDefault() *Config {
RetryPeriod: 2 * time.Second,
ReleaseOnCancel: true,
},
WatchedNamespace: "",
SyncPeriod: 0,
WatchedNamespaces: []string{},
SyncPeriod: 0,
}
}
@@ -195,3 +195,31 @@ func (c *Config) IsNamespaceIgnored(namespace string) bool {
}
return false
}
// IsGlobalMode reports whether Reloader watches all namespaces. Global mode is
// the absence of an explicit watched-namespace list.
func (c *Config) IsGlobalMode() bool {
return len(c.WatchedNamespaces) == 0
}
// ApplyNamespaceScope enforces master-parity semantics: namespace-selector and
// namespaces-to-ignore are only honored in global (all-namespaces) mode. In
// scoped or single-namespace mode the watched set is already explicit, so both
// are cleared. It returns human-readable warnings for any setting it dropped so
// the caller can log them.
func (c *Config) ApplyNamespaceScope() []string {
if c.IsGlobalMode() {
return nil
}
var warnings []string
if len(c.NamespaceSelectors) > 0 {
warnings = append(warnings, "namespace-selector is set but is only honored in global mode; ignoring it")
c.NamespaceSelectors = nil
c.NamespaceSelectorStrings = nil
}
if len(c.IgnoredNamespaces) > 0 {
warnings = append(warnings, "namespaces-to-ignore is set but is only honored in global mode; ignoring it")
c.IgnoredNamespaces = nil
}
return warnings
}
+44
View File
@@ -3,6 +3,8 @@ package config
import (
"testing"
"time"
"k8s.io/apimachinery/pkg/labels"
)
func TestNewDefault(t *testing.T) {
@@ -218,3 +220,45 @@ func TestConfig_IsNamespaceIgnored(t *testing.T) {
)
}
}
func TestIsGlobalMode(t *testing.T) {
c := &Config{WatchedNamespaces: nil}
if !c.IsGlobalMode() {
t.Errorf("empty WatchedNamespaces should be global mode")
}
c.WatchedNamespaces = []string{"team-a"}
if c.IsGlobalMode() {
t.Errorf("non-empty WatchedNamespaces should not be global mode")
}
}
func TestApplyNamespaceScope_GlobalKeepsSettings(t *testing.T) {
c := &Config{
WatchedNamespaces: nil,
IgnoredNamespaces: []string{"kube-system"},
NamespaceSelectors: []labels.Selector{labels.Everything()},
}
warnings := c.ApplyNamespaceScope()
if len(warnings) != 0 {
t.Errorf("global mode should produce no warnings, got %v", warnings)
}
if len(c.IgnoredNamespaces) != 1 || len(c.NamespaceSelectors) != 1 {
t.Errorf("global mode should keep selectors and ignored namespaces")
}
}
func TestApplyNamespaceScope_ScopedClearsSettings(t *testing.T) {
c := &Config{
WatchedNamespaces: []string{"team-a"},
IgnoredNamespaces: []string{"kube-system"},
NamespaceSelectors: []labels.Selector{labels.Everything()},
NamespaceSelectorStrings: []string{"env=prod"},
}
warnings := c.ApplyNamespaceScope()
if len(warnings) != 2 {
t.Errorf("scoped mode should warn about both dropped settings, got %v", warnings)
}
if len(c.IgnoredNamespaces) != 0 || len(c.NamespaceSelectors) != 0 || len(c.NamespaceSelectorStrings) != 0 {
t.Errorf("scoped mode should clear selectors and ignored namespaces")
}
}
+33 -7
View File
@@ -223,10 +223,11 @@ func BindFlags(fs *pflag.FlagSet, cfg *Config) {
"Annotation to indicate when a deployment was paused by Reloader",
)
// Watched namespace (for single-namespace mode)
fs.String(
"watch-namespace", cfg.WatchedNamespace,
"Namespace to watch (empty for all namespaces)",
// Watched namespaces (scoped mode). Empty means watch all namespaces;
// KUBERNETES_NAMESPACE (env) is used as a single-namespace fallback.
fs.StringSlice(
"namespaces", nil,
"explicit list of namespaces to watch (scoped mode; creates no ClusterRole)",
)
// Alerting
@@ -294,9 +295,16 @@ func ApplyFlags(cfg *Config) error {
cfg.MetricsAddr = v.GetString("metrics-addr")
cfg.HealthAddr = v.GetString("health-addr")
cfg.PProfAddr = v.GetString("pprof-addr")
cfg.WatchedNamespace = v.GetString("watch-namespace")
if cfg.WatchedNamespace == "" {
cfg.WatchedNamespace = v.GetString("KUBERNETES_NAMESPACE")
// Namespace scope: an explicit --namespaces list takes precedence (scoped
// mode); otherwise fall back to KUBERNETES_NAMESPACE for single-namespace
// mode; an empty result means global (all-namespaces) mode.
// Trim and drop empty entries from the slice to prevent empty strings from
// being treated as "watch all namespaces" by controller-runtime.
cfg.WatchedNamespaces = trimAndDropEmptyStrings(v.GetStringSlice("namespaces"))
if len(cfg.WatchedNamespaces) == 0 {
if ns := v.GetString("KUBERNETES_NAMESPACE"); ns != "" {
cfg.WatchedNamespaces = []string{ns}
}
}
// Leader election
@@ -412,3 +420,21 @@ func splitAndTrim(s string) []string {
}
return result
}
// trimAndDropEmptyStrings trims whitespace from each string in a slice and drops empty entries.
func trimAndDropEmptyStrings(ss []string) []string {
if len(ss) == 0 {
return nil
}
result := make([]string, 0, len(ss))
for _, s := range ss {
s = strings.TrimSpace(s)
if s != "" {
result = append(result, s)
}
}
if len(result) == 0 {
return nil
}
return result
}
+112 -1
View File
@@ -65,7 +65,7 @@ func TestBindFlags(t *testing.T) {
"ignore-annotation",
"pause-deployment-annotation",
"pause-deployment-time-annotation",
"watch-namespace",
"namespaces",
"alert-on-reload",
"alert-webhook-url",
"alert-sink",
@@ -582,3 +582,114 @@ func TestSplitAndTrim(t *testing.T) {
)
}
}
func TestApplyFlags_NamespacesScoped(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{"--namespaces=team-a,team-b"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.WatchedNamespaces) != 2 {
t.Fatalf("WatchedNamespaces length = %d, want 2", len(cfg.WatchedNamespaces))
}
if cfg.WatchedNamespaces[0] != "team-a" || cfg.WatchedNamespaces[1] != "team-b" {
t.Errorf("WatchedNamespaces = %v", cfg.WatchedNamespaces)
}
if cfg.IsGlobalMode() {
t.Errorf("explicit namespaces should not be global mode")
}
}
func TestApplyFlags_NamespacesFromEnv(t *testing.T) {
resetViper()
t.Setenv("KUBERNETES_NAMESPACE", "single-ns")
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.WatchedNamespaces) != 1 || cfg.WatchedNamespaces[0] != "single-ns" {
t.Errorf("WatchedNamespaces = %v, want [single-ns]", cfg.WatchedNamespaces)
}
}
func TestApplyFlags_NamespacesGlobal(t *testing.T) {
resetViper()
t.Setenv("KUBERNETES_NAMESPACE", "")
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.WatchedNamespaces) != 0 {
t.Errorf("WatchedNamespaces = %v, want empty (global)", cfg.WatchedNamespaces)
}
if !cfg.IsGlobalMode() {
t.Errorf("no namespaces and no env should be global mode")
}
}
func TestApplyFlags_NamespacesTrimsEmptyEntries(t *testing.T) {
resetViper()
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{"--namespaces=team-a, ,team-b,"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.WatchedNamespaces) != 2 {
t.Fatalf("WatchedNamespaces length = %d, want 2", len(cfg.WatchedNamespaces))
}
if cfg.WatchedNamespaces[0] != "team-a" || cfg.WatchedNamespaces[1] != "team-b" {
t.Errorf("WatchedNamespaces = %v, want [team-a team-b]", cfg.WatchedNamespaces)
}
if cfg.IsGlobalMode() {
t.Errorf("trimmed namespaces should not be global mode")
}
}
func TestApplyFlags_NamespacesAllEmptyIsGlobal(t *testing.T) {
resetViper()
t.Setenv("KUBERNETES_NAMESPACE", "")
cfg := NewDefault()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
BindFlags(fs, cfg)
if err := fs.Parse([]string{"--namespaces=, ,"}); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if err := ApplyFlags(cfg); err != nil {
t.Fatalf("ApplyFlags() error = %v", err)
}
if len(cfg.WatchedNamespaces) != 0 {
t.Errorf("WatchedNamespaces = %v, want empty (global)", cfg.WatchedNamespaces)
}
if !cfg.IsGlobalMode() {
t.Errorf("all-empty namespaces should be global mode")
}
}