From 6aa0215137237ff29105ad05fc99fa65fc262c80 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Fri, 23 Aug 2019 13:03:51 -0700 Subject: [PATCH] create backups from schedules using velero create backup (#1734) * add --from-schedule to `velero backup create` to create backups from schedules Signed-off-by: Adnan Abdulhussein --- changelogs/unreleased/1734-prydonius | 1 + pkg/builder/backup_builder.go | 19 +++-- pkg/builder/object_meta.go | 19 +++++ pkg/cmd/cli/backup/create.go | 86 ++++++++++++++------- pkg/cmd/cli/backup/create_test.go | 88 ++++++++++++++++++++++ pkg/controller/schedule_controller.go | 26 ++----- pkg/controller/schedule_controller_test.go | 6 +- 7 files changed, 189 insertions(+), 56 deletions(-) create mode 100644 changelogs/unreleased/1734-prydonius create mode 100644 pkg/cmd/cli/backup/create_test.go diff --git a/changelogs/unreleased/1734-prydonius b/changelogs/unreleased/1734-prydonius new file mode 100644 index 000000000..8d13fcbca --- /dev/null +++ b/changelogs/unreleased/1734-prydonius @@ -0,0 +1 @@ +adds --from-schedule flag to the `velero create backup` command to create a Backup from an existing Schedule diff --git a/pkg/builder/backup_builder.go b/pkg/builder/backup_builder.go index ec80f7c2e..4f3a4f27d 100644 --- a/pkg/builder/backup_builder.go +++ b/pkg/builder/backup_builder.go @@ -73,6 +73,19 @@ func (b *BackupBuilder) ObjectMeta(opts ...ObjectMetaOpt) *BackupBuilder { return b } +// FromSchedule sets the Backup's spec and labels from the Schedule template +func (b *BackupBuilder) FromSchedule(schedule *velerov1api.Schedule) *BackupBuilder { + labels := schedule.Labels + if labels == nil { + labels = make(map[string]string) + } + labels[velerov1api.ScheduleNameLabel] = schedule.Name + + b.object.Spec = schedule.Spec.Template + b.ObjectMeta(WithLabelsMap(labels)) + return b +} + // IncludedNamespaces sets the Backup's included namespaces. func (b *BackupBuilder) IncludedNamespaces(namespaces ...string) *BackupBuilder { b.object.Spec.IncludedNamespaces = namespaces @@ -151,12 +164,6 @@ func (b *BackupBuilder) StartTimestamp(val time.Time) *BackupBuilder { return b } -// NoTypeMeta removes the type meta from the Backup. -func (b *BackupBuilder) NoTypeMeta() *BackupBuilder { - b.object.TypeMeta = metav1.TypeMeta{} - return b -} - // Hooks sets the Backup's hooks. func (b *BackupBuilder) Hooks(hooks velerov1api.BackupHooks) *BackupBuilder { b.object.Spec.Hooks = hooks diff --git a/pkg/builder/object_meta.go b/pkg/builder/object_meta.go index 1a2b56089..931da8034 100644 --- a/pkg/builder/object_meta.go +++ b/pkg/builder/object_meta.go @@ -42,6 +42,24 @@ func WithLabels(vals ...string) func(obj metav1.Object) { } } +// WithLabelsMap is a functional option that applies the specified labels map to +// an object. +func WithLabelsMap(labels map[string]string) func(obj metav1.Object) { + return func(obj metav1.Object) { + objLabels := obj.GetLabels() + if objLabels == nil { + objLabels = make(map[string]string) + } + + // If the label already exists in the object, it will be overwritten + for k, v := range labels { + objLabels[k] = v + } + + obj.SetLabels(objLabels) + } +} + // WithAnnotations is a functional option that applies the specified // annotation keys/values to an object. func WithAnnotations(vals ...string) func(obj metav1.Object) { @@ -66,6 +84,7 @@ func setMapEntries(m map[string]string, vals ...string) map[string]string { key := vals[i] val := vals[i+1] + // If the label already exists in the object, it will be overwritten m[key] = val } diff --git a/pkg/cmd/cli/backup/create.go b/pkg/cmd/cli/backup/create.go index 61bc105b1..aa46bdece 100644 --- a/pkg/cmd/cli/backup/create.go +++ b/pkg/cmd/cli/backup/create.go @@ -25,7 +25,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" - api "github.com/heptio/velero/pkg/apis/velero/v1" + velerov1api "github.com/heptio/velero/pkg/apis/velero/v1" + "github.com/heptio/velero/pkg/builder" "github.com/heptio/velero/pkg/client" "github.com/heptio/velero/pkg/cmd" "github.com/heptio/velero/pkg/cmd/util/flag" @@ -34,6 +35,8 @@ import ( v1 "github.com/heptio/velero/pkg/generated/informers/externalversions/velero/v1" ) +const DefaultBackupTTL time.Duration = 30 * 24 * time.Hour + func NewCreateCommand(f client.Factory, use string) *cobra.Command { o := NewCreateOptions() @@ -64,6 +67,7 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command { o.BindFlags(c.Flags()) o.BindWait(c.Flags()) + o.BindFromSchedule(c.Flags()) output.BindFlags(c.Flags()) output.ClearOutputFlagDefault(c) @@ -84,13 +88,14 @@ type CreateOptions struct { Wait bool StorageLocation string SnapshotLocations []string + FromSchedule string client veleroclient.Interface } func NewCreateOptions() *CreateOptions { return &CreateOptions{ - TTL: 30 * 24 * time.Hour, + TTL: DefaultBackupTTL, IncludeNamespaces: flag.NewStringArray("*"), Labels: flag.NewMap(), SnapshotVolumes: flag.NewOptionalBool(nil), @@ -123,6 +128,12 @@ func (o *CreateOptions) BindWait(flags *pflag.FlagSet) { flags.BoolVarP(&o.Wait, "wait", "w", o.Wait, "wait for the operation to complete") } +// BindFromSchedule binds the from-schedule flag separately so it is not called +// by other create commands that reuse CreateOptions's BindFlags method. +func (o *CreateOptions) BindFromSchedule(flags *pflag.FlagSet) { + flags.StringVar(&o.FromSchedule, "from-schedule", "", "create a backup from the template of an existing schedule. Cannot be used with any other filters.") +} + func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { if err := output.ValidateFlags(c); err != nil { return err @@ -154,44 +165,33 @@ func (o *CreateOptions) Complete(args []string, f client.Factory) error { } func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { - backup := &api.Backup{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: f.Namespace(), - Name: o.Name, - Labels: o.Labels.Data(), - }, - Spec: api.BackupSpec{ - IncludedNamespaces: o.IncludeNamespaces, - ExcludedNamespaces: o.ExcludeNamespaces, - IncludedResources: o.IncludeResources, - ExcludedResources: o.ExcludeResources, - LabelSelector: o.Selector.LabelSelector, - SnapshotVolumes: o.SnapshotVolumes.Value, - TTL: metav1.Duration{Duration: o.TTL}, - IncludeClusterResources: o.IncludeClusterResources.Value, - StorageLocation: o.StorageLocation, - VolumeSnapshotLocations: o.SnapshotLocations, - }, + backup, err := o.BuildBackup(f.Namespace()) + if err != nil { + return err } if printed, err := output.PrintWithFormat(c, backup); printed || err != nil { return err } + if o.FromSchedule != "" { + fmt.Println("Creating backup from schedule, all other filters are ignored.") + } + var backupInformer cache.SharedIndexInformer - var updates chan *api.Backup + var updates chan *velerov1api.Backup if o.Wait { stop := make(chan struct{}) defer close(stop) - updates = make(chan *api.Backup) + updates = make(chan *velerov1api.Backup) backupInformer = v1.NewBackupInformer(o.client, f.Namespace(), 0, nil) backupInformer.AddEventHandler( cache.FilteringResourceEventHandler{ FilterFunc: func(obj interface{}) bool { - backup, ok := obj.(*api.Backup) + backup, ok := obj.(*velerov1api.Backup) if !ok { return false } @@ -199,14 +199,14 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { }, Handler: cache.ResourceEventHandlerFuncs{ UpdateFunc: func(_, obj interface{}) { - backup, ok := obj.(*api.Backup) + backup, ok := obj.(*velerov1api.Backup) if !ok { return } updates <- backup }, DeleteFunc: func(obj interface{}) { - backup, ok := obj.(*api.Backup) + backup, ok := obj.(*velerov1api.Backup) if !ok { return } @@ -218,7 +218,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { go backupInformer.Run(stop) } - _, err := o.client.VeleroV1().Backups(backup.Namespace).Create(backup) + _, err = o.client.VeleroV1().Backups(backup.Namespace).Create(backup) if err != nil { return err } @@ -239,7 +239,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { return nil } - if backup.Status.Phase != api.BackupPhaseNew && backup.Status.Phase != api.BackupPhaseInProgress { + if backup.Status.Phase != velerov1api.BackupPhaseNew && backup.Status.Phase != velerov1api.BackupPhaseInProgress { fmt.Printf("\nBackup completed with status: %s. You may check for more information using the commands `velero backup describe %s` and `velero backup logs %s`.\n", backup.Status.Phase, backup.Name, backup.Name) return nil } @@ -253,3 +253,35 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error { return nil } + +func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, error) { + backupBuilder := builder.ForBackup(namespace, o.Name) + + if o.FromSchedule != "" { + schedule, err := o.client.VeleroV1().Schedules(namespace).Get(o.FromSchedule, metav1.GetOptions{}) + if err != nil { + return nil, err + } + backupBuilder.FromSchedule(schedule) + } else { + backupBuilder. + IncludedNamespaces(o.IncludeNamespaces...). + ExcludedNamespaces(o.ExcludeNamespaces...). + IncludedResources(o.IncludeResources...). + ExcludedResources(o.ExcludeResources...). + LabelSelector(o.Selector.LabelSelector). + TTL(o.TTL). + StorageLocation(o.StorageLocation). + VolumeSnapshotLocations(o.SnapshotLocations...) + + if o.SnapshotVolumes.Value != nil { + backupBuilder.SnapshotVolumes(*o.SnapshotVolumes.Value) + } + if o.IncludeClusterResources.Value != nil { + backupBuilder.IncludeClusterResources(*o.IncludeClusterResources.Value) + } + } + + backup := backupBuilder.ObjectMeta(builder.WithLabelsMap(o.Labels.Data())).Result() + return backup, nil +} diff --git a/pkg/cmd/cli/backup/create_test.go b/pkg/cmd/cli/backup/create_test.go new file mode 100644 index 000000000..4797a8d86 --- /dev/null +++ b/pkg/cmd/cli/backup/create_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2019 the Velero 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 backup + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/heptio/velero/pkg/apis/velero/v1" + "github.com/heptio/velero/pkg/builder" + "github.com/heptio/velero/pkg/generated/clientset/versioned/fake" +) + +const testNamespace = "velero" + +func TestCreateOptions_BuildBackup(t *testing.T) { + o := NewCreateOptions() + o.Labels.Set("velero.io/test=true") + + backup, err := o.BuildBackup(testNamespace) + assert.NoError(t, err) + + assert.Equal(t, velerov1api.BackupSpec{ + TTL: metav1.Duration{Duration: o.TTL}, + IncludedNamespaces: []string(o.IncludeNamespaces), + SnapshotVolumes: o.SnapshotVolumes.Value, + IncludeClusterResources: o.IncludeClusterResources.Value, + }, backup.Spec) + + assert.Equal(t, map[string]string{ + "velero.io/test": "true", + }, backup.GetLabels()) +} + +func TestCreateOptions_BuildBackupFromSchedule(t *testing.T) { + o := NewCreateOptions() + o.FromSchedule = "test" + o.client = fake.NewSimpleClientset() + + t.Run("inexistant schedule", func(t *testing.T) { + _, err := o.BuildBackup(testNamespace) + assert.Error(t, err) + }) + + expectedBackupSpec := builder.ForBackup("test", testNamespace).IncludedNamespaces("test").Result().Spec + schedule := builder.ForSchedule(testNamespace, "test").Template(expectedBackupSpec).ObjectMeta(builder.WithLabels("velero.io/test", "true")).Result() + o.client.VeleroV1().Schedules(testNamespace).Create(schedule) + + t.Run("existing schedule", func(t *testing.T) { + backup, err := o.BuildBackup(testNamespace) + assert.NoError(t, err) + + assert.Equal(t, expectedBackupSpec, backup.Spec) + assert.Equal(t, map[string]string{ + "velero.io/test": "true", + velerov1api.ScheduleNameLabel: "test", + }, backup.GetLabels()) + }) + + t.Run("command line labels take precedence over schedule labels", func(t *testing.T) { + o.Labels.Set("velero.io/test=yes,custom-label=true") + backup, err := o.BuildBackup(testNamespace) + assert.NoError(t, err) + + assert.Equal(t, expectedBackupSpec, backup.Spec) + assert.Equal(t, map[string]string{ + "velero.io/test": "yes", + velerov1api.ScheduleNameLabel: "test", + "custom-label": "true", + }, backup.GetLabels()) + }) +} diff --git a/pkg/controller/schedule_controller.go b/pkg/controller/schedule_controller.go index ad0336e87..e0bf31c76 100644 --- a/pkg/controller/schedule_controller.go +++ b/pkg/controller/schedule_controller.go @@ -33,7 +33,7 @@ import ( "k8s.io/client-go/tools/cache" api "github.com/heptio/velero/pkg/apis/velero/v1" - velerov1api "github.com/heptio/velero/pkg/apis/velero/v1" + "github.com/heptio/velero/pkg/builder" velerov1client "github.com/heptio/velero/pkg/generated/clientset/versioned/typed/velero/v1" informers "github.com/heptio/velero/pkg/generated/informers/externalversions/velero/v1" listers "github.com/heptio/velero/pkg/generated/listers/velero/v1" @@ -286,29 +286,15 @@ func getNextRunTime(schedule *api.Schedule, cronSchedule cron.Schedule, asOf tim } func getBackup(item *api.Schedule, timestamp time.Time) *api.Backup { - backup := &api.Backup{ - Spec: item.Spec.Template, - ObjectMeta: metav1.ObjectMeta{ - Namespace: item.Namespace, - Name: fmt.Sprintf("%s-%s", item.Name, timestamp.Format("20060102150405")), - }, - } - - addLabelsToBackup(item, backup) + name := fmt.Sprintf("%s-%s", item.Name, timestamp.Format("20060102150405")) + backup := builder. + ForBackup(item.Namespace, name). + FromSchedule(item). + Result() return backup } -func addLabelsToBackup(item *api.Schedule, backup *api.Backup) { - labels := item.Labels - if labels == nil { - labels = make(map[string]string) - } - labels[velerov1api.ScheduleNameLabel] = item.Name - - backup.Labels = labels -} - func patchSchedule(original, updated *api.Schedule, client velerov1client.SchedulesGetter) (*api.Schedule, error) { origBytes, err := json.Marshal(original) if err != nil { diff --git a/pkg/controller/schedule_controller_test.go b/pkg/controller/schedule_controller_test.go index 0930a7ef2..1edee4905 100644 --- a/pkg/controller/schedule_controller_test.go +++ b/pkg/controller/schedule_controller_test.go @@ -97,7 +97,7 @@ func TestProcessSchedule(t *testing.T) { fakeClockTime: "2017-01-01 12:00:00", expectedErr: false, expectedPhase: string(velerov1api.SchedulePhaseEnabled), - expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).NoTypeMeta().Result(), + expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).Result(), expectedLastBackup: "2017-01-01 12:00:00", }, { @@ -105,7 +105,7 @@ func TestProcessSchedule(t *testing.T) { schedule: newScheduleBuilder(velerov1api.SchedulePhaseEnabled).CronSchedule("@every 5m").Result(), fakeClockTime: "2017-01-01 12:00:00", expectedErr: false, - expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).NoTypeMeta().Result(), + expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).Result(), expectedLastBackup: "2017-01-01 12:00:00", }, { @@ -113,7 +113,7 @@ func TestProcessSchedule(t *testing.T) { schedule: newScheduleBuilder(velerov1api.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").Result(), fakeClockTime: "2017-01-01 12:00:00", expectedErr: false, - expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).NoTypeMeta().Result(), + expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "name")).Result(), expectedLastBackup: "2017-01-01 12:00:00", }, }