Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaces included by labelSelector act as IncludedNamespaces for Backup #8342

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
// 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 @@
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 @@
namespace.GetName(),
)

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

Expand All @@ -130,7 +138,7 @@
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 @@

// 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 @@
// 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 @@
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 @@
}

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 @@
// 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 @@
// 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 @@
}

// 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 @@
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 @@
path: path,
})
}

return items, nil
}
4 changes: 3 additions & 1 deletion site/content/docs/main/api-types/backup.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ spec:
# all namespace-scoped resources are included. Optional.
# Cannot work with include-resources, exclude-resources and include-cluster-resources.
includedNamespaceScopedResources: {}
# Individual objects must match this label selector to be included in the backup. Optional.
# Individual objects must match this label selector to be included in the backup.
# If matched object is Namespace, the namespace will act as IncludedNamespaces, and all resources in the namespace are included in the backup.
# Optional.
labelSelector:
matchLabels:
app: velero
Expand Down
Loading