add multiple label selector support to backup API

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

remove backup CLI bits

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

labelselectors spec option for velero restore

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

add changelog file

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

update spec name to OrLabelSelectors

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

minor fixes

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

add validations for labelSelector and orLabelSelectors

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

update crds.gp after fixing conflicts

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

fix CI and add unit tests

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

updated OrLabelSelector spec description and added validation failure unit tests

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

add comments and change log level

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

update site docs

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

wrap pager client calls in a function

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

resolve confilcts and update crds.go

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

rebase and update crds.go

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>

combine listing items for a given label

Signed-off-by: Shubham Pampattiwar <shubhampampattiwar7@gmail.com>
This commit is contained in:
Shubham Pampattiwar
2022-05-11 09:57:47 -04:00
parent e51865eec1
commit bfdb68a35a
20 changed files with 441 additions and 42 deletions
+9
View File
@@ -59,6 +59,15 @@ type BackupSpec struct {
// +nullable
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
// OrLabelSelectors is list of metav1.LabelSelector to filter with
// when adding individual objects to the backup. If multiple provided
// they will be joined by the OR operator. LabelSelector as well as
// OrLabelSelectors cannot co-exist in backup request, only one of them
// can be used.
// +optional
// +nullable
OrLabelSelectors []*metav1.LabelSelector `json:"orLabelSelectors,omitempty"`
// SnapshotVolumes specifies whether to take cloud snapshots
// of any PV's referenced in the set of objects included
// in the Backup.
+9
View File
@@ -71,6 +71,15 @@ type RestoreSpec struct {
// +nullable
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
// OrLabelSelectors is list of metav1.LabelSelector to filter with
// when restoring individual objects from the backup. If multiple provided
// they will be joined by the OR operator. LabelSelector as well as
// OrLabelSelectors cannot co-exist in restore request, only one of them
// can be used
// +optional
// +nullable
OrLabelSelectors []*metav1.LabelSelector `json:"orLabelSelectors,omitempty"`
// RestorePVs specifies whether to restore all included
// PVs from snapshot (via the cloudprovider).
// +optional
@@ -210,6 +210,17 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.OrLabelSelectors != nil {
in, out := &in.OrLabelSelectors, &out.OrLabelSelectors
*out = make([]*metav1.LabelSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
}
if in.SnapshotVolumes != nil {
in, out := &in.SnapshotVolumes, &out.SnapshotVolumes
*out = new(bool)
@@ -1251,6 +1262,17 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.OrLabelSelectors != nil {
in, out := &in.OrLabelSelectors, &out.OrLabelSelectors
*out = make([]*metav1.LabelSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
}
if in.RestorePVs != nil {
in, out := &in.RestorePVs, &out.RestorePVs
*out = new(bool)
+29
View File
@@ -331,6 +331,35 @@ func TestBackupResourceFiltering(t *testing.T) {
"resources/persistentvolumes/v1-preferredversion/cluster/bar.json",
},
},
{
name: "OrLabelSelector only backs up matching resources",
backup: defaultBackup().
OrLabelSelector([]*metav1.LabelSelector{{MatchLabels: map[string]string{"a1": "b1"}}, {MatchLabels: map[string]string{"a2": "b2"}},
{MatchLabels: map[string]string{"a3": "b3"}}, {MatchLabels: map[string]string{"a4": "b4"}}}).
Result(),
apiResources: []*test.APIResource{
test.Pods(
builder.ForPod("foo", "bar").ObjectMeta(builder.WithLabels("a1", "b1")).Result(),
builder.ForPod("zoo", "raz").Result(),
),
test.Deployments(
builder.ForDeployment("foo", "bar").Result(),
builder.ForDeployment("zoo", "raz").ObjectMeta(builder.WithLabels("a2", "b2")).Result(),
),
test.PVs(
builder.ForPersistentVolume("bar").ObjectMeta(builder.WithLabels("a4", "b4")).Result(),
builder.ForPersistentVolume("baz").ObjectMeta(builder.WithLabels("a5", "b5")).Result(),
),
},
want: []string{
"resources/pods/namespaces/foo/bar.json",
"resources/deployments.apps/namespaces/zoo/raz.json",
"resources/persistentvolumes/cluster/bar.json",
"resources/pods/v1-preferredversion/namespaces/foo/bar.json",
"resources/deployments.apps/v1-preferredversion/namespaces/zoo/raz.json",
"resources/persistentvolumes/v1-preferredversion/cluster/bar.json",
},
},
{
name: "resources with velero.io/exclude-from-backup=true label are not included",
backup: defaultBackup().
+87 -39
View File
@@ -291,53 +291,44 @@ func (r *itemCollector) getResourceItems(log logrus.FieldLogger, gv schema.Group
continue
}
var labelSelector string
if selector := r.backupRequest.Spec.LabelSelector; selector != nil {
labelSelector = metav1.FormatLabelSelector(selector)
var orLabelSelectors []string
if r.backupRequest.Spec.OrLabelSelectors != nil {
for _, s := range r.backupRequest.Spec.OrLabelSelectors {
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(s))
}
} else {
orLabelSelectors = []string{}
}
log.Info("Listing items")
unstructuredItems := make([]unstructured.Unstructured, 0)
if r.pageSize > 0 {
// If limit is positive, use a pager to split list over multiple requests
// Use Velero's dynamic list function instead of the default
listPager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
return resourceClient.List(opts)
}))
// Use the page size defined in the server config
// TODO allow configuration of page buffer size
listPager.PageSize = int64(r.pageSize)
// Add each item to temporary slice
list, paginated, err := listPager.List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector})
// Listing items for orLabelSelectors
errListingForNS := false
for _, label := range orLabelSelectors {
unstructuredItems, err = r.listItemsForLabel(unstructuredItems, gr, label, resourceClient)
if err != nil {
log.WithError(errors.WithStack(err)).Error("Error listing resources")
errListingForNS = true
}
}
if errListingForNS {
log.WithError(err).Error("Error listing items")
continue
}
var labelSelector string
if selector := r.backupRequest.Spec.LabelSelector; selector != nil {
labelSelector = metav1.FormatLabelSelector(selector)
}
// Listing items for labelSelector (singular)
if len(orLabelSelectors) == 0 {
unstructuredItems, err = r.listItemsForLabel(unstructuredItems, gr, labelSelector, resourceClient)
if err != nil {
log.WithError(err).Error("Error listing items")
continue
}
if !paginated {
log.Infof("list for groupResource %s was not paginated", gr)
}
err = meta.EachListItem(list, func(object runtime.Object) error {
u, ok := object.(*unstructured.Unstructured)
if !ok {
log.WithError(errors.WithStack(fmt.Errorf("expected *unstructured.Unstructured but got %T", u))).Error("unable to understand entry in the list")
return fmt.Errorf("expected *unstructured.Unstructured but got %T", u)
}
unstructuredItems = append(unstructuredItems, *u)
return nil
})
if err != nil {
log.WithError(errors.WithStack(err)).Error("unable to understand paginated list")
continue
}
} else {
// If limit is not positive, do not use paging. Instead, request all items at once
unstructuredList, err := resourceClient.List(metav1.ListOptions{LabelSelector: labelSelector})
if err != nil {
log.WithError(errors.WithStack(err)).Error("Error listing items")
continue
}
unstructuredItems = append(unstructuredItems, unstructuredList.Items...)
}
log.Infof("Retrieved %d items", len(unstructuredItems))
@@ -467,3 +458,60 @@ func newCohabitatingResource(resource, group1, group2 string) *cohabitatingResou
seen: false,
}
}
// function to process pager client calls when the pageSize is specified
func (r *itemCollector) processPagerClientCalls(gr schema.GroupResource, label string, resourceClient client.Dynamic) (runtime.Object, error) {
// If limit is positive, use a pager to split list over multiple requests
// Use Velero's dynamic list function instead of the default
listPager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
return resourceClient.List(opts)
}))
// Use the page size defined in the server config
// TODO allow configuration of page buffer size
listPager.PageSize = int64(r.pageSize)
// Add each item to temporary slice
list, paginated, err := listPager.List(context.Background(), metav1.ListOptions{LabelSelector: label})
if err != nil {
r.log.WithError(errors.WithStack(err)).Error("Error listing resources")
return list, err
}
if !paginated {
r.log.Infof("list for groupResource %s was not paginated", gr)
}
return list, nil
}
func (r *itemCollector) listItemsForLabel(unstructuredItems []unstructured.Unstructured, gr schema.GroupResource, label string, resourceClient client.Dynamic) ([]unstructured.Unstructured, error) {
if r.pageSize > 0 {
// process pager client calls
list, err := r.processPagerClientCalls(gr, label, resourceClient)
if err != nil {
return unstructuredItems, err
}
err = meta.EachListItem(list, func(object runtime.Object) error {
u, ok := object.(*unstructured.Unstructured)
if !ok {
r.log.WithError(errors.WithStack(fmt.Errorf("expected *unstructured.Unstructured but got %T", u))).Error("unable to understand entry in the list")
return fmt.Errorf("expected *unstructured.Unstructured but got %T", u)
}
unstructuredItems = append(unstructuredItems, *u)
return nil
})
if err != nil {
r.log.WithError(errors.WithStack(err)).Error("unable to understand paginated list")
return unstructuredItems, err
}
} else {
unstructuredList, err := resourceClient.List(metav1.ListOptions{LabelSelector: label})
if err != nil {
r.log.WithError(errors.WithStack(err)).Error("Error listing items")
return unstructuredItems, err
}
unstructuredItems = append(unstructuredItems, unstructuredList.Items...)
}
return unstructuredItems, nil
}
+6
View File
@@ -162,6 +162,12 @@ func (b *BackupBuilder) LabelSelector(selector *metav1.LabelSelector) *BackupBui
return b
}
// OrLabelSelector sets the Backup's orLabelSelector set.
func (b *BackupBuilder) OrLabelSelector(orSelectors []*metav1.LabelSelector) *BackupBuilder {
b.object.Spec.OrLabelSelectors = orSelectors
return b
}
// SnapshotVolumes sets the Backup's "snapshot volumes" flag.
func (b *BackupBuilder) SnapshotVolumes(val bool) *BackupBuilder {
b.object.Spec.SnapshotVolumes = &val
+6
View File
@@ -113,6 +113,12 @@ func (b *RestoreBuilder) LabelSelector(selector *metav1.LabelSelector) *RestoreB
return b
}
// OrLabelSelector sets the Restore's orLabelSelector set.
func (b *RestoreBuilder) OrLabelSelector(orSelectors []*metav1.LabelSelector) *RestoreBuilder {
b.object.Spec.OrLabelSelectors = orSelectors
return b
}
// NamespaceMappings sets the Restore's namespace mappings.
func (b *RestoreBuilder) NamespaceMappings(mapping ...string) *RestoreBuilder {
if b.object.Spec.NamespaceMapping == nil {
+5
View File
@@ -461,6 +461,11 @@ func (c *backupController) prepareBackupRequest(backup *velerov1api.Backup) *pkg
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
}
// validate that only one exists orLabelSelector or just labelSelector (singular)
if request.Spec.OrLabelSelectors != nil && request.Spec.LabelSelector != nil {
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified"))
}
return request
}
+7
View File
@@ -189,6 +189,13 @@ func TestProcessBackupValidationFailures(t *testing.T) {
backupLocation: builder.ForBackupStorageLocation("velero", "read-only").AccessMode(velerov1api.BackupStorageLocationAccessModeReadOnly).Result(),
expectedErrs: []string{"backup can't be created because backup storage location read-only is currently in read-only mode"},
},
{
name: "labelSelector as well as orLabelSelectors both are specified in backup request fails validation",
backup: defaultBackup().LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}).OrLabelSelector([]*metav1.LabelSelector{{MatchLabels: map[string]string{"a1": "b1"}}, {MatchLabels: map[string]string{"a2": "b2"}},
{MatchLabels: map[string]string{"a3": "b3"}}, {MatchLabels: map[string]string{"a4": "b4"}}}).Result(),
backupLocation: defaultBackupLocation,
expectedErrs: []string{"encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified"},
},
}
for _, test := range tests {
+5
View File
@@ -326,6 +326,11 @@ func (c *restoreController) validateAndComplete(restore *api.Restore, pluginMana
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
}
// validate that only one exists orLabelSelector or just labelSelector (singular)
if restore.Spec.OrLabelSelectors != nil && restore.Spec.LabelSelector != nil {
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("encountered labelSelector as well as orLabelSelectors in restore spec, only one can be specified"))
}
// validate that exactly one of BackupName and ScheduleName have been specified
if !backupXorScheduleProvided(restore) {
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Either a backup or schedule must be specified as a source for the restore, but not both")
@@ -308,6 +308,15 @@ func TestProcessQueueItem(t *testing.T) {
expectedPhase: string(velerov1api.RestorePhaseFailedValidation),
expectedValidationErrors: []string{"Either a backup or schedule must be specified as a source for the restore, but not both"},
},
{
name: "new restore with labelSelector as well as orLabelSelector fails validation",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}).OrLabelSelector([]*metav1.LabelSelector{{MatchLabels: map[string]string{"a1": "b1"}}, {MatchLabels: map[string]string{"a2": "b2"}}, {MatchLabels: map[string]string{"a3": "b3"}}, {MatchLabels: map[string]string{"a4": "b4"}}}).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
expectedErr: false,
expectedValidationErrors: []string{"encountered labelSelector as well as orLabelSelectors in restore spec, only one can be specified"},
expectedPhase: string(velerov1api.RestorePhaseFailedValidation),
},
{
name: "valid restore with schedule name gets executed",
location: defaultStorageLocation,
+34
View File
@@ -183,6 +183,17 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
ls = &metav1.LabelSelector{}
}
var OrSelectors []labels.Selector
if req.Restore.Spec.OrLabelSelectors != nil {
for _, s := range req.Restore.Spec.OrLabelSelectors {
labelAsSelector, err := metav1.LabelSelectorAsSelector(s)
if err != nil {
return Result{}, Result{Velero: []string{err.Error()}}
}
OrSelectors = append(OrSelectors, labelAsSelector)
}
}
selector, err := metav1.LabelSelectorAsSelector(ls)
if err != nil {
return Result{}, Result{Velero: []string{err.Error()}}
@@ -264,6 +275,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
namespaceIncludesExcludes: namespaceIncludesExcludes,
chosenGrpVersToRestore: make(map[string]ChosenGroupVersion),
selector: selector,
OrSelectors: OrSelectors,
log: req.Log,
dynamicFactory: kr.dynamicFactory,
fileSystem: kr.fileSystem,
@@ -305,6 +317,7 @@ type restoreContext struct {
namespaceIncludesExcludes *collections.IncludesExcludes
chosenGrpVersToRestore map[string]ChosenGroupVersion
selector labels.Selector
OrSelectors []labels.Selector
log logrus.FieldLogger
dynamicFactory client.DynamicFactory
fileSystem filesystem.Interface
@@ -1862,6 +1875,27 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource, targetNamespace
continue
}
// Processing OrLabelSelectors when specified in the restore request. LabelSelectors as well as OrLabelSelectors
// cannot co-exist, only one of them can be specified
var skipItem = false
var skip = 0
ctx.log.Debugf("orSelectors specified: %s for item: %s", ctx.OrSelectors, item)
for _, s := range ctx.OrSelectors {
if !s.Matches(labels.Set(obj.GetLabels())) {
skip++
}
if len(ctx.OrSelectors) == skip && skip > 0 {
ctx.log.Infof("setting skip flag to true for item: %s", item)
skipItem = true
}
}
if skipItem {
ctx.log.Infof("restore orSelector labels did not match, skipping restore of item: %s", skipItem, item)
continue
}
selectedItem := restoreableItem{
path: itemPath,
name: item,
+30
View File
@@ -252,6 +252,36 @@ func TestRestoreResourceFiltering(t *testing.T) {
test.PVs(): {"/pv-1"},
},
},
{
name: "OrLabelSelectors only restores matching resources",
restore: defaultRestore().OrLabelSelector([]*metav1.LabelSelector{{MatchLabels: map[string]string{"a1": "b1"}}, {MatchLabels: map[string]string{"a2": "b2"}},
{MatchLabels: map[string]string{"a3": "b3"}}, {MatchLabels: map[string]string{"a4": "b4"}}}).Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("pods",
builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("a1", "b1")).Result(),
builder.ForPod("ns-2", "pod-2").Result(),
).
AddItems("deployments.apps",
builder.ForDeployment("ns-1", "deploy-1").Result(),
builder.ForDeployment("ns-2", "deploy-2").ObjectMeta(builder.WithLabels("a3", "b3")).Result(),
).
AddItems("persistentvolumes",
builder.ForPersistentVolume("pv-1").ObjectMeta(builder.WithLabels("a5", "b5")).Result(),
builder.ForPersistentVolume("pv-2").ObjectMeta(builder.WithLabels("a4", "b4")).Result(),
).
Done(),
apiResources: []*test.APIResource{
test.Pods(),
test.Deployments(),
test.PVs(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.Deployments(): {"ns-2/deploy-2"},
test.PVs(): {"/pv-2"},
},
},
{
name: "should include cluster-scoped resources if restoring subset of namespaces and IncludeClusterResources=true",
restore: defaultRestore().IncludedNamespaces("ns-1").IncludeClusterResources(true).Result(),