mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-05 04:55:22 +00:00
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 <aadnan@vmware.com>
This commit is contained in:
committed by
KubeKween
parent
686f41ebec
commit
6aa0215137
1
changelogs/unreleased/1734-prydonius
Normal file
1
changelogs/unreleased/1734-prydonius
Normal file
@@ -0,0 +1 @@
|
||||
adds --from-schedule flag to the `velero create backup` command to create a Backup from an existing Schedule
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
88
pkg/cmd/cli/backup/create_test.go
Normal file
88
pkg/cmd/cli/backup/create_test.go
Normal file
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user