From ea1f23f3f6a23a76581b22c5a03ab46757e561ad Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 1 Jun 2026 09:32:25 -0700 Subject: [PATCH] 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",