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/cloudprovider"
"github.com/heptio/ark/pkg/discovery" "github.com/heptio/ark/pkg/discovery"
"github.com/heptio/ark/pkg/util/collections" "github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging" "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) log.Infof("label %q is not present on PersistentVolume", zoneLabel)
} }
volumeID, err := kubeutil.GetVolumeID(pv.UnstructuredContent()) volumeID, err := ib.snapshotService.GetVolumeID(pv)
// non-nil error means it's a supported PV source but volume ID can't be found
if err != nil { if err != nil {
return errors.Wrapf(err, "error getting volume ID for PersistentVolume") return errors.Wrapf(err, "error getting volume ID for PersistentVolume")
} }
// no volumeID / nil error means unsupported PV source
if volumeID == "" { if volumeID == "" {
log.Info("PersistentVolume is not a supported volume type for snapshots, skipping.") log.Info("PersistentVolume is not a supported volume type for snapshots, skipping.")
return nil return nil

View File

@ -324,7 +324,10 @@ func TestBackupItemNoSkips(t *testing.T) {
var snapshotService *arktest.FakeSnapshotService var snapshotService *arktest.FakeSnapshotService
if test.snapshottableVolumes != nil { if test.snapshottableVolumes != nil {
snapshotService = &arktest.FakeSnapshotService{SnapshottableVolumes: test.snapshottableVolumes} snapshotService = &arktest.FakeSnapshotService{
SnapshottableVolumes: test.snapshottableVolumes,
VolumeID: "vol-abc123",
}
b.snapshotService = snapshotService b.snapshotService = snapshotService
} }
@ -356,7 +359,7 @@ func TestBackupItemNoSkips(t *testing.T) {
err = b.backupItem(arktest.NewLogger(), obj, groupResource) err = b.backupItem(arktest.NewLogger(), obj, groupResource)
gotError := err != nil gotError := err != nil
if e, a := test.expectError, gotError; e != a { 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 { if test.expectError {
return return
@ -445,12 +448,6 @@ func TestTakePVSnapshot(t *testing.T) {
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`, pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
snapshotEnabled: false, 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", name: "unsupported PV source type",
snapshotEnabled: true, snapshotEnabled: true,
@ -458,19 +455,7 @@ func TestTakePVSnapshot(t *testing.T) {
expectError: false, expectError: false,
}, },
{ {
name: "can't find volume id - aws but no volume id", name: "without iops",
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",
snapshotEnabled: true, 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"}}}`, 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, expectError: false,
@ -482,7 +467,7 @@ func TestTakePVSnapshot(t *testing.T) {
}, },
}, },
{ {
name: "aws - simple volume id with provisioned IOPS", name: "with iops",
snapshotEnabled: true, 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"}}}`, 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, expectError: false,
@ -493,42 +478,6 @@ func TestTakePVSnapshot(t *testing.T) {
"vol-abc123": {Type: "io1", Iops: &iops, SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"}, "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", name: "preexisting volume backup info in backup status",
snapshotEnabled: true, snapshotEnabled: true,
@ -545,10 +494,11 @@ func TestTakePVSnapshot(t *testing.T) {
}, },
}, },
{ {
name: "create snapshot error", name: "create snapshot error",
snapshotEnabled: true, snapshotEnabled: true,
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`, pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
expectError: true, expectedVolumeID: "pd-abc123",
expectError: true,
}, },
{ {
name: "PV with label metadata but no failureDomainZone", name: "PV with label metadata but no failureDomainZone",
@ -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} ib := &defaultItemBackupper{snapshotService: snapshotService}

View File

@ -17,14 +17,18 @@ limitations under the License.
package aws package aws
import ( import (
"regexp"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
) )
const regionKey = "region" const regionKey = "region"
@ -205,3 +209,29 @@ func (b *blockStore) DeleteSnapshot(snapshotID string) error {
return errors.WithStack(err) 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" "context"
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
"github.com/Azure/azure-sdk-for-go/arm/disk" "github.com/Azure/azure-sdk-for-go/arm/disk"
@ -29,8 +30,10 @@ import (
"github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/satori/uuid" "github.com/satori/uuid"
"k8s.io/apimachinery/pkg/runtime"
"github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
) )
const ( const (
@ -292,3 +295,36 @@ func getFullDiskName(subscription string, resourceGroup string, diskName string)
func getFullSnapshotName(subscription string, resourceGroup string, snapshotName string) 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) 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" "golang.org/x/oauth2/google"
"google.golang.org/api/compute/v0.beta" "google.golang.org/api/compute/v0.beta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/collections"
) )
const projectKey = "project" const projectKey = "project"
@ -191,3 +193,27 @@ func (b *blockStore) DeleteSnapshot(snapshotID string) error {
return errors.WithStack(err) 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" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
) )
// SnapshotService exposes Ark-specific operations for snapshotting and restoring block // 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 gets the type and IOPS (if applicable) from the cloud API.
GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) 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 ( const (
@ -120,3 +127,11 @@ func (sr *snapshotService) DeleteSnapshot(snapshotID string) error {
func (sr *snapshotService) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) { func (sr *snapshotService) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) {
return sr.blockStore.GetVolumeInfo(volumeID, volumeAZ) 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 ( import (
"io" "io"
"time" "time"
"k8s.io/apimachinery/pkg/runtime"
) )
// ObjectStore exposes basic object-storage operations required // 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). // and with the specified type and IOPS (if using provisioned IOPS).
CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (volumeID string, err error) 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 // GetVolumeInfo returns the type and IOPS (if using provisioned IOPS) for a specified block
// volume. // volume.
GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error)

View File

@ -17,9 +17,13 @@ limitations under the License.
package plugin package plugin
import ( import (
"encoding/json"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider"
proto "github.com/heptio/ark/pkg/plugin/generated" proto "github.com/heptio/ark/pkg/plugin/generated"
@ -150,6 +154,49 @@ func (c *BlockStoreGRPCClient) DeleteSnapshot(snapshotID string) error {
return err 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 // BlockStoreGRPCServer implements the proto-generated BlockStoreServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface. // gRPC calls and forwards them to an implementation of the pluggable interface.
type BlockStoreGRPCServer struct { type BlockStoreGRPCServer struct {
@ -245,3 +292,38 @@ func (s *BlockStoreGRPCServer) DeleteSnapshot(ctx context.Context, req *proto.De
return &proto.Empty{}, nil 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 CreateSnapshotRequest
CreateSnapshotResponse CreateSnapshotResponse
DeleteSnapshotRequest DeleteSnapshotRequest
GetVolumeIDRequest
GetVolumeIDResponse
SetVolumeIDRequest
SetVolumeIDResponse
PutObjectRequest PutObjectRequest
GetObjectRequest GetObjectRequest
Bytes Bytes

View File

@ -257,6 +257,78 @@ func (m *DeleteSnapshotRequest) GetSnapshotID() string {
return "" 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() { func init() {
proto.RegisterType((*CreateVolumeRequest)(nil), "generated.CreateVolumeRequest") proto.RegisterType((*CreateVolumeRequest)(nil), "generated.CreateVolumeRequest")
proto.RegisterType((*CreateVolumeResponse)(nil), "generated.CreateVolumeResponse") proto.RegisterType((*CreateVolumeResponse)(nil), "generated.CreateVolumeResponse")
@ -269,6 +341,10 @@ func init() {
proto.RegisterType((*CreateSnapshotRequest)(nil), "generated.CreateSnapshotRequest") proto.RegisterType((*CreateSnapshotRequest)(nil), "generated.CreateSnapshotRequest")
proto.RegisterType((*CreateSnapshotResponse)(nil), "generated.CreateSnapshotResponse") proto.RegisterType((*CreateSnapshotResponse)(nil), "generated.CreateSnapshotResponse")
proto.RegisterType((*DeleteSnapshotRequest)(nil), "generated.DeleteSnapshotRequest") 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. // 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) ListSnapshots(ctx context.Context, in *ListSnapshotsRequest, opts ...grpc.CallOption) (*ListSnapshotsResponse, error)
CreateSnapshot(ctx context.Context, in *CreateSnapshotRequest, opts ...grpc.CallOption) (*CreateSnapshotResponse, error) CreateSnapshot(ctx context.Context, in *CreateSnapshotRequest, opts ...grpc.CallOption) (*CreateSnapshotResponse, error)
DeleteSnapshot(ctx context.Context, in *DeleteSnapshotRequest, opts ...grpc.CallOption) (*Empty, 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 { type blockStoreClient struct {
@ -362,6 +440,24 @@ func (c *blockStoreClient) DeleteSnapshot(ctx context.Context, in *DeleteSnapsho
return out, nil 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 // Server API for BlockStore service
type BlockStoreServer interface { type BlockStoreServer interface {
@ -372,6 +468,8 @@ type BlockStoreServer interface {
ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error) ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)
CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error) CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error)
DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*Empty, 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) { 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) 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{ var _BlockStore_serviceDesc = grpc.ServiceDesc{
ServiceName: "generated.BlockStore", ServiceName: "generated.BlockStore",
HandlerType: (*BlockStoreServer)(nil), HandlerType: (*BlockStoreServer)(nil),
@ -536,6 +670,14 @@ var _BlockStore_serviceDesc = grpc.ServiceDesc{
MethodName: "DeleteSnapshot", MethodName: "DeleteSnapshot",
Handler: _BlockStore_DeleteSnapshot_Handler, Handler: _BlockStore_DeleteSnapshot_Handler,
}, },
{
MethodName: "GetVolumeID",
Handler: _BlockStore_GetVolumeID_Handler,
},
{
MethodName: "SetVolumeID",
Handler: _BlockStore_SetVolumeID_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "BlockStore.proto", Metadata: "BlockStore.proto",
@ -544,39 +686,44 @@ var _BlockStore_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("BlockStore.proto", fileDescriptor1) } func init() { proto.RegisterFile("BlockStore.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{ var fileDescriptor1 = []byte{
// 539 bytes of a gzipped FileDescriptorProto // 620 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xc1, 0x6e, 0xd3, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xd1, 0x6e, 0xd3, 0x3c,
0x10, 0xd5, 0xc6, 0x06, 0x35, 0x53, 0x5a, 0xa2, 0xc5, 0xae, 0x2c, 0x1f, 0x8a, 0xf1, 0x29, 0x42, 0x14, 0x56, 0x9a, 0xee, 0xd7, 0x7a, 0xba, 0xed, 0xaf, 0xdc, 0x76, 0x8a, 0x22, 0x51, 0x42, 0xae,
0x22, 0xa0, 0x70, 0x68, 0x41, 0x02, 0x09, 0x48, 0x8b, 0x22, 0x50, 0x91, 0x9c, 0xc2, 0x01, 0x4e, 0xaa, 0x49, 0x14, 0x28, 0x17, 0x1b, 0x48, 0x20, 0x06, 0xdd, 0x50, 0x45, 0x35, 0xa4, 0x64, 0x70,
0x86, 0x2c, 0x69, 0x54, 0xc7, 0x6b, 0x76, 0x37, 0x95, 0xfc, 0x01, 0xfc, 0x0a, 0x5f, 0xc0, 0x47, 0x01, 0xdc, 0x04, 0x6a, 0xba, 0x6a, 0x6d, 0x1c, 0x6c, 0x77, 0x52, 0x1f, 0x80, 0x57, 0xe1, 0x59,
0xf0, 0x59, 0xc8, 0xf6, 0xda, 0xde, 0xb5, 0xdd, 0x54, 0x55, 0x6e, 0x9e, 0x19, 0xcf, 0xdb, 0x37, 0xb8, 0xe4, 0x91, 0x50, 0x12, 0x27, 0xb1, 0x53, 0xb7, 0x63, 0xea, 0x5d, 0x7d, 0x4e, 0xbe, 0xcf,
0xcf, 0x6f, 0xd6, 0x30, 0x78, 0x1b, 0xd1, 0x1f, 0x97, 0x33, 0x41, 0x19, 0x19, 0x25, 0x8c, 0x0a, 0xdf, 0x39, 0x3e, 0xe7, 0x2b, 0x34, 0x5e, 0xcf, 0xc8, 0xb7, 0x6b, 0x9f, 0x13, 0x8a, 0x7b, 0x11,
0x8a, 0xfb, 0x0b, 0x12, 0x13, 0x16, 0x0a, 0x32, 0x77, 0xef, 0xcd, 0x2e, 0x42, 0x46, 0xe6, 0x45, 0x25, 0x9c, 0xa0, 0xda, 0x04, 0x87, 0x98, 0x06, 0x1c, 0x8f, 0xed, 0x3d, 0xff, 0x2a, 0xa0, 0x78,
0xc1, 0xff, 0x8d, 0xe0, 0xc1, 0x3b, 0x46, 0x42, 0x41, 0xbe, 0xd0, 0x68, 0xbd, 0x22, 0x01, 0xf9, 0x9c, 0x26, 0xdc, 0x9f, 0x06, 0x34, 0xdf, 0x50, 0x1c, 0x70, 0xfc, 0x91, 0xcc, 0x16, 0x73, 0xec,
0xb5, 0x26, 0x5c, 0xe0, 0x43, 0x00, 0x1e, 0x87, 0x09, 0xbf, 0xa0, 0x62, 0x3a, 0x71, 0x90, 0x87, 0xe1, 0x1f, 0x0b, 0xcc, 0x38, 0xea, 0x00, 0xb0, 0x30, 0x88, 0xd8, 0x15, 0xe1, 0xc3, 0x81, 0x65,
0x86, 0xfd, 0x40, 0xc9, 0x64, 0xf5, 0xab, 0xbc, 0xe1, 0x3c, 0x4d, 0x88, 0xd3, 0x2b, 0xea, 0x75, 0x38, 0x46, 0xb7, 0xe6, 0x49, 0x91, 0x38, 0x7f, 0x93, 0x00, 0x2e, 0x97, 0x11, 0xb6, 0x2a, 0x69,
0x06, 0xbb, 0xb0, 0x53, 0x44, 0x6f, 0xbe, 0x3a, 0x46, 0x5e, 0xad, 0x62, 0x8c, 0xc1, 0x5c, 0xd2, 0xbe, 0x88, 0x20, 0x1b, 0x76, 0xd3, 0xd3, 0xe9, 0x27, 0xcb, 0x4c, 0xb2, 0xf9, 0x19, 0x21, 0xa8,
0x84, 0x3b, 0xa6, 0x87, 0x86, 0x46, 0x90, 0x3f, 0xfb, 0x63, 0xb0, 0x74, 0x1a, 0x3c, 0xa1, 0x31, 0x4e, 0x49, 0xc4, 0xac, 0xaa, 0x63, 0x74, 0x4d, 0x2f, 0xf9, 0xed, 0xf6, 0xa1, 0xa5, 0xca, 0x60,
0x57, 0x70, 0x2a, 0x16, 0x55, 0xec, 0x9f, 0x81, 0xf5, 0x9e, 0x88, 0xa2, 0x61, 0x1a, 0xff, 0xa4, 0x11, 0x09, 0x99, 0xc4, 0x93, 0xab, 0xc8, 0xcf, 0xee, 0x05, 0xb4, 0xde, 0x62, 0x9e, 0x02, 0x86,
0x25, 0xf7, 0x0d, 0x3d, 0x1a, 0xaf, 0x9e, 0xce, 0xcb, 0xff, 0x00, 0x76, 0x03, 0x4f, 0x92, 0xd0, 0xe1, 0x77, 0x92, 0x69, 0xdf, 0x80, 0x51, 0x74, 0x55, 0x54, 0x5d, 0xee, 0x3b, 0x68, 0x97, 0xf8,
0x87, 0x45, 0xad, 0x61, 0xcb, 0x81, 0x7a, 0xca, 0x40, 0x67, 0x60, 0x4d, 0x79, 0x39, 0x4c, 0x38, 0x84, 0x08, 0xb5, 0x58, 0x63, 0xa5, 0xd8, 0xac, 0xa0, 0x8a, 0x54, 0xd0, 0x05, 0xb4, 0x86, 0x2c,
0x4f, 0xb7, 0x25, 0xf7, 0x04, 0xec, 0x06, 0x9e, 0x24, 0x67, 0xc1, 0x1d, 0x96, 0x25, 0x72, 0xb4, 0x2b, 0x26, 0x18, 0x2f, 0xb7, 0x15, 0xf7, 0x10, 0xda, 0x25, 0x3e, 0x21, 0xae, 0x05, 0x3b, 0x34,
0x9d, 0xa0, 0x08, 0xfc, 0x3f, 0x08, 0xac, 0x8f, 0x4b, 0x2e, 0x66, 0xf2, 0x93, 0xf1, 0xf2, 0xfc, 0x0e, 0x24, 0x6c, 0xbb, 0x5e, 0x7a, 0x70, 0x7f, 0x19, 0xd0, 0x1a, 0x4d, 0x19, 0xf7, 0xc5, 0x93,
0x4f, 0x00, 0x22, 0x5c, 0x9c, 0x2e, 0x23, 0x41, 0x18, 0x77, 0x90, 0x67, 0x0c, 0x77, 0xc7, 0x4f, 0xb1, 0xec, 0xfe, 0xf7, 0x00, 0x3c, 0x98, 0x9c, 0x4f, 0x67, 0x1c, 0x53, 0x66, 0x19, 0x8e, 0xd9,
0x47, 0x95, 0x3d, 0x46, 0x5d, 0x4d, 0xa3, 0xf3, 0xaa, 0xe3, 0x24, 0x16, 0x2c, 0x0d, 0x14, 0x08, 0xad, 0xf7, 0x1f, 0xf5, 0xf2, 0xf1, 0xe8, 0xe9, 0x40, 0xbd, 0xcb, 0x1c, 0x71, 0x16, 0x72, 0xba,
0xf7, 0x15, 0xdc, 0x6f, 0x94, 0xf1, 0x00, 0x8c, 0x4b, 0x92, 0xca, 0xf1, 0xb2, 0xc7, 0x8c, 0xe4, 0xf4, 0x24, 0x0a, 0xfb, 0x05, 0xfc, 0x5f, 0x4a, 0xa3, 0x06, 0x98, 0xd7, 0x78, 0x29, 0xca, 0x8b,
0x55, 0x18, 0xad, 0x4b, 0xa7, 0x14, 0xc1, 0xcb, 0xde, 0x31, 0xf2, 0x5f, 0x80, 0xdd, 0x38, 0x52, 0x7f, 0xc6, 0x22, 0x6f, 0x82, 0xd9, 0x22, 0x9b, 0x94, 0xf4, 0xf0, 0xbc, 0x72, 0x62, 0xb8, 0xcf,
0xce, 0xe5, 0xc1, 0x6e, 0xed, 0xb7, 0x4c, 0x5b, 0x63, 0xd8, 0x0f, 0xd4, 0x94, 0xff, 0x0f, 0x81, 0xa0, 0x5d, 0xba, 0x52, 0xd4, 0xe5, 0x40, 0xbd, 0x98, 0xb7, 0xb8, 0xb7, 0x66, 0xb7, 0xe6, 0xc9,
0x5d, 0x98, 0xa6, 0xec, 0xde, 0x52, 0x64, 0xfc, 0x1a, 0x4c, 0x11, 0x2e, 0xb8, 0x63, 0xe4, 0xb2, 0x21, 0xf7, 0xb7, 0x01, 0xed, 0x74, 0x68, 0x32, 0xf4, 0x96, 0x4d, 0x46, 0x2f, 0xa1, 0xca, 0x83,
0x3c, 0x56, 0x64, 0xe9, 0x3c, 0x27, 0xd3, 0x45, 0x2a, 0x92, 0xf7, 0xb9, 0x47, 0xd0, 0xaf, 0x52, 0x09, 0xb3, 0xcc, 0xa4, 0x2d, 0x47, 0x52, 0x5b, 0xb4, 0xf7, 0xc4, 0x7d, 0x11, 0x1d, 0x49, 0x70,
0xb7, 0x52, 0xe1, 0x18, 0x0e, 0x9a, 0x27, 0xd4, 0xde, 0xdb, 0xb4, 0x88, 0xfe, 0x11, 0xd8, 0x13, 0xf6, 0x31, 0xd4, 0xf2, 0xd0, 0x9d, 0xba, 0x70, 0x02, 0x87, 0xe5, 0x1b, 0x8a, 0xd9, 0xdb, 0xb4,
0x12, 0x91, 0xb6, 0x06, 0x37, 0x34, 0x8e, 0xff, 0x9a, 0x00, 0xf5, 0x3d, 0x81, 0x9f, 0x81, 0x39, 0x88, 0xee, 0x31, 0xb4, 0x07, 0x78, 0x86, 0x57, 0x7b, 0x70, 0x1b, 0xf0, 0x15, 0xa0, 0x62, 0xda,
0x8d, 0x97, 0x02, 0x1f, 0x28, 0x43, 0x67, 0x09, 0x09, 0xe7, 0x0e, 0x94, 0xfc, 0xc9, 0x2a, 0x11, 0x07, 0x19, 0xea, 0x08, 0x1a, 0x11, 0xa6, 0x6c, 0xca, 0x38, 0x0e, 0x45, 0x32, 0xc1, 0xee, 0x79,
0x29, 0xfe, 0x06, 0x8e, 0xba, 0xb2, 0xa7, 0x8c, 0xae, 0x4a, 0x0e, 0xf8, 0xb0, 0x25, 0x9d, 0x76, 0x2b, 0x71, 0xf7, 0x09, 0x34, 0x15, 0x86, 0x7f, 0x58, 0xd9, 0x2f, 0x80, 0xfc, 0xad, 0x2e, 0x55,
0xbd, 0xb8, 0x0f, 0xaf, 0xad, 0xcb, 0xb1, 0x03, 0xd8, 0xd3, 0x76, 0x11, 0xab, 0x1d, 0x5d, 0x5b, 0xd8, 0x2b, 0x25, 0xf6, 0x53, 0x68, 0xfa, 0x1a, 0x41, 0x77, 0xa0, 0xef, 0xff, 0xd9, 0x01, 0x28,
0xef, 0x7a, 0xd7, 0xbf, 0x50, 0x63, 0x6a, 0x2b, 0xa4, 0x61, 0x76, 0x2d, 0xab, 0x86, 0xd9, 0xbd, 0xdc, 0x13, 0x3d, 0x86, 0xea, 0x30, 0x9c, 0x72, 0x74, 0x28, 0x8d, 0x42, 0x1c, 0x10, 0xca, 0xed,
0x7d, 0x01, 0xec, 0x69, 0xf6, 0xd5, 0x30, 0xbb, 0x76, 0x49, 0xc3, 0xec, 0x76, 0xfe, 0x67, 0xd8, 0x86, 0x14, 0x3f, 0x9b, 0x47, 0x7c, 0x89, 0x3e, 0x83, 0x25, 0x1b, 0xd9, 0x39, 0x25, 0xf3, 0xec,
0xd7, 0xcd, 0x80, 0xbd, 0x9b, 0x9c, 0xe8, 0x3e, 0xda, 0xf0, 0x86, 0x84, 0x9d, 0xc0, 0xbe, 0xee, 0x65, 0x50, 0x67, 0x65, 0xa0, 0x14, 0xd3, 0xb5, 0xef, 0xaf, 0xcd, 0x8b, 0x4a, 0x3c, 0xd8, 0x57,
0x14, 0x0d, 0xb6, 0xd3, 0x44, 0xed, 0xaf, 0xfe, 0xfd, 0x6e, 0xfe, 0xdf, 0x78, 0xfe, 0x3f, 0x00, 0x1c, 0x0a, 0xc9, 0x08, 0x9d, 0x17, 0xda, 0xce, 0xfa, 0x0f, 0x0a, 0x4e, 0xc5, 0x58, 0x14, 0x4e,
0x00, 0xff, 0xff, 0xd7, 0x15, 0x7f, 0x32, 0x64, 0x06, 0x00, 0x00, 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; 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 { service BlockStore {
rpc Init(InitRequest) returns (Empty); rpc Init(InitRequest) returns (Empty);
rpc CreateVolumeFromSnapshot(CreateVolumeRequest) returns (CreateVolumeResponse); rpc CreateVolumeFromSnapshot(CreateVolumeRequest) returns (CreateVolumeResponse);
@ -63,4 +80,6 @@ service BlockStore {
rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse); rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse);
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse); rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
rpc DeleteSnapshot(DeleteSnapshotRequest) returns (Empty); 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/cloudprovider"
"github.com/heptio/ark/pkg/discovery" "github.com/heptio/ark/pkg/discovery"
arkv1client "github.com/heptio/ark/pkg/generated/clientset/versioned/typed/ark/v1" 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/collections"
"github.com/heptio/ark/pkg/util/kube" "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging" "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" { if groupResource.Group == "" && groupResource.Resource == "persistentvolumes" {
// restore the PV from snapshot (if applicable) // restore the PV from snapshot (if applicable)
updatedObj, warning, err := ctx.executePVAction(obj) updatedObj, err := ctx.executePVAction(obj)
if warning != nil {
addToResult(&warnings, namespace, fmt.Errorf("warning executing PVAction for %s: %v", fullPath, warning))
}
if err != nil { if err != nil {
addToResult(&errs, namespace, fmt.Errorf("error executing PVAction for %s: %v", fullPath, err)) addToResult(&errs, namespace, fmt.Errorf("error executing PVAction for %s: %v", fullPath, err))
continue continue
@ -661,95 +659,70 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
return warnings, errs return warnings, errs
} }
func (ctx *context) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error, error) { func (ctx *context) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
// we need to remove annotations from PVs since they potentially contain pvName := obj.GetName()
// information about dynamic provisioners which will confuse the controllers. if pvName == "" {
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata") return nil, errors.New("PersistentVolume is missing its name")
if err != nil {
return nil, nil, err
} }
delete(metadata, "annotations")
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
if err != nil { 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, "claimRef")
delete(spec, "storageClassName") delete(spec, "storageClassName")
// restore the PV from snapshot (if applicable) if boolptr.IsSetToFalse(ctx.backup.Spec.SnapshotVolumes) {
return ctx.restoreVolumeFromSnapshot(obj) // The backup had snapshots disabled, so we can return early
} return obj, nil
}
func (ctx *context) restoreVolumeFromSnapshot(obj *unstructured.Unstructured) (*unstructured.Unstructured, error, error) { if boolptr.IsSetToFalse(ctx.restore.Spec.RestorePVs) {
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") // The restore has pv restores disabled, so we can return early
return obj, 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
}
// Past this point, we expect to be doing a restore
if ctx.snapshotService == nil {
return nil, errors.New("you must configure a persistentVolumeProvider to restore PersistentVolumes from snapshots")
}
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 { if err != nil {
return nil, nil, err return nil, err
}
ctx.infof("successfully restored PersistentVolume %s from snapshot", pvName)
updated1, err := ctx.snapshotService.SetVolumeID(obj, volumeID)
if err != nil {
return nil, err
} }
// if it's an unsupported volume type for snapshot restores, don't try to updated2, ok := updated1.(*unstructured.Unstructured)
// do a snapshot restore if !ok {
if sourceType, _ := kube.GetPVSource(spec); sourceType == "" { return nil, errors.Errorf("unexpected type %T", updated1)
return obj, nil, nil
} }
var ( return updated2, nil
pvName = obj.GetName()
restoreFromSnapshot = false
restore = ctx.restore
backup = ctx.backup
)
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")
}
// 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
}
ctx.infof("successfully restored PersistentVolume %s from snapshot", pvName)
if err := kube.SetVolumeID(spec, volumeID); err != nil {
return nil, 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")
}
return obj, warning, nil
} }
func isPVReady(obj runtime.Unstructured) bool { func isPVReady(obj runtime.Unstructured) bool {

View File

@ -39,6 +39,7 @@ import (
api "github.com/heptio/ark/pkg/apis/ark/v1" api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/util/boolptr"
"github.com/heptio/ark/pkg/util/collections" "github.com/heptio/ark/pkg/util/collections"
arktest "github.com/heptio/ark/pkg/util/test" 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) iops := int64(1000)
tests := []struct { tests := []struct {
@ -696,9 +697,10 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
backup *api.Backup backup *api.Backup
volumeMap map[api.VolumeBackupInfo]string volumeMap map[api.VolumeBackupInfo]string
noSnapshotService bool noSnapshotService bool
expectedWarn bool
expectedErr bool expectedErr bool
expectedRes *unstructured.Unstructured expectedRes *unstructured.Unstructured
volumeID string
expectSetVolumeID bool
}{ }{
{ {
name: "no name should error", name: "no name should error",
@ -713,91 +715,79 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
expectedErr: true, 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, obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(false).Restore, 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, expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured, expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
}, },
{ {
name: "when RestorePVs=true, return without error if there is no PV->BackupInfo map", name: "restoring, return without error if there is no PV->BackupInfo map",
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, restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{}}, backup: &api.Backup{Status: api.BackupStatus{}},
expectedErr: false, 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", name: "restoring, return early if there is PV->BackupInfo map but no entry for this PV",
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, restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"another-pv": {}}}}, 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, expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured, expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
}, },
{ {
name: "when RestorePVs=true, GCE pdName should be set correctly", name: "volume type and IOPS are correctly passed to CreateVolume",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", make(map[string]interface{})).Unstructured, obj: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore, restore: arktest.NewDefaultTestRestore().WithRestorePVs(true).Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}}, 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"}: "volume-1"}, volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1", Type: "gp", Iops: &iops}: "volume-1"},
expectedErr: false, volumeID: "volume-1",
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", map[string]interface{}{"pdName": "volume-1"}).Unstructured, expectedErr: false,
expectSetVolumeID: true,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").Unstructured,
}, },
{ {
name: "when RestorePVs=true, Azure pdName should be set correctly", name: "restoring, snapshotService=nil, backup has at least 1 snapshot -> error",
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,
},
{
name: "volume type and IOPS are correctly passed to CreateVolume",
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", Type: "gp", Iops: &iops}}}},
volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1", Type: "gp", Iops: &iops}: "volume-1"},
expectedErr: false,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", map[string]interface{}{"volumeID": "volume-1"}).Unstructured,
},
{
name: "When no SnapshotService, warn if backup has snapshots that will not be restored",
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured, obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
restore: arktest.NewDefaultTestRestore().Restore, restore: arktest.NewDefaultTestRestore().Restore,
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": {SnapshotID: "snap-1"}}}}, 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"}, volumeMap: map[api.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
volumeID: "volume-1",
noSnapshotService: true, noSnapshotService: true,
expectedErr: false, expectedErr: true,
expectedWarn: true,
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured, expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
var snapshotService cloudprovider.SnapshotService var (
snapshotService cloudprovider.SnapshotService
fakeSnapshotService *arktest.FakeSnapshotService
)
if !test.noSnapshotService { if !test.noSnapshotService {
snapshotService = &arktest.FakeSnapshotService{RestorableVolumes: test.volumeMap} fakeSnapshotService = &arktest.FakeSnapshotService{
RestorableVolumes: test.volumeMap,
VolumeID: test.volumeID,
}
snapshotService = fakeSnapshotService
} }
ctx := &context{ ctx := &context{
@ -807,13 +797,20 @@ func TestRestoreVolumeFromSnapshot(t *testing.T) {
logger: arktest.NewLogger(), logger: arktest.NewLogger(),
} }
res, warn, err := ctx.restoreVolumeFromSnapshot(test.obj) res, err := ctx.executePVAction(test.obj)
assert.Equal(t, test.expectedWarn, warn != nil) if test.expectedErr {
require.Error(t, err)
if assert.Equal(t, test.expectedErr, err != nil) { return
assert.Equal(t, test.expectedRes, res)
} }
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 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 ( import (
"fmt" "fmt"
"regexp"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -27,8 +25,6 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/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> // 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) 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 ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/heptio/ark/pkg/util/collections"
) )
func TestSetVolumeID(t *testing.T) { func TestNamespaceAndName(t *testing.T) {
tests := []struct { //TODO
name string }
spec map[string]interface{}
volumeID string func TestEnsureNamespaceExists(t *testing.T) {
expectedErr error //TODO
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,
},
}
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)
}
})
}
} }

View File

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