Prioritize exact namespace match in restore (#10033)
Run the E2E test on kind / get-go-version (push) Failing after 1m12s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 4s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 12s
Main CI / Build (push) Failing after 26s

* 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 <adam.zhang@broadcom.com>

* 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 <adam.zhang@broadcom.com>

---------

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
This commit is contained in:
Adam Zhang
2026-07-20 21:18:13 +08:00
committed by GitHub
parent b96bb73308
commit d59e7d4eb8
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"},
},
},
{