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

Issue 7068: add a finalizer to protect retained VSC #7095

Merged
merged 1 commit into from
Nov 14, 2023
Merged
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/7095-Lyndon-Li
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix issue #7068, due to an behavior of CSI external snapshotter, manipulations of VS and VSC may not be handled in the same order inside external snapshotter as the API is called. So add a protection finalizer to ensure the order
4 changes: 3 additions & 1 deletion pkg/exposer/csi_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1.Obje
return errors.Wrap(err, "error to retain volume snapshot content")
}

vsc = retained

curLog.WithField("vsc name", vsc.Name).WithField("retained", (retained != nil)).Info("Finished to retain VSC")

defer func() {
Expand All @@ -134,7 +136,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1.Obje

curLog.WithField("vs name", volumeSnapshot.Name).Infof("VS is deleted in namespace %s", volumeSnapshot.Namespace)

err = csi.EnsureDeleteVSC(ctx, e.csiSnapshotClient, vsc.Name, csiExposeParam.OperationTimeout)
err = csi.EnsureDeleteVSC(ctx, e.csiSnapshotClient, vsc, csiExposeParam.OperationTimeout)
if err != nil {
return errors.Wrap(err, "error to delete volume snapshot content")
}
Expand Down
78 changes: 48 additions & 30 deletions pkg/util/csi/volume_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/stringptr"
"github.com/vmware-tanzu/velero/pkg/util/stringslice"

snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
snapshotter "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned/typed/volumesnapshot/v1"
Expand All @@ -41,7 +42,8 @@
)

const (
waitInternal = 2 * time.Second
waitInternal = 2 * time.Second
volumeSnapshotContentProtectFinalizer = "velero.io/volume-snapshot-content-protect-finalizer"
)

// WaitVolumeSnapshotReady waits a VS to become ready to use until the timeout reaches
Expand Down Expand Up @@ -97,36 +99,17 @@
return vsc, nil
}

// RetainVSC updates the VSC's deletion policy to Retain and return the update VSC
// RetainVSC updates the VSC's deletion policy to Retain and add a finalier and then return the update VSC
func RetainVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vsc *snapshotv1api.VolumeSnapshotContent) (*snapshotv1api.VolumeSnapshotContent, error) {
if vsc.Spec.DeletionPolicy == snapshotv1api.VolumeSnapshotContentRetain {
return vsc, nil
}
origBytes, err := json.Marshal(vsc)
if err != nil {
return nil, errors.Wrap(err, "error marshaling original VSC")
}

updated := vsc.DeepCopy()
updated.Spec.DeletionPolicy = snapshotv1api.VolumeSnapshotContentRetain

updatedBytes, err := json.Marshal(updated)
if err != nil {
return nil, errors.Wrap(err, "error marshaling updated VSC")
}

patchBytes, err := jsonpatch.CreateMergePatch(origBytes, updatedBytes)
if err != nil {
return nil, errors.Wrap(err, "error creating json merge patch for VSC")
}

retained, err := snapshotClient.VolumeSnapshotContents().Patch(ctx, vsc.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
if err != nil {
return nil, errors.Wrap(err, "error patching VSC")
}

return retained, nil
return patchVSC(ctx, snapshotClient, vsc, func(updated *snapshotv1api.VolumeSnapshotContent) {
updated.Spec.DeletionPolicy = snapshotv1api.VolumeSnapshotContentRetain
updated.Finalizers = append(updated.Finalizers, volumeSnapshotContentProtectFinalizer)
})
}

// DeleteVolumeSnapshotContentIfAny deletes a VSC by name if it exists, and log an error when the deletion fails
Expand Down Expand Up @@ -171,27 +154,34 @@

// EnsureDeleteVSC asserts the existence of a VSC by name, deletes it and waits for its disappearance and returns errors on any failure
func EnsureDeleteVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vscName string, timeout time.Duration) error {
err := snapshotClient.VolumeSnapshotContents().Delete(ctx, vscName, metav1.DeleteOptions{})
vsc *snapshotv1api.VolumeSnapshotContent, timeout time.Duration) error {
_, err := patchVSC(ctx, snapshotClient, vsc, func(updated *snapshotv1api.VolumeSnapshotContent) {
updated.Finalizers = stringslice.Except(updated.Finalizers, volumeSnapshotContentProtectFinalizer)
})
if err != nil {
return errors.Wrapf(err, "error to remove protect finalizer from vsc %s", vsc.Name)
}

err = snapshotClient.VolumeSnapshotContents().Delete(ctx, vsc.Name, metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return errors.Wrap(err, "error to delete volume snapshot content")
}

err = wait.PollImmediate(waitInternal, timeout, func() (bool, error) {
_, err := snapshotClient.VolumeSnapshotContents().Get(ctx, vscName, metav1.GetOptions{})
_, err := snapshotClient.VolumeSnapshotContents().Get(ctx, vsc.Name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return true, nil
}

return false, errors.Wrapf(err, fmt.Sprintf("error to get VolumeSnapshotContent %s", vscName))
return false, errors.Wrapf(err, fmt.Sprintf("error to get VolumeSnapshotContent %s", vsc.Name))
}

return false, nil
})

if err != nil {
return errors.Wrapf(err, "error to assure VolumeSnapshotContent is deleted, %s", vscName)
return errors.Wrapf(err, "error to assure VolumeSnapshotContent is deleted, %s", vsc.Name)
}

return nil
Expand All @@ -208,3 +198,31 @@
}
}
}

func patchVSC(ctx context.Context, snapshotClient snapshotter.SnapshotV1Interface,
vsc *snapshotv1api.VolumeSnapshotContent, updateFunc func(*snapshotv1api.VolumeSnapshotContent)) (*snapshotv1api.VolumeSnapshotContent, error) {
origBytes, err := json.Marshal(vsc)
if err != nil {
return nil, errors.Wrap(err, "error marshaling original VSC")
}

Check warning on line 207 in pkg/util/csi/volume_snapshot.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/csi/volume_snapshot.go#L206-L207

Added lines #L206 - L207 were not covered by tests

updated := vsc.DeepCopy()
updateFunc(updated)

updatedBytes, err := json.Marshal(updated)
if err != nil {
return nil, errors.Wrap(err, "error marshaling updated VSC")
}

Check warning on line 215 in pkg/util/csi/volume_snapshot.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/csi/volume_snapshot.go#L214-L215

Added lines #L214 - L215 were not covered by tests

patchBytes, err := jsonpatch.CreateMergePatch(origBytes, updatedBytes)
if err != nil {
return nil, errors.Wrap(err, "error creating json merge patch for VSC")
}

Check warning on line 220 in pkg/util/csi/volume_snapshot.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/csi/volume_snapshot.go#L219-L220

Added lines #L219 - L220 were not covered by tests

patched, err := snapshotClient.VolumeSnapshotContents().Patch(ctx, vsc.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{})
if err != nil {
return nil, errors.Wrap(err, "error patching VSC")
}

return patched, nil
}
41 changes: 33 additions & 8 deletions pkg/util/csi/volume_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,41 @@ func TestEnsureDeleteVSC(t *testing.T) {
name string
clientObj []runtime.Object
reactors []reactor
vscName string
vscObj *snapshotv1api.VolumeSnapshotContent
err string
}{
{
name: "delete fail",
vscName: "fake-vsc",
err: "error to delete volume snapshot content: volumesnapshotcontents.snapshot.storage.k8s.io \"fake-vsc\" not found",
name: "remove finalizer fail",
vscObj: vscObj,
reactors: []reactor{
{
verb: "patch",
resource: "volumesnapshotcontents",
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("fake-patch-error")
},
},
},
err: "error to remove protect finalizer from vsc fake-vsc: error patching VSC: fake-patch-error",
},
{
name: "delete fail",
vscObj: vscObj,
clientObj: []runtime.Object{vscObj},
reactors: []reactor{
{
verb: "delete",
resource: "volumesnapshotcontents",
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("fake-delete-error")
},
},
},
err: "error to delete volume snapshot content: fake-delete-error",
},
{
name: "wait fail",
vscName: "fake-vsc",
vscObj: vscObj,
clientObj: []runtime.Object{vscObj},
reactors: []reactor{
{
Expand All @@ -385,7 +409,7 @@ func TestEnsureDeleteVSC(t *testing.T) {
},
{
name: "success",
vscName: "fake-vsc",
vscObj: vscObj,
clientObj: []runtime.Object{vscObj},
},
}
Expand All @@ -398,7 +422,7 @@ func TestEnsureDeleteVSC(t *testing.T) {
fakeSnapshotClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
}

err := EnsureDeleteVSC(context.Background(), fakeSnapshotClient.SnapshotV1(), test.vscName, time.Millisecond)
err := EnsureDeleteVSC(context.Background(), fakeSnapshotClient.SnapshotV1(), test.vscObj, time.Millisecond)
if err != nil {
assert.EqualError(t, err, test.err)
} else {
Expand Down Expand Up @@ -601,7 +625,8 @@ func TestRetainVSC(t *testing.T) {
clientObj: []runtime.Object{vscObj},
updated: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-vsc",
Name: "fake-vsc",
Finalizers: []string{volumeSnapshotContentProtectFinalizer},
},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Expand Down
Loading