[Cherry-Pick] RIA MustInclude annotation and VSC fix for 1.18 (#10101)
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
e2e-test-kind.yaml / extract (push) Successful in 12s
Run the E2E test on kind / get-go-version (push) Successful in 13s
push.yml / extract (push) Successful in 11s
Main CI / get-go-version (push) Successful in 11s
Run the E2E test on kind / build (push) Failing after 28s
Run the E2E test on kind / run-e2e-test (push) Skipped
Main CI / Build (push) Failing after 32s

* design for RIA must-include-additional-items

Design for `restore.velero.io/must-include-additional-items` annotation
and its usage and interaction with existing filtering mechanism.

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
(cherry picked from commit ac76402aa0)
Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* Add RIA must-include additional items (#10082)

Let RestoreItemActions opt in via annotation to
bypass global restore filters for AdditionalItems,
mirroring the backup-side must-include behavior.

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
(cherry picked from commit a43a1bce6a)
Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* update v1.18 custom-plugins docs for RIA must-include additional items

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* remove VolumeSnapshotContents from resourceMustHave list

Stop force-including VolumeSnapshotContents via resourceMustHave on
every restore; CSI VolumeSnapshot/PVC RestoreItemActions now set
`restore.velero.io/must-include-additional-items` so bound snapshot
dependencies are restored only when their parent is restored.

Fixes: #9957

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
(cherry picked from commit ef100da89b)
Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* add tests to cover pvc and vsc ria

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
(cherry picked from commit 63cfddd18d)
Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* fix change logs

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

---------

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
This commit is contained in:
Adam Zhang
2026-07-29 19:20:52 -04:00
committed by GitHub
parent ad0f74ee9d
commit 8b761803ff
13 changed files with 1095 additions and 38 deletions
+8
View File
@@ -166,6 +166,14 @@ const (
// Velero checks this annotation to determine whether to skip resource excluding check.
MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
// MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem
// to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false)
// for that action's AdditionalItems. Value must be "true" to enable the bypass. The annotation is
// always stripped before the item is applied to the cluster when present, including non-"true" values.
//
// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the
// annotation is never inspected and AdditionalItems are not processed.
MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items"
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
+9
View File
@@ -176,6 +176,15 @@ func (p *pvcRestoreItemAction) Execute(
Name: vsName,
Namespace: pvc.Namespace,
})
// Force-restore the VolumeSnapshot even when restore resource filters
// would otherwise exclude it (mirrors backup-side must-include).
annotations := pvc.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
pvc.SetAnnotations(annotations)
}
}
+46 -8
View File
@@ -371,6 +371,7 @@ func TestExecute(t *testing.T) {
backup *velerov1api.Backup
restore *velerov1api.Restore
pvc *corev1api.PersistentVolumeClaim
pvcFromBackup *corev1api.PersistentVolumeClaim
vs *snapshotv1api.VolumeSnapshot
dataUploadResult *corev1api.ConfigMap
expectedErr string
@@ -402,15 +403,40 @@ func TestExecute(t *testing.T) {
vs: builder.ForVolumeSnapshot("velero", vsName).ObjectMeta(
builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi"),
).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(
velerov1api.VolumeSnapshotLabel, "vsName",
velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true",
)).Result(),
},
{
name: "Restore from VolumeSnapshot without volume-snapshot-name annotation",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(),
vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node1")).Result(),
name: "Restore from VolumeSnapshot with nil PVC annotations",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").ObjectMeta(builder.WithUID("restoreUID")).Backup("testBackup").Result(),
pvc: &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "testPVC",
Namespace: "velero",
},
},
pvcFromBackup: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName")).Result(),
vs: builder.ForVolumeSnapshot("velero", vsName).ObjectMeta(
builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi"),
).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(
velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true",
)).Result(),
},
{
name: "Restore from VolumeSnapshot without volume-snapshot-name annotation",
backup: builder.ForBackup("velero", "testBackup").Result(),
restore: builder.ForRestore("velero", "testRestore").Backup("testBackup").Result(),
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotLabel, "vsName", AnnSelectedNode, "node1")).Result(),
vs: builder.ForVolumeSnapshot("velero", "testVS").ObjectMeta(builder.WithAnnotations(velerov1api.VolumeSnapshotRestoreSize, "10Gi")).Result(),
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").ObjectMeta(builder.WithAnnotations(
velerov1api.VolumeSnapshotLabel, "vsName",
AnnSelectedNode, "node1",
velerov1api.MustIncludeAdditionalItemRestoreAnnotation, "true",
)).Result(),
},
{
name: "DataUploadResult cannot be found",
@@ -480,7 +506,13 @@ func TestExecute(t *testing.T) {
require.NoError(t, err)
input.Item = &unstructured.Unstructured{Object: pvcMap}
input.ItemFromBackup = &unstructured.Unstructured{Object: pvcMap}
if tc.pvcFromBackup != nil {
pvcFromBackupMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvcFromBackup)
require.NoError(t, err)
input.ItemFromBackup = &unstructured.Unstructured{Object: pvcFromBackupMap}
} else {
input.ItemFromBackup = &unstructured.Unstructured{Object: pvcMap}
}
input.Restore = tc.restore
}
if tc.preCreatePVC {
@@ -508,6 +540,12 @@ func TestExecute(t *testing.T) {
err := runtime.DefaultUnstructuredConverter.FromUnstructured(output.UpdatedItem.UnstructuredContent(), pvc)
require.NoError(t, err)
require.Equal(t, tc.expectedPVC.GetObjectMeta(), pvc.GetObjectMeta())
if tc.name == "Restore from VolumeSnapshot" {
require.Equal(t, "true", pvc.GetAnnotations()[velerov1api.MustIncludeAdditionalItemRestoreAnnotation])
require.Len(t, output.AdditionalItems, 1)
require.Equal(t, "volumesnapshots.snapshot.storage.k8s.io", output.AdditionalItems[0].GroupResource.String())
require.Equal(t, "vsName", output.AdditionalItems[0].Name)
}
if pvc.Spec.Selector != nil && pvc.Spec.Selector.MatchLabels != nil {
// This is used for long name and namespace case.
if len(tc.pvc.Namespace+"."+tc.pvc.Name) >= validation.DNS1035LabelMaxLength {
@@ -66,6 +66,9 @@ func resetVolumeSnapshotSpecForRestore(vs *snapshotv1api.VolumeSnapshot, vscName
}
func resetVolumeSnapshotAnnotation(vs *snapshotv1api.VolumeSnapshot) {
if vs.ObjectMeta.Annotations == nil {
vs.ObjectMeta.Annotations = make(map[string]string)
}
vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation] =
string(snapshotv1api.VolumeSnapshotContentRetain)
}
@@ -282,12 +285,6 @@ func (p *volumeSnapshotRestoreItemAction) Execute(
vs.Namespace, vs.Name)
}
vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&vs)
if err != nil {
p.log.Errorf("Fail to convert VS %s to unstructured", vs.Namespace+"/"+vs.Name)
return nil, errors.WithStack(err)
}
if vsFromBackup.Status == nil ||
vsFromBackup.Status.BoundVolumeSnapshotContentName == nil {
p.log.Errorf("VS %s doesn't have bound VSC", vsFromBackup.Name)
@@ -299,6 +296,21 @@ func (p *volumeSnapshotRestoreItemAction) Execute(
Name: *vsFromBackup.Status.BoundVolumeSnapshotContentName,
}
// Force-restore the bound VSC even when restore resource filters would
// otherwise exclude it (mirrors backup-side must-include for CSI deps).
annotations := vs.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
vs.SetAnnotations(annotations)
vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&vs)
if err != nil {
p.log.Errorf("Fail to convert VS %s to unstructured", vs.Namespace+"/"+vs.Name)
return nil, errors.WithStack(err)
}
p.log.Infof(`Returning from VolumeSnapshotRestoreItemAction with
VolumeSnapshotContent in additionalItems`)
@@ -103,6 +103,26 @@ func TestResetVolumeSnapshotSpecForRestore(t *testing.T) {
}
}
func TestResetVolumeSnapshotAnnotation(t *testing.T) {
t.Run("should set deletion policy annotation when annotations is nil", func(t *testing.T) {
vs := snapshotv1api.VolumeSnapshot{}
resetVolumeSnapshotAnnotation(&vs)
assert.NotNil(t, vs.ObjectMeta.Annotations)
assert.Equal(t, string(snapshotv1api.VolumeSnapshotContentRetain), vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation])
})
t.Run("should preserve existing annotations and set deletion policy annotation", func(t *testing.T) {
vs := snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
}
resetVolumeSnapshotAnnotation(&vs)
assert.Equal(t, "bar", vs.ObjectMeta.Annotations["foo"])
assert.Equal(t, string(snapshotv1api.VolumeSnapshotContentRetain), vs.ObjectMeta.Annotations[velerov1api.VSCDeletionPolicyAnnotation])
})
}
func TestVSExecute(t *testing.T) {
newVscName := util.GenerateSha256FromRestoreUIDAndVsName("restoreUID", "vsName")
tests := []struct {
@@ -145,6 +165,18 @@ func TestVSExecute(t *testing.T) {
expectErr: false,
expectedVS: builder.ForVolumeSnapshot("ns", "test").SourceVolumeSnapshotContentName(newVscName).Result(),
},
{
name: "Normal case with nil VS annotations, VSC should be created",
vs: builder.ForVolumeSnapshot("ns", "vsName").
SourceVolumeSnapshotContentName(newVscName).
VolumeSnapshotClass("vscClass").
Status().
BoundVolumeSnapshotContentName("vscName").
Result(),
restore: builder.ForRestore("velero", "restore").ObjectMeta(builder.WithUID("restoreUID")).Result(),
expectErr: false,
expectedVS: builder.ForVolumeSnapshot("ns", "test").SourceVolumeSnapshotContentName(newVscName).Result(),
},
}
for _, test := range tests {
@@ -184,6 +216,10 @@ func TestVSExecute(t *testing.T) {
require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(
result.UpdatedItem.UnstructuredContent(), &vs))
require.Equal(t, test.expectedVS.Spec, vs.Spec)
require.Equal(t, "true", vs.GetAnnotations()[velerov1api.MustIncludeAdditionalItemRestoreAnnotation])
require.Len(t, result.AdditionalItems, 1)
require.Equal(t, "volumesnapshotcontents.snapshot.storage.k8s.io", result.AdditionalItems[0].GroupResource.String())
require.Equal(t, "vscName", result.AdditionalItems[0].Name)
}
})
}
+48 -24
View File
@@ -87,7 +87,6 @@ const ObjectStatusRestoreAnnotationKey = "velero.io/restore-status"
var resourceMustHave = []string{
"datauploads.velero.io",
"volumesnapshotcontents.snapshot.storage.k8s.io",
}
type VolumeSnapshotterGetter interface {
@@ -1060,7 +1059,7 @@ func (ctx *restoreContext) processSelectedResource(
continue
}
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS)
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS, false)
warnings.Merge(&w)
errs.Merge(&e)
processedItems++
@@ -1386,7 +1385,7 @@ func (ctx *restoreContext) getResource(groupResource schema.GroupResource, obj *
return u, nil
}
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string) (results.Result, results.Result, bool) {
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string, mustInclude bool) (results.Result, results.Result, bool) {
warnings, errs := results.Result{}, results.Result{}
// itemExists bool is used to determine whether to include this item in the "wait for additional items" list
itemExists := false
@@ -1403,27 +1402,41 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
// Check if group/resource should be restored. We need to do this here since
// this method may be getting called for an additional item which is a group/resource
// that's excluded.
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because resource is excluded")
return warnings, errs, itemExists
}
// Check if namespace/cluster-scoped resource should be restored. We need
// to do this here since this method may be getting called for an additional
// item which is in a namespace that's excluded, or which is cluster-scoped
// and should be excluded. Note that we're checking the object's namespace (
// via obj.GetNamespace()) instead of the namespace parameter, because we want
// to check the *original* namespace, not the remapped one if it's been remapped.
//
// Note: Additional items intentionally bypass fine-grained resource filter policies
// (like per-namespace label/name selectors) to avoid breaking semantic dependencies,
// but they must still pass the global exclusions enforced below.
if namespace != "" {
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because namespace is excluded")
// but they must still pass the global exclusions enforced below unless mustInclude is set.
if mustInclude {
restoreLogger.Info("Skipping the resource/namespace exclusion checks because the item is marked as must-include")
} else {
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because resource is excluded")
return warnings, errs, itemExists
}
// Check if namespace/cluster-scoped resource should be restored. We need
// to do this here since this method may be getting called for an additional
// item which is in a namespace that's excluded, or which is cluster-scoped
// and should be excluded. Note that we're checking the object's namespace (
// via obj.GetNamespace()) instead of the namespace parameter, because we want
// to check the *original* namespace, not the remapped one if it's been remapped.
if namespace != "" {
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because namespace is excluded")
return warnings, errs, itemExists
}
} else {
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
restoreLogger.Info("Not restoring item because it's cluster-scoped")
return warnings, errs, itemExists
}
}
}
// Namespace creation runs unconditionally when namespace != "", regardless of
// mustInclude. This ensures target namespaces exist for additional items that
// bypass the namespace-exclusion check above.
if namespace != "" {
// If the namespace scoped resource should be restored, ensure that the
// namespace into which the resource is being restored into exists.
// This is the *remapped* namespace that we are ensuring exists.
@@ -1442,11 +1455,6 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
ctx.restoredItems[itemKey] = restoredItemStatus{action: ItemRestoreResultCreated, itemExists: true, createdName: nsToEnsure.Name}
}
} else {
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
restoreLogger.Info("Not restoring item because it's cluster-scoped")
return warnings, errs, itemExists
}
}
// Make a copy of object retrieved from backup to make it available unchanged
@@ -1668,6 +1676,21 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
obj = unstructuredObj
mustIncludeAdditionalItems := false
if annotations := obj.GetAnnotations(); annotations != nil {
if _, present := annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]; present {
// Only the string value "true" enables the bypass.
if annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] == "true" {
mustIncludeAdditionalItems = true
restoreLogger.Info("RestoreItemAction marked additional items as must-include; bypassing resource/namespace exclusion checks for them")
}
// Always strip the annotation so it never lands on the cluster,
// regardless of whether the value enabled the bypass.
delete(annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
obj.SetAnnotations(annotations)
}
}
var filteredAdditionalItems []velero.ResourceIdentifier
for _, additionalItem := range executeOutput.AdditionalItems {
itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name)
@@ -1687,6 +1710,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath)
if err != nil {
errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID))
continue
}
additionalItemNamespace := additionalItem.Namespace
@@ -1696,7 +1720,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
}
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace)
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace, mustIncludeAdditionalItems)
if additionalItemExists {
filteredAdditionalItems = append(filteredAdditionalItems, additionalItem)
}
+483
View File
@@ -754,6 +754,29 @@ func TestRestoreResourceFiltering(t *testing.T) {
apiResources: []*test.APIResource{test.ServiceAccounts()},
want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}},
},
{
// Regression for #9957: VSC must not be force-included via resourceMustHave
// when the restore only selects unrelated resource types.
name: "volumesnapshotcontents are not force-included for selective resource restores",
restore: defaultRestore().IncludedResources("storageclasses").IncludeClusterResources(true).Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("storageclasses.storage.k8s.io",
builder.ForStorageClass("sc-1").Result(),
).
AddItems("volumesnapshotcontents.snapshot.storage.k8s.io",
builder.ForVolumeSnapshotContent("vsc-1").Result(),
).
Done(),
apiResources: []*test.APIResource{
test.StorageClasses(),
test.VolumeSnapshotContents(),
},
want: map[*test.APIResource][]string{
test.StorageClasses(): {"/sc-1"},
test.VolumeSnapshotContents(): nil,
},
},
}
for _, tc := range tests {
@@ -2150,6 +2173,102 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
test.PVs(): nil,
},
},
{
name: "must-include annotation bypasses resource exclusion for additional items",
restore: defaultRestore().IncludedResources("pods").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
},
},
{
name: "must-include annotation bypasses namespace exclusion for additional items",
restore: defaultRestore().IncludedNamespaces("ns-1").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(),
apiResources: []*test.APIResource{test.Pods()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedNamespaces: []string{"ns-1"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1", "ns-2/pod-2"},
},
},
{
name: "must-include annotation bypasses IncludeClusterResources=false for additional items",
restore: defaultRestore().IncludeClusterResources(false).Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
},
},
}
for _, tc := range tests {
@@ -2180,6 +2299,370 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
}
}
// TestRestoreMustIncludeAdditionalItems covers restore must-include edge cases beyond the
// basic filter-bypass cases in TestRestoreActionAdditionalItems.
func TestRestoreMustIncludeAdditionalItems(t *testing.T) {
t.Run("must-include annotation is stripped from the restored item", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
annotations["keep-me"] = "yes"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
assert.Equal(t, "yes", annotations["keep-me"])
})
t.Run("non-true must-include annotation is stripped without bypassing filters", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "True"
annotations["keep-me"] = "yes"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): nil,
})
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
assert.Equal(t, "yes", annotations["keep-me"])
})
t.Run("SkipRestore supersedes must-include annotation and skips additional items", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
SkipRestore: true,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): nil,
test.PVs(): nil,
})
})
t.Run("must-include does not restore additional items missing from the backup tarball", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-missing"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, errs)
assertNonEmptyResults(t, "warning", warnings)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): nil,
})
})
t.Run("transitive must-include requires each RIA level to re-set the annotation", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
// Parent pod RIA force-includes the excluded PV.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
// Child PV RIA also re-sets the annotation to force-include an excluded PVC.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
test.PVCs(): {"ns-2/pvc-1"},
})
})
t.Run("without re-annotating, transitive additional items still respect filters", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
// Child PV RIA returns an additional PVC but does NOT set must-include.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
test.PVCs(): nil,
})
})
t.Run("VS must-include restores excluded VolumeSnapshotContent additional item", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.VolumeSnapshots())
h.AddItems(t, test.VolumeSnapshotContents())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("volumesnapshots.snapshot.storage.k8s.io").IncludeClusterResources(true).Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("volumesnapshots.snapshot.storage.k8s.io", builder.ForVolumeSnapshot("ns-1", "vs-1").Result()).
AddItems("volumesnapshotcontents.snapshot.storage.k8s.io", builder.ForVolumeSnapshotContent("vsc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.VolumeSnapshotContents, Name: "vsc-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.VolumeSnapshots(): {"ns-1/vs-1"},
test.VolumeSnapshotContents(): {"/vsc-1"},
})
})
}
// TestShouldRestore runs the ShouldRestore function for various permutations of
// existing/nonexisting/being-deleted PVs, PVCs, and namespaces, and verifies the
// result/error matches expectations.
+3
View File
@@ -58,6 +58,9 @@ func NewAPIServer(t *testing.T) *APIServer {
{Group: "velero.io", Version: "v2alpha1", Resource: "datauploads"}: "DataUploadsList",
{Group: "mygroup.io", Version: "v1", Resource: "mycustomkinds"}: "MyCustomKindList",
{Group: "mygroup.io", Version: "v1", Resource: "myclustercustomkinds"}: "MyClusterCustomKindList",
{Group: "storage.k8s.io", Version: "v1", Resource: "storageclasses"}: "StorageClassList",
{Group: "snapshot.storage.k8s.io", Version: "v1", Resource: "volumesnapshots"}: "VolumeSnapshotList",
{Group: "snapshot.storage.k8s.io", Version: "v1", Resource: "volumesnapshotcontents"}: "VolumeSnapshotContentList",
})
discoveryClient = &DiscoveryClient{FakeDiscovery: kubeClient.Discovery().(*discoveryfake.FakeDiscovery)}
)
+34
View File
@@ -220,3 +220,37 @@ func DataUploads(items ...metav1.Object) *APIResource {
Items: items,
}
}
func StorageClasses(items ...metav1.Object) *APIResource {
return &APIResource{
Group: "storage.k8s.io",
Version: "v1",
Name: "storageclasses",
ShortName: "sc",
Kind: "StorageClass",
Namespaced: false,
Items: items,
}
}
func VolumeSnapshotContents(items ...metav1.Object) *APIResource {
return &APIResource{
Group: "snapshot.storage.k8s.io",
Version: "v1",
Name: "volumesnapshotcontents",
Kind: "VolumeSnapshotContent",
Namespaced: false,
Items: items,
}
}
func VolumeSnapshots(items ...metav1.Object) *APIResource {
return &APIResource{
Group: "snapshot.storage.k8s.io",
Version: "v1",
Name: "volumesnapshots",
Kind: "VolumeSnapshot",
Namespaced: true,
Items: items,
}
}