Merge pull request #7061 from blackpiglet/6595_backward_compatability

Add DataUpload Result and CSI VolumeSnapshot check for restore PV.
pull/7081/head
Xun Jiang/Bruce Jiang 2023-11-10 09:37:19 +08:00 committed by GitHub
commit cb5ffe2753
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 375 additions and 8 deletions

View File

@ -0,0 +1 @@
Add DataUpload Result and CSI VolumeSnapshot check for restore PV.

View File

@ -67,3 +67,8 @@ func (v *VolumeSnapshotBuilder) BoundVolumeSnapshotContentName(vscName string) *
v.object.Status.BoundVolumeSnapshotContentName = &vscName
return v
}
func (v *VolumeSnapshotBuilder) SourcePVC(name string) *VolumeSnapshotBuilder {
v.object.Spec.Source.PersistentVolumeClaimName = &name
return v
}

View File

@ -515,6 +515,11 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
return errors.Wrap(err, "error fetching volume snapshots metadata")
}
csiVolumeSnapshots, err := backupStore.GetCSIVolumeSnapshots(restore.Spec.BackupName)
if err != nil {
return errors.Wrap(err, "fail to fetch CSI VolumeSnapshots metadata")
}
restoreLog.Info("starting restore")
var podVolumeBackups []*api.PodVolumeBackup
@ -531,6 +536,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
BackupReader: backupFile,
ResourceModifiers: resourceModifiers,
DisableInformerCache: r.disableInformerCache,
CSIVolumeSnapshots: csiVolumeSnapshots,
}
restoreWarnings, restoreErrors := r.restorer.RestoreWithResolvers(restoreReq, actionsResolver, pluginManager)

View File

@ -23,6 +23,7 @@ import (
"testing"
"time"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@ -471,6 +472,7 @@ func TestRestoreReconcile(t *testing.T) {
}
if test.expectedRestorerCall != nil {
backupStore.On("GetBackupContents", test.backup.Name).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil)
backupStore.On("GetCSIVolumeSnapshots", test.backup.Name).Return([]*snapshotv1api.VolumeSnapshot{}, nil)
restorer.On("RestoreWithResolvers", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(warnings, errors)

View File

@ -21,6 +21,7 @@ import (
"io"
"sort"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
@ -60,6 +61,7 @@ type Request struct {
itemOperationsList *[]*itemoperation.RestoreOperation
ResourceModifiers *resourcemodifiers.ResourceModifiers
DisableInformerCache bool
CSIVolumeSnapshots []*snapshotv1api.VolumeSnapshot
}
type restoredItemStatus struct {

View File

@ -30,6 +30,7 @@ import (
"time"
"github.com/google/uuid"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
@ -299,6 +300,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
pvsToProvision: sets.NewString(),
pvRestorer: pvRestorer,
volumeSnapshots: req.VolumeSnapshots,
csiVolumeSnapshots: req.CSIVolumeSnapshots,
podVolumeBackups: req.PodVolumeBackups,
resourceTerminatingTimeout: kr.resourceTerminatingTimeout,
resourceTimeout: kr.resourceTimeout,
@ -348,6 +350,7 @@ type restoreContext struct {
pvsToProvision sets.String
pvRestorer PVRestorer
volumeSnapshots []*volume.Snapshot
csiVolumeSnapshots []*snapshotv1api.VolumeSnapshot
podVolumeBackups []*velerov1api.PodVolumeBackup
resourceTerminatingTimeout time.Duration
resourceTimeout time.Duration
@ -1288,7 +1291,35 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
case hasPodVolumeBackup(obj, ctx):
ctx.log.Infof("Dynamically re-provisioning persistent volume because it has a pod volume backup to be restored.")
ctx.log.WithFields(logrus.Fields{
"namespace": obj.GetNamespace(),
"name": obj.GetName(),
"groupResource": groupResource.String(),
}).Infof("Dynamically re-provisioning persistent volume because it has a pod volume backup to be restored.")
ctx.pvsToProvision.Insert(name)
// Return early because we don't want to restore the PV itself, we
// want to dynamically re-provision it.
return warnings, errs, itemExists
case hasCSIVolumeSnapshot(ctx, obj):
ctx.log.WithFields(logrus.Fields{
"namespace": obj.GetNamespace(),
"name": obj.GetName(),
"groupResource": groupResource.String(),
}).Infof("Dynamically re-provisioning persistent volume because it has a related CSI VolumeSnapshot.")
ctx.pvsToProvision.Insert(name)
// Return early because we don't want to restore the PV itself, we
// want to dynamically re-provision it.
return warnings, errs, itemExists
case hasSnapshotDataUpload(ctx, obj):
ctx.log.WithFields(logrus.Fields{
"namespace": obj.GetNamespace(),
"name": obj.GetName(),
"groupResource": groupResource.String(),
}).Infof("Dynamically re-provisioning persistent volume because it has a related snapshot DataUpload.")
ctx.pvsToProvision.Insert(name)
// Return early because we don't want to restore the PV itself, we
@ -1296,7 +1327,11 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
return warnings, errs, itemExists
case hasDeleteReclaimPolicy(obj.Object):
ctx.log.Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.")
ctx.log.WithFields(logrus.Fields{
"namespace": obj.GetNamespace(),
"name": obj.GetName(),
"groupResource": groupResource.String(),
}).Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.")
ctx.pvsToProvision.Insert(name)
// Return early because we don't want to restore the PV itself, we
@ -1304,7 +1339,11 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
return warnings, errs, itemExists
default:
ctx.log.Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.")
ctx.log.WithFields(logrus.Fields{
"namespace": obj.GetNamespace(),
"name": obj.GetName(),
"groupResource": groupResource.String(),
}).Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.")
// Check to see if the claimRef.namespace field needs to be remapped, and do so if necessary.
_, err = remapClaimRefNS(ctx, obj)
@ -1938,6 +1977,55 @@ func hasSnapshot(pvName string, snapshots []*volume.Snapshot) bool {
return false
}
func hasCSIVolumeSnapshot(ctx *restoreContext, unstructuredPV *unstructured.Unstructured) bool {
pv := new(v1.PersistentVolume)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredPV.Object, pv); err != nil {
ctx.log.WithError(err).Warnf("Unable to convert PV from unstructured to structured")
return false
}
for _, vs := range ctx.csiVolumeSnapshots {
if pv.Spec.ClaimRef.Name == *vs.Spec.Source.PersistentVolumeClaimName &&
pv.Spec.ClaimRef.Namespace == vs.Namespace {
return true
}
}
return false
}
func hasSnapshotDataUpload(ctx *restoreContext, unstructuredPV *unstructured.Unstructured) bool {
pv := new(v1.PersistentVolume)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredPV.Object, pv); err != nil {
ctx.log.WithError(err).Warnf("Unable to convert PV from unstructured to structured")
return false
}
if pv.Spec.ClaimRef == nil {
return false
}
dataUploadResultList := new(v1.ConfigMapList)
err := ctx.kbClient.List(go_context.TODO(), dataUploadResultList, &crclient.ListOptions{
LabelSelector: labels.SelectorFromSet(map[string]string{
velerov1api.RestoreUIDLabel: label.GetValidName(string(ctx.restore.GetUID())),
velerov1api.PVCNamespaceNameLabel: label.GetValidName(pv.Spec.ClaimRef.Namespace + "." + pv.Spec.ClaimRef.Name),
velerov1api.ResourceUsageLabel: label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)),
}),
})
if err != nil {
ctx.log.WithError(err).Warnf("Fail to list DataUpload result CM.")
return false
}
if len(dataUploadResultList.Items) != 1 {
ctx.log.WithError(fmt.Errorf("dataupload result number is not expected")).
Warnf("Got %d DataUpload result. Expect one.", len(dataUploadResultList.Items))
return false
}
return true
}
func hasPodVolumeBackup(unstructuredPV *unstructured.Unstructured, ctx *restoreContext) bool {
if len(ctx.podVolumeBackups) == 0 {
return false

View File

@ -25,6 +25,7 @@ import (
"testing"
"time"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@ -2256,6 +2257,7 @@ func (*volumeSnapshotter) DeleteSnapshot(snapshotID string) error {
// Verification is done by looking at the contents of the API and the metadata/spec/status of
// the items in the API.
func TestRestorePersistentVolumes(t *testing.T) {
testPVCName := "testPVC"
tests := []struct {
name string
restore *velerov1api.Restore
@ -2265,6 +2267,8 @@ func TestRestorePersistentVolumes(t *testing.T) {
volumeSnapshots []*volume.Snapshot
volumeSnapshotLocations []*velerov1api.VolumeSnapshotLocation
volumeSnapshotterGetter volumeSnapshotterGetter
csiVolumeSnapshots []*snapshotv1api.VolumeSnapshot
dataUploadResult *corev1api.ConfigMap
want []*test.APIResource
wantError bool
wantWarning bool
@ -2923,6 +2927,77 @@ func TestRestorePersistentVolumes(t *testing.T) {
),
},
},
{
name: "when a PV with a reclaim policy of retain has a CSI VolumeSnapshot and does not exist in-cluster, the PV is not restored",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("persistentvolumes",
builder.ForPersistentVolume("pv-1").
ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).
ClaimRef("velero", testPVCName).
Result(),
).
Done(),
apiResources: []*test.APIResource{
test.PVs(),
test.PVCs(),
},
csiVolumeSnapshots: []*snapshotv1api.VolumeSnapshot{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "test",
},
Spec: snapshotv1api.VolumeSnapshotSpec{
Source: snapshotv1api.VolumeSnapshotSource{
PersistentVolumeClaimName: &testPVCName,
},
},
},
},
volumeSnapshotLocations: []*velerov1api.VolumeSnapshotLocation{
builder.ForVolumeSnapshotLocation(velerov1api.DefaultNamespace, "default").Provider("provider-1").Result(),
},
volumeSnapshotterGetter: map[string]vsv1.VolumeSnapshotter{
"provider-1": &volumeSnapshotter{
snapshotVolumes: map[string]string{"snapshot-1": "new-volume"},
},
},
want: []*test.APIResource{},
},
{
name: "when a PV with a reclaim policy of retain has a DataUpload result CM and does not exist in-cluster, the PV is not restored",
restore: defaultRestore().ObjectMeta(builder.WithUID("fakeUID")).Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("persistentvolumes",
builder.ForPersistentVolume("pv-1").
ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).
ClaimRef("velero", testPVCName).
Result(),
).
Done(),
apiResources: []*test.APIResource{
test.PVs(),
test.PVCs(),
test.ConfigMaps(),
},
volumeSnapshotLocations: []*velerov1api.VolumeSnapshotLocation{
builder.ForVolumeSnapshotLocation(velerov1api.DefaultNamespace, "default").Provider("provider-1").Result(),
},
volumeSnapshotterGetter: map[string]vsv1.VolumeSnapshotter{
"provider-1": &volumeSnapshotter{
snapshotVolumes: map[string]string{"snapshot-1": "new-volume"},
},
},
dataUploadResult: builder.ForConfigMap("velero", "test").ObjectMeta(builder.WithLabelsMap(map[string]string{
velerov1api.RestoreUIDLabel: "fakeUID",
velerov1api.PVCNamespaceNameLabel: "velero/testPVC",
velerov1api.ResourceUsageLabel: string(velerov1api.VeleroResourceUsageDataUploadResult),
})).Result(),
want: []*test.APIResource{},
},
}
for _, tc := range tests {
@ -2939,6 +3014,10 @@ func TestRestorePersistentVolumes(t *testing.T) {
require.NoError(t, h.restorer.kbClient.Create(context.Background(), vsl))
}
if tc.dataUploadResult != nil {
require.NoError(t, h.restorer.kbClient.Create(context.TODO(), tc.dataUploadResult))
}
for _, r := range tc.apiResources {
h.AddItems(t, r)
}
@ -2955,11 +3034,12 @@ func TestRestorePersistentVolumes(t *testing.T) {
}
data := &Request{
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
VolumeSnapshots: tc.volumeSnapshots,
BackupReader: tc.tarball,
Log: h.log,
Restore: tc.restore,
Backup: tc.backup,
VolumeSnapshots: tc.volumeSnapshots,
BackupReader: tc.tarball,
CSIVolumeSnapshots: tc.csiVolumeSnapshots,
}
warnings, errs := h.restorer.Restore(
data,
@ -3652,3 +3732,175 @@ func TestIsAlreadyExistsError(t *testing.T) {
})
}
}
func TestHasCSIVolumeSnapshot(t *testing.T) {
tests := []struct {
name string
vs *snapshotv1api.VolumeSnapshot
obj *unstructured.Unstructured
expectedResult bool
}{
{
name: "Invalid PV, expect false.",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": 1,
},
},
expectedResult: false,
},
{
name: "Cannot find VS, expect false",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "test",
},
},
},
expectedResult: false,
},
{
name: "Find VS, expect true.",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "velero",
"name": "test",
},
"spec": map[string]interface{}{
"claimRef": map[string]interface{}{
"namespace": "velero",
"name": "test",
},
},
},
},
vs: builder.ForVolumeSnapshot("velero", "test").SourcePVC("test").Result(),
expectedResult: true,
},
}
for _, tc := range tests {
h := newHarness(t)
ctx := &restoreContext{
log: h.log,
}
if tc.vs != nil {
ctx.csiVolumeSnapshots = []*snapshotv1api.VolumeSnapshot{tc.vs}
}
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expectedResult, hasCSIVolumeSnapshot(ctx, tc.obj))
})
}
}
func TestHasSnapshotDataUpload(t *testing.T) {
tests := []struct {
name string
duResult *corev1api.ConfigMap
obj *unstructured.Unstructured
expectedResult bool
restore *velerov1api.Restore
}{
{
name: "Invalid PV, expect false.",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": 1,
},
},
expectedResult: false,
},
{
name: "PV without ClaimRef, expect false",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "test",
},
},
},
duResult: builder.ForConfigMap("velero", "test").Result(),
restore: builder.ForRestore("velero", "test").ObjectMeta(builder.WithUID("fakeUID")).Result(),
expectedResult: false,
},
{
name: "Cannot find DataUploadResult CM, expect false",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "test",
},
"spec": map[string]interface{}{
"claimRef": map[string]interface{}{
"namespace": "velero",
"name": "testPVC",
},
},
},
},
duResult: builder.ForConfigMap("velero", "test").Result(),
restore: builder.ForRestore("velero", "test").ObjectMeta(builder.WithUID("fakeUID")).Result(),
expectedResult: false,
},
{
name: "Find DataUploadResult CM, expect true",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "test",
},
"spec": map[string]interface{}{
"claimRef": map[string]interface{}{
"namespace": "velero",
"name": "testPVC",
},
},
},
},
duResult: builder.ForConfigMap("velero", "test").ObjectMeta(builder.WithLabelsMap(map[string]string{
velerov1api.RestoreUIDLabel: "fakeUID",
velerov1api.PVCNamespaceNameLabel: "velero/testPVC",
velerov1api.ResourceUsageLabel: string(velerov1api.VeleroResourceUsageDataUploadResult),
})).Result(),
restore: builder.ForRestore("velero", "test").ObjectMeta(builder.WithUID("fakeUID")).Result(),
expectedResult: false,
},
}
for _, tc := range tests {
h := newHarness(t)
ctx := &restoreContext{
log: h.log,
kbClient: h.restorer.kbClient,
restore: tc.restore,
}
if tc.duResult != nil {
require.NoError(t, ctx.kbClient.Create(context.TODO(), tc.duResult))
}
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expectedResult, hasSnapshotDataUpload(ctx, tc.obj))
})
}
}

View File

@ -142,6 +142,17 @@ func ServiceAccounts(items ...metav1.Object) *APIResource {
}
}
func ConfigMaps(items ...metav1.Object) *APIResource {
return &APIResource{
Group: "",
Version: "v1",
Name: "configmaps",
ShortName: "cm",
Namespaced: true,
Items: items,
}
}
func CRDs(items ...metav1.Object) *APIResource {
return &APIResource{
Group: "apiextensions.k8s.io",