make --snapshot-volumes, --restore-volumes optional with sensible default behavior based on PVProvider existence
Signed-off-by: Steve Kriss <steve@heptio.com>pull/43/head
parent
8d5c8ffcbb
commit
768aed4ddd
|
@ -23,7 +23,7 @@ ark backup create NAME
|
|||
-o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'.
|
||||
-l, --selector labelSelector only back up resources matching this label selector (default <none>)
|
||||
--show-labels show labels in the last column
|
||||
--snapshot-volumes take snapshots of PersistentVolumes as part of the backup (default true)
|
||||
--snapshot-volumes optionalBool[=true] take snapshots of PersistentVolumes as part of the backup
|
||||
--ttl duration how long before the backup can be garbage collected (default 24h0m0s)
|
||||
```
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ ark restore create BACKUP
|
|||
--namespace-mappings mapStringString namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,...
|
||||
--namespaces stringArray comma-separated list of namespaces to restore
|
||||
-o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'.
|
||||
--restore-volumes whether to restore volumes from snapshots (default true)
|
||||
--restore-volumes optionalBool[=true] whether to restore volumes from snapshots
|
||||
-l, --selector labelSelector only restore resources matching this label selector (default <none>)
|
||||
--show-labels show labels in the last column
|
||||
```
|
||||
|
|
|
@ -24,7 +24,7 @@ ark schedule create NAME
|
|||
--schedule string a cron expression specifying a recurring schedule for this backup to run
|
||||
-l, --selector labelSelector only back up resources matching this label selector (default <none>)
|
||||
--show-labels show labels in the last column
|
||||
--snapshot-volumes take snapshots of PersistentVolumes as part of the backup (default true)
|
||||
--snapshot-volumes optionalBool[=true] take snapshots of PersistentVolumes as part of the backup
|
||||
--ttl duration how long before the backup can be garbage collected (default 24h0m0s)
|
||||
```
|
||||
|
||||
|
|
|
@ -41,10 +41,10 @@ type BackupSpec struct {
|
|||
// or nil, all objects are included. Optional.
|
||||
LabelSelector *metav1.LabelSelector `json:"labelSelector"`
|
||||
|
||||
// SnapshotVolumes is a bool which specifies whether to take
|
||||
// cloud snapshots of any PV's referenced in the set of objects
|
||||
// included in the Backup.
|
||||
SnapshotVolumes bool `json:"snapshotVolumes"`
|
||||
// SnapshotVolumes specifies whether to take cloud snapshots
|
||||
// of any PV's referenced in the set of objects included
|
||||
// in the Backup.
|
||||
SnapshotVolumes *bool `json:"snapshotVolumes"`
|
||||
|
||||
// TTL is a time.Duration-parseable string describing how long
|
||||
// the Backup should be retained for.
|
||||
|
|
|
@ -38,9 +38,9 @@ type RestoreSpec struct {
|
|||
// or nil, all objects are included. Optional.
|
||||
LabelSelector *metav1.LabelSelector `json:"labelSelector"`
|
||||
|
||||
// RestorePVs is a bool defining whether to restore all included
|
||||
// PVs from snapshot (via the cloudprovider). Default false.
|
||||
RestorePVs bool `json:"restorePVs"`
|
||||
// RestorePVs specifies whether to restore all included
|
||||
// PVs from snapshot (via the cloudprovider).
|
||||
RestorePVs *bool `json:"restorePVs"`
|
||||
}
|
||||
|
||||
// RestorePhase is a string representation of the lifecycle phase
|
||||
|
|
|
@ -364,7 +364,10 @@ func (*realItemBackupper) backupItem(ctx *backupContext, item map[string]interfa
|
|||
|
||||
if action != nil {
|
||||
glog.V(4).Infof("Executing action on %s, ns=%s, name=%s", groupResource, namespace, name)
|
||||
action.Execute(item, ctx.backup)
|
||||
|
||||
if err := action.Execute(item, ctx.backup); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Backing up resource=%s, ns=%s, name=%s", groupResource, namespace, name)
|
||||
|
|
|
@ -19,7 +19,6 @@ package backup
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
|
@ -27,7 +26,7 @@ import (
|
|||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
)
|
||||
|
||||
// volumeSnapshotAction is a struct that knows how to take snapshots of PersistentVolumes
|
||||
|
@ -55,7 +54,7 @@ func NewVolumeSnapshotAction(snapshotService cloudprovider.SnapshotService) (Act
|
|||
// disk type and IOPS (if applicable) to be able to restore to current state later.
|
||||
func (a *volumeSnapshotAction) Execute(volume map[string]interface{}, backup *api.Backup) error {
|
||||
backupName := fmt.Sprintf("%s/%s", backup.Namespace, backup.Name)
|
||||
if !backup.Spec.SnapshotVolumes {
|
||||
if backup.Spec.SnapshotVolumes != nil && !*backup.Spec.SnapshotVolumes {
|
||||
glog.V(2).Infof("Backup %q has volume snapshots disabled; skipping volume snapshot action.", backupName)
|
||||
return nil
|
||||
}
|
||||
|
@ -63,14 +62,20 @@ func (a *volumeSnapshotAction) Execute(volume map[string]interface{}, backup *ap
|
|||
metadata := volume["metadata"].(map[string]interface{})
|
||||
name := metadata["name"].(string)
|
||||
|
||||
volumeID := getVolumeID(volume)
|
||||
volumeID, err := kubeutil.GetVolumeID(volume)
|
||||
// non-nil error means it's a supported PV source but volume ID can't be found
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting volume ID for backup %q, PersistentVolume %q: %v", backupName, name, err)
|
||||
}
|
||||
// no volumeID / nil error means unsupported PV source
|
||||
if volumeID == "" {
|
||||
return fmt.Errorf("unable to determine volume ID for backup %q, PersistentVolume %q", backupName, name)
|
||||
glog.V(2).Infof("Backup %q: PersistentVolume %q is not a supported volume type for snapshots, skipping.", backupName, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
expiration := a.clock.Now().Add(backup.Spec.TTL.Duration)
|
||||
|
||||
glog.Infof("Backup %q: snapshotting PersistenVolume %q, volume-id %q, expiration %v", backupName, name, volumeID, expiration)
|
||||
glog.Infof("Backup %q: snapshotting PersistentVolume %q, volume-id %q, expiration %v", backupName, name, volumeID, expiration)
|
||||
|
||||
snapshotID, err := a.snapshotService.CreateSnapshot(volumeID)
|
||||
if err != nil {
|
||||
|
@ -96,38 +101,3 @@ func (a *volumeSnapshotAction) Execute(volume map[string]interface{}, backup *ap
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ebsVolumeIDRegex = regexp.MustCompile("vol-.*")
|
||||
|
||||
func getVolumeID(pv map[string]interface{}) string {
|
||||
spec, err := collections.GetMap(pv, "spec")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if aws, err := collections.GetMap(spec, "awsElasticBlockStore"); err == nil {
|
||||
volumeID, err := collections.GetString(aws, "volumeID")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return ebsVolumeIDRegex.FindString(volumeID)
|
||||
}
|
||||
|
||||
if gce, err := collections.GetMap(spec, "gcePersistentDisk"); err == nil {
|
||||
volumeID, err := collections.GetString(gce, "pdName")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return volumeID
|
||||
}
|
||||
|
||||
if gce, err := collections.GetMap(spec, "azureDisk"); err == nil {
|
||||
volumeID, err := collections.GetString(gce, "diskName")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return volumeID
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
ttl time.Duration
|
||||
expectError bool
|
||||
expectedVolumeID string
|
||||
expectedSnapshotsTaken int
|
||||
existingVolumeBackups map[string]*v1.VolumeBackupInfo
|
||||
volumeInfo map[string]v1.VolumeBackupInfo
|
||||
}{
|
||||
|
@ -55,10 +56,10 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - spec but no volume source defined",
|
||||
name: "unsupported PV source type",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {}}`,
|
||||
expectError: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"unsupportedPVSource": {}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - aws but no volume id",
|
||||
|
@ -77,6 +78,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"awsElasticBlockStore": {"volumeID": "vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
|
@ -88,6 +90,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"awsElasticBlockStore": {"volumeID": "vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
|
@ -99,6 +102,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "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{
|
||||
|
@ -110,17 +114,31 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"pd-abc123": v1.VolumeBackupInfo{Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
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": v1.VolumeBackupInfo{Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preexisting volume backup info in backup status",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
existingVolumeBackups: map[string]*v1.VolumeBackupInfo{
|
||||
|
@ -146,7 +164,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
Name: "mybackup",
|
||||
},
|
||||
Spec: v1.BackupSpec{
|
||||
SnapshotVolumes: test.snapshotEnabled,
|
||||
SnapshotVolumes: &test.snapshotEnabled,
|
||||
TTL: metav1.Duration{Duration: test.ttl},
|
||||
},
|
||||
Status: v1.BackupStatus{
|
||||
|
@ -188,8 +206,9 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
}
|
||||
|
||||
// we should have one snapshot taken exactly
|
||||
require.Equal(t, 1, snapshotService.SnapshotsTaken.Len())
|
||||
require.Equal(t, test.expectedSnapshotsTaken, snapshotService.SnapshotsTaken.Len())
|
||||
|
||||
if test.expectedSnapshotsTaken > 0 {
|
||||
// the snapshotID should be the one in the entry in snapshotService.SnapshottableVolumes
|
||||
// for the volume we ran the test for
|
||||
snapshotID, _ := snapshotService.SnapshotsTaken.PopAny()
|
||||
|
@ -203,6 +222,7 @@ func TestVolumeSnapshotAction(t *testing.T) {
|
|||
if e, a := expectedVolumeBackups, backup.Status.VolumeBackups; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("backup.status.VolumeBackups: expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ func NewCreateCommand(f client.Factory) *cobra.Command {
|
|||
type CreateOptions struct {
|
||||
Name string
|
||||
TTL time.Duration
|
||||
SnapshotVolumes bool
|
||||
SnapshotVolumes flag.OptionalBool
|
||||
IncludeNamespaces flag.StringArray
|
||||
ExcludeNamespaces flag.StringArray
|
||||
IncludeResources flag.StringArray
|
||||
|
@ -70,19 +70,22 @@ func NewCreateOptions() *CreateOptions {
|
|||
TTL: 24 * time.Hour,
|
||||
IncludeNamespaces: flag.NewStringArray("*"),
|
||||
Labels: flag.NewMap(),
|
||||
SnapshotVolumes: true,
|
||||
SnapshotVolumes: flag.NewOptionalBool(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.DurationVar(&o.TTL, "ttl", o.TTL, "how long before the backup can be garbage collected")
|
||||
flags.BoolVar(&o.SnapshotVolumes, "snapshot-volumes", o.SnapshotVolumes, "take snapshots of PersistentVolumes as part of the backup")
|
||||
flags.Var(&o.IncludeNamespaces, "include-namespaces", "namespaces to include in the backup (use '*' for all namespaces)")
|
||||
flags.Var(&o.ExcludeNamespaces, "exclude-namespaces", "namespaces to exclude from the backup")
|
||||
flags.Var(&o.IncludeResources, "include-resources", "resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources)")
|
||||
flags.Var(&o.ExcludeResources, "exclude-resources", "resources to exclude from the backup, formatted as resource.group, such as storageclasses.storage.k8s.io")
|
||||
flags.Var(&o.Labels, "labels", "labels to apply to the backup")
|
||||
flags.VarP(&o.Selector, "selector", "l", "only back up resources matching this label selector")
|
||||
f := flags.VarPF(&o.SnapshotVolumes, "snapshot-volumes", "", "take snapshots of PersistentVolumes as part of the backup")
|
||||
// this allows the user to just specify "--snapshot-volumes" as shorthand for "--snapshot-volumes=true"
|
||||
// like a normal bool flag
|
||||
f.NoOptDefVal = "true"
|
||||
}
|
||||
|
||||
func (o *CreateOptions) Validate(c *cobra.Command, args []string) error {
|
||||
|
@ -120,7 +123,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
|||
IncludedResources: o.IncludeResources,
|
||||
ExcludedResources: o.ExcludeResources,
|
||||
LabelSelector: o.Selector.LabelSelector,
|
||||
SnapshotVolumes: o.SnapshotVolumes,
|
||||
SnapshotVolumes: o.SnapshotVolumes.Value,
|
||||
TTL: metav1.Duration{Duration: o.TTL},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ func NewCreateCommand(f client.Factory) *cobra.Command {
|
|||
|
||||
type CreateOptions struct {
|
||||
BackupName string
|
||||
RestoreVolumes bool
|
||||
RestoreVolumes flag.OptionalBool
|
||||
Labels flag.Map
|
||||
Namespaces flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
|
@ -66,16 +66,19 @@ func NewCreateOptions() *CreateOptions {
|
|||
return &CreateOptions{
|
||||
Labels: flag.NewMap(),
|
||||
NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"),
|
||||
RestoreVolumes: true,
|
||||
RestoreVolumes: flag.NewOptionalBool(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&o.RestoreVolumes, "restore-volumes", o.RestoreVolumes, "whether to restore volumes from snapshots")
|
||||
flags.Var(&o.Labels, "labels", "labels to apply to the restore")
|
||||
flags.Var(&o.Namespaces, "namespaces", "comma-separated list of namespaces to restore")
|
||||
flags.Var(&o.NamespaceMappings, "namespace-mappings", "namespace mappings from name in the backup to desired restored name in the form src1:dst1,src2:dst2,...")
|
||||
flags.VarP(&o.Selector, "selector", "l", "only restore resources matching this label selector")
|
||||
f := flags.VarPF(&o.RestoreVolumes, "restore-volumes", "", "whether to restore volumes from snapshots")
|
||||
// this allows the user to just specify "--restore-volumes" as shorthand for "--restore-volumes=true"
|
||||
// like a normal bool flag
|
||||
f.NoOptDefVal = "true"
|
||||
}
|
||||
|
||||
func (o *CreateOptions) Validate(c *cobra.Command, args []string) error {
|
||||
|
@ -112,7 +115,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
|||
Namespaces: o.Namespaces,
|
||||
NamespaceMapping: o.NamespaceMappings.Data(),
|
||||
LabelSelector: o.Selector.LabelSelector,
|
||||
RestorePVs: o.RestoreVolumes,
|
||||
RestorePVs: o.RestoreVolumes.Value,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
|||
IncludedResources: o.BackupOptions.IncludeResources,
|
||||
ExcludedResources: o.BackupOptions.ExcludeResources,
|
||||
LabelSelector: o.BackupOptions.Selector.LabelSelector,
|
||||
SnapshotVolumes: o.BackupOptions.SnapshotVolumes,
|
||||
SnapshotVolumes: o.BackupOptions.SnapshotVolumes.Value,
|
||||
TTL: metav1.Duration{Duration: o.BackupOptions.TTL},
|
||||
},
|
||||
Schedule: o.Schedule,
|
||||
|
|
|
@ -250,7 +250,7 @@ func (s *server) initBackupService(config *api.Config) error {
|
|||
|
||||
func (s *server) initSnapshotService(config *api.Config) error {
|
||||
if config.PersistentVolumeProvider == nil {
|
||||
glog.Infof("PersistentVolumeProvider config not provided, skipping SnapshotService creation")
|
||||
glog.Infof("PersistentVolumeProvider config not provided, volume snapshots and restores are disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2017 Heptio Inc.
|
||||
|
||||
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 flag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// Enum is a Cobra-compatible wrapper for defining
|
||||
// a string flag that can be one of a specified set
|
||||
// of values.
|
||||
type Enum struct {
|
||||
allowedValues sets.String
|
||||
value string
|
||||
}
|
||||
|
||||
// NewEnum returns a new enum flag with the specified list
|
||||
// of allowed values. The first value specified is used
|
||||
// as the default.
|
||||
func NewEnum(allowedValues ...string) Enum {
|
||||
return Enum{
|
||||
allowedValues: sets.NewString(allowedValues...),
|
||||
value: allowedValues[0],
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of the
|
||||
// enum flag.
|
||||
func (e *Enum) String() string {
|
||||
return e.value
|
||||
}
|
||||
|
||||
// Set assigns the provided string to the enum
|
||||
// receiver. It returns an error if the string
|
||||
// is not an allowed value.
|
||||
func (e *Enum) Set(s string) error {
|
||||
if !e.allowedValues.Has(s) {
|
||||
return fmt.Errorf("invalid value: %q", s)
|
||||
}
|
||||
|
||||
e.value = s
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns a string representation of the
|
||||
// Enum type.
|
||||
func (e *Enum) Type() string {
|
||||
return "enum"
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2017 Heptio Inc.
|
||||
|
||||
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 flag
|
||||
|
||||
import "strconv"
|
||||
|
||||
type OptionalBool struct {
|
||||
Value *bool
|
||||
}
|
||||
|
||||
func NewOptionalBool(defaultValue *bool) OptionalBool {
|
||||
return OptionalBool{
|
||||
Value: defaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of the
|
||||
// enum flag.
|
||||
func (f *OptionalBool) String() string {
|
||||
switch f.Value {
|
||||
case nil:
|
||||
return "<nil>"
|
||||
default:
|
||||
return strconv.FormatBool(*f.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *OptionalBool) Set(val string) error {
|
||||
if val == "" {
|
||||
f.Value = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Value = &parsed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *OptionalBool) Type() string {
|
||||
return "optionalBool"
|
||||
}
|
|
@ -52,7 +52,7 @@ type backupController struct {
|
|||
backupper backup.Backupper
|
||||
backupService cloudprovider.BackupService
|
||||
bucket string
|
||||
allowSnapshots bool
|
||||
pvProviderExists bool
|
||||
|
||||
lister listers.BackupLister
|
||||
listerSynced cache.InformerSynced
|
||||
|
@ -69,13 +69,13 @@ func NewBackupController(
|
|||
backupper backup.Backupper,
|
||||
backupService cloudprovider.BackupService,
|
||||
bucket string,
|
||||
allowSnapshots bool,
|
||||
pvProviderExists bool,
|
||||
) Interface {
|
||||
c := &backupController{
|
||||
backupper: backupper,
|
||||
backupService: backupService,
|
||||
bucket: bucket,
|
||||
allowSnapshots: allowSnapshots,
|
||||
pvProviderExists: pvProviderExists,
|
||||
|
||||
lister: backupInformer.Lister(),
|
||||
listerSynced: backupInformer.Informer().HasSynced,
|
||||
|
@ -300,7 +300,7 @@ func (controller *backupController) getValidationErrors(itm *api.Backup) []strin
|
|||
validationErrors = append(validationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err))
|
||||
}
|
||||
|
||||
if !controller.allowSnapshots && itm.Spec.SnapshotVolumes {
|
||||
if !controller.pvProviderExists && itm.Spec.SnapshotVolumes != nil && *itm.Spec.SnapshotVolumes {
|
||||
validationErrors = append(validationErrors, "Server is not configured for PV snapshots")
|
||||
}
|
||||
|
||||
|
|
|
@ -243,7 +243,7 @@ func TestProcessBackup(t *testing.T) {
|
|||
WithExcludedResources(test.expectedExcludes...).
|
||||
WithIncludedNamespaces(expectedNSes...).
|
||||
WithTTL(test.backup.Spec.TTL.Duration).
|
||||
WithSnapshotVolumes(test.backup.Spec.SnapshotVolumes).
|
||||
WithSnapshotVolumesPointer(test.backup.Spec.SnapshotVolumes).
|
||||
WithExpiration(expiration).
|
||||
WithVersion(1).
|
||||
Backup,
|
||||
|
@ -259,7 +259,7 @@ func TestProcessBackup(t *testing.T) {
|
|||
WithExcludedResources(test.expectedExcludes...).
|
||||
WithIncludedNamespaces(expectedNSes...).
|
||||
WithTTL(test.backup.Spec.TTL.Duration).
|
||||
WithSnapshotVolumes(test.backup.Spec.SnapshotVolumes).
|
||||
WithSnapshotVolumesPointer(test.backup.Spec.SnapshotVolumes).
|
||||
WithExpiration(expiration).
|
||||
WithVersion(1).
|
||||
Backup,
|
||||
|
|
|
@ -47,7 +47,7 @@ type restoreController struct {
|
|||
restorer restore.Restorer
|
||||
backupService cloudprovider.BackupService
|
||||
bucket string
|
||||
allowSnapshotRestores bool
|
||||
pvProviderExists bool
|
||||
|
||||
backupLister listers.BackupLister
|
||||
backupListerSynced cache.InformerSynced
|
||||
|
@ -65,7 +65,7 @@ func NewRestoreController(
|
|||
backupService cloudprovider.BackupService,
|
||||
bucket string,
|
||||
backupInformer informers.BackupInformer,
|
||||
allowSnapshotRestores bool,
|
||||
pvProviderExists bool,
|
||||
) Interface {
|
||||
c := &restoreController{
|
||||
restoreClient: restoreClient,
|
||||
|
@ -73,7 +73,7 @@ func NewRestoreController(
|
|||
restorer: restorer,
|
||||
backupService: backupService,
|
||||
bucket: bucket,
|
||||
allowSnapshotRestores: allowSnapshotRestores,
|
||||
pvProviderExists: pvProviderExists,
|
||||
backupLister: backupInformer.Lister(),
|
||||
backupListerSynced: backupInformer.Informer().HasSynced,
|
||||
restoreLister: restoreInformer.Lister(),
|
||||
|
@ -278,7 +278,7 @@ func (controller *restoreController) getValidationErrors(itm *api.Restore) []str
|
|||
validationErrors = append(validationErrors, "BackupName must be non-empty and correspond to the name of a backup in object storage.")
|
||||
}
|
||||
|
||||
if !controller.allowSnapshotRestores && itm.Spec.RestorePVs {
|
||||
if !controller.pvProviderExists && itm.Spec.RestorePVs != nil && *itm.Spec.RestorePVs {
|
||||
validationErrors = append(validationErrors, "Server is not configured for PV snapshot restores")
|
||||
}
|
||||
|
||||
|
|
|
@ -382,7 +382,6 @@ func TestGetBackup(t *testing.T) {
|
|||
IncludedResources: []string{"foo", "bar"},
|
||||
ExcludedResources: []string{"baz"},
|
||||
LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
|
||||
SnapshotVolumes: true,
|
||||
TTL: metav1.Duration{Duration: time.Duration(300)},
|
||||
},
|
||||
},
|
||||
|
@ -399,7 +398,6 @@ func TestGetBackup(t *testing.T) {
|
|||
IncludedResources: []string{"foo", "bar"},
|
||||
ExcludedResources: []string{"baz"},
|
||||
LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
|
||||
SnapshotVolumes: true,
|
||||
TTL: metav1.Duration{Duration: time.Duration(300)},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -432,7 +432,10 @@ func (kr *kubernetesRestorer) restoreResourceForNamespace(
|
|||
continue
|
||||
}
|
||||
|
||||
preparedObj, err := restorer.Prepare(obj, restore, backup)
|
||||
preparedObj, warning, err := restorer.Prepare(obj, restore, backup)
|
||||
if warning != nil {
|
||||
addToResult(&warnings, namespace, fmt.Errorf("warning preparing %s: %v", fullPath, warning))
|
||||
}
|
||||
if err != nil {
|
||||
addToResult(&errors, namespace, fmt.Errorf("error preparing %s: %v", fullPath, err))
|
||||
continue
|
||||
|
|
|
@ -607,10 +607,10 @@ func newFakeCustomRestorer() *fakeCustomRestorer {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *fakeCustomRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (r *fakeCustomRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if _, found := metadata["labels"]; !found {
|
||||
|
|
|
@ -37,11 +37,11 @@ func (r *jobRestorer) Handles(obj runtime.Unstructured, restore *api.Restore) bo
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *jobRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (r *jobRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
glog.V(4).Infof("resetting metadata and status")
|
||||
_, err := resetMetadataAndStatus(obj, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
glog.V(4).Infof("getting spec.selector.matchLabels")
|
||||
|
@ -59,7 +59,7 @@ func (r *jobRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, ba
|
|||
delete(templateLabels, "controller-uid")
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
return obj, nil, nil
|
||||
}
|
||||
|
||||
func (r *jobRestorer) Wait() bool {
|
||||
|
|
|
@ -128,7 +128,7 @@ func TestJobRestorerPrepare(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
restorer := NewJobRestorer()
|
||||
|
||||
res, err := restorer.Prepare(test.obj, nil, nil)
|
||||
res, _, err := restorer.Prepare(test.obj, nil, nil)
|
||||
|
||||
if assert.Equal(t, test.expectedErr, err != nil) {
|
||||
assert.Equal(t, test.expectedRes, res)
|
||||
|
|
|
@ -46,27 +46,27 @@ func (nsr *namespaceRestorer) Handles(obj runtime.Unstructured, restore *api.Res
|
|||
return false
|
||||
}
|
||||
|
||||
func (nsr *namespaceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (nsr *namespaceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
updated, err := resetMetadataAndStatus(obj, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
currentName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if newName, mapped := restore.Spec.NamespaceMapping[currentName]; mapped {
|
||||
metadata["name"] = newName
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
return updated, nil, nil
|
||||
}
|
||||
|
||||
func (nsr *namespaceRestorer) Wait() bool {
|
||||
|
|
|
@ -19,11 +19,12 @@ package restorers
|
|||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
testutil "github.com/heptio/ark/pkg/util/test"
|
||||
)
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
|
@ -36,19 +37,19 @@ func TestHandles(t *testing.T) {
|
|||
{
|
||||
name: "restorable NS",
|
||||
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
|
||||
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
|
||||
restore: testutil.NewDefaultTestRestore().WithRestorableNamespace("ns-1").Restore,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "non-restorable NS",
|
||||
obj: NewTestUnstructured().WithName("ns-1").Unstructured,
|
||||
restore: newTestRestore().WithRestorableNamespace("ns-2").Restore,
|
||||
restore: testutil.NewDefaultTestRestore().WithRestorableNamespace("ns-2").Restore,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "namespace obj doesn't have name",
|
||||
obj: NewTestUnstructured().WithMetadata().Unstructured,
|
||||
restore: newTestRestore().WithRestorableNamespace("ns-1").Restore,
|
||||
restore: testutil.NewDefaultTestRestore().WithRestorableNamespace("ns-1").Restore,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
@ -72,27 +73,27 @@ func TestPrepare(t *testing.T) {
|
|||
{
|
||||
name: "standard non-mapped namespace",
|
||||
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
|
||||
restore: newTestRestore().Restore,
|
||||
restore: testutil.NewDefaultTestRestore().Restore,
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("ns-1").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "standard mapped namespace",
|
||||
obj: NewTestUnstructured().WithStatus().WithName("ns-1").Unstructured,
|
||||
restore: newTestRestore().WithMappedNamespace("ns-1", "ns-2").Restore,
|
||||
restore: testutil.NewDefaultTestRestore().WithMappedNamespace("ns-1", "ns-2").Restore,
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("ns-2").Unstructured,
|
||||
},
|
||||
{
|
||||
name: "object without name results in error",
|
||||
obj: NewTestUnstructured().WithMetadata().WithStatus().Unstructured,
|
||||
restore: newTestRestore().Restore,
|
||||
restore: testutil.NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotations are kept",
|
||||
obj: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
|
||||
restore: newTestRestore().Restore,
|
||||
restore: testutil.NewDefaultTestRestore().Restore,
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("ns-1").WithAnnotations().Unstructured,
|
||||
},
|
||||
|
@ -102,7 +103,7 @@ func TestPrepare(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
restorer := NewNamespaceRestorer()
|
||||
|
||||
res, err := restorer.Prepare(test.obj, test.restore, nil)
|
||||
res, _, err := restorer.Prepare(test.obj, test.restore, nil)
|
||||
|
||||
if assert.Equal(t, test.expectedErr, err != nil) {
|
||||
assert.Equal(t, test.expectedRes, res)
|
||||
|
@ -110,36 +111,3 @@ func TestPrepare(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testRestore struct {
|
||||
*api.Restore
|
||||
}
|
||||
|
||||
func newTestRestore() *testRestore {
|
||||
return &testRestore{
|
||||
Restore: &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: api.DefaultNamespace,
|
||||
},
|
||||
Spec: api.RestoreSpec{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *testRestore) WithRestorableNamespace(namespace string) *testRestore {
|
||||
r.Spec.Namespaces = append(r.Spec.Namespaces, namespace)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *testRestore) WithMappedNamespace(from string, to string) *testRestore {
|
||||
if r.Spec.NamespaceMapping == nil {
|
||||
r.Spec.NamespaceMapping = make(map[string]string)
|
||||
}
|
||||
r.Spec.NamespaceMapping[from] = to
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *testRestore) WithRestorePVs(restorePVs bool) *testRestore {
|
||||
r.Spec.RestorePVs = restorePVs
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -43,17 +43,17 @@ var (
|
|||
defaultTokenRegex = regexp.MustCompile("default-token-.*")
|
||||
)
|
||||
|
||||
func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
glog.V(4).Infof("resetting metadata and status")
|
||||
_, err := resetMetadataAndStatus(obj, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
glog.V(4).Infof("getting spec")
|
||||
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
glog.V(4).Infof("deleting spec.NodeName")
|
||||
|
@ -79,7 +79,7 @@ func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore,
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
glog.V(4).Infof("setting spec.volumes")
|
||||
|
@ -114,10 +114,10 @@ func (nsr *podRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore,
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
return obj, nil, nil
|
||||
}
|
||||
|
||||
func (nsr *podRestorer) Wait() bool {
|
||||
|
|
|
@ -98,7 +98,7 @@ func TestPodRestorerPrepare(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
restorer := NewPodRestorer()
|
||||
|
||||
res, err := restorer.Prepare(test.obj, nil, nil)
|
||||
res, _, err := restorer.Prepare(test.obj, nil, nil)
|
||||
|
||||
if assert.Equal(t, test.expectedErr, err != nil) {
|
||||
assert.Equal(t, test.expectedRes, res)
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
)
|
||||
|
||||
type persistentVolumeRestorer struct {
|
||||
|
@ -43,35 +44,79 @@ func (sr *persistentVolumeRestorer) Handles(obj runtime.Unstructured, restore *a
|
|||
return true
|
||||
}
|
||||
|
||||
func (sr *persistentVolumeRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (sr *persistentVolumeRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
if _, err := resetMetadataAndStatus(obj, false); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
delete(spec, "claimRef")
|
||||
delete(spec, "storageClassName")
|
||||
|
||||
if restore.Spec.RestorePVs {
|
||||
if sr.snapshotService == nil {
|
||||
return nil, errors.New("PV restorer is not configured for PV snapshot restores")
|
||||
}
|
||||
|
||||
volumeID, err := sr.restoreVolume(obj.UnstructuredContent(), restore, backup)
|
||||
pvName, err := collections.GetString(obj.UnstructuredContent(), "metadata.name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := setVolumeID(spec, volumeID); err != nil {
|
||||
return nil, err
|
||||
// if it's an unsupported volume type for snapshot restores, we're done
|
||||
if sourceType, _ := kubeutil.GetPVSource(spec); sourceType == "" {
|
||||
return obj, nil, nil
|
||||
}
|
||||
|
||||
restoreFromSnapshot := false
|
||||
|
||||
if restore.Spec.RestorePVs != nil && *restore.Spec.RestorePVs {
|
||||
// when RestorePVs = yes, it's an error if we don't have a snapshot service
|
||||
if sr.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, fmt.Errorf("no snapshot found to restore volume %s from", pvName)
|
||||
}
|
||||
|
||||
restoreFromSnapshot = true
|
||||
}
|
||||
if restore.Spec.RestorePVs == nil && sr.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]
|
||||
|
||||
volumeID, err := sr.snapshotService.CreateVolumeFromSnapshot(backupInfo.SnapshotID, backupInfo.Type, backupInfo.Iops)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := kubeutil.SetVolumeID(spec, volumeID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
var warning error
|
||||
|
||||
if sr.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 (sr *persistentVolumeRestorer) Wait() bool {
|
||||
|
@ -83,39 +128,3 @@ func (sr *persistentVolumeRestorer) Ready(obj runtime.Unstructured) bool {
|
|||
|
||||
return err == nil && phase == "Available"
|
||||
}
|
||||
|
||||
func setVolumeID(spec map[string]interface{}, volumeID string) error {
|
||||
if pvSource, found := spec["awsElasticBlockStore"]; found {
|
||||
pvSourceObj := pvSource.(map[string]interface{})
|
||||
pvSourceObj["volumeID"] = volumeID
|
||||
return nil
|
||||
} else if pvSource, found := spec["gcePersistentDisk"]; found {
|
||||
pvSourceObj := pvSource.(map[string]interface{})
|
||||
pvSourceObj["pdName"] = volumeID
|
||||
return nil
|
||||
} else if pvSource, found := spec["azureDisk"]; found {
|
||||
pvSourceObj := pvSource.(map[string]interface{})
|
||||
pvSourceObj["diskName"] = volumeID
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("persistent volume source is not compatible")
|
||||
}
|
||||
|
||||
func (sr *persistentVolumeRestorer) restoreVolume(item map[string]interface{}, restore *api.Restore, backup *api.Backup) (string, error) {
|
||||
pvName, err := collections.GetString(item, "metadata.name")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if backup.Status.VolumeBackups == nil {
|
||||
return "", fmt.Errorf("VolumeBackups map not found for persistent volume %s", pvName)
|
||||
}
|
||||
|
||||
backupInfo, found := backup.Status.VolumeBackups[pvName]
|
||||
if !found {
|
||||
return "", fmt.Errorf("BackupInfo not found for PersistentVolume %s", pvName)
|
||||
}
|
||||
|
||||
return sr.snapshotService.CreateVolumeFromSnapshot(backupInfo.SnapshotID, backupInfo.Type, backupInfo.Iops)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
. "github.com/heptio/ark/pkg/util/test"
|
||||
)
|
||||
|
||||
|
@ -37,36 +38,45 @@ func TestPVRestorerPrepare(t *testing.T) {
|
|||
restore *api.Restore
|
||||
backup *api.Backup
|
||||
volumeMap map[api.VolumeBackupInfo]string
|
||||
noSnapshotService bool
|
||||
expectedWarn bool
|
||||
expectedErr bool
|
||||
expectedRes runtime.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "no name should error",
|
||||
obj: NewTestUnstructured().WithMetadata().Unstructured,
|
||||
restore: newTestRestore().Restore,
|
||||
restore: NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "no spec should error",
|
||||
obj: NewTestUnstructured().WithName("pv-1").Unstructured,
|
||||
restore: newTestRestore().Restore,
|
||||
restore: NewDefaultTestRestore().Restore,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "when RestorePVs=false, should not error if there is no PV->BackupInfo map",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(false).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{}},
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
},
|
||||
{
|
||||
name: "when RestorePVs=true, error if there is no PV->BackupInfo map",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpec().Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(true).Restore,
|
||||
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,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{}},
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).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,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"another-pv": &api.VolumeBackupInfo{}}}},
|
||||
expectedErr: true,
|
||||
expectedRes: nil,
|
||||
},
|
||||
{
|
||||
name: "claimRef and storageClassName (only) should be cleared from spec",
|
||||
|
@ -76,7 +86,7 @@ func TestPVRestorerPrepare(t *testing.T) {
|
|||
WithSpecField("storageClassName", "foo").
|
||||
WithSpecField("foo", "bar").
|
||||
Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(false).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(false).Restore,
|
||||
expectedErr: false,
|
||||
expectedRes: NewTestUnstructured().
|
||||
WithName("pv-1").
|
||||
|
@ -86,7 +96,7 @@ func TestPVRestorerPrepare(t *testing.T) {
|
|||
{
|
||||
name: "when RestorePVs=true, AWS volume ID should be set correctly",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("awsElasticBlockStore", make(map[string]interface{})).Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(true).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
|
||||
expectedErr: false,
|
||||
|
@ -95,7 +105,7 @@ func TestPVRestorerPrepare(t *testing.T) {
|
|||
{
|
||||
name: "when RestorePVs=true, GCE pdName should be set correctly",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("gcePersistentDisk", make(map[string]interface{})).Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(true).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
|
||||
expectedErr: false,
|
||||
|
@ -104,37 +114,54 @@ func TestPVRestorerPrepare(t *testing.T) {
|
|||
{
|
||||
name: "when RestorePVs=true, Azure pdName should be set correctly",
|
||||
obj: NewTestUnstructured().WithName("pv-1").WithSpecField("azureDisk", make(map[string]interface{})).Unstructured,
|
||||
restore: newTestRestore().WithRestorePVs(true).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{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 cause error",
|
||||
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: newTestRestore().WithRestorePVs(true).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
|
||||
expectedErr: true,
|
||||
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: newTestRestore().WithRestorePVs(true).Restore,
|
||||
restore: NewDefaultTestRestore().WithRestorePVs(true).Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1", Type: "gp", Iops: &iops}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{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,
|
||||
restore: NewDefaultTestRestore().Restore,
|
||||
backup: &api.Backup{Status: api.BackupStatus{VolumeBackups: map[string]*api.VolumeBackupInfo{"pv-1": &api.VolumeBackupInfo{SnapshotID: "snap-1"}}}},
|
||||
volumeMap: map[api.VolumeBackupInfo]string{api.VolumeBackupInfo{SnapshotID: "snap-1"}: "volume-1"},
|
||||
noSnapshotService: true,
|
||||
expectedErr: false,
|
||||
expectedWarn: 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) {
|
||||
snapService := &FakeSnapshotService{RestorableVolumes: test.volumeMap}
|
||||
restorer := NewPersistentVolumeRestorer(snapService)
|
||||
var snapshotService cloudprovider.SnapshotService
|
||||
if !test.noSnapshotService {
|
||||
snapshotService = &FakeSnapshotService{RestorableVolumes: test.volumeMap}
|
||||
}
|
||||
restorer := NewPersistentVolumeRestorer(snapshotService)
|
||||
|
||||
res, err := restorer.Prepare(test.obj, test.restore, test.backup)
|
||||
res, warn, err := restorer.Prepare(test.obj, test.restore, test.backup)
|
||||
|
||||
assert.Equal(t, test.expectedWarn, warn != nil)
|
||||
|
||||
if assert.Equal(t, test.expectedErr, err != nil) {
|
||||
assert.Equal(t, test.expectedRes, res)
|
||||
|
|
|
@ -35,8 +35,10 @@ func (sr *persistentVolumeClaimRestorer) Handles(obj runtime.Unstructured, resto
|
|||
return true
|
||||
}
|
||||
|
||||
func (sr *persistentVolumeClaimRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
return resetMetadataAndStatus(obj, true)
|
||||
func (sr *persistentVolumeClaimRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
res, err := resetMetadataAndStatus(obj, true)
|
||||
|
||||
return res, nil, err
|
||||
}
|
||||
|
||||
func (sr *persistentVolumeClaimRestorer) Wait() bool {
|
||||
|
|
|
@ -29,8 +29,8 @@ type ResourceRestorer interface {
|
|||
// Handles returns true if the Restorer should restore this object.
|
||||
Handles(obj runtime.Unstructured, restore *api.Restore) bool
|
||||
|
||||
// Prepare gets an item ready to be restored
|
||||
Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error)
|
||||
// Prepare gets an item ready to be restored.
|
||||
Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (res runtime.Unstructured, warning error, err error)
|
||||
|
||||
// Wait returns true if restoration should wait for all of this restorer's resources to be ready before moving on to the next restorer.
|
||||
Wait() bool
|
||||
|
@ -66,8 +66,10 @@ func (br *basicRestorer) Handles(obj runtime.Unstructured, restore *api.Restore)
|
|||
return true
|
||||
}
|
||||
|
||||
func (br *basicRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
return resetMetadataAndStatus(obj, br.saveAnnotations)
|
||||
func (br *basicRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
obj, err := resetMetadataAndStatus(obj, br.saveAnnotations)
|
||||
|
||||
return obj, err, nil
|
||||
}
|
||||
|
||||
func (br *basicRestorer) Wait() bool {
|
||||
|
|
|
@ -35,21 +35,21 @@ func (sr *serviceRestorer) Handles(obj runtime.Unstructured, restore *api.Restor
|
|||
return true
|
||||
}
|
||||
|
||||
func (sr *serviceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error) {
|
||||
func (sr *serviceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restore, backup *api.Backup) (runtime.Unstructured, error, error) {
|
||||
if _, err := resetMetadataAndStatus(obj, true); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
delete(spec, "clusterIP")
|
||||
|
||||
ports, err := collections.GetSlice(obj.UnstructuredContent(), "spec.ports")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
|
@ -57,7 +57,7 @@ func (sr *serviceRestorer) Prepare(obj runtime.Unstructured, restore *api.Restor
|
|||
delete(p, "nodePort")
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
return obj, nil, nil
|
||||
}
|
||||
|
||||
func (sr *serviceRestorer) Wait() bool {
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestServiceRestorerPrepare(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
restorer := NewServiceRestorer()
|
||||
|
||||
res, err := restorer.Prepare(test.obj, nil, nil)
|
||||
res, _, err := restorer.Prepare(test.obj, nil, nil)
|
||||
|
||||
if assert.Equal(t, test.expectedErr, err != nil) {
|
||||
assert.Equal(t, test.expectedRes, res)
|
||||
|
|
|
@ -17,9 +17,14 @@ limitations under the License.
|
|||
package kube
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
)
|
||||
|
||||
// EnsureNamespaceExists attempts to create the provided Kubernetes namespace. It returns two values:
|
||||
|
@ -35,3 +40,67 @@ func EnsureNamespaceExists(namespace *v1.Namespace, client corev1.NamespaceInter
|
|||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
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 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")
|
||||
}
|
||||
|
||||
source[supportedVolumeTypes[sourceType]] = volumeID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -106,6 +106,11 @@ func (b *TestBackup) WithSnapshot(pv string, snapshot string) *TestBackup {
|
|||
}
|
||||
|
||||
func (b *TestBackup) WithSnapshotVolumes(value bool) *TestBackup {
|
||||
b.Spec.SnapshotVolumes = &value
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *TestBackup) WithSnapshotVolumesPointer(value *bool) *TestBackup {
|
||||
b.Spec.SnapshotVolumes = value
|
||||
return b
|
||||
}
|
||||
|
|
|
@ -41,6 +41,10 @@ func NewTestRestore(ns, name string, phase api.RestorePhase) *TestRestore {
|
|||
}
|
||||
}
|
||||
|
||||
func NewDefaultTestRestore() *TestRestore {
|
||||
return NewTestRestore(api.DefaultNamespace, "", api.RestorePhase(""))
|
||||
}
|
||||
|
||||
func (r *TestRestore) WithRestorableNamespace(name string) *TestRestore {
|
||||
r.Spec.Namespaces = append(r.Spec.Namespaces, name)
|
||||
return r
|
||||
|
@ -62,6 +66,14 @@ func (r *TestRestore) WithErrors(e api.RestoreResult) *TestRestore {
|
|||
}
|
||||
|
||||
func (r *TestRestore) WithRestorePVs(value bool) *TestRestore {
|
||||
r.Spec.RestorePVs = value
|
||||
r.Spec.RestorePVs = &value
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *TestRestore) WithMappedNamespace(from string, to string) *TestRestore {
|
||||
if r.Spec.NamespaceMapping == nil {
|
||||
r.Spec.NamespaceMapping = make(map[string]string)
|
||||
}
|
||||
r.Spec.NamespaceMapping[from] = to
|
||||
return r
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue