From ea1f23f3f6a23a76581b22c5a03ab46757e561ad Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 1 Jun 2026 09:32:25 -0700 Subject: [PATCH 1/2] Fix backup performance regression with includedNamespaces ["*"] Commit 8ac8f49b5 ("Remove wildcard check from getNamespacesToList") removed the optimization that prevented "*" from being expanded to individual namespace names. This caused getNamespacesToList to return all namespace names instead of "" (cross-namespace listing), resulting in N separate API list calls per resource type instead of 1. On clusters with many namespaces (e.g. 178 on an ACM cluster), this means ~35,000 API calls instead of ~200, causing backups to take 18-20 minutes for just 8 items. Restore the "*" special case in ShouldExpandWildcards so that plain "*" is not expanded, and restore the ShouldInclude("*") check in getNamespacesToList so that cross-namespace listing is used. The restore fix from 8ac8f49b5 (fromBackup flag) is preserved since restores already return false before reaching the "*" check. Namespace exclusion continues to work correctly: the nsTracker filters excluded namespace objects via ShouldInclude, and backupItem filters namespace-scoped resources at line 124 of item_backupper.go. Fixes #9869 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/9870-shubham-pampattiwar | 1 + pkg/backup/backup_test.go | 6 ++++++ pkg/backup/item_collector.go | 11 +++++++---- pkg/util/collections/includes_excludes_test.go | 6 +++--- pkg/util/wildcard/expand.go | 7 +++++++ pkg/util/wildcard/expand_test.go | 18 +++++++++--------- 6 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/9870-shubham-pampattiwar diff --git a/changelogs/unreleased/9870-shubham-pampattiwar b/changelogs/unreleased/9870-shubham-pampattiwar new file mode 100644 index 000000000..76e44931c --- /dev/null +++ b/changelogs/unreleased/9870-shubham-pampattiwar @@ -0,0 +1 @@ +Fix backup performance regression when using includedNamespaces ["*"] by restoring cross-namespace API listing optimization diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 072f3cd51..af15224da 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -5407,6 +5407,8 @@ func TestBackupNamespaces(t *testing.T) { want: []string{ "resources/namespaces/cluster/ns-1.json", "resources/namespaces/v1-preferredversion/cluster/ns-1.json", + "resources/namespaces/cluster/ns-3.json", + "resources/namespaces/v1-preferredversion/cluster/ns-3.json", }, }, { @@ -5440,6 +5442,10 @@ func TestBackupNamespaces(t *testing.T) { want: []string{ "resources/namespaces/cluster/ns-1.json", "resources/namespaces/v1-preferredversion/cluster/ns-1.json", + "resources/namespaces/cluster/ns-2.json", + "resources/namespaces/v1-preferredversion/cluster/ns-2.json", + "resources/namespaces/cluster/ns-3.json", + "resources/namespaces/v1-preferredversion/cluster/ns-3.json", "resources/deployments.apps/namespaces/ns-1/deploy-1.json", "resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json", }, diff --git a/pkg/backup/item_collector.go b/pkg/backup/item_collector.go index 34c42e2a2..7f5ed6a4f 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -633,15 +633,18 @@ func coreGroupResourcePriority(resource string) int { } // getNamespacesToList examines ie and resolves the includes and excludes to a full list of -// namespaces to list. If ie is nil, the result is just "" (list across all namespaces). -// Otherwise, the result is a list of every included namespace minus all excluded ones. -// Because the namespace IE filter is expanded from 1.18, there is no need to consider -// wildcard characters anymore. +// namespaces to list. If ie is nil or it includes *, the result is just "" (list across all +// namespaces). Otherwise, the result is a list of every included namespace minus all excluded ones. func getNamespacesToList(ie *collections.NamespaceIncludesExcludes) []string { if ie == nil { return []string{""} } + if ie.ShouldInclude("*") { + // "" means all namespaces + return []string{""} + } + var list []string for _, n := range ie.GetIncludes() { if ie.ShouldInclude(n) { diff --git a/pkg/util/collections/includes_excludes_test.go b/pkg/util/collections/includes_excludes_test.go index 867397cbb..241fc9fea 100644 --- a/pkg/util/collections/includes_excludes_test.go +++ b/pkg/util/collections/includes_excludes_test.go @@ -1063,13 +1063,13 @@ func TestExpandIncludesExcludes(t *testing.T) { fromBackup: true, }, { - name: "asterisk alone - should expand", + name: "asterisk alone - should not expand", includes: []string{"*"}, excludes: []string{}, activeNamespaces: []string{"default", "kube-system", "test"}, - expectedIncludes: []string{"default", "kube-system", "test"}, + expectedIncludes: []string{"*"}, expectedExcludes: []string{}, - expectedWildcardExpanded: true, + expectedWildcardExpanded: false, expectError: false, fromBackup: true, }, diff --git a/pkg/util/wildcard/expand.go b/pkg/util/wildcard/expand.go index 2d35e5dd5..d34d8f805 100644 --- a/pkg/util/wildcard/expand.go +++ b/pkg/util/wildcard/expand.go @@ -26,6 +26,13 @@ func ShouldExpandWildcards(includes []string, excludes []string, fromBackup bool wildcardFound := false for _, include := range includes { + // "*" alone means "match all" - don't expand, so that + // getNamespacesToList can use a single cross-namespace API call + // instead of per-namespace calls. + if include == "*" { + return false + } + if containsWildcardPattern(include) { wildcardFound = true } diff --git a/pkg/util/wildcard/expand_test.go b/pkg/util/wildcard/expand_test.go index 9851106da..36053ff64 100644 --- a/pkg/util/wildcard/expand_test.go +++ b/pkg/util/wildcard/expand_test.go @@ -23,11 +23,11 @@ func TestShouldExpandWildcards(t *testing.T) { expected: false, }, { - name: "includes has star - should expand", + name: "includes has star - should not expand", includes: []string{"*"}, excludes: []string{"ns1"}, fromBackup: true, - expected: true, + expected: false, }, { excludes: []string{"ns3", "ns4"}, @@ -35,18 +35,18 @@ func TestShouldExpandWildcards(t *testing.T) { expected: false, }, { - name: "includes has star - should expand", + name: "includes has star with wildcard excludes - should not expand", includes: []string{"*"}, - excludes: []string{"ns1"}, + excludes: []string{"ns*"}, fromBackup: true, - expected: true, + expected: false, }, { - name: "includes has star after a wildcard pattern - should expand", + name: "includes has star after a wildcard pattern - should not expand", includes: []string{"ns*", "*"}, excludes: []string{"ns1"}, fromBackup: true, - expected: true, + expected: false, }, { name: "includes has wildcard pattern", @@ -70,11 +70,11 @@ func TestShouldExpandWildcards(t *testing.T) { expected: true, }, { - name: "includes has star and wildcard - should expand", + name: "includes has star and wildcard - should not expand", includes: []string{"*", "ns*"}, excludes: []string{}, fromBackup: true, - expected: true, + expected: false, }, { name: "double asterisk should be detected as wildcard", From a5e3c25fc92ef68924f83b2342dcd3951648cd8c Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 3 Jun 2026 10:46:18 -0700 Subject: [PATCH 2/2] Fix excluded namespace objects leaking into backup with cross-namespace listing When getNamespacesToList returns "" (cross-namespace listing), resources from all namespaces are listed. The nsTracker.track call for each resource's namespace would inadvertently track excluded namespaces, causing their namespace objects to pass through filterNamespaces. Add a ShouldInclude check before tracking a namespace from non-namespace resources to ensure excluded namespaces are not tracked. Add test case for includedNamespaces ["*"] with excludedNamespaces to verify both the namespace object exclusion and resource exclusion. Signed-off-by: Shubham Pampattiwar --- pkg/backup/backup_test.go | 23 +++++++++++++++++++++++ pkg/backup/item_collector.go | 3 ++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index af15224da..5607db9dd 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -5426,6 +5426,29 @@ func TestBackupNamespaces(t *testing.T) { }, want: []string{}, }, + { + name: "Wildcard star with excluded namespaces test", + backup: defaultBackup().IncludedNamespaces("*").ExcludedNamespaces("ns-2").Result(), + apiResources: []*test.APIResource{ + test.Namespaces( + builder.ForNamespace("ns-1").Phase(corev1api.NamespaceActive).Result(), + builder.ForNamespace("ns-2").Phase(corev1api.NamespaceActive).Result(), + builder.ForNamespace("ns-3").Phase(corev1api.NamespaceActive).Result(), + ), + test.Deployments( + builder.ForDeployment("ns-1", "deploy-1").Result(), + builder.ForDeployment("ns-2", "deploy-2").Result(), + ), + }, + want: []string{ + "resources/namespaces/cluster/ns-1.json", + "resources/namespaces/v1-preferredversion/cluster/ns-1.json", + "resources/namespaces/cluster/ns-3.json", + "resources/namespaces/v1-preferredversion/cluster/ns-3.json", + "resources/deployments.apps/namespaces/ns-1/deploy-1.json", + "resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json", + }, + }, { name: "Default namespace filter test", backup: defaultBackup().Result(), diff --git a/pkg/backup/item_collector.go b/pkg/backup/item_collector.go index 7f5ed6a4f..15efca2db 100644 --- a/pkg/backup/item_collector.go +++ b/pkg/backup/item_collector.go @@ -497,7 +497,8 @@ func (r *itemCollector) getResourceItems( kind: resource.Kind, }) - if item.GetNamespace() != "" { + if item.GetNamespace() != "" && + r.backupRequest.NamespaceIncludesExcludes.ShouldInclude(item.GetNamespace()) { log.Debugf("Track namespace %s in nsTracker", item.GetNamespace()) r.nsTracker.track(item.GetNamespace()) }