Skip to content

Commit a1cdf3c

Browse files
committed
NS included via labelSelector act as IncludedNamespaces
every item under NS is included. Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
1 parent 294bbbc commit a1cdf3c

File tree

8 files changed

+120
-38
lines changed

8 files changed

+120
-38
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
breaking: Namespaces included by labelSelector act as IncludedNamespaces, all items including unlabeled under a namespace with labelSelector selected by backup selector will be included in the backup.

config/crd/v1/bases/velero.io_backups.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ spec:
337337
description: |-
338338
LabelSelector is a metav1.LabelSelector to filter with
339339
when adding individual objects to the backup. If empty
340-
or nil, all objects are included. Optional.
340+
or nil, all objects are included.
341+
If matched object is Namespace, the namespace will act as IncludedNamespaces,
342+
and all resources in the namespace are included in the backup. Optional.
341343
nullable: true
342344
properties:
343345
matchExpressions:

config/crd/v1/bases/velero.io_schedules.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,9 @@ spec:
378378
description: |-
379379
LabelSelector is a metav1.LabelSelector to filter with
380380
when adding individual objects to the backup. If empty
381-
or nil, all objects are included. Optional.
381+
or nil, all objects are included.
382+
If matched object is Namespace, the namespace will act as IncludedNamespaces,
383+
and all resources in the namespace are included in the backup. Optional.
382384
nullable: true
383385
properties:
384386
matchExpressions:

config/crd/v1/crds/crds.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/velero/v1/backup_types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ type BackupSpec struct {
8787

8888
// LabelSelector is a metav1.LabelSelector to filter with
8989
// when adding individual objects to the backup. If empty
90-
// or nil, all objects are included. Optional.
90+
// or nil, all objects are included.
91+
// If matched object is Namespace, the namespace will act as IncludedNamespaces,
92+
// and all resources in the namespace are included in the backup. Optional.
9193
// +optional
9294
// +nullable
9395
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`

pkg/backup/backup_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,41 @@ func TestBackupOldResourceFiltering(t *testing.T) {
269269
"resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json",
270270
},
271271
},
272+
{
273+
name: "included namespaces via label filter backs up all resources in those namespaces and other includedNamespaces resources containing label",
274+
backup: defaultBackup().
275+
IncludedNamespaces("moo").
276+
LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"backup-ns": "true"}}).
277+
Result(),
278+
apiResources: []*test.APIResource{
279+
test.Namespaces(
280+
builder.ForNamespace("foo").ObjectMeta(builder.WithLabels("backup-ns", "true")).Result(),
281+
builder.ForNamespace("moo").Result(),
282+
),
283+
test.Pods(
284+
builder.ForPod("foo", "bar").Result(),
285+
builder.ForPod("moo", "mar").ObjectMeta(builder.WithLabels("backup-ns", "true")).Result(),
286+
builder.ForPod("moo", "unlabeled-not-included").Result(),
287+
builder.ForPod("zoo", "raz").Result(),
288+
),
289+
test.Deployments(
290+
builder.ForDeployment("foo", "bar").Result(),
291+
builder.ForDeployment("zoo", "raz").Result(),
292+
),
293+
},
294+
want: []string{
295+
"resources/deployments.apps/namespaces/foo/bar.json",
296+
"resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json",
297+
"resources/pods/v1-preferredversion/namespaces/foo/bar.json",
298+
"resources/pods/v1-preferredversion/namespaces/moo/mar.json",
299+
"resources/pods/namespaces/foo/bar.json",
300+
"resources/pods/namespaces/moo/mar.json",
301+
"resources/namespaces/cluster/foo.json",
302+
"resources/namespaces/cluster/moo.json",
303+
"resources/namespaces/v1-preferredversion/cluster/foo.json",
304+
"resources/namespaces/v1-preferredversion/cluster/moo.json",
305+
},
306+
},
272307
{
273308
name: "excluded namespaces filter only backs up resources not in those namespaces",
274309
backup: defaultBackup().

pkg/backup/item_collector.go

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"k8s.io/apimachinery/pkg/labels"
3333
"k8s.io/apimachinery/pkg/runtime"
3434
"k8s.io/apimachinery/pkg/runtime/schema"
35+
"k8s.io/apimachinery/pkg/util/sets"
3536
"k8s.io/client-go/tools/pager"
3637

3738
"github.com/vmware-tanzu/velero/pkg/client"
@@ -44,14 +45,16 @@ import (
4445
// itemCollector collects items from the Kubernetes API according to
4546
// the backup spec and writes them to files inside dir.
4647
type itemCollector struct {
47-
log logrus.FieldLogger
48-
backupRequest *Request
49-
discoveryHelper discovery.Helper
50-
dynamicFactory client.DynamicFactory
51-
cohabitatingResources map[string]*cohabitatingResource
52-
dir string
53-
pageSize int
54-
nsTracker nsTracker
48+
log logrus.FieldLogger
49+
backupRequest *Request
50+
// Namespaces that are included by the backup's labelSelector will backup all resources in that namespace even if resources are not labeled.
51+
namespacesIncludedByLabelSelector sets.Set[string]
52+
discoveryHelper discovery.Helper
53+
dynamicFactory client.DynamicFactory
54+
cohabitatingResources map[string]*cohabitatingResource
55+
dir string
56+
pageSize int
57+
nsTracker nsTracker
5558
}
5659

5760
// nsTracker is used to integrate several namespace filters together.
@@ -72,42 +75,47 @@ type nsTracker struct {
7275
orLabelSelector []labels.Selector
7376
namespaceFilter *collections.IncludesExcludes
7477
logger logrus.FieldLogger
75-
76-
namespaceMap map[string]bool
78+
namespaceMap sets.Set[string] // namespaceMap is a set of namespaces tracked to include in backup
79+
labeledNamespaces sets.Set[string] // labeledNamespaces is a subset of namespaceMap added via label selectors.
7780
}
7881

7982
// track add the namespace into the namespaceMap.
8083
func (nt *nsTracker) track(ns string) {
8184
if nt.namespaceMap == nil {
82-
nt.namespaceMap = make(map[string]bool)
85+
nt.namespaceMap = make(sets.Set[string])
8386
}
87+
nt.namespaceMap.Insert(ns)
88+
}
8489

85-
if _, ok := nt.namespaceMap[ns]; !ok {
86-
nt.namespaceMap[ns] = true
90+
// trackViaLabel add the namespace into the labeledNamespaces and namespacesMap.
91+
func (nt *nsTracker) trackViaLabel(ns string) {
92+
if nt.labeledNamespaces == nil {
93+
nt.labeledNamespaces = make(sets.Set[string])
8794
}
95+
nt.labeledNamespaces.Insert(ns)
96+
nt.track(ns)
8897
}
8998

9099
// isTracked check whether the namespace's name exists in
91100
// namespaceMap.
92101
func (nt *nsTracker) isTracked(ns string) bool {
93-
if nt.namespaceMap != nil {
94-
return nt.namespaceMap[ns]
95-
}
96-
return false
102+
return nt.namespaceMap.Has(ns)
103+
}
104+
105+
func (nt *nsTracker) isTrackedViaLabel(ns string) bool {
106+
return nt.labeledNamespaces.Has(ns)
97107
}
98108

99-
// init initialize the namespaceMap, and add elements according to
109+
// init initialize the namespaceMap, and add (track) elements according to
100110
// namespace include/exclude filters and the backup label selectors.
111+
// and return only namespaces that are added to tracker.
101112
func (nt *nsTracker) init(
102113
unstructuredNSs []unstructured.Unstructured,
103114
singleLabelSelector labels.Selector,
104115
orLabelSelector []labels.Selector,
105116
namespaceFilter *collections.IncludesExcludes,
106117
logger logrus.FieldLogger,
107118
) {
108-
if nt.namespaceMap == nil {
109-
nt.namespaceMap = make(map[string]bool)
110-
}
111119
nt.singleLabelSelector = singleLabelSelector
112120
nt.orLabelSelector = orLabelSelector
113121
nt.namespaceFilter = namespaceFilter
@@ -120,7 +128,7 @@ func (nt *nsTracker) init(
120128
namespace.GetName(),
121129
)
122130

123-
nt.track(namespace.GetName())
131+
nt.trackViaLabel(namespace.GetName())
124132
continue
125133
}
126134

@@ -130,7 +138,7 @@ func (nt *nsTracker) init(
130138
nt.logger.Debugf("Track namespace %s, because its labels match the backup OrLabelSelector.",
131139
namespace.GetName(),
132140
)
133-
nt.track(namespace.GetName())
141+
nt.trackViaLabel(namespace.GetName())
134142
continue
135143
}
136144
}
@@ -196,6 +204,7 @@ func (r *itemCollector) getItemsFromResourceIdentifiers(
196204

197205
// getAllItems gets all backup-relevant items from all API groups.
198206
func (r *itemCollector) getAllItems() []*kubernetesResource {
207+
// getItems should return all namespaces and will be filtered by nsTracker.filterNamespaces
199208
resources := r.getItems(nil)
200209

201210
return r.nsTracker.filterNamespaces(resources)
@@ -438,13 +447,33 @@ func (r *itemCollector) getResourceItems(
438447
// Namespace are filtered by namespace include/exclude filters,
439448
// backup LabelSelectors and OrLabelSelectors are checked too.
440449
if gr == kuberesource.Namespaces {
441-
return r.collectNamespaces(
450+
// collectNamespaces initializes the nsTracker which should only contain namespaces to backup but returns all namespaces.
451+
namespaces, err := r.collectNamespaces(
442452
resource,
443453
gv,
444454
gr,
445455
preferredGVR,
446456
log,
447457
)
458+
if err != nil {
459+
return nil, err
460+
}
461+
// The namespaces collected contains namespaces selected by namespace filters like include/exclude and label selector.
462+
// Per https://github.com/vmware-tanzu/velero/issues/7492#issuecomment-1986146411 we want labelSelector included namespace to act as IncludedNamespaces so add to NamespaceIncludesExcludes string set.
463+
r.namespacesIncludedByLabelSelector = make(sets.Set[string])
464+
for i := range namespaces {
465+
if namespaces[i] != nil {
466+
if r.nsTracker.isTrackedViaLabel(namespaces[i].name) {
467+
// this namespace is included by the backup's label selector, not the namespace include/exclude filter
468+
// this is a special case where we want to include this namespace in the backup and all resources in it regardless of the resource's label selector
469+
// in other cases, if label selector is set, we only want to include resources that match the label selector
470+
r.namespacesIncludedByLabelSelector.Insert(namespaces[i].name)
471+
r.backupRequest.NamespaceIncludesExcludes.Includes(namespaces[i].name)
472+
r.log.Infof("including namespace %s by labelselector\n", namespaces[i].name)
473+
}
474+
}
475+
}
476+
return namespaces, err
448477
}
449478

450479
clusterScoped := !resource.Namespaced
@@ -511,9 +540,13 @@ func (r *itemCollector) listResourceByLabelsPerNamespace(
511540
logger.WithError(err).Error("Error getting dynamic client")
512541
return nil, err
513542
}
514-
543+
labeledNsSoBackupUnlabeled := false
544+
if r.namespacesIncludedByLabelSelector.Has(namespace) {
545+
logger.Infof("Listing all items including unlabeled in namespace %s because ns was selected by the backup's label selector and acts as IncludedNamespaces", namespace)
546+
labeledNsSoBackupUnlabeled = true
547+
}
515548
var orLabelSelectors []string
516-
if r.backupRequest.Spec.OrLabelSelectors != nil {
549+
if r.backupRequest.Spec.OrLabelSelectors != nil && !labeledNsSoBackupUnlabeled {
517550
for _, s := range r.backupRequest.Spec.OrLabelSelectors {
518551
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(s))
519552
}
@@ -539,7 +572,7 @@ func (r *itemCollector) listResourceByLabelsPerNamespace(
539572
}
540573

541574
var labelSelector string
542-
if selector := r.backupRequest.Spec.LabelSelector; selector != nil {
575+
if selector := r.backupRequest.Spec.LabelSelector; selector != nil && !labeledNsSoBackupUnlabeled {
543576
labelSelector = metav1.FormatLabelSelector(selector)
544577
}
545578

@@ -592,11 +625,13 @@ func sortCoreGroup(group *metav1.APIResourceList) {
592625
}
593626

594627
// These constants represent the relative priorities for resources in the core API group. We want to
595-
// ensure that we process pods, then pvcs, then pvs, then anything else. This ensures that when a
596-
// pod is backed up, we can perform a pre hook, then process pvcs and pvs (including taking a
628+
// ensure that we process namespaces, pods, then pvcs, then pvs, then anything else. This ensures that when a
629+
// namespaces is labeled as included, that it is processed first to act as includedNamespaces for the following resources.
630+
// if pod is backed up, we can perform a pre hook, then process pvcs and pvs (including taking a
597631
// snapshot), then perform a post hook on the pod.
598632
const (
599-
pod = iota
633+
namespaces = iota
634+
pod
600635
pvc
601636
pv
602637
other
@@ -606,6 +641,8 @@ const (
606641
// pods, pvcs, pvs, everything else.
607642
func coreGroupResourcePriority(resource string) int {
608643
switch strings.ToLower(resource) {
644+
case "namespaces":
645+
return namespaces
609646
case "pods":
610647
return pod
611648
case "persistentvolumeclaims":
@@ -724,6 +761,7 @@ func (r *itemCollector) listItemsForLabel(
724761
}
725762

726763
// collectNamespaces process namespace resource according to namespace filters.
764+
// returns a list of ALL namespaces. Filtering will happen outside of this function.
727765
func (r *itemCollector) collectNamespaces(
728766
resource metav1.APIResource,
729767
gv schema.GroupVersion,
@@ -782,7 +820,8 @@ func (r *itemCollector) collectNamespaces(
782820
orSelectors = append(orSelectors, orSelector)
783821
}
784822
}
785-
823+
// init initialize the namespaceMap, and add elements according to
824+
// namespace include/exclude filters and the backup label selectors.
786825
r.nsTracker.init(
787826
unstructuredList.Items,
788827
singleSelector,
@@ -808,6 +847,5 @@ func (r *itemCollector) collectNamespaces(
808847
path: path,
809848
})
810849
}
811-
812850
return items, nil
813851
}

site/content/docs/main/api-types/backup.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ spec:
9393
# all namespace-scoped resources are included. Optional.
9494
# Cannot work with include-resources, exclude-resources and include-cluster-resources.
9595
includedNamespaceScopedResources: {}
96-
# Individual objects must match this label selector to be included in the backup. Optional.
96+
# Individual objects must match this label selector to be included in the backup.
97+
# If matched object is Namespace, the namespace will act as IncludedNamespaces, and all resources in the namespace are included in the backup.
98+
# Optional.
9799
labelSelector:
98100
matchLabels:
99101
app: velero

0 commit comments

Comments
 (0)