Skip to content

Commit

Permalink
Snapshot helpers for Kanister (#5012)
Browse files Browse the repository at this point in the history
Adds helper function to create and manipulate VolumeSnapshots.

* Create takes snapshot of PVC by posting a VolumeSnapshot
* Clone copies a VolumeSnapshot to another namespace
* Delete deletes VolumeSnapshot
* CreatePVCFromSnapshot will create a PVC by using the content of VolumeSnapshot
  • Loading branch information
Hakan Memisoglu authored and Ilya Kislenko committed Mar 13, 2019
1 parent dfd6d4c commit d990456
Show file tree
Hide file tree
Showing 11 changed files with 609 additions and 18 deletions.
16 changes: 14 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import:
repo: https://github.com/kastenhq/ibmcloud-storage-volume-lib.git
- package: github.com/jpillora/backoff
version: 1.0.0
- package: github.com/kubernetes-csi/external-snapshotter
version: v1.0.1
- package: github.com/Masterminds/sprig
version: v2.15.0
- package: github.com/mitchellh/mapstructure
Expand Down
3 changes: 2 additions & 1 deletion pkg/blockstorage/zone/zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"k8s.io/client-go/kubernetes"

"github.com/kanisterio/kanister/pkg/kube"
kubevolume "github.com/kanisterio/kanister/pkg/kube/volume"
)

type (
Expand Down Expand Up @@ -124,7 +125,7 @@ func nodeZones(ctx context.Context) (map[string]struct{}, error) {
}
zoneSet := make(map[string]struct{}, len(ns.Items))
for _, n := range ns.Items {
if v, ok := n.Labels[kube.PVZoneLabelName]; ok {
if v, ok := n.Labels[kubevolume.PVZoneLabelName]; ok {
zoneSet[v] = struct{}{}
}
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/function/create_volume_from_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/kanisterio/kanister/pkg/blockstorage/awsebs"
"github.com/kanisterio/kanister/pkg/blockstorage/getter"
"github.com/kanisterio/kanister/pkg/kube"
kubevolume "github.com/kanisterio/kanister/pkg/kube/volume"
"github.com/kanisterio/kanister/pkg/param"
)

Expand Down Expand Up @@ -72,7 +73,7 @@ func createVolumeFromSnapshot(ctx context.Context, cli kubernetes.Interface, nam
}
_, err = cli.Core().PersistentVolumeClaims(namespace).Get(pvcName, metav1.GetOptions{})
if err == nil {
if err = kube.DeletePVC(cli, namespace, pvcName); err != nil {
if err = kubevolume.DeletePVC(cli, namespace, pvcName); err != nil {
return nil, err
}
}
Expand All @@ -93,11 +94,11 @@ func createVolumeFromSnapshot(ctx context.Context, cli kubernetes.Interface, nam
}

annotations := map[string]string{}
pvc, err := kube.CreatePVC(ctx, cli, namespace, pvcName, vol.Size, vol.ID, annotations)
pvc, err := kubevolume.CreatePVC(ctx, cli, namespace, pvcName, vol.Size, vol.ID, annotations)
if err != nil {
return nil, errors.Wrapf(err, "Unable to create PVC for volume %v", *vol)
}
pv, err := kube.CreatePV(ctx, cli, vol, vol.Type, annotations)
pv, err := kubevolume.CreatePV(ctx, cli, vol, vol.Type, annotations)
if err != nil {
return nil, errors.Wrapf(err, "Unable to create PV for volume %v", *vol)
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/function/create_volume_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/kanisterio/kanister/pkg/blockstorage/awsebs"
"github.com/kanisterio/kanister/pkg/blockstorage/getter"
"github.com/kanisterio/kanister/pkg/kube"
kubevolume "github.com/kanisterio/kanister/pkg/kube/volume"
"github.com/kanisterio/kanister/pkg/param"
)

Expand Down Expand Up @@ -189,15 +190,15 @@ func getPVCInfo(ctx context.Context, kubeCli kubernetes.Interface, namespace str
return nil, errors.Wrap(err, "Profile validation failed")
}
// Get Region from PV label or EC2 metadata
if pvRegion, ok := pvLabels[kube.PVRegionLabelName]; ok {
if pvRegion, ok := pvLabels[kubevolume.PVRegionLabelName]; ok {
region = pvRegion
} else {
region, err = awsebs.GetRegionFromEC2Metadata()
if err != nil {
return nil, err
}
}
if pvZone, ok := pvLabels[kube.PVZoneLabelName]; ok {
if pvZone, ok := pvLabels[kubevolume.PVZoneLabelName]; ok {
config[awsebs.ConfigRegion] = region
config[awsebs.AccessKeyID] = tp.Profile.Credential.KeyPair.ID
config[awsebs.SecretAccessKey] = tp.Profile.Credential.KeyPair.Secret
Expand All @@ -215,7 +216,7 @@ func getPVCInfo(ctx context.Context, kubeCli kubernetes.Interface, namespace str
if err = ValidateProfile(tp.Profile, blockstorage.TypeGPD); err != nil {
return nil, errors.Wrap(err, "Profile validation failed")
}
if pvZone, ok := pvLabels[kube.PVZoneLabelName]; ok {
if pvZone, ok := pvLabels[kubevolume.PVZoneLabelName]; ok {
config[blockstorage.GoogleProjectID] = tp.Profile.Credential.KeyPair.ID
config[blockstorage.GoogleServiceKey] = tp.Profile.Credential.KeyPair.Secret
provider, err = getter.Get(blockstorage.TypeGPD, config)
Expand Down
8 changes: 4 additions & 4 deletions pkg/function/create_volume_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1"
"github.com/kanisterio/kanister/pkg/blockstorage"
"github.com/kanisterio/kanister/pkg/kube"
kubevolume "github.com/kanisterio/kanister/pkg/kube/volume"
"github.com/kanisterio/kanister/pkg/param"
"github.com/kanisterio/kanister/pkg/testutil/mockblockstorage"
)
Expand Down Expand Up @@ -53,8 +53,8 @@ func (s *CreateVolumeSnapshotTestSuite) TestGetPVCInfo(c *C) {
ObjectMeta: metav1.ObjectMeta{
Name: "pv-test-1",
Labels: map[string]string{
kube.PVZoneLabelName: "us-west-2a",
kube.PVRegionLabelName: "us-west-2",
kubevolume.PVZoneLabelName: "us-west-2a",
kubevolume.PVRegionLabelName: "us-west-2",
},
},
Spec: v1.PersistentVolumeSpec{
Expand Down Expand Up @@ -105,7 +105,7 @@ func (s *CreateVolumeSnapshotTestSuite) TestGetPVCInfo(c *C) {
ObjectMeta: metav1.ObjectMeta{
Name: "pv-test-3",
Labels: map[string]string{
kube.PVZoneLabelName: "us-west-2a",
kubevolume.PVZoneLabelName: "us-west-2a",
},
},
Spec: v1.PersistentVolumeSpec{
Expand Down
19 changes: 17 additions & 2 deletions pkg/kube/client.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package kube

import (
snapshot "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned"
"github.com/pkg/errors"
"k8s.io/client-go/kubernetes"
// Load the GCP plugin - required to authenticate against
"k8s.io/client-go/kubernetes" // Load the GCP plugin - required to authenticate against
// GKE clusters
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
Expand Down Expand Up @@ -50,3 +50,18 @@ func NewClient() (kubernetes.Interface, error) {
}
return clientset, nil
}

// NewClientSnapshot returns a VolumeSnapshot client configured by the Kanister environment.
func NewSnapshotClient() (snapshot.Interface, error) {
config, err := LoadConfig()
if err != nil {
return nil, err
}

// creates the clientset
clientset, err := snapshot.NewForConfig(config)
if err != nil {
return nil, err
}
return clientset, nil
}
199 changes: 199 additions & 0 deletions pkg/kube/snapshot/snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package snapshot

import (
"context"

snapshot "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1"
snapshotclient "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
k8errors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/client-go/kubernetes"

"github.com/kanisterio/kanister/pkg/poll"
)

const (
snapshotKind = "VolumeSnapshot"
pvcKind = "PersistentVolumeClaim"
)

var snapshotAPIGroup = "snapshot.storage.k8s.io"

// Create creates a VolumeSnapshot and returns it or any error happened meanwhile.
//
// 'name' is the name of the VolumeSnapshot.
// 'namespace' is namespace of the PVC. VolumeSnapshot will be crated in the same namespace.
// 'volumeName' is the name of the PVC of which we will take snapshot. It must be in the same namespace 'ns'.
// 'waitForReady' will block the caller until the snapshot status is 'ReadyToUse'.
// or 'ctx.Done()' is signalled. Otherwise it will return immediately after the snapshot is cut.
func Create(ctx context.Context, kubeCli kubernetes.Interface, snapCli snapshotclient.Interface, name, namespace, volumeName string, snapshotClass *string, waitForReady bool) error {
if _, err := kubeCli.CoreV1().PersistentVolumeClaims(namespace).Get(volumeName, metav1.GetOptions{}); err != nil {
if k8errors.IsNotFound(err) {
return errors.Errorf("Failed to find PVC %s, Namespace %s", volumeName, namespace)
}
return errors.Errorf("Failed to query PVC %s, Namespace %s: %v", volumeName, namespace, err)
}

snap := &snapshot.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: snapshot.VolumeSnapshotSpec{
Source: &corev1.TypedLocalObjectReference{
Kind: pvcKind,
Name: volumeName,
},
VolumeSnapshotClassName: snapshotClass,
},
}

snap, err := snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Create(snap)
if err != nil {
return err
}

if !waitForReady {
return nil
}

err = WaitOnReadyToUse(ctx, snapCli, name, namespace)
if err != nil {
return err
}

_, err = snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Get(name, metav1.GetOptions{})
return err
}

// Get will return the VolumeSnapshot in the namespace 'namespace' with given 'name'.
//
// 'name' is the name of the VolumeSnapshot that will be returned.
// 'namespace' is the namespace of the VolumeSnapshot that will be returned.
func Get(ctx context.Context, snapCli snapshotclient.Interface, name, namespace string) (*snapshot.VolumeSnapshot, error) {
return snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Get(name, metav1.GetOptions{})
}

// Delete will delete the VolumeSnapshot and returns any error as a result.
//
// 'name' is the name of the VolumeSnapshot that will be deleted.
// 'namespace' is the namespace of the VolumeSnapshot that will be deleted.
func Delete(ctx context.Context, snapCli snapshotclient.Interface, name, namespace string) error {
return snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Delete(name, &metav1.DeleteOptions{})
}

// Clone will clone the VolumeSnapshot to namespace 'cloneNamespace'.
// Underlying VolumeSnapshotContent will be cloned with a different name.
//
// 'name' is the name of the VolumeSnapshot that will be cloned.
// 'namespace' is the namespace of the VolumeSnapshot that will be cloned.
// 'cloneName' is name of the clone.
// 'cloneNamespace' is the namespace where the clone will be created.
// 'waitForReady' will make the function blocks until the clone's status is ready to use.
func Clone(ctx context.Context, snapCli snapshotclient.Interface, name, namespace, cloneName, cloneNamespace string, waitForReady bool) error {
snap, err := snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return err
}
if !snap.Status.ReadyToUse {
return errors.Errorf("Original snapshot is not ready, VolumeSnapshot: %s, Namespace: %s", cloneName, cloneNamespace)
}
if snap.Spec.SnapshotContentName == "" {
return errors.Errorf("Original snapshot does not have content, VolumeSnapshot: %s, Namespace: %s", cloneName, cloneNamespace)
}

_, err = snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(cloneNamespace).Get(cloneName, metav1.GetOptions{})
if err == nil {
return errors.Errorf("Target snapshot already exists in target namespace, Volumesnapshot: %s, Namespace: %s", cloneName, cloneNamespace)
}
if !k8errors.IsNotFound(err) {
return errors.Errorf("Failed to query target Volumesnapshot: %s, Namespace: %s: %v", cloneName, cloneNamespace, err)
}

content, err := snapCli.VolumesnapshotV1alpha1().VolumeSnapshotContents().Get(snap.Spec.SnapshotContentName, metav1.GetOptions{})
if err != nil {
return errors.Errorf("Failed to get original snapshot content, VolumesnapshotContent: %s: %v", snap.Spec.SnapshotContentName, err)
}

// Create a 'VolumeSnapshotContent' referenced by a 'VolumeSnapshot' 'cloneName' in namespace 'cloneNamespace'.
clonedSnap, clonedContent := cloneSnapshotAndContent(content, cloneName, cloneNamespace)

// Create cloned VolumeSnapshotContent first, then cloned VolumeSnapshot.
// Snapshotter will check the binding and set the status of VolumeSnapshot.
_, err = snapCli.VolumesnapshotV1alpha1().VolumeSnapshotContents().Create(clonedContent)
if err != nil {
return errors.Errorf("Failed to create target snapshot content, VolumeSnapshotContent: %s: %v", clonedContent.Name, err)
}

clonedSnap, err = snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(cloneNamespace).Create(clonedSnap)
if err != nil {
return errors.Errorf("Failed to create target snapshot, VolumeSnapshot: %s: %v", clonedSnap.Name, err)
}

if !waitForReady {
return nil
}

err = WaitOnReadyToUse(ctx, snapCli, clonedSnap.Name, clonedSnap.Namespace)
if err != nil {
return err
}

return nil
}

// WaitOnReadyToUse will block until the Volumesnapshot in namespace 'namespace' with name 'snapshotName'
// has status 'ReadyToUse' or 'ctx.Done()' is signalled.
func WaitOnReadyToUse(ctx context.Context, snapCli snapshotclient.Interface, snapshotName, namespace string) error {
return poll.Wait(ctx, func(context.Context) (bool, error) {
snap, err := snapCli.VolumesnapshotV1alpha1().VolumeSnapshots(namespace).Get(snapshotName, metav1.GetOptions{})
if err != nil {
return false, err
}
return snap.Status.ReadyToUse, nil
})
}

// clonedSnapshotAndContent will return a 'VolumeSnapshot' and 'VolumeSnapshotContent' for a snapshot clone.
// Cloned 'VolumeSnapshotContent' will copy the value for 'Source.CSI.{Driver,SnapshotHandle}'
// since these are the minimum info for snapshotter to get information from the source.
// It needs to prepopulate 'VolumeSnapshotRef' field with the values from the 'VolumeSnapshot'
// that will also be returned.
func cloneSnapshotAndContent(content *snapshot.VolumeSnapshotContent, clonedName, clonedNamespace string) (*snapshot.VolumeSnapshot, *snapshot.VolumeSnapshotContent) {
clonedContent := &snapshot.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: clonedContentName(content.Name),
},
Spec: snapshot.VolumeSnapshotContentSpec{
VolumeSnapshotSource: snapshot.VolumeSnapshotSource{
CSI: &snapshot.CSIVolumeSnapshotSource{
Driver: content.Spec.VolumeSnapshotSource.CSI.Driver,
SnapshotHandle: content.Spec.VolumeSnapshotSource.CSI.SnapshotHandle,
},
},
VolumeSnapshotRef: &corev1.ObjectReference{
Kind: snapshotKind,
Namespace: clonedNamespace,
Name: clonedName,
},
VolumeSnapshotClassName: content.Spec.VolumeSnapshotClassName,
},
}
clonedSnap := &snapshot.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: clonedName,
},
Spec: snapshot.VolumeSnapshotSpec{
SnapshotContentName: clonedContent.Name,
VolumeSnapshotClassName: content.Spec.VolumeSnapshotClassName,
},
}
return clonedSnap, clonedContent
}

func clonedContentName(contentName string) string {
return contentName + "-k10clone-" + string(uuid.NewUUID())
}
Loading

0 comments on commit d990456

Please sign in to comment.