/* Copyright 2017 the Velero contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/plugin/velero" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) func TestBackupPVAction(t *testing.T) { pvc := &unstructured.Unstructured{ Object: map[string]any{ "spec": map[string]any{}, "status": map[string]any{}, }, } backup := &v1.Backup{} a := NewPVCAction(velerotest.NewLogger()) // no spec.volumeName should result in no error // and no additional items _, additional, err := a.Execute(pvc, backup) require.NoError(t, err) assert.Empty(t, additional) // empty spec.volumeName should result in no error // and no additional items pvc.Object["spec"].(map[string]any)["volumeName"] = "" _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) assert.Empty(t, additional) // Action should clean the spec.Selector when the StorageClassName is not set. input := builder.ForPersistentVolumeClaim("abc", "abc").VolumeName("pv").Selector(&metav1.LabelSelector{MatchLabels: map[string]string{"abc": "abc"}}).Phase(corev1api.ClaimBound).Result() inputUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(input) require.NoError(t, err) item, additional, err := a.Execute(&unstructured.Unstructured{Object: inputUnstructured}, backup) require.NoError(t, err) require.Len(t, additional, 1) modifiedPVC := new(corev1api.PersistentVolumeClaim) require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), modifiedPVC)) require.Nil(t, modifiedPVC.Spec.Selector) // Action should clean the spec.Selector when the StorageClassName is set to specific StorageClass input2 := builder.ForPersistentVolumeClaim("abc", "abc").VolumeName("pv").StorageClass("sc1").Selector(&metav1.LabelSelector{MatchLabels: map[string]string{"abc": "abc"}}).Phase(corev1api.ClaimBound).Result() inputUnstructured2, err2 := runtime.DefaultUnstructuredConverter.ToUnstructured(input2) require.NoError(t, err2) item2, additional2, err2 := a.Execute(&unstructured.Unstructured{Object: inputUnstructured2}, backup) require.NoError(t, err2) require.Len(t, additional2, 1) modifiedPVC2 := new(corev1api.PersistentVolumeClaim) require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(item2.UnstructuredContent(), modifiedPVC2)) require.Nil(t, modifiedPVC2.Spec.Selector) // Action should keep the spec.Selector when the StorageClassName is set to "" input3 := builder.ForPersistentVolumeClaim("abc", "abc").StorageClass("").Selector(&metav1.LabelSelector{MatchLabels: map[string]string{"abc": "abc"}}).VolumeName("pv").Phase(corev1api.ClaimBound).Result() inputUnstructured3, err3 := runtime.DefaultUnstructuredConverter.ToUnstructured(input3) require.NoError(t, err3) item3, additional3, err3 := a.Execute(&unstructured.Unstructured{Object: inputUnstructured3}, backup) require.NoError(t, err3) require.Len(t, additional3, 1) modifiedPVC3 := new(corev1api.PersistentVolumeClaim) require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(item3.UnstructuredContent(), modifiedPVC3)) require.Equal(t, input3.Spec.Selector, modifiedPVC3.Spec.Selector) // Action should delete label started with"velero.io/" from the spec.Selector when the StorageClassName is set to "" input4 := builder.ForPersistentVolumeClaim("abc", "abc").StorageClass("").Selector(&metav1.LabelSelector{MatchLabels: map[string]string{"velero.io/abc": "abc", "abc": "abc"}}).VolumeName("pv").Phase(corev1api.ClaimBound).Result() inputUnstructured4, err4 := runtime.DefaultUnstructuredConverter.ToUnstructured(input4) require.NoError(t, err4) item4, additional4, err4 := a.Execute(&unstructured.Unstructured{Object: inputUnstructured4}, backup) require.NoError(t, err4) require.Len(t, additional4, 1) modifiedPVC4 := new(corev1api.PersistentVolumeClaim) require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(item4.UnstructuredContent(), modifiedPVC4)) require.Equal(t, &metav1.LabelSelector{MatchLabels: map[string]string{"abc": "abc"}}, modifiedPVC4.Spec.Selector) // Action should clean the spec.Selector when the StorageClassName has value input5 := builder.ForPersistentVolumeClaim("abc", "abc").StorageClass("sc1").Selector(&metav1.LabelSelector{MatchLabels: map[string]string{"velero.io/abc": "abc", "abc": "abc"}}).VolumeName("pv").Phase(corev1api.ClaimBound).Result() inputUnstructured5, err5 := runtime.DefaultUnstructuredConverter.ToUnstructured(input5) require.NoError(t, err5) item5, additional5, err5 := a.Execute(&unstructured.Unstructured{Object: inputUnstructured5}, backup) require.NoError(t, err5) require.Len(t, additional5, 1) modifiedPVC5 := new(corev1api.PersistentVolumeClaim) require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(item5.UnstructuredContent(), modifiedPVC5)) require.Nil(t, modifiedPVC5.Spec.Selector) // non-empty spec.volumeName when status.phase is empty // should result in no error and no additional items pvc.Object["spec"].(map[string]any)["volumeName"] = "myVolume" _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) require.Empty(t, additional) // non-empty spec.volumeName when status.phase is 'Pending' // should result in no error and no additional items pvc.Object["status"].(map[string]any)["phase"] = corev1api.ClaimPending _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) require.Empty(t, additional) // non-empty spec.volumeName when status.phase is 'Lost' // should result in no error and no additional items pvc.Object["status"].(map[string]any)["phase"] = corev1api.ClaimLost _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) require.Empty(t, additional) // non-empty spec.volumeName when status.phase is 'Bound' // should result in no error and one additional item for the PV pvc.Object["status"].(map[string]any)["phase"] = corev1api.ClaimBound _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) require.Len(t, additional, 1) assert.Equal(t, velero.ResourceIdentifier{GroupResource: kuberesource.PersistentVolumes, Name: "myVolume"}, additional[0]) // empty spec.volumeName when status.phase is 'Bound' should // result in no error and no additional items pvc.Object["spec"].(map[string]any)["volumeName"] = "" _, additional, err = a.Execute(pvc, backup) require.NoError(t, err) assert.Empty(t, additional) } func TestCleanupStaleVeleroLabels(t *testing.T) { tests := []struct { name string inputPVC *corev1api.PersistentVolumeClaim backup *v1.Backup expectedLabels map[string]string expectedSelector *metav1.LabelSelector }{ { name: "removes restore-name labels", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "velero.io/restore-name": "old-restore", "app": "myapp", }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "app": "myapp", }, }, { name: "removes backup-name labels that don't match current backup", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "velero.io/backup-name": "old-backup", "app": "myapp", }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "app": "myapp", }, }, { name: "keeps backup-name labels that match current backup", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "velero.io/backup-name": "current-backup", "app": "myapp", }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "velero.io/backup-name": "current-backup", "app": "myapp", }, }, { name: "removes volume-snapshot-name labels", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "velero.io/volume-snapshot-name": "old-snapshot", "app": "myapp", }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "app": "myapp", }, }, { name: "removes velero labels from selector match labels", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", }, Spec: corev1api.PersistentVolumeClaimSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "velero.io/restore-name": "old-restore", "velero.io/backup-name": "old-backup", "app": "myapp", }, }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: nil, expectedSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "myapp", }, }, }, { name: "handles PVC with no labels", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: nil, }, { name: "handles PVC with no selector", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "app": "myapp", }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "app": "myapp", }, expectedSelector: nil, }, { name: "removes multiple stale velero labels", inputPVC: &corev1api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pvc", Labels: map[string]string{ "velero.io/restore-name": "old-restore", "velero.io/backup-name": "old-backup", "velero.io/volume-snapshot-name": "old-snapshot", "app": "myapp", "env": "prod", }, }, Spec: corev1api.PersistentVolumeClaimSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "velero.io/restore-name": "old-restore", "app": "myapp", }, }, }, }, backup: &v1.Backup{ObjectMeta: metav1.ObjectMeta{Name: "current-backup"}}, expectedLabels: map[string]string{ "app": "myapp", "env": "prod", }, expectedSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "myapp", }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { action := NewPVCAction(velerotest.NewLogger()) // Create a copy of the input PVC to avoid modifying the test case pvcCopy := tc.inputPVC.DeepCopy() action.cleanupStaleVeleroLabels(pvcCopy, tc.backup) assert.Equal(t, tc.expectedLabels, pvcCopy.Labels, "Labels should match expected values") assert.Equal(t, tc.expectedSelector, pvcCopy.Spec.Selector, "Selector should match expected values") }) } }