Skip to content

Commit

Permalink
NS included via labelSelector act as IncludedNamespaces
Browse files Browse the repository at this point in the history
every item under NS is included.

Signed-off-by: Tiger Kaovilai <[email protected]>
  • Loading branch information
kaovilai committed Oct 24, 2024
1 parent ebbeb7a commit f2f044b
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 30 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/8342-kaovilai
Original file line number Diff line number Diff line change
@@ -0,0 +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.
35 changes: 35 additions & 0 deletions pkg/backup/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,41 @@ func TestBackupOldResourceFiltering(t *testing.T) {
"resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json",
},
},
{
name: "included namespaces via label filter backs up all resources in those namespaces and other includedNamespaces resources containing label",
backup: defaultBackup().
IncludedNamespaces("moo").
LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"backup-ns": "true"}}).
Result(),
apiResources: []*test.APIResource{
test.Namespaces(
builder.ForNamespace("foo").ObjectMeta(builder.WithLabels("backup-ns", "true")).Result(),
builder.ForNamespace("moo").Result(),
),
test.Pods(
builder.ForPod("foo", "bar").Result(),
builder.ForPod("moo", "mar").ObjectMeta(builder.WithLabels("backup-ns", "true")).Result(),
builder.ForPod("moo", "unlabeled-not-included").Result(),
builder.ForPod("zoo", "raz").Result(),
),
test.Deployments(
builder.ForDeployment("foo", "bar").Result(),
builder.ForDeployment("zoo", "raz").Result(),
),
},
want: []string{
"resources/deployments.apps/namespaces/foo/bar.json",
"resources/deployments.apps/v1-preferredversion/namespaces/foo/bar.json",
"resources/pods/v1-preferredversion/namespaces/foo/bar.json",
"resources/pods/v1-preferredversion/namespaces/moo/mar.json",
"resources/pods/namespaces/foo/bar.json",
"resources/pods/namespaces/moo/mar.json",
"resources/namespaces/cluster/foo.json",
"resources/namespaces/cluster/moo.json",
"resources/namespaces/v1-preferredversion/cluster/foo.json",
"resources/namespaces/v1-preferredversion/cluster/moo.json",
},
},
{
name: "excluded namespaces filter only backs up resources not in those namespaces",
backup: defaultBackup().
Expand Down
97 changes: 67 additions & 30 deletions pkg/backup/item_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/pager"

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

// nsTracker is used to integrate several namespace filters together.
Expand All @@ -72,42 +75,47 @@ type nsTracker struct {
orLabelSelector []labels.Selector
namespaceFilter *collections.IncludesExcludes
logger logrus.FieldLogger

namespaceMap map[string]bool
namespaceMap sets.Set[string] // namespaceMap is a set of namespaces tracked to include in backup
labeledNamespaces sets.Set[string] // labeledNamespaces is a subset of namespaceMap added via label selectors.
}

// track add the namespace into the namespaceMap.
func (nt *nsTracker) track(ns string) {
if nt.namespaceMap == nil {
nt.namespaceMap = make(map[string]bool)
nt.namespaceMap = make(sets.Set[string])
}
nt.namespaceMap.Insert(ns)
}

if _, ok := nt.namespaceMap[ns]; !ok {
nt.namespaceMap[ns] = true
// trackViaLabel add the namespace into the labeledNamespaces and namespacesMap.
func (nt *nsTracker) trackViaLabel(ns string) {
if nt.labeledNamespaces == nil {
nt.labeledNamespaces = make(sets.Set[string])
}
nt.labeledNamespaces.Insert(ns)
nt.track(ns)
}

// isTracked check whether the namespace's name exists in
// namespaceMap.
func (nt *nsTracker) isTracked(ns string) bool {
if nt.namespaceMap != nil {
return nt.namespaceMap[ns]
}
return false
return nt.namespaceMap.Has(ns)
}

func (nt *nsTracker) isTrackedViaLabel(ns string) bool {
return nt.labeledNamespaces.Has(ns)
}

// init initialize the namespaceMap, and add elements according to
// init initialize the namespaceMap, and add (track) elements according to
// namespace include/exclude filters and the backup label selectors.
// and return only namespaces that are added to tracker.
func (nt *nsTracker) init(
unstructuredNSs []unstructured.Unstructured,
singleLabelSelector labels.Selector,
orLabelSelector []labels.Selector,
namespaceFilter *collections.IncludesExcludes,
logger logrus.FieldLogger,
) {
if nt.namespaceMap == nil {
nt.namespaceMap = make(map[string]bool)
}
nt.singleLabelSelector = singleLabelSelector
nt.orLabelSelector = orLabelSelector
nt.namespaceFilter = namespaceFilter
Expand All @@ -120,7 +128,7 @@ func (nt *nsTracker) init(
namespace.GetName(),
)

nt.track(namespace.GetName())
nt.trackViaLabel(namespace.GetName())
continue
}

Expand All @@ -130,7 +138,7 @@ func (nt *nsTracker) init(
nt.logger.Debugf("Track namespace %s, because its labels match the backup OrLabelSelector.",
namespace.GetName(),
)
nt.track(namespace.GetName())
nt.trackViaLabel(namespace.GetName())
continue
}
}
Expand Down Expand Up @@ -194,6 +202,7 @@ func (r *itemCollector) getItemsFromResourceIdentifiers(

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

return r.nsTracker.filterNamespaces(resources)
Expand Down Expand Up @@ -436,13 +445,33 @@ func (r *itemCollector) getResourceItems(
// Namespace are filtered by namespace include/exclude filters,
// backup LabelSelectors and OrLabelSelectors are checked too.
if gr == kuberesource.Namespaces {
return r.collectNamespaces(
// collectNamespaces initializes the nsTracker which should only contain namespaces to backup but returns all namespaces.
namespaces, err := r.collectNamespaces(
resource,
gv,
gr,
preferredGVR,
log,
)
if err != nil {
return nil, err
}

Check warning on line 458 in pkg/backup/item_collector.go

View check run for this annotation

Codecov / codecov/patch

pkg/backup/item_collector.go#L457-L458

Added lines #L457 - L458 were not covered by tests
// The namespaces collected contains namespaces selected by namespace filters like include/exclude and label selector.
// 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.
r.namespacesIncludedByLabelSelector = make(sets.Set[string])
for i := range namespaces {
if namespaces[i] != nil {
if r.nsTracker.isTrackedViaLabel(namespaces[i].name) {
// this namespace is included by the backup's label selector, not the namespace include/exclude filter
// 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
// in other cases, if label selector is set, we only want to include resources that match the label selector
r.namespacesIncludedByLabelSelector.Insert(namespaces[i].name)
r.backupRequest.NamespaceIncludesExcludes.Includes(namespaces[i].name)
r.log.Infof("including namespace %s by labelselector\n", namespaces[i].name)
}
}
}
return namespaces, err
}

clusterScoped := !resource.Namespaced
Expand Down Expand Up @@ -509,9 +538,13 @@ func (r *itemCollector) listResourceByLabelsPerNamespace(
logger.WithError(err).Error("Error getting dynamic client")
return nil, err
}

labeledNsSoBackupUnlabeled := false
if r.namespacesIncludedByLabelSelector.Has(namespace) {
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)
labeledNsSoBackupUnlabeled = true
}
var orLabelSelectors []string
if r.backupRequest.Spec.OrLabelSelectors != nil {
if r.backupRequest.Spec.OrLabelSelectors != nil && !labeledNsSoBackupUnlabeled {
for _, s := range r.backupRequest.Spec.OrLabelSelectors {
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(s))
}
Expand All @@ -537,7 +570,7 @@ func (r *itemCollector) listResourceByLabelsPerNamespace(
}

var labelSelector string
if selector := r.backupRequest.Spec.LabelSelector; selector != nil {
if selector := r.backupRequest.Spec.LabelSelector; selector != nil && !labeledNsSoBackupUnlabeled {
labelSelector = metav1.FormatLabelSelector(selector)
}

Expand Down Expand Up @@ -594,7 +627,8 @@ func sortCoreGroup(group *metav1.APIResourceList) {
// pod is backed up, we can perform a pre hook, then process pvcs and pvs (including taking a
// snapshot), then perform a post hook on the pod.
const (
pod = iota
namespaces = iota
pod
pvc
pv
other
Expand All @@ -604,6 +638,8 @@ const (
// pods, pvcs, pvs, everything else.
func coreGroupResourcePriority(resource string) int {
switch strings.ToLower(resource) {
case "namespaces":
return namespaces
case "pods":
return pod
case "persistentvolumeclaims":
Expand Down Expand Up @@ -722,6 +758,7 @@ func (r *itemCollector) listItemsForLabel(
}

// collectNamespaces process namespace resource according to namespace filters.
// returns a list of ALL namespaces. Filtering will happen outside of this function.
func (r *itemCollector) collectNamespaces(
resource metav1.APIResource,
gv schema.GroupVersion,
Expand Down Expand Up @@ -780,7 +817,8 @@ func (r *itemCollector) collectNamespaces(
orSelectors = append(orSelectors, orSelector)
}
}

// init initialize the namespaceMap, and add elements according to
// namespace include/exclude filters and the backup label selectors.
r.nsTracker.init(
unstructuredList.Items,
singleSelector,
Expand All @@ -806,6 +844,5 @@ func (r *itemCollector) collectNamespaces(
path: path,
})
}

return items, nil
}

0 comments on commit f2f044b

Please sign in to comment.