Skip to content

Commit 1c932b9

Browse files
Joeavaikathclaude
andcommitted
Fix wildcard expansion when includes is empty and excludes has wildcards (velero-io#9684)
* 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>
1 parent 4c91959 commit 1c932b9

5 files changed

Lines changed: 66 additions & 19 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix wildcard expansion when includes is empty and excludes has wildcards

pkg/controller/backup_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,13 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
570570
}
571571
}
572572

573+
// Empty IncludedNamespaces means "include all namespaces". Normalize
574+
// to ["*"] so that downstream wildcard expansion does not collapse
575+
// an empty-includes + wildcard-excludes combination into "back up nothing".
576+
if len(request.Spec.IncludedNamespaces) == 0 {
577+
request.Spec.IncludedNamespaces = []string{"*"}
578+
}
579+
573580
// validate the included/excluded namespaces
574581
for _, err := range collections.ValidateNamespaceIncludesExcludes(request.Spec.IncludedNamespaces, request.Spec.ExcludedNamespaces) {
575582
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))

pkg/controller/backup_controller_test.go

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,34 @@ func TestBackupLocationLabel(t *testing.T) {
320320
}
321321
}
322322

323+
func TestPrepareBackupRequest_EmptyIncludedNamespacesNormalizedToWildcard(t *testing.T) {
324+
formatFlag := logging.FormatText
325+
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
326+
327+
apiServer := velerotest.NewAPIServer(t)
328+
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
329+
require.NoError(t, err)
330+
331+
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Result()
332+
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backupLocation)
333+
334+
c := &backupReconciler{
335+
discoveryHelper: discoveryHelper,
336+
kbClient: fakeClient,
337+
defaultBackupLocation: backupLocation.Name,
338+
clock: &clock.RealClock{},
339+
formatFlag: formatFlag,
340+
}
341+
342+
backup := defaultBackup().Result()
343+
backup.Spec.IncludedNamespaces = nil
344+
345+
res := c.prepareBackupRequest(ctx, backup, logger)
346+
defer res.WorkerPool.Stop()
347+
348+
assert.Equal(t, []string{"*"}, res.Spec.IncludedNamespaces)
349+
}
350+
323351
func Test_prepareBackupRequest_BackupStorageLocation(t *testing.T) {
324352
var (
325353
defaultBackupTTL = metav1.Duration{Duration: 24 * 30 * time.Hour}
@@ -709,11 +737,11 @@ func TestProcessBackupCompletions(t *testing.T) {
709737
},
710738
Spec: velerov1api.BackupSpec{
711739
StorageLocation: defaultBackupLocation.Name,
740+
IncludedNamespaces: []string{"*"},
712741
DefaultVolumesToFsBackup: boolptr.True(),
713742
SnapshotMoveData: boolptr.False(),
714743
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
715744
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
716-
IncludedNamespaces: []string{"*"},
717745
},
718746
Status: velerov1api.BackupStatus{
719747
Phase: velerov1api.BackupPhaseFinalizing,
@@ -749,11 +777,11 @@ func TestProcessBackupCompletions(t *testing.T) {
749777
},
750778
Spec: velerov1api.BackupSpec{
751779
StorageLocation: "alt-loc",
780+
IncludedNamespaces: []string{"*"},
752781
DefaultVolumesToFsBackup: boolptr.False(),
753782
SnapshotMoveData: boolptr.False(),
754783
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
755784
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
756-
IncludedNamespaces: []string{"*"},
757785
},
758786
Status: velerov1api.BackupStatus{
759787
Phase: velerov1api.BackupPhaseFinalizing,
@@ -793,11 +821,11 @@ func TestProcessBackupCompletions(t *testing.T) {
793821
},
794822
Spec: velerov1api.BackupSpec{
795823
StorageLocation: "read-write",
824+
IncludedNamespaces: []string{"*"},
796825
DefaultVolumesToFsBackup: boolptr.True(),
797826
SnapshotMoveData: boolptr.False(),
798827
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
799828
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
800-
IncludedNamespaces: []string{"*"},
801829
},
802830
Status: velerov1api.BackupStatus{
803831
Phase: velerov1api.BackupPhaseFinalizing,
@@ -834,11 +862,11 @@ func TestProcessBackupCompletions(t *testing.T) {
834862
Spec: velerov1api.BackupSpec{
835863
TTL: metav1.Duration{Duration: 10 * time.Minute},
836864
StorageLocation: defaultBackupLocation.Name,
865+
IncludedNamespaces: []string{"*"},
837866
DefaultVolumesToFsBackup: boolptr.False(),
838867
SnapshotMoveData: boolptr.False(),
839868
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
840869
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
841-
IncludedNamespaces: []string{"*"},
842870
},
843871
Status: velerov1api.BackupStatus{
844872
Phase: velerov1api.BackupPhaseFinalizing,
@@ -875,11 +903,11 @@ func TestProcessBackupCompletions(t *testing.T) {
875903
},
876904
Spec: velerov1api.BackupSpec{
877905
StorageLocation: defaultBackupLocation.Name,
906+
IncludedNamespaces: []string{"*"},
878907
DefaultVolumesToFsBackup: boolptr.True(),
879908
SnapshotMoveData: boolptr.False(),
880909
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
881910
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
882-
IncludedNamespaces: []string{"*"},
883911
},
884912
Status: velerov1api.BackupStatus{
885913
Phase: velerov1api.BackupPhaseFinalizing,
@@ -917,11 +945,11 @@ func TestProcessBackupCompletions(t *testing.T) {
917945
},
918946
Spec: velerov1api.BackupSpec{
919947
StorageLocation: defaultBackupLocation.Name,
948+
IncludedNamespaces: []string{"*"},
920949
DefaultVolumesToFsBackup: boolptr.False(),
921950
SnapshotMoveData: boolptr.False(),
922951
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
923952
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
924-
IncludedNamespaces: []string{"*"},
925953
},
926954
Status: velerov1api.BackupStatus{
927955
Phase: velerov1api.BackupPhaseFinalizing,
@@ -959,11 +987,11 @@ func TestProcessBackupCompletions(t *testing.T) {
959987
},
960988
Spec: velerov1api.BackupSpec{
961989
StorageLocation: defaultBackupLocation.Name,
990+
IncludedNamespaces: []string{"*"},
962991
DefaultVolumesToFsBackup: boolptr.True(),
963992
SnapshotMoveData: boolptr.False(),
964993
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
965994
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
966-
IncludedNamespaces: []string{"*"},
967995
},
968996
Status: velerov1api.BackupStatus{
969997
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1001,11 +1029,11 @@ func TestProcessBackupCompletions(t *testing.T) {
10011029
},
10021030
Spec: velerov1api.BackupSpec{
10031031
StorageLocation: defaultBackupLocation.Name,
1032+
IncludedNamespaces: []string{"*"},
10041033
DefaultVolumesToFsBackup: boolptr.True(),
10051034
SnapshotMoveData: boolptr.False(),
10061035
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
10071036
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1008-
IncludedNamespaces: []string{"*"},
10091037
},
10101038
Status: velerov1api.BackupStatus{
10111039
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1043,11 +1071,11 @@ func TestProcessBackupCompletions(t *testing.T) {
10431071
},
10441072
Spec: velerov1api.BackupSpec{
10451073
StorageLocation: defaultBackupLocation.Name,
1074+
IncludedNamespaces: []string{"*"},
10461075
DefaultVolumesToFsBackup: boolptr.False(),
10471076
SnapshotMoveData: boolptr.False(),
10481077
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
10491078
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1050-
IncludedNamespaces: []string{"*"},
10511079
},
10521080
Status: velerov1api.BackupStatus{
10531081
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1086,11 +1114,11 @@ func TestProcessBackupCompletions(t *testing.T) {
10861114
},
10871115
Spec: velerov1api.BackupSpec{
10881116
StorageLocation: defaultBackupLocation.Name,
1117+
IncludedNamespaces: []string{"*"},
10891118
DefaultVolumesToFsBackup: boolptr.True(),
10901119
SnapshotMoveData: boolptr.False(),
10911120
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
10921121
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1093-
IncludedNamespaces: []string{"*"},
10941122
},
10951123
Status: velerov1api.BackupStatus{
10961124
Phase: velerov1api.BackupPhaseFailed,
@@ -1129,11 +1157,11 @@ func TestProcessBackupCompletions(t *testing.T) {
11291157
},
11301158
Spec: velerov1api.BackupSpec{
11311159
StorageLocation: defaultBackupLocation.Name,
1160+
IncludedNamespaces: []string{"*"},
11321161
DefaultVolumesToFsBackup: boolptr.True(),
11331162
SnapshotMoveData: boolptr.False(),
11341163
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
11351164
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1136-
IncludedNamespaces: []string{"*"},
11371165
},
11381166
Status: velerov1api.BackupStatus{
11391167
Phase: velerov1api.BackupPhaseFailed,
@@ -1172,11 +1200,11 @@ func TestProcessBackupCompletions(t *testing.T) {
11721200
},
11731201
Spec: velerov1api.BackupSpec{
11741202
StorageLocation: defaultBackupLocation.Name,
1203+
IncludedNamespaces: []string{"*"},
11751204
DefaultVolumesToFsBackup: boolptr.False(),
11761205
SnapshotMoveData: boolptr.True(),
11771206
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
11781207
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1179-
IncludedNamespaces: []string{"*"},
11801208
},
11811209
Status: velerov1api.BackupStatus{
11821210
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1216,11 +1244,11 @@ func TestProcessBackupCompletions(t *testing.T) {
12161244
},
12171245
Spec: velerov1api.BackupSpec{
12181246
StorageLocation: defaultBackupLocation.Name,
1247+
IncludedNamespaces: []string{"*"},
12191248
DefaultVolumesToFsBackup: boolptr.False(),
12201249
SnapshotMoveData: boolptr.False(),
12211250
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
12221251
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1223-
IncludedNamespaces: []string{"*"},
12241252
},
12251253
Status: velerov1api.BackupStatus{
12261254
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1260,11 +1288,11 @@ func TestProcessBackupCompletions(t *testing.T) {
12601288
},
12611289
Spec: velerov1api.BackupSpec{
12621290
StorageLocation: defaultBackupLocation.Name,
1291+
IncludedNamespaces: []string{"*"},
12631292
DefaultVolumesToFsBackup: boolptr.False(),
12641293
SnapshotMoveData: boolptr.False(),
12651294
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
12661295
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1267-
IncludedNamespaces: []string{"*"},
12681296
},
12691297
Status: velerov1api.BackupStatus{
12701298
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1304,11 +1332,11 @@ func TestProcessBackupCompletions(t *testing.T) {
13041332
},
13051333
Spec: velerov1api.BackupSpec{
13061334
StorageLocation: defaultBackupLocation.Name,
1335+
IncludedNamespaces: []string{"*"},
13071336
DefaultVolumesToFsBackup: boolptr.False(),
13081337
SnapshotMoveData: boolptr.True(),
13091338
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
13101339
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1311-
IncludedNamespaces: []string{"*"},
13121340
},
13131341
Status: velerov1api.BackupStatus{
13141342
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1349,11 +1377,11 @@ func TestProcessBackupCompletions(t *testing.T) {
13491377
},
13501378
Spec: velerov1api.BackupSpec{
13511379
StorageLocation: defaultBackupLocation.Name,
1380+
IncludedNamespaces: []string{"*"},
13521381
DefaultVolumesToFsBackup: boolptr.False(),
13531382
SnapshotMoveData: boolptr.False(),
13541383
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
13551384
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1356-
IncludedNamespaces: []string{"*"},
13571385
},
13581386
Status: velerov1api.BackupStatus{
13591387
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1393,11 +1421,11 @@ func TestProcessBackupCompletions(t *testing.T) {
13931421
},
13941422
Spec: velerov1api.BackupSpec{
13951423
StorageLocation: defaultBackupLocation.Name,
1424+
IncludedNamespaces: []string{"*"},
13961425
DefaultVolumesToFsBackup: boolptr.False(),
13971426
SnapshotMoveData: boolptr.True(),
13981427
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
13991428
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
1400-
IncludedNamespaces: []string{"*"},
14011429
},
14021430
Status: velerov1api.BackupStatus{
14031431
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1441,13 +1469,13 @@ func TestProcessBackupCompletions(t *testing.T) {
14411469
},
14421470
Spec: velerov1api.BackupSpec{
14431471
StorageLocation: defaultBackupLocation.Name,
1472+
IncludedNamespaces: []string{"*"},
14441473
DefaultVolumesToFsBackup: boolptr.False(),
14451474
SnapshotMoveData: boolptr.True(),
14461475
IncludedClusterScopedResources: []string{"storageclasses"},
14471476
ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...),
14481477
IncludedNamespaceScopedResources: []string{"pods"},
14491478
ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...),
1450-
IncludedNamespaces: []string{"*"},
14511479
},
14521480
Status: velerov1api.BackupStatus{
14531481
Phase: velerov1api.BackupPhaseFinalizing,
@@ -1491,13 +1519,13 @@ func TestProcessBackupCompletions(t *testing.T) {
14911519
},
14921520
Spec: velerov1api.BackupSpec{
14931521
StorageLocation: defaultBackupLocation.Name,
1522+
IncludedNamespaces: []string{"*"},
14941523
DefaultVolumesToFsBackup: boolptr.False(),
14951524
SnapshotMoveData: boolptr.True(),
14961525
IncludedClusterScopedResources: []string{"storageclasses"},
14971526
ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...),
14981527
IncludedNamespaceScopedResources: []string{"pods"},
14991528
ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...),
1500-
IncludedNamespaces: []string{"*"},
15011529
},
15021530
Status: velerov1api.BackupStatus{
15031531
Phase: velerov1api.BackupPhaseFinalizing,

pkg/util/wildcard/expand.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ func ShouldExpandWildcards(includes []string, excludes []string, fromBackup bool
1919
return false
2020
}
2121

22+
// Empty includes is equivalent to * (match all) - don't expand
23+
if len(includes) == 0 {
24+
return false
25+
}
26+
2227
wildcardFound := false
2328
for _, include := range includes {
2429
if containsWildcardPattern(include) {

pkg/util/wildcard/expand_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ func TestShouldExpandWildcards(t *testing.T) {
9090
fromBackup: true,
9191
expected: false,
9292
},
93+
{
94+
name: "empty includes with wildcard excludes - should not expand",
95+
includes: []string{},
96+
excludes: []string{"ns*"},
97+
expected: false,
98+
},
9399
{
94100
name: "complex wildcard patterns",
95101
includes: []string{"*-prod"},

0 commit comments

Comments
 (0)