Merge pull request #10052 from adam-jian-zhang/prioritize-exact-ns-match-for-1.18
Run the E2E test on kind / get-go-version (push) Successful in 1m10s
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Main CI / get-go-version (push) Successful in 11s
Run the E2E test on kind / build (push) Failing after 24s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / Build (push) Failing after 28s

[Cherry-pick]Prioritize exact namespace match in restore (#10033)
This commit is contained in:
Adam Zhang
2026-07-22 14:56:56 +08:00
committed by GitHub
4 changed files with 62 additions and 16 deletions
@@ -0,0 +1 @@
Fix issue #10032, prioritize exact namespace match in restore
@@ -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:**
+10 -6
View File
@@ -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
}
+38 -2
View File
@@ -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"},
},
},
{