Fix wildcard expansion when includes is empty and excludes has wildcards (#9684)
Some checks failed
Run the E2E test on kind / get-go-version (push) Failing after 1m5s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 2s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 11s
Main CI / Build (push) Failing after 33s
Close stale issues and PRs / stale (push) Successful in 12s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m53s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m8s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m11s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m13s

* Fix wildcard expansion when includes is empty and excludes has wildcards

When a Backup CR is applied via kubectl with empty includedNamespaces
and a wildcard in excludedNamespaces, ShouldExpandWildcards triggers
expansion. The empty includes expands to nil, but wildcardExpanded is
set to true, causing ShouldInclude to return false for all namespaces.

Populate expanded includes with all active namespaces when the original
includes was empty (meaning "include all") so that the wildcardExpanded
check does not falsely reject everything.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Changelog

Signed-off-by: Joseph <jvaikath@redhat.com>

* Normalize empty includes to * instead of active namespaces list

This ensures consistent behavior between CLI and kubectl-apply paths
for Namespace CR inclusion when excludes contain wildcards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Move empty includes normalization to backup controller

Instead of normalizing empty IncludedNamespaces to ["*"] in the
collections layer's ExpandIncludesExcludes, do it earlier in
prepareBackupRequest. This ensures the spec is correct before any
downstream processing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Update TestProcessBackupCompletions for wildcard normalization

Add IncludedNamespaces: []string{"*"} to all expected BackupSpec
structs, reflecting the new prepareBackupRequest normalization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Add checks around empty includenamespaces

Signed-off-by: Joseph <jvaikath@redhat.com>

* gofmt

Signed-off-by: Joseph <jvaikath@redhat.com>

---------

Signed-off-by: Joseph <jvaikath@redhat.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Antony Vaikath
2026-04-22 16:24:22 -04:00
committed by GitHub
parent 2d6865d6e5
commit 3b34fb8eff
5 changed files with 66 additions and 0 deletions

View File

@@ -0,0 +1 @@
Fix wildcard expansion when includes is empty and excludes has wildcards

View File

@@ -570,6 +570,13 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
}
}
// Empty IncludedNamespaces means "include all namespaces". Normalize
// to ["*"] so that downstream wildcard expansion does not collapse
// an empty-includes + wildcard-excludes combination into "back up nothing".
if len(request.Spec.IncludedNamespaces) == 0 {
request.Spec.IncludedNamespaces = []string{"*"}
}
// validate the included/excluded namespaces
for _, err := range collections.ValidateNamespaceIncludesExcludes(request.Spec.IncludedNamespaces, request.Spec.ExcludedNamespaces) {
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))

View File

@@ -320,6 +320,34 @@ func TestBackupLocationLabel(t *testing.T) {
}
}
func TestPrepareBackupRequest_EmptyIncludedNamespacesNormalizedToWildcard(t *testing.T) {
formatFlag := logging.FormatText
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
apiServer := velerotest.NewAPIServer(t)
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
require.NoError(t, err)
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Result()
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backupLocation)
c := &backupReconciler{
discoveryHelper: discoveryHelper,
kbClient: fakeClient,
defaultBackupLocation: backupLocation.Name,
clock: &clock.RealClock{},
formatFlag: formatFlag,
}
backup := defaultBackup().Result()
backup.Spec.IncludedNamespaces = nil
res := c.prepareBackupRequest(ctx, backup, logger)
defer res.WorkerPool.Stop()
assert.Equal(t, []string{"*"}, res.Spec.IncludedNamespaces)
}
func Test_prepareBackupRequest_BackupStorageLocation(t *testing.T) {
var (
defaultBackupTTL = metav1.Duration{Duration: 24 * 30 * time.Hour}
@@ -709,6 +737,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -748,6 +777,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: "alt-loc",
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -791,6 +821,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: "read-write",
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -831,6 +862,7 @@ func TestProcessBackupCompletions(t *testing.T) {
Spec: velerov1api.BackupSpec{
TTL: metav1.Duration{Duration: 10 * time.Minute},
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -871,6 +903,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -912,6 +945,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -953,6 +987,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -994,6 +1029,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1035,6 +1071,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1077,6 +1114,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1119,6 +1157,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.True(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1161,6 +1200,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.True(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1204,6 +1244,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1247,6 +1288,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1290,6 +1332,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.True(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1334,6 +1377,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.False(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1377,6 +1421,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.True(),
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
@@ -1424,6 +1469,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.True(),
IncludedClusterScopedResources: []string{"storageclasses"},
@@ -1473,6 +1519,7 @@ func TestProcessBackupCompletions(t *testing.T) {
},
Spec: velerov1api.BackupSpec{
StorageLocation: defaultBackupLocation.Name,
IncludedNamespaces: []string{"*"},
DefaultVolumesToFsBackup: boolptr.False(),
SnapshotMoveData: boolptr.True(),
IncludedClusterScopedResources: []string{"storageclasses"},

View File

@@ -9,6 +9,11 @@ import (
)
func ShouldExpandWildcards(includes []string, excludes []string) bool {
// Empty includes is equivalent to * (match all) - don't expand
if len(includes) == 0 {
return false
}
wildcardFound := false
for _, include := range includes {
// Special case: "*" alone means "match all" - don't expand

View File

@@ -68,6 +68,12 @@ func TestShouldExpandWildcards(t *testing.T) {
excludes: []string{},
expected: false,
},
{
name: "empty includes with wildcard excludes - should not expand",
includes: []string{},
excludes: []string{"ns*"},
expected: false,
},
{
name: "complex wildcard patterns",
includes: []string{"*-prod"},