Merge pull request #215 from ncdc/support-custom-snapshots
Support custom volume snapshots & restorespull/223/head
commit
c129d1cec3
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
@ -545,10 +494,11 @@ func TestTakePVSnapshot(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "create snapshot error",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: true,
|
||||
name: "create snapshot error",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
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}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -26,6 +26,10 @@ It has these top-level messages:
|
|||
CreateSnapshotRequest
|
||||
CreateSnapshotResponse
|
||||
DeleteSnapshotRequest
|
||||
GetVolumeIDRequest
|
||||
GetVolumeIDResponse
|
||||
SetVolumeIDRequest
|
||||
SetVolumeIDResponse
|
||||
PutObjectRequest
|
||||
GetObjectRequest
|
||||
Bytes
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 boolptr.IsSetToFalse(ctx.restore.Spec.RestorePVs) {
|
||||
// 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 {
|
||||
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
|
||||
// do a snapshot restore
|
||||
if sourceType, _ := kube.GetPVSource(spec); sourceType == "" {
|
||||
return obj, nil, nil
|
||||
updated2, ok := updated1.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected type %T", updated1)
|
||||
}
|
||||
|
||||
var (
|
||||
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
|
||||
return updated2, nil
|
||||
}
|
||||
|
||||
func isPVReady(obj runtime.Unstructured) bool {
|
||||
|
|
|
@ -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,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").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: "volume type and IOPS are correctly passed to CreateVolume",
|
||||
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,
|
||||
expectSetVolumeID: true,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec("xyz").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,
|
||||
},
|
||||
{
|
||||
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",
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 boolptr
|
||||
|
||||
// IsSetToTrue returns true if and only if the bool pointer is non-nil and set to true.
|
||||
func IsSetToTrue(b *bool) bool {
|
||||
return b != nil && *b == true
|
||||
}
|
||||
|
||||
// IsSetToFalse returns true if and only if the bool pointer is non-nil and set to false.
|
||||
func IsSetToFalse(b *bool) bool {
|
||||
return b != nil && *b == false
|
||||
}
|
||||
|
||||
// True returns a *bool whose underlying value is true.
|
||||
func True() *bool {
|
||||
t := true
|
||||
return &t
|
||||
}
|
||||
|
||||
// False returns a *bool whose underlying value is false.
|
||||
func False() *bool {
|
||||
t := false
|
||||
return &t
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
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 TestNamespaceAndName(t *testing.T) {
|
||||
//TODO
|
||||
}
|
||||
|
||||
func TestEnsureNamespaceExists(t *testing.T) {
|
||||
//TODO
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue