From d59e7d4eb8f2136a327ceab627feb3efc0ea299d Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Mon, 20 Jul 2026 21:18:13 +0800 Subject: [PATCH] Prioritize exact namespace match in restore (#10033) * Prioritize exact namespace match in restore Align restore pipeline with backup pipeline by evaluating exact namespace matches before glob patterns in namespacedFilterPolicies. This ensures specific overrides always win regardless of list order. Signed-off-by: Adam Zhang * improve test cases add test cases for exact listed first, and excat listed last to ensure the behavior that the order does not matter for exact listed namespace, the rule will be always honored. Signed-off-by: Adam Zhang --------- Signed-off-by: Adam Zhang --- changelogs/unreleased/10033-adam-jian-zhang | 1 + .../fine-grained-restore-filters-design.md | 21 ++++++---- pkg/restore/restore.go | 16 +++++--- pkg/restore/restore_policies_test.go | 40 ++++++++++++++++++- 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/10033-adam-jian-zhang diff --git a/changelogs/unreleased/10033-adam-jian-zhang b/changelogs/unreleased/10033-adam-jian-zhang new file mode 100644 index 000000000..62a676ab9 --- /dev/null +++ b/changelogs/unreleased/10033-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #10032, prioritize exact namespace match in restore diff --git a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md index 0e4107171..913c056c0 100644 --- a/design/restore-filter-enhancement/fine-grained-restore-filters-design.md +++ b/design/restore-filter-enhancement/fine-grained-restore-filters-design.md @@ -307,6 +307,9 @@ The `getNamespaceFilter()` method on `restoreContext` takes the original namespa **Plugin Additional Items (Restore-Side):** Like the backup side — which is permissive at Stage 2 to allow CSI plugin-injected resources through — the restore side is permissive for AdditionalItems in `restoreItem()`. If a restore plugin requests an additional item, it is allowed to bypass the fine-grained `namespacedFilterPolicies` and `clusterScopedFilterPolicy` kind, name, and label selector checks. This allows plugins to successfully restore dependencies (like a PV needed by a PVC, or a specific Secret) without the user having to explicitly authorize every single dependent resource type in their configuration. Note that these additional items must still pass global resource/namespace exclusions. +**Exact Namespace Match Priority:** +If a namespace matches both an exact name pattern and a glob pattern across different `namespacedFilterPolicies` entries, the exact match always takes precedence, regardless of list order. This aligns with the backup pipeline behavior and ensures specific overrides are always honored. + **Multiple Glob Patterns Matching Same Namespace (Incorrect Order):** ```yaml namespacedFilterPolicies: @@ -665,7 +668,7 @@ data: ### Restore with Glob Namespace Patterns -Apply the same filter to all namespaces matching a pattern. **Critical: Order patterns from most specific to least specific:** +Apply the same filter to all namespaces matching a pattern. **Note on Precedence:** Exact namespace matches always take precedence regardless of where they are listed. However, if multiple glob patterns could match a namespace, they are evaluated in the order they appear. Always list specific globs before broad globs. ```yaml apiVersion: v1 @@ -677,19 +680,21 @@ data: policy: | version: v1 namespacedFilterPolicies: - # More specific patterns first + # Globs must be ordered specific-to-broad - namespaces: - - "team-frontend-prod" # Most specific (exact match) - resourceFilters: - - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] - - namespaces: - - "team-frontend-*" # Less specific (pattern match) + - "team-frontend-*" # specific pattern match resourceFilters: - kinds: [Deployment, Service, ConfigMap] - namespaces: - - "team-*" # Least specific (broad pattern) + - "team-*" # broad pattern resourceFilters: - kinds: [Deployment, Service] + + # Exact matches always win, even if placed at the bottom + - namespaces: + - "team-frontend-prod" # exact match + resourceFilters: + - kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim] ``` **Pattern Matching Results:** diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index a71fc4b23..8205accb9 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -478,7 +478,15 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa return filter } - // 2. Walk patterns in definition order (first-match semantics) + // 2. Check for exact match first (O(1) map lookup) + // This ensures exact namespace matches take precedence over globs, + // regardless of where they are listed in the configuration. + if filter, ok := ctx.namespacedFilterMap[namespace]; ok { + ctx.namespaceFilterCache[namespace] = filter + return filter + } + + // 3. Walk patterns in definition order using pre-compiled globs // Note: namespaceFilterCache is mutated below without synchronization. This is safe // today because resource collection runs sequentially. If the restore loop is // parallelized in the future, these map writes will need a lock to prevent data races. @@ -489,14 +497,10 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa ctx.namespaceFilterCache[namespace] = filter return filter } - } else if p.pattern == namespace { - filter := ctx.namespacedFilterMap[p.pattern] - ctx.namespaceFilterCache[namespace] = filter - return filter } } - // 3. Cache the miss so we don't re-evaluate failed matches + // 4. Cache the miss so we don't re-evaluate failed matches ctx.namespaceFilterCache[namespace] = nil return nil } diff --git a/pkg/restore/restore_policies_test.go b/pkg/restore/restore_policies_test.go index 42b8fb11f..a027f66aa 100644 --- a/pkg/restore/restore_policies_test.go +++ b/pkg/restore/restore_policies_test.go @@ -62,7 +62,7 @@ namespacedFilterPolicies: }, }, { - name: "namespaced filter policy with glob namespace match and first-match semantics", + name: "namespaced filter policy with exact match priority over glob (glob listed first)", restore: defaultRestore().Result(), backup: defaultBackup().Result(), policyYAML: `version: v1 @@ -94,7 +94,43 @@ namespacedFilterPolicies: test.Pods(), }, want: map[*test.APIResource][]string{ - test.Pods(): {"ns-1/pod-1", "ns-2/pod-1"}, + test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"}, + }, + }, + { + name: "namespaced filter policy with exact match priority over glob (exact listed first)", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + policyYAML: `version: v1 +namespacedFilterPolicies: + - namespaces: + - ns-1 + resourceFilters: + - kinds: + - pods + names: + - pod-2 + - namespaces: + - ns-* + resourceFilters: + - kinds: + - pods + names: + - pod-1 +`, + tarball: test.NewTarWriter(t). + AddItems("pods", + builder.ForPod("ns-1", "pod-1").Result(), + builder.ForPod("ns-1", "pod-2").Result(), + builder.ForPod("ns-2", "pod-1").Result(), + builder.ForPod("ns-2", "pod-2").Result(), + ). + Done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"}, }, }, {