Support custom volume snapshots & restores

The main Ark code was hard-coding specific support for AWS, GCE, and
Azure volume snapshots and restores, and anything else was considered
unsupported.

Add GetVolumeID and SetVolumeID to the BlockStore interface, to allow
block store plugins to handle volume snapshots and restores.

Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
pull/215/head
Andy Goldstein 2017-11-29 12:23:21 -05:00
parent 526b604237
commit c700455272
20 changed files with 805 additions and 409 deletions

View File

@ -36,7 +36,6 @@ import (
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/discovery"
"github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
)
@ -289,12 +288,10 @@ func (ib *defaultItemBackupper) takePVSnapshot(pv runtime.Unstructured, backup *
log.Infof("label %q is not present on PersistentVolume", zoneLabel)
}
volumeID, err := kubeutil.GetVolumeID(pv.UnstructuredContent())
// non-nil error means it's a supported PV source but volume ID can't be found
volumeID, err := ib.snapshotService.GetVolumeID(pv)
if err != nil {
return errors.Wrapf(err, "error getting volume ID for PersistentVolume")
}
// no volumeID / nil error means unsupported PV source
if volumeID == "" {
log.Info("PersistentVolume is not a supported volume type for snapshots, skipping.")
return nil

View File

@ -324,7 +324,10 @@ func TestBackupItemNoSkips(t *testing.T) {
var snapshotService *arktest.FakeSnapshotService
if test.snapshottableVolumes != nil {
snapshotService = &arktest.FakeSnapshotService{SnapshottableVolumes: test.snapshottableVolumes}
snapshotService = &arktest.FakeSnapshotService{
SnapshottableVolumes: test.snapshottableVolumes,
VolumeID: "vol-abc123",
}
b.snapshotService = snapshotService
}
@ -356,7 +359,7 @@ func TestBackupItemNoSkips(t *testing.T) {
err = b.backupItem(arktest.NewLogger(), obj, groupResource)
gotError := err != nil
if e, a := test.expectError, gotError; e != a {
t.Fatalf("error: expected %t, got %t", e, a)
t.Fatalf("error: expected %t, got %t: %v", e, a, err)
}
if test.expectError {
return
@ -445,12 +448,6 @@ func TestTakePVSnapshot(t *testing.T) {
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
snapshotEnabled: false,
},
{
name: "can't find volume id - missing spec",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
expectError: true,
},
{
name: "unsupported PV source type",
snapshotEnabled: true,
@ -458,19 +455,7 @@ func TestTakePVSnapshot(t *testing.T) {
expectError: false,
},
{
name: "can't find volume id - aws but no volume id",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"awsElasticBlockStore": {}}}`,
expectError: true,
},
{
name: "can't find volume id - gce but no volume id",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {}}}`,
expectError: true,
},
{
name: "aws - simple volume id",
name: "without iops",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
expectError: false,
@ -482,7 +467,7 @@ func TestTakePVSnapshot(t *testing.T) {
},
},
{
name: "aws - simple volume id with provisioned IOPS",
name: "with iops",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
expectError: false,
@ -493,42 +478,6 @@ func TestTakePVSnapshot(t *testing.T) {
"vol-abc123": {Type: "io1", Iops: &iops, SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
},
},
{
name: "aws - dynamically provisioned volume id",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-west-2a"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-west-2a/vol-abc123"}}}`,
expectError: false,
expectedSnapshotsTaken: 1,
expectedVolumeID: "vol-abc123",
ttl: 5 * time.Minute,
volumeInfo: map[string]v1.VolumeBackupInfo{
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-west-2a"},
},
},
{
name: "gce",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "gcp-zone2"}}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
expectError: false,
expectedSnapshotsTaken: 1,
expectedVolumeID: "pd-abc123",
ttl: 5 * time.Minute,
volumeInfo: map[string]v1.VolumeBackupInfo{
"pd-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "gcp-zone2"},
},
},
{
name: "azure",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"azureDisk": {"diskName": "foo-disk"}}}`,
expectError: false,
expectedSnapshotsTaken: 1,
expectedVolumeID: "foo-disk",
ttl: 5 * time.Minute,
volumeInfo: map[string]v1.VolumeBackupInfo{
"foo-disk": {Type: "gp", SnapshotID: "snap-1"},
},
},
{
name: "preexisting volume backup info in backup status",
snapshotEnabled: true,
@ -548,6 +497,7 @@ func TestTakePVSnapshot(t *testing.T) {
name: "create snapshot error",
snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
expectedVolumeID: "pd-abc123",
expectError: true,
},
{
@ -580,7 +530,10 @@ func TestTakePVSnapshot(t *testing.T) {
},
}
snapshotService := &arktest.FakeSnapshotService{SnapshottableVolumes: test.volumeInfo}
snapshotService := &arktest.FakeSnapshotService{
SnapshottableVolumes: test.volumeInfo,
VolumeID: test.expectedVolumeID,
}
ib := &defaultItemBackupper{snapshotService: snapshotService}

View File

@ -17,14 +17,18 @@ limitations under the License.
package aws
import (
"regexp"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
)
const regionKey = "region"
@ -205,3 +209,29 @@ func (b *blockStore) DeleteSnapshot(snapshotID string) error {
return errors.WithStack(err)
}
var ebsVolumeIDRegex = regexp.MustCompile("vol-.*")
func (b *blockStore) GetVolumeID(pv runtime.Unstructured) (string, error) {
if !collections.Exists(pv.UnstructuredContent(), "spec.awsElasticBlockStore") {
return "", nil
}
volumeID, err := collections.GetString(pv.UnstructuredContent(), "spec.awsElasticBlockStore.volumeID")
if err != nil {
return "", err
}
return ebsVolumeIDRegex.FindString(volumeID), nil
}
func (b *blockStore) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
aws, err := collections.GetMap(pv.UnstructuredContent(), "spec.awsElasticBlockStore")
if err != nil {
return nil, err
}
aws["volumeID"] = volumeID
return pv, nil
}

View File

@ -0,0 +1,86 @@
/*
Copyright 2017 the Heptio Ark contributors.
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 aws
import (
"testing"
"github.com/heptio/ark/pkg/util/collections"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestGetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.awsElasticBlockStore -> no error
volumeID, err := b.GetVolumeID(pv)
require.NoError(t, err)
assert.Equal(t, "", volumeID)
// missing spec.awsElasticBlockStore.volumeID -> error
aws := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"awsElasticBlockStore": aws,
}
volumeID, err = b.GetVolumeID(pv)
assert.Error(t, err)
assert.Equal(t, "", volumeID)
// regex miss
aws["volumeID"] = "foo"
volumeID, err = b.GetVolumeID(pv)
assert.NoError(t, err)
assert.Equal(t, "", volumeID)
// regex match 1
aws["volumeID"] = "aws://us-east-1c/vol-abc123"
volumeID, err = b.GetVolumeID(pv)
assert.NoError(t, err)
assert.Equal(t, "vol-abc123", volumeID)
// regex match 2
aws["volumeID"] = "vol-abc123"
volumeID, err = b.GetVolumeID(pv)
assert.NoError(t, err)
assert.Equal(t, "vol-abc123", volumeID)
}
func TestSetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.awsElasticBlockStore -> error
updatedPV, err := b.SetVolumeID(pv, "vol-updated")
require.Error(t, err)
// happy path
aws := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"awsElasticBlockStore": aws,
}
updatedPV, err = b.SetVolumeID(pv, "vol-updated")
require.NoError(t, err)
actual, err := collections.GetString(updatedPV.UnstructuredContent(), "spec.awsElasticBlockStore.volumeID")
require.NoError(t, err)
assert.Equal(t, "vol-updated", actual)
}

View File

@ -20,6 +20,7 @@ import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/arm/disk"
@ -29,8 +30,10 @@ import (
"github.com/Azure/go-autorest/autorest/azure"
"github.com/pkg/errors"
"github.com/satori/uuid"
"k8s.io/apimachinery/pkg/runtime"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
)
const (
@ -292,3 +295,36 @@ func getFullDiskName(subscription string, resourceGroup string, diskName string)
func getFullSnapshotName(subscription string, resourceGroup string, snapshotName string) string {
return fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/Microsoft.Compute/snapshots/%v", subscription, resourceGroup, snapshotName)
}
func (b *blockStore) GetVolumeID(pv runtime.Unstructured) (string, error) {
if !collections.Exists(pv.UnstructuredContent(), "spec.azureDisk") {
return "", nil
}
volumeID, err := collections.GetString(pv.UnstructuredContent(), "spec.azureDisk.diskName")
if err != nil {
return "", err
}
return volumeID, nil
}
func (b *blockStore) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
azure, err := collections.GetMap(pv.UnstructuredContent(), "spec.azureDisk")
if err != nil {
return nil, err
}
if uri, err := collections.GetString(azure, "diskURI"); err == nil {
previousVolumeID, err := collections.GetString(azure, "diskName")
if err != nil {
return nil, err
}
azure["diskURI"] = strings.Replace(uri, previousVolumeID, volumeID, -1)
}
azure["diskName"] = volumeID
return pv, nil
}

View File

@ -0,0 +1,86 @@
/*
Copyright 2017 the Heptio Ark contributors.
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 azure
import (
"testing"
"github.com/heptio/ark/pkg/util/collections"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestGetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.azureDisk -> no error
volumeID, err := b.GetVolumeID(pv)
require.NoError(t, err)
assert.Equal(t, "", volumeID)
// missing spec.azureDisk.diskName -> error
azure := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"azureDisk": azure,
}
volumeID, err = b.GetVolumeID(pv)
assert.Error(t, err)
assert.Equal(t, "", volumeID)
// valid
azure["diskName"] = "foo"
volumeID, err = b.GetVolumeID(pv)
assert.NoError(t, err)
assert.Equal(t, "foo", volumeID)
}
func TestSetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.azureDisk -> error
updatedPV, err := b.SetVolumeID(pv, "updated")
require.Error(t, err)
// happy path, no diskURI
azure := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"azureDisk": azure,
}
updatedPV, err = b.SetVolumeID(pv, "updated")
require.NoError(t, err)
actual, err := collections.GetString(updatedPV.UnstructuredContent(), "spec.azureDisk.diskName")
require.NoError(t, err)
assert.Equal(t, "updated", actual)
assert.NotContains(t, azure, "diskURI")
// with diskURI
azure["diskURI"] = "/foo/bar/updated/blarg"
updatedPV, err = b.SetVolumeID(pv, "revised")
require.NoError(t, err)
actual, err = collections.GetString(updatedPV.UnstructuredContent(), "spec.azureDisk.diskName")
require.NoError(t, err)
assert.Equal(t, "revised", actual)
actual, err = collections.GetString(updatedPV.UnstructuredContent(), "spec.azureDisk.diskURI")
require.NoError(t, err)
assert.Equal(t, "/foo/bar/revised/blarg", actual)
}

View File

@ -26,9 +26,11 @@ import (
"golang.org/x/oauth2/google"
"google.golang.org/api/compute/v0.beta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
)
const projectKey = "project"
@ -191,3 +193,27 @@ func (b *blockStore) DeleteSnapshot(snapshotID string) error {
return errors.WithStack(err)
}
func (b *blockStore) GetVolumeID(pv runtime.Unstructured) (string, error) {
if !collections.Exists(pv.UnstructuredContent(), "spec.gcePersistentDisk") {
return "", nil
}
volumeID, err := collections.GetString(pv.UnstructuredContent(), "spec.gcePersistentDisk.pdName")
if err != nil {
return "", err
}
return volumeID, nil
}
func (b *blockStore) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
gce, err := collections.GetMap(pv.UnstructuredContent(), "spec.gcePersistentDisk")
if err != nil {
return nil, err
}
gce["pdName"] = volumeID
return pv, nil
}

View File

@ -0,0 +1,74 @@
/*
Copyright 2017 the Heptio Ark contributors.
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 gcp
import (
"testing"
"github.com/heptio/ark/pkg/util/collections"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestGetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.gcePersistentDisk -> no error
volumeID, err := b.GetVolumeID(pv)
require.NoError(t, err)
assert.Equal(t, "", volumeID)
// missing spec.gcePersistentDisk.pdName -> error
gce := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"gcePersistentDisk": gce,
}
volumeID, err = b.GetVolumeID(pv)
assert.Error(t, err)
assert.Equal(t, "", volumeID)
// valid
gce["pdName"] = "abc123"
volumeID, err = b.GetVolumeID(pv)
assert.NoError(t, err)
assert.Equal(t, "abc123", volumeID)
}
func TestSetVolumeID(t *testing.T) {
b := &blockStore{}
pv := &unstructured.Unstructured{}
// missing spec.gcePersistentDisk -> error
updatedPV, err := b.SetVolumeID(pv, "abc123")
require.Error(t, err)
// happy path
gce := map[string]interface{}{}
pv.Object["spec"] = map[string]interface{}{
"gcePersistentDisk": gce,
}
updatedPV, err = b.SetVolumeID(pv, "123abc")
require.NoError(t, err)
actual, err := collections.GetString(updatedPV.UnstructuredContent(), "spec.gcePersistentDisk.pdName")
require.NoError(t, err)
assert.Equal(t, "123abc", actual)
}

View File

@ -20,6 +20,7 @@ import (
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
// SnapshotService exposes Ark-specific operations for snapshotting and restoring block
@ -46,6 +47,12 @@ type SnapshotService interface {
// GetVolumeInfo gets the type and IOPS (if applicable) from the cloud API.
GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error)
// GetVolumeID returns the cloud provider specific identifier for the PersistentVolume.
GetVolumeID(pv runtime.Unstructured) (string, error)
// SetVolumeID sets the cloud provider specific identifier for the PersistentVolume.
SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error)
}
const (
@ -120,3 +127,11 @@ func (sr *snapshotService) DeleteSnapshot(snapshotID string) error {
func (sr *snapshotService) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) {
return sr.blockStore.GetVolumeInfo(volumeID, volumeAZ)
}
func (sr *snapshotService) GetVolumeID(pv runtime.Unstructured) (string, error) {
return sr.blockStore.GetVolumeID(pv)
}
func (sr *snapshotService) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
return sr.blockStore.SetVolumeID(pv, volumeID)
}

View File

@ -19,6 +19,8 @@ package cloudprovider
import (
"io"
"time"
"k8s.io/apimachinery/pkg/runtime"
)
// ObjectStore exposes basic object-storage operations required
@ -65,6 +67,12 @@ type BlockStore interface {
// and with the specified type and IOPS (if using provisioned IOPS).
CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (volumeID string, err error)
// GetVolumeID returns the cloud provider specific identifier for the PersistentVolume.
GetVolumeID(pv runtime.Unstructured) (string, error)
// SetVolumeID sets the cloud provider specific identifier for the PersistentVolume.
SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error)
// GetVolumeInfo returns the type and IOPS (if using provisioned IOPS) for a specified block
// volume.
GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error)

View File

@ -17,9 +17,13 @@ limitations under the License.
package plugin
import (
"encoding/json"
"github.com/hashicorp/go-plugin"
"golang.org/x/net/context"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/heptio/ark/pkg/cloudprovider"
proto "github.com/heptio/ark/pkg/plugin/generated"
@ -150,6 +154,49 @@ func (c *BlockStoreGRPCClient) DeleteSnapshot(snapshotID string) error {
return err
}
func (c *BlockStoreGRPCClient) GetVolumeID(pv runtime.Unstructured) (string, error) {
encodedPV, err := json.Marshal(pv.UnstructuredContent())
if err != nil {
return "", err
}
req := &proto.GetVolumeIDRequest{
PersistentVolume: encodedPV,
}
resp, err := c.grpcClient.GetVolumeID(context.Background(), req)
if err != nil {
return "", err
}
return resp.VolumeID, nil
}
func (c *BlockStoreGRPCClient) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
encodedPV, err := json.Marshal(pv.UnstructuredContent())
if err != nil {
return nil, err
}
req := &proto.SetVolumeIDRequest{
PersistentVolume: encodedPV,
VolumeID: volumeID,
}
resp, err := c.grpcClient.SetVolumeID(context.Background(), req)
if err != nil {
return nil, err
}
var updatedPV unstructured.Unstructured
if err := json.Unmarshal(resp.PersistentVolume, &updatedPV); err != nil {
return nil, err
}
return &updatedPV, nil
}
// BlockStoreGRPCServer implements the proto-generated BlockStoreServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type BlockStoreGRPCServer struct {
@ -245,3 +292,38 @@ func (s *BlockStoreGRPCServer) DeleteSnapshot(ctx context.Context, req *proto.De
return &proto.Empty{}, nil
}
func (s *BlockStoreGRPCServer) GetVolumeID(ctx context.Context, req *proto.GetVolumeIDRequest) (*proto.GetVolumeIDResponse, error) {
var pv unstructured.Unstructured
if err := json.Unmarshal(req.PersistentVolume, &pv); err != nil {
return nil, err
}
volumeID, err := s.impl.GetVolumeID(&pv)
if err != nil {
return nil, err
}
return &proto.GetVolumeIDResponse{VolumeID: volumeID}, nil
}
func (s *BlockStoreGRPCServer) SetVolumeID(ctx context.Context, req *proto.SetVolumeIDRequest) (*proto.SetVolumeIDResponse, error) {
var pv unstructured.Unstructured
if err := json.Unmarshal(req.PersistentVolume, &pv); err != nil {
return nil, err
}
updatedPV, err := s.impl.SetVolumeID(&pv, req.VolumeID)
if err != nil {
return nil, err
}
updatedPVBytes, err := json.Marshal(updatedPV.UnstructuredContent())
if err != nil {
return nil, err
}
return &proto.SetVolumeIDResponse{PersistentVolume: updatedPVBytes}, nil
}

View File

@ -26,6 +26,10 @@ It has these top-level messages:
CreateSnapshotRequest
CreateSnapshotResponse
DeleteSnapshotRequest
GetVolumeIDRequest
GetVolumeIDResponse
SetVolumeIDRequest
SetVolumeIDResponse
PutObjectRequest
GetObjectRequest
Bytes

View File

@ -257,6 +257,78 @@ func (m *DeleteSnapshotRequest) GetSnapshotID() string {
return ""
}
type GetVolumeIDRequest struct {
PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
}
func (m *GetVolumeIDRequest) Reset() { *m = GetVolumeIDRequest{} }
func (m *GetVolumeIDRequest) String() string { return proto.CompactTextString(m) }
func (*GetVolumeIDRequest) ProtoMessage() {}
func (*GetVolumeIDRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{11} }
func (m *GetVolumeIDRequest) GetPersistentVolume() []byte {
if m != nil {
return m.PersistentVolume
}
return nil
}
type GetVolumeIDResponse struct {
VolumeID string `protobuf:"bytes,1,opt,name=volumeID" json:"volumeID,omitempty"`
}
func (m *GetVolumeIDResponse) Reset() { *m = GetVolumeIDResponse{} }
func (m *GetVolumeIDResponse) String() string { return proto.CompactTextString(m) }
func (*GetVolumeIDResponse) ProtoMessage() {}
func (*GetVolumeIDResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{12} }
func (m *GetVolumeIDResponse) GetVolumeID() string {
if m != nil {
return m.VolumeID
}
return ""
}
type SetVolumeIDRequest struct {
PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
VolumeID string `protobuf:"bytes,2,opt,name=volumeID" json:"volumeID,omitempty"`
}
func (m *SetVolumeIDRequest) Reset() { *m = SetVolumeIDRequest{} }
func (m *SetVolumeIDRequest) String() string { return proto.CompactTextString(m) }
func (*SetVolumeIDRequest) ProtoMessage() {}
func (*SetVolumeIDRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{13} }
func (m *SetVolumeIDRequest) GetPersistentVolume() []byte {
if m != nil {
return m.PersistentVolume
}
return nil
}
func (m *SetVolumeIDRequest) GetVolumeID() string {
if m != nil {
return m.VolumeID
}
return ""
}
type SetVolumeIDResponse struct {
PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
}
func (m *SetVolumeIDResponse) Reset() { *m = SetVolumeIDResponse{} }
func (m *SetVolumeIDResponse) String() string { return proto.CompactTextString(m) }
func (*SetVolumeIDResponse) ProtoMessage() {}
func (*SetVolumeIDResponse) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{14} }
func (m *SetVolumeIDResponse) GetPersistentVolume() []byte {
if m != nil {
return m.PersistentVolume
}
return nil
}
func init() {
proto.RegisterType((*CreateVolumeRequest)(nil), "generated.CreateVolumeRequest")
proto.RegisterType((*CreateVolumeResponse)(nil), "generated.CreateVolumeResponse")
@ -269,6 +341,10 @@ func init() {
proto.RegisterType((*CreateSnapshotRequest)(nil), "generated.CreateSnapshotRequest")
proto.RegisterType((*CreateSnapshotResponse)(nil), "generated.CreateSnapshotResponse")
proto.RegisterType((*DeleteSnapshotRequest)(nil), "generated.DeleteSnapshotRequest")
proto.RegisterType((*GetVolumeIDRequest)(nil), "generated.GetVolumeIDRequest")
proto.RegisterType((*GetVolumeIDResponse)(nil), "generated.GetVolumeIDResponse")
proto.RegisterType((*SetVolumeIDRequest)(nil), "generated.SetVolumeIDRequest")
proto.RegisterType((*SetVolumeIDResponse)(nil), "generated.SetVolumeIDResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
@ -289,6 +365,8 @@ type BlockStoreClient interface {
ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ListSnapshotsResponse, error)
CreateSnapshot(ctx context.Context, in *CreateSnapshotRequest, opts ...grpc.CallOption) (*CreateSnapshotResponse, error)
DeleteSnapshot(ctx context.Context, in *DeleteSnapshotRequest, opts ...grpc.CallOption) (*Empty, error)
GetVolumeID(ctx context.Context, in *GetVolumeIDRequest, opts ...grpc.CallOption) (*GetVolumeIDResponse, error)
SetVolumeID(ctx context.Context, in *SetVolumeIDRequest, opts ...grpc.CallOption) (*SetVolumeIDResponse, error)
}
type blockStoreClient struct {
@ -362,6 +440,24 @@ func (c *blockStoreClient) DeleteSnapshot(ctx context.Context, in *DeleteSnapsho
return out, nil
}
func (c *blockStoreClient) GetVolumeID(ctx context.Context, in *GetVolumeIDRequest, opts ...grpc.CallOption) (*GetVolumeIDResponse, error) {
out := new(GetVolumeIDResponse)
err := grpc.Invoke(ctx, "/generated.BlockStore/GetVolumeID", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *blockStoreClient) SetVolumeID(ctx context.Context, in *SetVolumeIDRequest, opts ...grpc.CallOption) (*SetVolumeIDResponse, error) {
out := new(SetVolumeIDResponse)
err := grpc.Invoke(ctx, "/generated.BlockStore/SetVolumeID", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for BlockStore service
type BlockStoreServer interface {
@ -372,6 +468,8 @@ type BlockStoreServer interface {
ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)
CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error)
DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*Empty, error)
GetVolumeID(context.Context, *GetVolumeIDRequest) (*GetVolumeIDResponse, error)
SetVolumeID(context.Context, *SetVolumeIDRequest) (*SetVolumeIDResponse, error)
}
func RegisterBlockStoreServer(s *grpc.Server, srv BlockStoreServer) {
@ -504,6 +602,42 @@ func _BlockStore_DeleteSnapshot_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _BlockStore_GetVolumeID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetVolumeIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BlockStoreServer).GetVolumeID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/generated.BlockStore/GetVolumeID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BlockStoreServer).GetVolumeID(ctx, req.(*GetVolumeIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BlockStore_SetVolumeID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetVolumeIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BlockStoreServer).SetVolumeID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/generated.BlockStore/SetVolumeID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BlockStoreServer).SetVolumeID(ctx, req.(*SetVolumeIDRequest))
}
return interceptor(ctx, in, info, handler)
}
var _BlockStore_serviceDesc = grpc.ServiceDesc{
ServiceName: "generated.BlockStore",
HandlerType: (*BlockStoreServer)(nil),
@ -536,6 +670,14 @@ var _BlockStore_serviceDesc = grpc.ServiceDesc{
MethodName: "DeleteSnapshot",
Handler: _BlockStore_DeleteSnapshot_Handler,
},
{
MethodName: "GetVolumeID",
Handler: _BlockStore_GetVolumeID_Handler,
},
{
MethodName: "SetVolumeID",
Handler: _BlockStore_SetVolumeID_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "BlockStore.proto",
@ -544,39 +686,44 @@ var _BlockStore_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("BlockStore.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{
// 539 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xc1, 0x6e, 0xd3, 0x40,
0x10, 0xd5, 0xc6, 0x06, 0x35, 0x53, 0x5a, 0xa2, 0xc5, 0xae, 0x2c, 0x1f, 0x8a, 0xf1, 0x29, 0x42,
0x22, 0xa0, 0x70, 0x68, 0x41, 0x02, 0x09, 0x48, 0x8b, 0x22, 0x50, 0x91, 0x9c, 0xc2, 0x01, 0x4e,
0x86, 0x2c, 0x69, 0x54, 0xc7, 0x6b, 0x76, 0x37, 0x95, 0xfc, 0x01, 0xfc, 0x0a, 0x5f, 0xc0, 0x47,
0xf0, 0x59, 0xc8, 0xf6, 0xda, 0xde, 0xb5, 0xdd, 0x54, 0x55, 0x6e, 0x9e, 0x19, 0xcf, 0xdb, 0x37,
0xcf, 0x6f, 0xd6, 0x30, 0x78, 0x1b, 0xd1, 0x1f, 0x97, 0x33, 0x41, 0x19, 0x19, 0x25, 0x8c, 0x0a,
0x8a, 0xfb, 0x0b, 0x12, 0x13, 0x16, 0x0a, 0x32, 0x77, 0xef, 0xcd, 0x2e, 0x42, 0x46, 0xe6, 0x45,
0xc1, 0xff, 0x8d, 0xe0, 0xc1, 0x3b, 0x46, 0x42, 0x41, 0xbe, 0xd0, 0x68, 0xbd, 0x22, 0x01, 0xf9,
0xb5, 0x26, 0x5c, 0xe0, 0x43, 0x00, 0x1e, 0x87, 0x09, 0xbf, 0xa0, 0x62, 0x3a, 0x71, 0x90, 0x87,
0x86, 0xfd, 0x40, 0xc9, 0x64, 0xf5, 0xab, 0xbc, 0xe1, 0x3c, 0x4d, 0x88, 0xd3, 0x2b, 0xea, 0x75,
0x06, 0xbb, 0xb0, 0x53, 0x44, 0x6f, 0xbe, 0x3a, 0x46, 0x5e, 0xad, 0x62, 0x8c, 0xc1, 0x5c, 0xd2,
0x84, 0x3b, 0xa6, 0x87, 0x86, 0x46, 0x90, 0x3f, 0xfb, 0x63, 0xb0, 0x74, 0x1a, 0x3c, 0xa1, 0x31,
0x57, 0x70, 0x2a, 0x16, 0x55, 0xec, 0x9f, 0x81, 0xf5, 0x9e, 0x88, 0xa2, 0x61, 0x1a, 0xff, 0xa4,
0x25, 0xf7, 0x0d, 0x3d, 0x1a, 0xaf, 0x9e, 0xce, 0xcb, 0xff, 0x00, 0x76, 0x03, 0x4f, 0x92, 0xd0,
0x87, 0x45, 0xad, 0x61, 0xcb, 0x81, 0x7a, 0xca, 0x40, 0x67, 0x60, 0x4d, 0x79, 0x39, 0x4c, 0x38,
0x4f, 0xb7, 0x25, 0xf7, 0x04, 0xec, 0x06, 0x9e, 0x24, 0x67, 0xc1, 0x1d, 0x96, 0x25, 0x72, 0xb4,
0x9d, 0xa0, 0x08, 0xfc, 0x3f, 0x08, 0xac, 0x8f, 0x4b, 0x2e, 0x66, 0xf2, 0x93, 0xf1, 0xf2, 0xfc,
0x4f, 0x00, 0x22, 0x5c, 0x9c, 0x2e, 0x23, 0x41, 0x18, 0x77, 0x90, 0x67, 0x0c, 0x77, 0xc7, 0x4f,
0x47, 0x95, 0x3d, 0x46, 0x5d, 0x4d, 0xa3, 0xf3, 0xaa, 0xe3, 0x24, 0x16, 0x2c, 0x0d, 0x14, 0x08,
0xf7, 0x15, 0xdc, 0x6f, 0x94, 0xf1, 0x00, 0x8c, 0x4b, 0x92, 0xca, 0xf1, 0xb2, 0xc7, 0x8c, 0xe4,
0x55, 0x18, 0xad, 0x4b, 0xa7, 0x14, 0xc1, 0xcb, 0xde, 0x31, 0xf2, 0x5f, 0x80, 0xdd, 0x38, 0x52,
0xce, 0xe5, 0xc1, 0x6e, 0xed, 0xb7, 0x4c, 0x5b, 0x63, 0xd8, 0x0f, 0xd4, 0x94, 0xff, 0x0f, 0x81,
0x5d, 0x98, 0xa6, 0xec, 0xde, 0x52, 0x64, 0xfc, 0x1a, 0x4c, 0x11, 0x2e, 0xb8, 0x63, 0xe4, 0xb2,
0x3c, 0x56, 0x64, 0xe9, 0x3c, 0x27, 0xd3, 0x45, 0x2a, 0x92, 0xf7, 0xb9, 0x47, 0xd0, 0xaf, 0x52,
0xb7, 0x52, 0xe1, 0x18, 0x0e, 0x9a, 0x27, 0xd4, 0xde, 0xdb, 0xb4, 0x88, 0xfe, 0x11, 0xd8, 0x13,
0x12, 0x91, 0xb6, 0x06, 0x37, 0x34, 0x8e, 0xff, 0x9a, 0x00, 0xf5, 0x3d, 0x81, 0x9f, 0x81, 0x39,
0x8d, 0x97, 0x02, 0x1f, 0x28, 0x43, 0x67, 0x09, 0x09, 0xe7, 0x0e, 0x94, 0xfc, 0xc9, 0x2a, 0x11,
0x29, 0xfe, 0x06, 0x8e, 0xba, 0xb2, 0xa7, 0x8c, 0xae, 0x4a, 0x0e, 0xf8, 0xb0, 0x25, 0x9d, 0x76,
0xbd, 0xb8, 0x0f, 0xaf, 0xad, 0xcb, 0xb1, 0x03, 0xd8, 0xd3, 0x76, 0x11, 0xab, 0x1d, 0x5d, 0x5b,
0xef, 0x7a, 0xd7, 0xbf, 0x50, 0x63, 0x6a, 0x2b, 0xa4, 0x61, 0x76, 0x2d, 0xab, 0x86, 0xd9, 0xbd,
0x7d, 0x01, 0xec, 0x69, 0xf6, 0xd5, 0x30, 0xbb, 0x76, 0x49, 0xc3, 0xec, 0x76, 0xfe, 0x67, 0xd8,
0xd7, 0xcd, 0x80, 0xbd, 0x9b, 0x9c, 0xe8, 0x3e, 0xda, 0xf0, 0x86, 0x84, 0x9d, 0xc0, 0xbe, 0xee,
0x14, 0x0d, 0xb6, 0xd3, 0x44, 0xed, 0xaf, 0xfe, 0xfd, 0x6e, 0xfe, 0xdf, 0x78, 0xfe, 0x3f, 0x00,
0x00, 0xff, 0xff, 0xd7, 0x15, 0x7f, 0x32, 0x64, 0x06, 0x00, 0x00,
// 620 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xd1, 0x6e, 0xd3, 0x3c,
0x14, 0x56, 0x9a, 0xee, 0xd7, 0x7a, 0xba, 0xed, 0xaf, 0xdc, 0x76, 0x8a, 0x22, 0x51, 0x42, 0xae,
0xaa, 0x49, 0x14, 0x28, 0x17, 0x1b, 0x48, 0x20, 0x06, 0xdd, 0x50, 0x45, 0x35, 0xa4, 0x64, 0x70,
0x01, 0xdc, 0x04, 0x6a, 0xba, 0x6a, 0x6d, 0x1c, 0x6c, 0x77, 0x52, 0x1f, 0x80, 0x57, 0xe1, 0x59,
0xb8, 0xe4, 0x91, 0x50, 0x12, 0x27, 0xb1, 0x53, 0xb7, 0x63, 0xea, 0x5d, 0x7d, 0x4e, 0xbe, 0xcf,
0xdf, 0x39, 0x3e, 0xe7, 0x2b, 0x34, 0x5e, 0xcf, 0xc8, 0xb7, 0x6b, 0x9f, 0x13, 0x8a, 0x7b, 0x11,
0x25, 0x9c, 0xa0, 0xda, 0x04, 0x87, 0x98, 0x06, 0x1c, 0x8f, 0xed, 0x3d, 0xff, 0x2a, 0xa0, 0x78,
0x9c, 0x26, 0xdc, 0x9f, 0x06, 0x34, 0xdf, 0x50, 0x1c, 0x70, 0xfc, 0x91, 0xcc, 0x16, 0x73, 0xec,
0xe1, 0x1f, 0x0b, 0xcc, 0x38, 0xea, 0x00, 0xb0, 0x30, 0x88, 0xd8, 0x15, 0xe1, 0xc3, 0x81, 0x65,
0x38, 0x46, 0xb7, 0xe6, 0x49, 0x91, 0x38, 0x7f, 0x93, 0x00, 0x2e, 0x97, 0x11, 0xb6, 0x2a, 0x69,
0xbe, 0x88, 0x20, 0x1b, 0x76, 0xd3, 0xd3, 0xe9, 0x27, 0xcb, 0x4c, 0xb2, 0xf9, 0x19, 0x21, 0xa8,
0x4e, 0x49, 0xc4, 0xac, 0xaa, 0x63, 0x74, 0x4d, 0x2f, 0xf9, 0xed, 0xf6, 0xa1, 0xa5, 0xca, 0x60,
0x11, 0x09, 0x99, 0xc4, 0x93, 0xab, 0xc8, 0xcf, 0xee, 0x05, 0xb4, 0xde, 0x62, 0x9e, 0x02, 0x86,
0xe1, 0x77, 0x92, 0x69, 0xdf, 0x80, 0x51, 0x74, 0x55, 0x54, 0x5d, 0xee, 0x3b, 0x68, 0x97, 0xf8,
0x84, 0x08, 0xb5, 0x58, 0x63, 0xa5, 0xd8, 0xac, 0xa0, 0x8a, 0x54, 0xd0, 0x05, 0xb4, 0x86, 0x2c,
0x2b, 0x26, 0x18, 0x2f, 0xb7, 0x15, 0xf7, 0x10, 0xda, 0x25, 0x3e, 0x21, 0xae, 0x05, 0x3b, 0x34,
0x0e, 0x24, 0x6c, 0xbb, 0x5e, 0x7a, 0x70, 0x7f, 0x19, 0xd0, 0x1a, 0x4d, 0x19, 0xf7, 0xc5, 0x93,
0xb1, 0xec, 0xfe, 0xf7, 0x00, 0x3c, 0x98, 0x9c, 0x4f, 0x67, 0x1c, 0x53, 0x66, 0x19, 0x8e, 0xd9,
0xad, 0xf7, 0x1f, 0xf5, 0xf2, 0xf1, 0xe8, 0xe9, 0x40, 0xbd, 0xcb, 0x1c, 0x71, 0x16, 0x72, 0xba,
0xf4, 0x24, 0x0a, 0xfb, 0x05, 0xfc, 0x5f, 0x4a, 0xa3, 0x06, 0x98, 0xd7, 0x78, 0x29, 0xca, 0x8b,
0x7f, 0xc6, 0x22, 0x6f, 0x82, 0xd9, 0x22, 0x9b, 0x94, 0xf4, 0xf0, 0xbc, 0x72, 0x62, 0xb8, 0xcf,
0xa0, 0x5d, 0xba, 0x52, 0xd4, 0xe5, 0x40, 0xbd, 0x98, 0xb7, 0xb8, 0xb7, 0x66, 0xb7, 0xe6, 0xc9,
0x21, 0xf7, 0xb7, 0x01, 0xed, 0x74, 0x68, 0x32, 0xf4, 0x96, 0x4d, 0x46, 0x2f, 0xa1, 0xca, 0x83,
0x09, 0xb3, 0xcc, 0xa4, 0x2d, 0x47, 0x52, 0x5b, 0xb4, 0xf7, 0xc4, 0x7d, 0x11, 0x1d, 0x49, 0x70,
0xf6, 0x31, 0xd4, 0xf2, 0xd0, 0x9d, 0xba, 0x70, 0x02, 0x87, 0xe5, 0x1b, 0x8a, 0xd9, 0xdb, 0xb4,
0x88, 0xee, 0x31, 0xb4, 0x07, 0x78, 0x86, 0x57, 0x7b, 0x70, 0x1b, 0xf0, 0x15, 0xa0, 0x62, 0xda,
0x07, 0x19, 0xea, 0x08, 0x1a, 0x11, 0xa6, 0x6c, 0xca, 0x38, 0x0e, 0x45, 0x32, 0xc1, 0xee, 0x79,
0x2b, 0x71, 0xf7, 0x09, 0x34, 0x15, 0x86, 0x7f, 0x58, 0xd9, 0x2f, 0x80, 0xfc, 0xad, 0x2e, 0x55,
0xd8, 0x2b, 0x25, 0xf6, 0x53, 0x68, 0xfa, 0x1a, 0x41, 0x77, 0xa0, 0xef, 0xff, 0xd9, 0x01, 0x28,
0xdc, 0x13, 0x3d, 0x86, 0xea, 0x30, 0x9c, 0x72, 0x74, 0x28, 0x8d, 0x42, 0x1c, 0x10, 0xca, 0xed,
0x86, 0x14, 0x3f, 0x9b, 0x47, 0x7c, 0x89, 0x3e, 0x83, 0x25, 0x1b, 0xd9, 0x39, 0x25, 0xf3, 0xec,
0x65, 0x50, 0x67, 0x65, 0xa0, 0x14, 0xd3, 0xb5, 0xef, 0xaf, 0xcd, 0x8b, 0x4a, 0x3c, 0xd8, 0x57,
0x1c, 0x0a, 0xc9, 0x08, 0x9d, 0x17, 0xda, 0xce, 0xfa, 0x0f, 0x0a, 0x4e, 0xc5, 0x58, 0x14, 0x4e,
0x9d, 0x85, 0x29, 0x9c, 0x7a, 0x4f, 0xf2, 0x60, 0x5f, 0x59, 0x6a, 0x85, 0x53, 0xe7, 0x30, 0x0a,
0xa7, 0xde, 0x0f, 0x3e, 0xc0, 0x81, 0xba, 0x22, 0xc8, 0xb9, 0x6d, 0x3f, 0xed, 0x07, 0x1b, 0xbe,
0x10, 0xb4, 0x03, 0x38, 0x50, 0xf7, 0x47, 0xa1, 0xd5, 0xae, 0x96, 0xe6, 0xd5, 0x47, 0x50, 0x97,
0x56, 0x01, 0xdd, 0xd3, 0x76, 0x3d, 0x9b, 0x77, 0xbb, 0xb3, 0x2e, 0x2d, 0x34, 0x8d, 0xa0, 0xee,
0xaf, 0x61, 0xf3, 0x37, 0xb3, 0x69, 0xc6, 0xff, 0xeb, 0x7f, 0xc9, 0x3f, 0xfd, 0xd3, 0xbf, 0x01,
0x00, 0x00, 0xff, 0xff, 0xe7, 0x50, 0x0a, 0x1d, 0x16, 0x08, 0x00, 0x00,
}

View File

@ -55,6 +55,23 @@ message DeleteSnapshotRequest {
string snapshotID = 1;
}
message GetVolumeIDRequest {
bytes persistentVolume = 1;
}
message GetVolumeIDResponse {
string volumeID = 1;
}
message SetVolumeIDRequest {
bytes persistentVolume = 1;
string volumeID = 2;
}
message SetVolumeIDResponse {
bytes persistentVolume = 1;
}
service BlockStore {
rpc Init(InitRequest) returns (Empty);
rpc CreateVolumeFromSnapshot(CreateVolumeRequest) returns (CreateVolumeResponse);
@ -63,4 +80,6 @@ service BlockStore {
rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse);
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
rpc DeleteSnapshot(DeleteSnapshotRequest) returns (Empty);
rpc GetVolumeID(GetVolumeIDRequest) returns (GetVolumeIDResponse);
rpc SetVolumeID(SetVolumeIDRequest) returns (SetVolumeIDResponse);
}

View File

@ -45,6 +45,7 @@ import (
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/discovery"
arkv1client "github.com/heptio/ark/pkg/generated/clientset/versioned/typed/ark/v1"
"github.com/heptio/ark/pkg/util/boolptr"
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
@ -571,10 +572,7 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
if groupResource.Group == "" && groupResource.Resource == "persistentvolumes" {
// restore the PV from snapshot (if applicable)
updatedObj, warning, err := ctx.executePVAction(obj)
if warning != nil {
addToResult(&warnings, namespace, fmt.Errorf("warning executing PVAction for %s: %v", fullPath, warning))
}
updatedObj, err := ctx.executePVAction(obj)
if err != nil {
addToResult(&errs, namespace, fmt.Errorf("error executing PVAction for %s: %v", fullPath, err))
continue
@ -661,95 +659,70 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
return warnings, errs
}
func (ctx *context) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error, error) {
// we need to remove annotations from PVs since they potentially contain
// information about dynamic provisioners which will confuse the controllers.
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, nil, err
func (ctx *context) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
pvName := obj.GetName()
if pvName == "" {
return nil, errors.New("PersistentVolume is missing its name")
}
delete(metadata, "annotations")
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, nil, err
return nil, err
}
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
if err != nil {
return nil, err
}
// We need to remove annotations from PVs since they potentially contain
// information about dynamic provisioners which will confuse the controllers.
delete(metadata, "annotations")
delete(spec, "claimRef")
delete(spec, "storageClassName")
// restore the PV from snapshot (if applicable)
return ctx.restoreVolumeFromSnapshot(obj)
if boolptr.IsSetToFalse(ctx.backup.Spec.SnapshotVolumes) {
// The backup had snapshots disabled, so we can return early
return obj, nil
}
func (ctx *context) restoreVolumeFromSnapshot(obj *unstructured.Unstructured) (*unstructured.Unstructured, error, error) {
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil {
return nil, nil, err
if boolptr.IsSetToFalse(ctx.restore.Spec.RestorePVs) {
// The restore has pv restores disabled, so we can return early
return obj, nil
}
// if it's an unsupported volume type for snapshot restores, don't try to
// do a snapshot restore
if sourceType, _ := kube.GetPVSource(spec); sourceType == "" {
return obj, nil, nil
// If we can't find a snapshot record for this particular PV, it most likely wasn't a PV that Ark
// could snapshot, so return early instead of trying to restore from a snapshot.
backupInfo, found := ctx.backup.Status.VolumeBackups[pvName]
if !found {
return obj, nil
}
var (
pvName = obj.GetName()
restoreFromSnapshot = false
restore = ctx.restore
backup = ctx.backup
)
// Past this point, we expect to be doing a restore
if restore.Spec.RestorePVs != nil && *restore.Spec.RestorePVs {
// when RestorePVs = yes, it's an error if we don't have a snapshot service
if ctx.snapshotService == nil {
return nil, nil, errors.New("PV restorer is not configured for PV snapshot restores")
return nil, errors.New("you must configure a persistentVolumeProvider to restore PersistentVolumes from snapshots")
}
// if there are no snapshots in the backup, return without error
if backup.Status.VolumeBackups == nil {
return obj, nil, nil
}
// if there are snapshots, and this is a supported PV type, but there's no
// snapshot for this PV, it's an error
if backup.Status.VolumeBackups[pvName] == nil {
return nil, nil, errors.Errorf("no snapshot found to restore volume %s from", pvName)
}
restoreFromSnapshot = true
}
if restore.Spec.RestorePVs == nil && ctx.snapshotService != nil {
// when RestorePVs = Auto, don't error if the backup doesn't have snapshots
if backup.Status.VolumeBackups == nil || backup.Status.VolumeBackups[pvName] == nil {
return obj, nil, nil
}
restoreFromSnapshot = true
}
if restoreFromSnapshot {
backupInfo := backup.Status.VolumeBackups[pvName]
ctx.infof("restoring PersistentVolume %s from SnapshotID %s", pvName, backupInfo.SnapshotID)
volumeID, err := ctx.snapshotService.CreateVolumeFromSnapshot(backupInfo.SnapshotID, backupInfo.Type, backupInfo.AvailabilityZone, backupInfo.Iops)
if err != nil {
return nil, nil, err
return nil, err
}
ctx.infof("successfully restored PersistentVolume %s from snapshot", pvName)
if err := kube.SetVolumeID(spec, volumeID); err != nil {
return nil, nil, err
}
updated1, err := ctx.snapshotService.SetVolumeID(obj, volumeID)
if err != nil {
return nil, err
}
var warning error
if ctx.snapshotService == nil && len(backup.Status.VolumeBackups) > 0 {
warning = errors.New("unable to restore PV snapshots: Ark server is not configured with a PersistentVolumeProvider")
updated2, ok := updated1.(*unstructured.Unstructured)
if !ok {
return nil, errors.Errorf("unexpected type %T", updated1)
}
return obj, warning, nil
return updated2, nil
}
func isPVReady(obj runtime.Unstructured) bool {

View File

@ -39,6 +39,7 @@ import (
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/boolptr"
"github.com/heptio/ark/pkg/util/collections"
arktest "github.com/heptio/ark/pkg/util/test"
)
@ -686,7 +687,7 @@ func TestResetMetadataAndStatus(t *testing.T) {
}
}
func TestRestoreVolumeFromSnapshot(t *testing.T) {
func TestExecutePVAction(t *testing.T) {
iops := int64(1000)
tests := []struct {
@ -696,9 +697,10 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
backup *api.Backup
volumeMap map[api.VolumeBackupInfo]string
noSnapshotService bool
expectedWarn bool
expectedErr bool
expectedRes *unstructured.Unstructured
volumeID string
expectSetVolumeID bool
}{
{
name: "no name should error",
@ -713,91 +715,79 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
expectedErr: true,
},
{
name: "when RestorePVs=false, should not error if there is no PV->BackupInfo map",
name: "ensure annotations, spec.claimRef, spec.storageClassName are deleted",
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "storageClassName", "someOtherField").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
backup: &api.Backup{},
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("someOtherField").Unstructured,
},
{
name: "if backup.spec.snapshotVolumes is false, ignore restore.spec.restorePVs and return early",
obj: NewTestUnstructured().WithName("pv-1").WithAnnotations("a", "b").WithSpec("claimRef", "storageClassName", "someOtherField").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Spec: api.BackupSpec{SnapshotVolumes: boolptr.False()}},
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("someOtherField").Unstructured,
},
{
name: "not restoring, return early",
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(false).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
},
{
name: "when RestorePVs=true, return without error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
name: "restoring, return without error if there is no PV->BackupInfo map",
obj: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
},
{
name: "when RestorePVs=true, error if there is PV->BackupInfo map but no entry for this PV",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
name: "restoring, return early if there is PV->BackupInfo map but no entry for this PV",
obj: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"another-pv": {}}}},
expectedErr: true,
},
{
name: "when RestorePVs=true, AWS volume ID should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, GCE pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", map[string]interface{}{"pdName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, Azure pdName should be set correctly",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", map[string]interface{}{"diskName": "volume-1"}).Unstructured,
},
{
name: "when RestorePVs=true, unsupported PV source should not get snapshot restored",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("unsupportedPVSource", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("unsupportedPVSource", make(map[string]interface{})).Unstructured,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
},
{
name: "volume type and IOPS are correctly passed to CreateVolume",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
obj: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1", Type: "gp", Iops: &iops}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1", Type: "gp", Iops: &iops}: "volume-1"},
volumeID: "volume-1",
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
expectSetVolumeID: true,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
},
{
name: "When no SnapshotService, warn if backup has snapshots that will not be restored",
name: "restoring, snapshotService=nil, backup has at least 1 snapshot -> error",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
volumeID: "volume-1",
noSnapshotService: true,
expectedErr: false,
expectedWarn: true,
expectedErr: true,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var snapshotService cloudprovider.SnapshotService
var (
snapshotService cloudprovider.SnapshotService
fakeSnapshotService *arktest.FakeSnapshotService
)
if !test.noSnapshotService {
snapshotService = &arktest.FakeSnapshotService{RestorableVolumes: test.volumeMap}
fakeSnapshotService = &arktest.FakeSnapshotService{
RestorableVolumes: test.volumeMap,
VolumeID: test.volumeID,
}
snapshotService = fakeSnapshotService
}
ctx := &context{
@ -807,13 +797,20 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
logger: arktest.NewLogger(),
}
res, warn, err := ctx.restoreVolumeFromSnapshot(test.obj)
res, err := ctx.executePVAction(test.obj)
assert.Equal(t, test.expectedWarn, warn != nil)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, res)
if test.expectedErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if test.expectSetVolumeID {
assert.Equal(t, test.volumeID, fakeSnapshotService.VolumeIDSet)
} else {
assert.Equal(t, "", fakeSnapshotService.VolumeIDSet)
}
assert.Equal(t, test.expectedRes, res)
})
}
}

View File

@ -112,3 +112,13 @@ func ForEach(root map[string]interface{}, path string, fn func(obj map[string]in
return nil
}
// Exists returns true if root[path] exists, or false otherwise.
func Exists(root map[string]interface{}, path string) bool {
if root == nil {
return false
}
_, err := GetValue(root, path)
return err == nil
}

View File

@ -18,8 +18,6 @@ package kube
import (
"fmt"
"regexp"
"strings"
"github.com/pkg/errors"
@ -27,8 +25,6 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"github.com/heptio/ark/pkg/util/collections"
)
// NamespaceAndName returns a string in the format <namespace>/<name>
@ -52,80 +48,3 @@ func EnsureNamespaceExists(namespace *v1.Namespace, client corev1.NamespaceInter
return false, errors.Wrapf(err, "error creating namespace %s", namespace.Name)
}
}
var ebsVolumeIDRegex = regexp.MustCompile("vol-.*")
var supportedVolumeTypes = map[string]string{
"awsElasticBlockStore": "volumeID",
"gcePersistentDisk": "pdName",
"azureDisk": "diskName",
}
// GetVolumeID looks for a supported PV source within the provided PV unstructured
// data. It returns the appropriate volume ID field if found. If the PV source
// is supported but a volume ID cannot be found, an error is returned; if the PV
// source is not supported, zero values are returned.
func GetVolumeID(pv map[string]interface{}) (string, error) {
spec, err := collections.GetMap(pv, "spec")
if err != nil {
return "", err
}
for volumeType, volumeIDKey := range supportedVolumeTypes {
if pvSource, err := collections.GetMap(spec, volumeType); err == nil {
volumeID, err := collections.GetString(pvSource, volumeIDKey)
if err != nil {
return "", err
}
if volumeType == "awsElasticBlockStore" {
return ebsVolumeIDRegex.FindString(volumeID), nil
}
return volumeID, nil
}
}
return "", nil
}
// GetPVSource looks for a supported PV source within the provided PV spec data.
// It returns the name of the PV source type and the unstructured source data if
// one is found, or zero values otherwise.
func GetPVSource(spec map[string]interface{}) (string, map[string]interface{}) {
for volumeType := range supportedVolumeTypes {
if pvSource, found := spec[volumeType]; found {
return volumeType, pvSource.(map[string]interface{})
}
}
return "", nil
}
// SetVolumeID looks for a supported PV source within the provided PV spec data.
// If sets the appropriate ID field(s) within the source if found, and returns an
// error if a supported PV source is not found.
func SetVolumeID(spec map[string]interface{}, volumeID string) error {
sourceType, source := GetPVSource(spec)
if sourceType == "" {
return errors.New("persistent volume source is not compatible")
}
// for azureDisk, we need to do a find-replace within the diskURI (if it exists)
// to switch the old disk name with the new.
if sourceType == "azureDisk" {
uri, err := collections.GetString(source, "diskURI")
if err == nil {
priorVolumeID, err := collections.GetString(source, supportedVolumeTypes["azureDisk"])
if err != nil {
return err
}
source["diskURI"] = strings.Replace(uri, priorVolumeID, volumeID, -1)
}
}
source[supportedVolumeTypes[sourceType]] = volumeID
return nil
}

View File

@ -18,91 +18,12 @@ package kube
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/heptio/ark/pkg/util/collections"
)
func TestSetVolumeID(t *testing.T) {
tests := []struct {
name string
spec map[string]interface{}
volumeID string
expectedErr error
specFieldExpectations map[string]string
}{
{
name: "awsElasticBlockStore normal case",
spec: map[string]interface{}{
"awsElasticBlockStore": map[string]interface{}{
"volumeID": "vol-old",
},
},
volumeID: "vol-new",
expectedErr: nil,
},
{
name: "gcePersistentDisk normal case",
spec: map[string]interface{}{
"gcePersistentDisk": map[string]interface{}{
"pdName": "old-pd",
},
},
volumeID: "new-pd",
expectedErr: nil,
},
{
name: "azureDisk normal case",
spec: map[string]interface{}{
"azureDisk": map[string]interface{}{
"diskName": "old-disk",
"diskURI": "some-nonsense/old-disk",
},
},
volumeID: "new-disk",
expectedErr: nil,
specFieldExpectations: map[string]string{
"azureDisk.diskURI": "some-nonsense/new-disk",
},
},
{
name: "azureDisk with no diskURI",
spec: map[string]interface{}{
"azureDisk": map[string]interface{}{
"diskName": "old-disk",
},
},
volumeID: "new-disk",
expectedErr: nil,
},
func TestNamespaceAndName(t *testing.T) {
//TODO
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := SetVolumeID(test.spec, test.volumeID)
require.Equal(t, test.expectedErr, err)
if test.expectedErr != nil {
return
}
pv := map[string]interface{}{
"spec": test.spec,
}
volumeID, err := GetVolumeID(pv)
require.Nil(t, err)
assert.Equal(t, test.volumeID, volumeID)
for path, expected := range test.specFieldExpectations {
actual, err := collections.GetString(test.spec, path)
assert.Nil(t, err)
assert.Equal(t, expected, actual)
}
})
}
func TestEnsureNamespaceExists(t *testing.T) {
//TODO
}

View File

@ -19,6 +19,7 @@ package test
import (
"errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
api "github.com/heptio/ark/pkg/apis/ark/v1"
@ -33,6 +34,9 @@ type FakeSnapshotService struct {
// VolumeBackupInfo -> VolumeID
RestorableVolumes map[api.VolumeBackupInfo]string
VolumeID string
VolumeIDSet string
}
func (s *FakeSnapshotService) GetAllSnapshots() ([]string, error) {
@ -80,3 +84,12 @@ func (s *FakeSnapshotService) GetVolumeInfo(volumeID, volumeAZ string) (string,
return volumeInfo.Type, volumeInfo.Iops, nil
}
}
func (s *FakeSnapshotService) GetVolumeID(pv runtime.Unstructured) (string, error) {
return s.VolumeID, nil
}
func (s *FakeSnapshotService) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
s.VolumeIDSet = volumeID
return pv, nil
}