diff --git a/cmd/kubemirror/main.go b/cmd/kubemirror/main.go index 7f0fd73..5e54e26 100644 --- a/cmd/kubemirror/main.go +++ b/cmd/kubemirror/main.go @@ -102,10 +102,12 @@ func main() { "Burst limit for API server requests.") flag.DurationVar(&resyncPeriod, "resync-period", 10*time.Minute, "Period for resyncing all resources (catches updates missed due to informer cache delays).") - flag.BoolVar(&verifySourceFreshness, "verify-source-freshness", false, + flag.BoolVar(&verifySourceFreshness, "verify-source-freshness", true, "Verify source resource freshness by comparing cache with direct API read. "+ - "Prevents mirroring stale data when cache lags behind watch events. "+ - "Trade-off: Extra API call when cache is stale.") + "Prevents mirroring stale data and missed orphan cleanups when the informer "+ + "cache lags behind watch events. Trade-off: one extra API call per reconcile "+ + "when the cache is stale. Disable only if you are confident your cluster's "+ + "watch latency is negligible.") flag.BoolVar(&lazyWatcherInit, "lazy-watcher-init", false, "Enable lazy watcher initialization - only create informers for resource types that have resources marked for mirroring. "+ "Significantly reduces memory usage by avoiding watchers for unused resource types. "+ diff --git a/pkg/filter/namespace.go b/pkg/filter/namespace.go index 7fd941c..ed7db86 100644 --- a/pkg/filter/namespace.go +++ b/pkg/filter/namespace.go @@ -222,14 +222,19 @@ func ResolveTargetNamespaces( default: // Check if it's a pattern or direct namespace name if strings.Contains(pattern, "*") || strings.Contains(pattern, "?") { - // It's a glob pattern - match against all namespaces + // It's a glob pattern - match against all namespaces. Honor + // opt-out namespaces (allow-mirrors=false) the same way the + // "all" case does — otherwise an opt-out namespace can still + // receive a mirror via a glob like "app-*". for _, ns := range allNamespaces { - if matchesPattern(ns, pattern) && ns != sourceNamespace && filter.IsAllowed(ns) { + if matchesPattern(ns, pattern) && ns != sourceNamespace && filter.IsAllowed(ns) && !optOutMap[ns] { targetMap[ns] = true } } } else { - // Direct namespace name + // Direct namespace name. Explicit listing is treated as + // intentional opt-in by the source author and bypasses the + // opt-out guard (matches prior behavior). if pattern != sourceNamespace && filter.IsAllowed(pattern) { targetMap[pattern] = true } diff --git a/pkg/filter/namespace_test.go b/pkg/filter/namespace_test.go index 0013649..d972d63 100644 --- a/pkg/filter/namespace_test.go +++ b/pkg/filter/namespace_test.go @@ -376,6 +376,23 @@ func TestResolveTargetNamespaces_EdgeCases(t *testing.T) { // Only "specific-ns" would be allowed, but it's not in allNamespaces assert.Empty(t, got) }) + + t.Run("glob pattern honors opt-out namespaces", func(t *testing.T) { + // Regression: a namespace with allow-mirrors=false used to receive a + // mirror via a glob like "app-*" because the glob branch ignored the + // opt-out list (only the "all" branch checked it). + got := ResolveTargetNamespaces( + []string{"app-*"}, + []string{"app-1", "app-2", "app-optout"}, + []string{}, + []string{"app-optout"}, + "default", + NewNamespaceFilter([]string{}, []string{}), + ) + assert.Contains(t, got, "app-1") + assert.Contains(t, got, "app-2") + assert.NotContains(t, got, "app-optout", "opt-out namespace must not receive mirrors via glob match") + }) } // Benchmark tests for critical paths