Skip to content

Commit

Permalink
Adding a Kanister function to create CSI VolumeSnapshot (#1163)
Browse files Browse the repository at this point in the history
* Added new function CreateCSISnapshot

* Fixed the instantiated values for kubeCli and dynCli

* Added waitForReady=true as the default

* Added apiGroup snapshot.storage.k8s.io in cluster-role for kanister-operator

* Added documentation for Create CSI VolumeSnapshot Func

* Changed namespace to a required arg

* Rasults of running  gofmt

* Reverting changes to return type of snapshotter.Create()

* Changed snapShotter -> snapshotter

* grouped imports and added the comment for 'waitForReady'

* Updates suggested in docs/functions.rst

* Changed snapshot name argument to optional argument and added a default value

* Changes suggested in reviewer comments

* gofmt update

* gofmt update

* Updated const comments and function description in documentation

* Declared and used const for outputartifact keys

* Minor changes in docs/functions.rst

* Updated the rules in clusterrole

* added asterisk again in helm/kanister-operator/templates/rbac.yaml

* Updated rules in clusterrole to check Travis CI failure

* Created a test suite for CreateCSISanpshot func

* Updated the testcase

* Resolved the error in testcase

* Updated the create snapshot testcase

* removed AddKnownTypeWithName

* Updated the CreateCSISnapshot testcase

* golang-ci: fixed whitespace

* Reviewer comments

* Updated the testcase for CreateCSiSnapshot function

* Fixed RBAC YAML

* Added default context timeout

* Fixed the time.Minute issue

* Updated the defer cancel()

* Added a custom error for create

* Returned the custom error

* Updated the error msg

* Removed the timeout code snippet

* Removed unnecessary const for default timeout and import for time package

* Added the DeadlineExceeded error check and corrected the imports

* Added the wrapped error message

* Updated the wrapped error msg

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
Shlok Chaudhari and mergify[bot] committed Jan 7, 2022
1 parent 2de6893 commit 575144e
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 0 deletions.
57 changes: 57 additions & 0 deletions docs/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,7 @@ Example:
args:
snapshotID: "{{ .ArtifactsIn.backupInfo.KeyValue.snapshotID }}"
KubeOps
-------

Expand Down Expand Up @@ -1271,6 +1272,62 @@ Example:
name: "{{ .Namespace.Name }}"
CreateCSISnapshot
-----------------

This function is used to create CSI VolumeSnapshot for a PersistentVolumeClaim.
By default, it waits for the VolumeSnapshot to be ``ReadyToUse``.

Arguments:

.. csv-table::
:header: "Argument", "Required", "Type", "Description"
:align: left
:widths: 5,5,5,15

`name`, No, `string`, name of the VolumeSnapshot. Default value is ``<pvc>-snapshot-<random-alphanumeric-suffix>``
`pvc`, Yes, `string`, name of the PersistentVolumeClaim to be captured
`namespace`, Yes, `string`, namespace of the PersistentVolumeClaim and resultant VolumeSnapshot
`snapshotClass`, Yes, `string`, name of the VolumeSnapshotClass
`labels`, No, `map[string]string`, labels for the VolumeSnapshot

Outputs:

.. csv-table::
:header: "Output", "Type", "Description"
:align: left
:widths: 5,5,15

`name`,`string`, name of the CSI VolumeSnapshot
`pvc`,`string`, name of the captured PVC
`namespace`, string, namespace of the captured PVC and VolumeSnapshot
`restoreSize`, string, required memory size to restore PVC
`snapshotContent`, string, name of the VolumeSnapshotContent

Example:

.. code-block:: yaml
:linenos:
actions:
backup:
outputArtifacts:
snapshotInfo:
keyValue:
name: "{{ .Phases.createCSISnapshot.Output.name }}"
pvc: "{{ .Phases.createCSISnapshot.Output.pvc }}"
namespace: "{{ .Phases.createCSISnapshot.Output.namespace }}"
restoreSize: "{{ .Phases.createCSISnapshot.Output.restoreSize }}"
snapshotContent: "{{ .Phases.createCSISnapshot.Output.snapshotContent }}"
phases:
- func: CreateCSISnapshot
name: createCSISnapshot
args:
pvc: "{{ .PVC.Name }}"
namespace: "{{ .PVC.Namespace }}"
snapshotClass: do-block-storage
Registering Functions
---------------------

Expand Down
9 changes: 9 additions & 0 deletions helm/kanister-operator/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ rules:
- customresourcedefinitions
verbs:
- "*"
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
- volumesnapshotcontents
verbs:
- get
- create
- delete
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
Expand Down
130 changes: 130 additions & 0 deletions pkg/function/create_csi_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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"
"fmt"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/util/rand"

kanister "github.com/kanisterio/kanister/pkg"
"github.com/kanisterio/kanister/pkg/kube"
"github.com/kanisterio/kanister/pkg/kube/snapshot"
"github.com/kanisterio/kanister/pkg/param"
)

func init() {
_ = kanister.Register(&createCSISnapshotFunc{})
}

var (
_ kanister.Func = (*createCSISnapshotFunc)(nil)
)

const (
// CreateCSIVolumeSnapshotFuncName gives the name of the function
CreateCSISnapshotFuncName = "CreateCSISnapshot"
// CreateCSISnapshotNameArg provides name of the new VolumeSnapshot
CreateCSISnapshotNameArg = "name"
// CreateCSISnapshotPVCNameArg gives the name of the captured PVC
CreateCSISnapshotPVCNameArg = "pvc"
// CreateCSISnapshotNamespaceArg mentions the namespace of the captured PVC
CreateCSISnapshotNamespaceArg = "namespace"
// CreateCSISnapshotSnapshotClassArg specifies the name of the VolumeSnapshotClass
CreateCSISnapshotSnapshotClassArg = "snapshotClass"
// CreateCSISnapshotLabelsArg has labels that are to be added to the new VolumeSnapshot
CreateCSISnapshotLabelsArg = "labels"
// CreateCSISnapshotRestoreSizeArg gives the storage size required for PV/PVC restoration
CreateCSISnapshotRestoreSizeArg = "restoreSize"
// CreateCSISnapshotSnapshotContentNameArg provides the name of dynamically provisioned VolumeSnapshotContent
CreateCSISnapshotSnapshotContentNameArg = "snapshotContent"
)

type createCSISnapshotFunc struct{}

func (*createCSISnapshotFunc) Name() string {
return CreateCSISnapshotFuncName
}

func (*createCSISnapshotFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) {
var snapshotClass string
var labels map[string]string
var name, pvc, namespace string
if err := Arg(args, CreateCSISnapshotPVCNameArg, &pvc); err != nil {
return nil, err
}
if err := Arg(args, CreateCSISnapshotNamespaceArg, &namespace); err != nil {
return nil, err
}
if err := Arg(args, CreateCSISnapshotSnapshotClassArg, &snapshotClass); err != nil {
return nil, err
}
if err := OptArg(args, CreateCSISnapshotNameArg, &name, defaultSnapshotName(pvc, 20)); err != nil {

This comment has been minimized.

Copy link
@pavannd1

pavannd1 Jan 11, 2022

Contributor

I think 5 characters should be good enough here. It uses signed int64 I think which works for millions of combinations!

return nil, err
}
if err := OptArg(args, CreateCSISnapshotLabelsArg, &labels, map[string]string{}); err != nil {
return nil, err
}

kubeCli, err := kube.NewClient()
if err != nil {
return nil, err
}
dynCli, err := kube.NewDynamicClient()
if err != nil {
return nil, err
}
snapshotter, err := snapshot.NewSnapshotter(kubeCli, dynCli)
if err != nil {
if errors.Is(context.DeadlineExceeded, err) {
timeoutMsg := "SnapshotContent not provisioned within given timeout. Please check if CSI driver is installed correctly and supports VolumeSnapshot feature"
return nil, errors.Wrap(err, timeoutMsg)
}
return nil, err
}
// 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)
if err != nil {
return nil, err
}

snapshotInfo := map[string]interface{}{
CreateCSISnapshotNameArg: name,
CreateCSISnapshotPVCNameArg: pvc,
CreateCSISnapshotNamespaceArg: namespace,
CreateCSISnapshotRestoreSizeArg: vs.Status.RestoreSize.String(),
CreateCSISnapshotSnapshotContentNameArg: vs.Status.BoundVolumeSnapshotContentName,
}
return snapshotInfo, nil
}

func (*createCSISnapshotFunc) RequiredArgs() []string {
return []string{
CreateCSISnapshotPVCNameArg,
CreateCSISnapshotNamespaceArg,
CreateCSISnapshotSnapshotClassArg,
}
}

// defaultSnapshotName generates snapshot name using pvcName-snapshot-randomValue
func defaultSnapshotName(pvcName string, len int) string {
return fmt.Sprintf("%s-snapshot-%s", pvcName, rand.String(len))
}
126 changes: 126 additions & 0 deletions pkg/function/create_csi_snapshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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"

. "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"

"github.com/kanisterio/kanister/pkg/kube/snapshot"
)

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"
)

type CreateCSISnapshotTestSuite struct {
snapName string
pvcName string
namespace string
volumeSnapshotClass string
storageClass string
}

var _ = Suite(&CreateCSISnapshotTestSuite{})

func (testSuite *CreateCSISnapshotTestSuite) SetUpSuite(c *C) {
testSuite.volumeSnapshotClass = CreateCSISnapshotSnapshotClass
testSuite.storageClass = CreateCSISnapshotStorageClass
testSuite.pvcName = CreateCSISnapshotPVCName
testSuite.snapName = CreateCSISnapshotSnapshotName
testSuite.namespace = CreateCSISnapshotTestNamespace
}

func (testSuite *CreateCSISnapshotTestSuite) TestCreateCSISnapshot(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",
},
} {
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{})
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{})
c.Assert(err, IsNil)

err = fakeSnapshotter.Create(context.Background(), 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{})
c.Assert(err, IsNil)
}
}

func getPVCManifest(pvcName, storageClassName string) *v1.PersistentVolumeClaim {
return &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
},
Spec: v1.PersistentVolumeClaimSpec{
StorageClassName: &storageClassName,
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}
}

0 comments on commit 575144e

Please sign in to comment.