From 40ea244a5659a0269a3ec2d39ba0e45b91d5388b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20=22WanzenBug=22=20Wanzenb=C3=B6ck?= Date: Fri, 6 Oct 2023 10:45:33 +0200 Subject: [PATCH] support deletion of snapshots after restore from backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user requests a restore from an off-site backup, they might want the local snapshot to be deleted after the restore. We already have this option when creating the snapshot by using a special parameter on the snapshot class. However, we do not have that information available directly. To support this, we need to lookup the snapshot content and snapshot class ourselfes, then parse the parameters. This is all optional, as LINSTOR CSI still needs to work outside kubernetes, or inside Kubernetes with incomplete information. Signed-off-by: Moritz "WanzenBug" Wanzenböck --- CHANGELOG.md | 4 ++ cmd/linstor-csi/linstor-csi.go | 1 + pkg/client/linstor.go | 16 ++++++- pkg/client/mock.go | 2 +- pkg/driver/driver.go | 86 +++++++++++++++++++++++++++++++++- pkg/volume/volume.go | 2 +- 6 files changed, 106 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8b7229..6420ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support to delete downloaded backups after restore operation (k8s only). + ## [1.2.3] - 2023-08-31 ### Changed diff --git a/cmd/linstor-csi/linstor-csi.go b/cmd/linstor-csi/linstor-csi.go index 20b09f5..2d03217 100644 --- a/cmd/linstor-csi/linstor-csi.go +++ b/cmd/linstor-csi/linstor-csi.go @@ -139,6 +139,7 @@ func main() { driver.Expander(linstorClient), driver.NodeInformer(linstorClient), driver.TopologyPrefix(*propNs), + driver.ConfigureKubernetesIfAvailable(), ) if err != nil { log.Fatal(err) diff --git a/pkg/client/linstor.go b/pkg/client/linstor.go index 323499e..198071b 100644 --- a/pkg/client/linstor.go +++ b/pkg/client/linstor.go @@ -910,7 +910,7 @@ func (s *Linstor) SnapDelete(ctx context.Context, snap *volume.Snapshot) error { } // VolFromSnap creates the volume using the data contained within the snapshot. -func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, params *volume.Parameters, topologies *csi.TopologyRequirement) error { +func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, params *volume.Parameters, snapParams *volume.SnapshotParameters, topologies *csi.TopologyRequirement) error { logger := s.log.WithFields(logrus.Fields{ "volume": fmt.Sprintf("%+v", vol), "snapshot": fmt.Sprintf("%+v", snap), @@ -987,6 +987,20 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v return err } + if snap.Remote != "" && snapParams != nil && snapParams.DeleteLocal { + logger.Info("deleting local copy of backup") + + err := s.client.Resources.DeleteSnapshot(ctx, snap.SourceVolumeId, snap.SnapshotId) + if err != nil { + logger.WithError(err).Warn("deleting local copy of backup failed") + } + + err = s.deleteResourceDefinitionAndGroupIfUnused(ctx, snap.GetSourceVolumeId()) + if err != nil { + logger.WithError(err).Warn("deleting local RD of backup failed") + } + } + logger.Debug("success") return nil } diff --git a/pkg/client/mock.go b/pkg/client/mock.go index a9a193a..89f1926 100644 --- a/pkg/client/mock.go +++ b/pkg/client/mock.go @@ -190,7 +190,7 @@ func (s *MockStorage) FindSnapsBySource(ctx context.Context, sourceVol *volume.I return results[start:end], nil } -func (s *MockStorage) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, parameters *volume.Parameters, topologies *csi.TopologyRequirement) error { +func (s *MockStorage) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *volume.Info, parameters *volume.Parameters, snapParams *volume.SnapshotParameters, topologies *csi.TopologyRequirement) error { s.createdVolumes = append(s.createdVolumes, vol) return nil } diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 6614528..ff822e5 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -39,6 +39,11 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" "github.com/piraeusdatastore/linstor-csi/pkg/client" "github.com/piraeusdatastore/linstor-csi/pkg/linstor" @@ -57,6 +62,7 @@ type Driver struct { VolumeStatter volume.VolumeStatter Expander volume.Expander NodeInformer volume.NodeInformer + kubeClient dynamic.Interface srv *grpc.Server log *logrus.Entry version string @@ -236,6 +242,20 @@ func LogLevel(s string) func(*Driver) error { } } +func ConfigureKubernetesIfAvailable() func(*Driver) error { + return func(d *Driver) error { + cfg, err := rest.InClusterConfig() + if err != nil { + // Not running in kubernetes + return nil + } + + d.kubeClient, err = dynamic.NewForConfig(cfg) + + return err + } +} + // GetPluginInfo https://github.com/container-storage-interface/spec/blob/v1.6.0/spec.md#getplugininfo func (d Driver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { return &csi.GetPluginInfoResponse{ @@ -1259,7 +1279,14 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params * "CreateVolume failed for %s: snapshot not found in storage backend", req.GetName()) } - if err := d.Snapshots.VolFromSnap(ctx, snap, info, params, req.GetAccessibilityRequirements()); err != nil { + snapParams, err := d.maybeGetSnapshotParameters(ctx, snap) + if err != nil { + logger.WithError(err).Warn("failed to fetch snapshot parameters, continuing without it") + + snapParams = nil + } + + if err := d.Snapshots.VolFromSnap(ctx, snap, info, params, snapParams, req.GetAccessibilityRequirements()); err != nil { d.failpathDelete(ctx, info.ID) return nil, status.Errorf(codes.Internal, "CreateVolume failed for %s: %v", req.GetName(), err) @@ -1301,7 +1328,7 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params * } }() - err = d.Snapshots.VolFromSnap(ctx, snap, info, params, req.GetAccessibilityRequirements()) + err = d.Snapshots.VolFromSnap(ctx, snap, info, params, nil, req.GetAccessibilityRequirements()) if err != nil { d.failpathDelete(ctx, info.ID) @@ -1345,6 +1372,61 @@ func (d Driver) createNewVolume(ctx context.Context, info *volume.Info, params * }, nil } +func findMatchingSnapshotClassName(snap *volume.Snapshot, contents ...unstructured.Unstructured) string { + for i := range contents { + content := contents[i].Object + if driver, _, _ := unstructured.NestedString(content, "spec", "driver"); driver != linstor.DriverName { + continue + } + + if handle, _, _ := unstructured.NestedString(content, "status", "snapshotHandle"); handle != snap.SnapshotId { + continue + } + + if readyToUse, _, _ := unstructured.NestedBool(content, "status", "readyToUse"); !readyToUse { + continue + } + + snapshotClass, _, _ := unstructured.NestedString(content, "spec", "volumeSnapshotClassName") + + return snapshotClass + } + + return "" +} + +func (d Driver) maybeGetSnapshotParameters(ctx context.Context, snap *volume.Snapshot) (*volume.SnapshotParameters, error) { + if d.kubeClient == nil { + return nil, nil + } + + gv := schema.GroupVersion{Group: "snapshot.storage.k8s.io", Version: "v1"} + contentGvr := gv.WithResource("volumesnapshotcontents") + classGvr := gv.WithResource("volumesnapshotclasses") + + result, err := d.kubeClient.Resource(contentGvr).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to fetch list of snapshot contents") + } + + snapshotClassName := findMatchingSnapshotClassName(snap, result.Items...) + if snapshotClassName == "" { + return nil, fmt.Errorf("failed to determine snapshot class name") + } + + class, err := d.kubeClient.Resource(classGvr).Get(ctx, snapshotClassName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to fetch snapshot class: %w", err) + } + + rawParams, _, err := unstructured.NestedStringMap(class.Object, "parameters") + if err != nil { + return nil, fmt.Errorf("failed to parse snapshot class: %w", err) + } + + return volume.NewSnapshotParameters(rawParams, nil) +} + // maybeDeleteLocalSnapshot deletes the local portion of a snapshot according to their volume.SnapshotParameters. // It will not delete a snapshot that is not ready, does not have a remote target, or where local deletion is disabled. func (d Driver) maybeDeleteLocalSnapshot(ctx context.Context, snap *volume.Snapshot, params *volume.SnapshotParameters) error { diff --git a/pkg/volume/volume.go b/pkg/volume/volume.go index f70ada7..39eec58 100644 --- a/pkg/volume/volume.go +++ b/pkg/volume/volume.go @@ -80,7 +80,7 @@ type SnapshotCreateDeleter interface { // List Snapshots should return a sorted list of snapshots. ListSnaps(ctx context.Context, start, limit int) ([]*Snapshot, error) // VolFromSnap creates a new volume based on the provided snapshot. - VolFromSnap(ctx context.Context, snap *Snapshot, vol *Info, params *Parameters, topologies *csi.TopologyRequirement) error + VolFromSnap(ctx context.Context, snap *Snapshot, vol *Info, params *Parameters, snapParams *SnapshotParameters, topologies *csi.TopologyRequirement) error } // AttacherDettacher handles operations relating to volume accessiblity on nodes.