diff --git a/docs/functions.rst b/docs/functions.rst index 7bde36f110..54dde332b1 100644 --- a/docs/functions.rst +++ b/docs/functions.rst @@ -1328,6 +1328,52 @@ Example: snapshotClass: do-block-storage +RestoreCSISnapshot +------------------ + +This function restores a new PersistentVolumeClaim using CSI VolumeSnapshot. + +Arguments: + +.. csv-table:: + :header: "Argument", "Required", "Type", "Description" + :align: left + :widths: 5,5,5,15 + + `name`, Yes, `string`, name of the VolumeSnapshot + `pvc`, Yes, `string`, name of the new PVC + `namespace`, Yes, `string`, namespace of the VolumeSnapshot and resultant PersistentVolumeClaim + `storageClass`, Yes, `string`, name of the StorageClass + `restoreSize`, Yes, `string`, required memory size to restore PVC + `accessModes`, No, `[]string`, access modes for the underlying PV (Default is ``[]{"ReadWriteOnce"}```) + `volumeMode`, No, `string`, mode of volume (Default is ``"Filesystem"```) + `labels`, No, `map[string]string`, optional labels for the PersistentVolumeClaim + +.. note:: + Output artifact ``snapshotInfo`` from ``CreateCSISnapshot`` function can be used as an input artifact in this function. + +Example: + +.. code-block:: yaml + :linenos: + + actions: + restore: + inputArtifactNames: + - snapshotInfo + phases: + - func: RestoreCSISnapshot + name: restoreCSISnapshot + args: + name: "{{ .ArtifactsIn.snapshotInfo.KeyValue.name }}" + pvc: "{{ .ArtifactsIn.snapshotInfo.KeyValue.pvc }}-restored" + namespace: "{{ .ArtifactsIn.snapshotInfo.KeyValue.namespace }}" + storageClass: do-block-storage + restoreSize: "{{ .ArtifactsIn.snapshotInfo.KeyValue.restoreSize }}" + accessModes: ["ReadWriteOnce"] + volumeMode: "Filesystem" + + Registering Functions --------------------- diff --git a/pkg/function/create_csi_snapshot.go b/pkg/function/create_csi_snapshot.go index 471d134cff..cfbd3af068 100644 --- a/pkg/function/create_csi_snapshot.go +++ b/pkg/function/create_csi_snapshot.go @@ -18,6 +18,7 @@ import ( "context" "fmt" + v1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/util/rand" @@ -73,7 +74,7 @@ func (*createCSISnapshotFunc) Exec(ctx context.Context, tp param.TemplateParams, if err := Arg(args, CreateCSISnapshotSnapshotClassArg, &snapshotClass); err != nil { return nil, err } - if err := OptArg(args, CreateCSISnapshotNameArg, &name, defaultSnapshotName(pvc, 20)); err != nil { + if err := OptArg(args, CreateCSISnapshotNameArg, &name, defaultSnapshotName(pvc, 5)); err != nil { return nil, err } if err := OptArg(args, CreateCSISnapshotLabelsArg, &labels, map[string]string{}); err != nil { @@ -98,10 +99,7 @@ func (*createCSISnapshotFunc) Exec(ctx context.Context, tp param.TemplateParams, } // waitForReady is set to true by default because snapshot information is needed as output artifacts waitForReady := true - if err := snapshotter.Create(ctx, name, namespace, pvc, &snapshotClass, waitForReady, labels); err != nil { - return nil, err - } - vs, err := snapshotter.Get(ctx, name, namespace) + vs, err := createCSISnapshot(ctx, snapshotter, name, namespace, pvc, snapshotClass, waitForReady, labels) if err != nil { return nil, err } @@ -124,7 +122,18 @@ func (*createCSISnapshotFunc) RequiredArgs() []string { } } -// defaultSnapshotName generates snapshot name using pvcName-snapshot-randomValue +func createCSISnapshot(ctx context.Context, snapshotter snapshot.Snapshotter, name, namespace, pvc, snapshotClass string, wait bool, labels map[string]string) (*v1.VolumeSnapshot, error) { + if err := snapshotter.Create(ctx, name, namespace, pvc, &snapshotClass, wait, labels); err != nil { + return nil, err + } + vs, err := snapshotter.Get(ctx, name, namespace) + if err != nil { + return nil, err + } + return vs, nil +} + +// defaultSnapshotName generates snapshot name using -snapshot- func defaultSnapshotName(pvcName string, len int) string { return fmt.Sprintf("%s-snapshot-%s", pvcName, rand.String(len)) } diff --git a/pkg/function/create_csi_snapshot_test.go b/pkg/function/create_csi_snapshot_test.go index 520d1acb2f..a0b3a71d31 100644 --- a/pkg/function/create_csi_snapshot_test.go +++ b/pkg/function/create_csi_snapshot_test.go @@ -29,16 +29,16 @@ import ( ) const ( - // CreateCSISnapshotTestNamespace is the namespace where testing is done - CreateCSISnapshotTestNamespace = "test-create-csi-snapshot" - // CreateCSISnapshotPVCName is the name of the PVC that will be captured - CreateCSISnapshotPVCName = "test-pvc" - // CreateCSISnapshotSnapshotName is the name of the snapshot - CreateCSISnapshotSnapshotName = "test-snapshot" - // CreateCSISnapshotSnapshotClass is the fake snapshot class - CreateCSISnapshotSnapshotClass = "test-snapshot-class" - // CreateCSISnapshotStorageClass is the fake storage class - CreateCSISnapshotStorageClass = "test-storage-class" + // testCreateNamespace is the namespace where testing is done + testCreateNamespace = "test-create-csi-snapshot" + // pvcName is the name of the PVC that will be captured + pvcName = "test-pvc" + // snapshotName is the name of the snapshot + snapshotName = "test-snapshot" + // snapshotClass is the fake snapshot class + snapshotClass = "test-snapshot-class" + // storageClass is the fake storage class + storageClass = "test-storage-class" ) type CreateCSISnapshotTestSuite struct { @@ -52,11 +52,11 @@ type CreateCSISnapshotTestSuite struct { var _ = Suite(&CreateCSISnapshotTestSuite{}) func (testSuite *CreateCSISnapshotTestSuite) SetUpSuite(c *C) { - testSuite.volumeSnapshotClass = CreateCSISnapshotSnapshotClass - testSuite.storageClass = CreateCSISnapshotStorageClass - testSuite.pvcName = CreateCSISnapshotPVCName - testSuite.snapName = CreateCSISnapshotSnapshotName - testSuite.namespace = CreateCSISnapshotTestNamespace + testSuite.volumeSnapshotClass = snapshotClass + testSuite.storageClass = storageClass + testSuite.pvcName = pvcName + testSuite.snapName = snapshotName + testSuite.namespace = testCreateNamespace } func (testSuite *CreateCSISnapshotTestSuite) TestCreateCSISnapshot(c *C) { @@ -83,27 +83,24 @@ func (testSuite *CreateCSISnapshotTestSuite) TestCreateCSISnapshot(c *C) { GroupVersion: "snapshot.storage.k8s.io/v1", }, } { + ctx := context.Background() fakeCli := fake.NewSimpleClientset() fakeCli.Resources = []*metav1.APIResourceList{apiResourceList} - _, err := fakeCli.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testSuite.namespace}}, metav1.CreateOptions{}) + _, err := fakeCli.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testSuite.namespace}}, metav1.CreateOptions{}) c.Assert(err, IsNil) scheme := runtime.NewScheme() fakeSnapshotter, err := snapshot.NewSnapshotter(fakeCli, dynfake.NewSimpleDynamicClient(scheme)) c.Assert(err, IsNil) - _, err = fakeCli.CoreV1().PersistentVolumeClaims(testSuite.namespace).Create(context.TODO(), getPVCManifest(testSuite.pvcName, testSuite.storageClass), metav1.CreateOptions{}) + _, err = fakeCli.CoreV1().PersistentVolumeClaims(testSuite.namespace).Create(ctx, getPVCManifest(testSuite.pvcName, testSuite.storageClass), metav1.CreateOptions{}) c.Assert(err, IsNil) - err = fakeSnapshotter.Create(context.Background(), testSuite.snapName, testSuite.namespace, testSuite.pvcName, &testSuite.volumeSnapshotClass, false, nil) + _, err = createCSISnapshot(ctx, fakeSnapshotter, testSuite.snapName, testSuite.namespace, testSuite.pvcName, testSuite.volumeSnapshotClass, false, nil) c.Assert(err, IsNil) - vs, err := fakeSnapshotter.Get(context.Background(), testSuite.snapName, testSuite.namespace) - c.Assert(err, IsNil) - c.Assert(vs.Name, Equals, testSuite.snapName) - - err = fakeCli.CoreV1().Namespaces().Delete(context.Background(), testSuite.namespace, metav1.DeleteOptions{}) + err = fakeCli.CoreV1().Namespaces().Delete(ctx, testSuite.namespace, metav1.DeleteOptions{}) c.Assert(err, IsNil) } } diff --git a/pkg/function/restore_csi_snapshot.go b/pkg/function/restore_csi_snapshot.go new file mode 100644 index 0000000000..b85a2099bb --- /dev/null +++ b/pkg/function/restore_csi_snapshot.go @@ -0,0 +1,201 @@ +// Copyright 2022 The Kanister Authors. +// +// 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 function + +import ( + "context" + "errors" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + kanister "github.com/kanisterio/kanister/pkg" + "github.com/kanisterio/kanister/pkg/kube" + "github.com/kanisterio/kanister/pkg/param" +) + +func init() { + _ = kanister.Register(&restoreCSISnapshotFunc{}) +} + +var ( + _ kanister.Func = (*restoreCSISnapshotFunc)(nil) +) + +const ( + // Snapshot API Group + SnapshotAPIGroup = "snapshot.storage.k8s.io" + // RestoreCSISnapshotFuncName gives the name of the function + RestoreCSISnapshotFuncName = "RestoreCSISnapshot" + // RestoreCSISnapshotNameArg provides name of the VolumeSnapshot + RestoreCSISnapshotNameArg = "name" + // RestoreCSISnapshotPVCNameArg gives the name of the newly restored PVC + RestoreCSISnapshotPVCNameArg = "pvc" + // RestoreCSISnapshotNamespaceArg mentions the namespace of the newly restored PVC + RestoreCSISnapshotNamespaceArg = "namespace" + // RestoreCSISnapshotStorageClassArg specifies the name of the StorageClass + RestoreCSISnapshotStorageClassArg = "storageClass" + // RestoreCSISnapshotStorageRequestArg provides the storage size to be requested for PV in PVC + RestoreCSISnapshotRestoreSizeArg = "restoreSize" + // RestoreCSISnapshotAccessModesArg lists down the accessmodes for the underlying PV + RestoreCSISnapshotAccessModesArg = "accessModes" + // RestoreCSISnapshotLabelsArg has labels that will be added to the newly restored PVC + RestoreCSISnapshotLabelsArg = "labels" + // RestoreCSISnapshotVolumeModeArg defines mode of volume + RestoreCSISnapshotVolumeModeArg = "volumeMode" +) + +type restoreCSISnapshotFunc struct{} + +type restoreCSISnapshotArgs struct { + Name string + PVC string + Namespace string + StorageClass string + RestoreSize *resource.Quantity + AccessModes []v1.PersistentVolumeAccessMode + Labels map[string]string + VolumeMode v1.PersistentVolumeMode +} + +func (*restoreCSISnapshotFunc) Name() string { + return RestoreCSISnapshotFuncName +} + +func (*restoreCSISnapshotFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) { + var restoreSize string + var restoreArgs restoreCSISnapshotArgs + if err := Arg(args, RestoreCSISnapshotNameArg, &restoreArgs.Name); err != nil { + return nil, err + } + if err := Arg(args, RestoreCSISnapshotPVCNameArg, &restoreArgs.PVC); err != nil { + return nil, err + } + if err := Arg(args, RestoreCSISnapshotNamespaceArg, &restoreArgs.Namespace); err != nil { + return nil, err + } + if err := Arg(args, RestoreCSISnapshotStorageClassArg, &restoreArgs.StorageClass); err != nil { + return nil, err + } + if err := Arg(args, RestoreCSISnapshotRestoreSizeArg, &restoreSize); err != nil { + return nil, err + } + if err := OptArg(args, RestoreCSISnapshotAccessModesArg, &restoreArgs.AccessModes, []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}); err != nil { + return nil, err + } + if err := validateVolumeAccessModesArg(restoreArgs.AccessModes); err != nil { + return nil, err + } + if err := OptArg(args, RestoreCSISnapshotVolumeModeArg, &restoreArgs.VolumeMode, v1.PersistentVolumeFilesystem); err != nil { + return nil, err + } + if err := validateVolumeModeArg(restoreArgs.VolumeMode); err != nil { + return nil, err + } + if err := OptArg(args, RestoreCSISnapshotLabelsArg, &restoreArgs.Labels, nil); err != nil { + return nil, err + } + size, err := resource.ParseQuantity(restoreSize) + if err != nil { + return nil, err + } + restoreArgs.RestoreSize = &size + + kubeCli, err := getClient() + if err != nil { + return nil, err + } + if _, err := restoreCSISnapshot(ctx, kubeCli, restoreArgs); err != nil { + return nil, err + } + return nil, nil +} + +func (*restoreCSISnapshotFunc) RequiredArgs() []string { + return []string{ + RestoreCSISnapshotNameArg, + RestoreCSISnapshotPVCNameArg, + RestoreCSISnapshotNamespaceArg, + RestoreCSISnapshotStorageClassArg, + RestoreCSISnapshotRestoreSizeArg, + } +} + +func getClient() (kubernetes.Interface, error) { + kubeCli, err := kube.NewClient() + return kubeCli, err +} + +func restoreCSISnapshot(ctx context.Context, kubeCli kubernetes.Interface, args restoreCSISnapshotArgs) (*v1.PersistentVolumeClaim, error) { + pvc := newPVCManifest(args) + if _, err := kubeCli.CoreV1().PersistentVolumeClaims(args.Namespace).Create(ctx, pvc, metav1.CreateOptions{}); err != nil { + return nil, err + } + return pvc, nil +} + +func newPVCManifest(args restoreCSISnapshotArgs) *v1.PersistentVolumeClaim { + snapshotAPIGroup := SnapshotAPIGroup + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: args.PVC, + Namespace: args.Namespace, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: args.AccessModes, + VolumeMode: &args.VolumeMode, + DataSource: &v1.TypedLocalObjectReference{ + APIGroup: &snapshotAPIGroup, + Kind: "VolumeSnapshot", + Name: args.Name, + }, + StorageClassName: &args.StorageClass, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: *args.RestoreSize, + }, + }, + }, + } + if args.Labels != nil { + pvc.ObjectMeta.Labels = args.Labels + } + return pvc +} + +func validateVolumeModeArg(volumeMode v1.PersistentVolumeMode) error { + switch volumeMode { + case v1.PersistentVolumeFilesystem, + v1.PersistentVolumeBlock: + default: + return errors.New("Given volumeMode " + string(volumeMode) + " is invalid") + } + return nil +} + +func validateVolumeAccessModesArg(accessModes []v1.PersistentVolumeAccessMode) error { + for _, accessModeInArg := range accessModes { + switch accessModeInArg { + case v1.ReadOnlyMany, + v1.ReadWriteMany, + v1.ReadWriteOnce: + default: + return errors.New("Given accessMode " + string(accessModeInArg) + " is invalid") + } + } + return nil +} diff --git a/pkg/function/restore_csi_snapshot_test.go b/pkg/function/restore_csi_snapshot_test.go new file mode 100644 index 0000000000..be72ced748 --- /dev/null +++ b/pkg/function/restore_csi_snapshot_test.go @@ -0,0 +1,184 @@ +// Copyright 2022 The Kanister Authors. +// +// 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 function + +import ( + "context" + + "github.com/kanisterio/kanister/pkg/kube/snapshot" + + . "gopkg.in/check.v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + dynfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/fake" +) + +const ( + // testRestoreNamespace is the namespace where testing is done + testRestoreNamespace = "test-restore-csi-snapshot" + // originalPVCName is the name of the PVC that will be captured + originalPVCName = "test-pvc" + // newPVCName is the name of the new PVC that will be restored + newPVCName = "test-pvc-restored" +) + +type RestoreCSISnapshotTestSuite struct { + snapName string + pvcName string + newPVCName string + namespace string + volumeSnapshotClass string + storageClass string +} + +var _ = Suite(&RestoreCSISnapshotTestSuite{}) + +func (testSuite *RestoreCSISnapshotTestSuite) SetUpSuite(c *C) { + testSuite.volumeSnapshotClass = snapshotClass + testSuite.storageClass = storageClass + testSuite.pvcName = originalPVCName + testSuite.newPVCName = newPVCName + testSuite.snapName = snapshotName + testSuite.namespace = testRestoreNamespace +} + +func (testSuite *RestoreCSISnapshotTestSuite) TestRestoreCSISnapshot(c *C) { + for _, apiResourceList := range []*metav1.APIResourceList{ + { + TypeMeta: metav1.TypeMeta{ + Kind: "VolumeSnapshot", + APIVersion: "v1alpha1", + }, + GroupVersion: "snapshot.storage.k8s.io/v1alpha1", + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "VolumeSnapshot", + APIVersion: "v1beta1", + }, + GroupVersion: "snapshot.storage.k8s.io/v1beta1", + }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "VolumeSnapshot", + APIVersion: "v1", + }, + GroupVersion: "snapshot.storage.k8s.io/v1", + }, + } { + ctx := context.Background() + fakeCli := fake.NewSimpleClientset() + fakeCli.Resources = []*metav1.APIResourceList{apiResourceList} + + _, err := fakeCli.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testSuite.namespace}}, metav1.CreateOptions{}) + c.Assert(err, IsNil) + + scheme := runtime.NewScheme() + fakeSnapshotter, err := snapshot.NewSnapshotter(fakeCli, dynfake.NewSimpleDynamicClient(scheme)) + c.Assert(err, IsNil) + + originalPVC := getOriginalPVCManifest(testSuite.pvcName, testSuite.storageClass) + createPVC(c, testSuite.namespace, originalPVC, fakeCli) + + err = fakeSnapshotter.Create(ctx, testSuite.snapName, testSuite.namespace, testSuite.pvcName, &testSuite.volumeSnapshotClass, false, nil) + c.Assert(err, IsNil) + + vs, err := fakeSnapshotter.Get(ctx, testSuite.snapName, testSuite.namespace) + c.Assert(err, IsNil) + c.Assert(vs.Name, Equals, testSuite.snapName) + + restoreArgs := restoreCSISnapshotArgs{ + Name: testSuite.snapName, + PVC: testSuite.newPVCName, + Namespace: testSuite.namespace, + StorageClass: testSuite.storageClass, + RestoreSize: originalPVC.Spec.Resources.Requests.Storage(), + VolumeMode: *originalPVC.Spec.VolumeMode, + AccessModes: originalPVC.Spec.AccessModes, + Labels: nil, + } + pvc, err := restoreCSISnapshot(ctx, fakeCli, restoreArgs) + c.Assert(err, IsNil) + c.Assert(pvc.Name, Equals, testSuite.newPVCName) + + err = fakeCli.CoreV1().Namespaces().Delete(ctx, testSuite.namespace, metav1.DeleteOptions{}) + c.Assert(err, IsNil) + } +} + +func (testSuite *RestoreCSISnapshotTestSuite) TestValidateVolumeModeArg(c *C) { + for _, scenario := range []struct { + Arg v1.PersistentVolumeMode + ExpectedErr Checker + }{ + { + Arg: "test", + ExpectedErr: NotNil, + }, + { + Arg: v1.PersistentVolumeFilesystem, + ExpectedErr: IsNil, + }, + } { + err := validateVolumeModeArg(scenario.Arg) + c.Assert(err, scenario.ExpectedErr) + } +} + +func (testSuite *RestoreCSISnapshotTestSuite) TestValidateAccessModeArg(c *C) { + for _, scenario := range []struct { + Arg []v1.PersistentVolumeAccessMode + ExpectedErr Checker + }{ + { + Arg: []v1.PersistentVolumeAccessMode{"test"}, + ExpectedErr: NotNil, + }, + { + Arg: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + ExpectedErr: IsNil, + }, + } { + err := validateVolumeAccessModesArg(scenario.Arg) + c.Assert(err, scenario.ExpectedErr) + } +} + +func createPVC(c *C, namespace string, pvc *v1.PersistentVolumeClaim, fakeCli *fake.Clientset) { + _, err := fakeCli.CoreV1().PersistentVolumeClaims(namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) + c.Assert(err, IsNil) +} + +func getOriginalPVCManifest(pvcName, storageClassName string) *v1.PersistentVolumeClaim { + volumeMode := v1.PersistentVolumeFilesystem + return &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvcName, + }, + Spec: v1.PersistentVolumeClaimSpec{ + StorageClassName: &storageClassName, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + VolumeMode: &volumeMode, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + } +}