mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-09 22:47:27 +00:00
Merge pull request #7169 from kaovilai/schedule-skip-immediately
Add `--skip-immediately` to schedule CLI/API, and related to server, install commands
This commit is contained in:
1
changelogs/unreleased/7169-kaovilai
Normal file
1
changelogs/unreleased/7169-kaovilai
Normal file
@@ -0,0 +1 @@
|
||||
Add `--skip-immediately` flag to schedule commands; `--schedule-skip-immediately` server and install
|
||||
@@ -61,6 +61,16 @@ spec:
|
||||
description: Schedule is a Cron expression defining when to run the
|
||||
Backup.
|
||||
type: string
|
||||
skipImmediately:
|
||||
description: 'SkipImmediately specifies whether to skip backup if
|
||||
schedule is due immediately from `schedule.status.lastBackup` timestamp
|
||||
when schedule is unpaused or if schedule is new. If true, backup
|
||||
will be skipped immediately when schedule is unpaused if it is due
|
||||
based on .Status.LastBackupTimestamp or schedule is new, and will
|
||||
run at next schedule time. If false, backup will not be skipped
|
||||
immediately when schedule is unpaused, but will run at next schedule
|
||||
time. If empty, will follow server configuration (default: false).'
|
||||
type: boolean
|
||||
template:
|
||||
description: Template is the definition of the Backup to be run on
|
||||
the provided schedule
|
||||
@@ -549,6 +559,11 @@ spec:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
lastSkipped:
|
||||
description: LastSkipped is the last time a Schedule was skipped
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
phase:
|
||||
description: Phase is the current phase of the Schedule
|
||||
enum:
|
||||
|
||||
File diff suppressed because one or more lines are too long
145
design/schedule-skip-immediately-config_design.md
Normal file
145
design/schedule-skip-immediately-config_design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Schedule Skip Immediately Config Design
|
||||
## Abstract
|
||||
When unpausing schedule, a backup could be due immediately.
|
||||
New Schedules also create new backup immediately.
|
||||
|
||||
This design allows user to *skip **immediately due** backup run upon unpausing or schedule creation*.
|
||||
|
||||
## Background
|
||||
Currently, the default behavior of schedule when `.Status.LastBackup` is nil or is due immediately after unpausing, a backup will be created. This may not be a desired by all users (https://github.com/vmware-tanzu/velero/issues/6517)
|
||||
|
||||
User want ability to skip the first immediately due backup when schedule is unpaused and or created.
|
||||
|
||||
If you create a schedule with cron "45 * * * *" and pause it at say the 43rd minute and then unpause it at say 50th minute, a backup gets triggered (since .Status.LastBackup is nil or >60min ago).
|
||||
|
||||
With this design, user can skip the first immediately due backup when schedule is unpaused and or created.
|
||||
|
||||
## Goals
|
||||
- Add an option so user can when unpausing (when immediately due) or creating new schedule, to not create a backup immediately.
|
||||
|
||||
## Non Goals
|
||||
- Changing the default behavior
|
||||
|
||||
## High-Level Design
|
||||
Add a new field with to the schedule spec and as a new cli flags for install, server, schedule commands; allowing user to skip immediately due backup when unpausing or schedule creation.
|
||||
|
||||
If CLI flag is specified during schedule unpause, velero will update the schedule spec accordingly and override prior spec for `skipImmediately``.
|
||||
|
||||
## Detailed Design
|
||||
### CLI Changes
|
||||
`velero schedule unpause` will now take an optional bool flag `--skip-immediately` to allow user to override the behavior configured for velero server (see `velero server` below).
|
||||
|
||||
`velero schedule unpause schedule-1 --skip-immediately=false` will unpause the schedule but not skip the backup if due immediately from `Schedule.Status.LastBackup` timestamp. Backup will be run at the next cron schedule.
|
||||
|
||||
`velero schedule unpause schedule-1 --skip-immediately=true` will unpause the schedule and skip the backup if due immediately from `Schedule.Status.LastBackup` timestamp. Backup will also be run at the next cron schedule.
|
||||
|
||||
`velero schedule unpause schedule-1` will check `.spec.SkipImmediately` in the schedule to determine behavior. This field will default to false to maintain prior behavior.
|
||||
|
||||
`velero server` will add a new flag `--schedule-skip-immediately` to configure default value to patch new schedules created without the field. This flag will default to false to maintain prior behavior if not set.
|
||||
|
||||
`velero install` will add a new flag `--schedule-skip-immediately` to configure default value to patch new schedules created without the field. This flag will default to false to maintain prior behavior if not set.
|
||||
|
||||
### API Changes
|
||||
`pkg/apis/velero/v1/schedule_types.go`
|
||||
```diff
|
||||
// ScheduleSpec defines the specification for a Velero schedule
|
||||
type ScheduleSpec struct {
|
||||
// Template is the definition of the Backup to be run
|
||||
// on the provided schedule
|
||||
Template BackupSpec `json:"template"`
|
||||
|
||||
// Schedule is a Cron expression defining when to run
|
||||
// the Backup.
|
||||
Schedule string `json:"schedule"`
|
||||
|
||||
// UseOwnerReferencesBackup specifies whether to use
|
||||
// OwnerReferences on backups created by this Schedule.
|
||||
// +optional
|
||||
// +nullable
|
||||
UseOwnerReferencesInBackup *bool `json:"useOwnerReferencesInBackup,omitempty"`
|
||||
|
||||
// Paused specifies whether the schedule is paused or not
|
||||
// +optional
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
|
||||
+ // SkipImmediately specifies whether to skip backup if schedule is due immediately from `Schedule.Status.LastBackup` timestamp when schedule is unpaused or if schedule is new.
|
||||
+ // If true, backup will be skipped immediately when schedule is unpaused if it is due based on .Status.LastBackupTimestamp or schedule is new, and will run at next schedule time.
|
||||
+ // If false, backup will not be skipped immediately when schedule is unpaused, but will run at next schedule time.
|
||||
+ // If empty, will follow server configuration (default: false).
|
||||
+ // +optional
|
||||
+ SkipImmediately bool `json:"skipImmediately,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
`LastSkipped` will be added to `ScheduleStatus` struct to track the last time a schedule was skipped.
|
||||
```diff
|
||||
// ScheduleStatus captures the current state of a Velero schedule
|
||||
type ScheduleStatus struct {
|
||||
// Phase is the current phase of the Schedule
|
||||
// +optional
|
||||
Phase SchedulePhase `json:"phase,omitempty"`
|
||||
|
||||
// LastBackup is the last time a Backup was run for this
|
||||
// Schedule schedule
|
||||
// +optional
|
||||
// +nullable
|
||||
LastBackup *metav1.Time `json:"lastBackup,omitempty"`
|
||||
|
||||
+ // LastSkipped is the last time a Schedule was skipped
|
||||
+ // +optional
|
||||
+ // +nullable
|
||||
+ LastSkipped *metav1.Time `json:"lastSkipped,omitempty"`
|
||||
|
||||
// ValidationErrors is a slice of all validation errors (if
|
||||
// applicable)
|
||||
// +optional
|
||||
ValidationErrors []string `json:"validationErrors,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
When `schedule.spec.SkipImmediately` is `true`, `LastSkipped` will be set to the current time, and `schedule.spec.SkipImmediately` set to nil so it can be used again.
|
||||
|
||||
The `getNextRunTime()` function below is updated so `LastSkipped` which is after `LastBackup` will be used to determine next run time.
|
||||
|
||||
```go
|
||||
func getNextRunTime(schedule *velerov1.Schedule, cronSchedule cron.Schedule, asOf time.Time) (bool, time.Time) {
|
||||
var lastBackupTime time.Time
|
||||
if schedule.Status.LastBackup != nil {
|
||||
lastBackupTime = schedule.Status.LastBackup.Time
|
||||
} else {
|
||||
lastBackupTime = schedule.CreationTimestamp.Time
|
||||
}
|
||||
if schedule.Status.LastSkipped != nil && schedule.Status.LastSkipped.After(lastBackupTime) {
|
||||
lastBackupTime = schedule.Status.LastSkipped.Time
|
||||
}
|
||||
|
||||
nextRunTime := cronSchedule.Next(lastBackupTime)
|
||||
|
||||
return asOf.After(nextRunTime), nextRunTime
|
||||
}
|
||||
```
|
||||
|
||||
When schedule is unpaused, and `Schedule.Status.LastBackup` is not nil, if `Schedule.Status.LastSkipped` is recent, a backup will not be created.
|
||||
|
||||
When schedule is unpaused or created with `Schedule.Status.LastBackup` set to nil or schedule is newly created, normally a backup will be created immediately. If `Schedule.Status.LastSkipped` is recent, a backup will not be created.
|
||||
|
||||
Backup will be run at the next cron schedule based on LastBackup or LastSkipped whichever is more recent.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
N/A
|
||||
|
||||
|
||||
## Security Considerations
|
||||
None
|
||||
|
||||
## Compatibility
|
||||
Upon upgrade, the new field will be added to the schedule spec automatically and will default to the prior behavior of running a backup when schedule is unpaused if it is due based on .Status.LastBackup or schedule is new.
|
||||
|
||||
Since this is a new field, it will be ignored by older versions of velero.
|
||||
|
||||
## Implementation
|
||||
TBD
|
||||
|
||||
## Open Issues
|
||||
N/A
|
||||
@@ -42,6 +42,13 @@ type ScheduleSpec struct {
|
||||
// Paused specifies whether the schedule is paused or not
|
||||
// +optional
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
|
||||
// SkipImmediately specifies whether to skip backup if schedule is due immediately from `schedule.status.lastBackup` timestamp when schedule is unpaused or if schedule is new.
|
||||
// If true, backup will be skipped immediately when schedule is unpaused if it is due based on .Status.LastBackupTimestamp or schedule is new, and will run at next schedule time.
|
||||
// If false, backup will not be skipped immediately when schedule is unpaused, but will run at next schedule time.
|
||||
// If empty, will follow server configuration (default: false).
|
||||
// +optional
|
||||
SkipImmediately *bool `json:"skipImmediately,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulePhase is a string representation of the lifecycle phase
|
||||
@@ -75,6 +82,11 @@ type ScheduleStatus struct {
|
||||
// +nullable
|
||||
LastBackup *metav1.Time `json:"lastBackup,omitempty"`
|
||||
|
||||
// LastSkipped is the last time a Schedule was skipped
|
||||
// +optional
|
||||
// +nullable
|
||||
LastSkipped *metav1.Time `json:"lastSkipped,omitempty"`
|
||||
|
||||
// ValidationErrors is a slice of all validation errors (if
|
||||
// applicable)
|
||||
// +optional
|
||||
|
||||
@@ -1514,6 +1514,11 @@ func (in *ScheduleSpec) DeepCopyInto(out *ScheduleSpec) {
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.SkipImmediately != nil {
|
||||
in, out := &in.SkipImmediately, &out.SkipImmediately
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleSpec.
|
||||
@@ -1533,6 +1538,10 @@ func (in *ScheduleStatus) DeepCopyInto(out *ScheduleStatus) {
|
||||
in, out := &in.LastBackup, &out.LastBackup
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.LastSkipped != nil {
|
||||
in, out := &in.LastSkipped, &out.LastSkipped
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.ValidationErrors != nil {
|
||||
in, out := &in.ValidationErrors, &out.ValidationErrors
|
||||
*out = make([]string, len(*in))
|
||||
|
||||
@@ -89,3 +89,9 @@ func (b *ScheduleBuilder) Template(spec velerov1api.BackupSpec) *ScheduleBuilder
|
||||
b.object.Spec.Template = spec
|
||||
return b
|
||||
}
|
||||
|
||||
// SkipImmediately sets the Schedule's SkipImmediately.
|
||||
func (b *ScheduleBuilder) SkipImmediately(skip *bool) *ScheduleBuilder {
|
||||
b.object.Spec.SkipImmediately = skip
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ type Options struct {
|
||||
UploaderType string
|
||||
DefaultSnapshotMoveData bool
|
||||
DisableInformerCache bool
|
||||
ScheduleSkipImmediately bool
|
||||
}
|
||||
|
||||
// BindFlags adds command line values to the options struct.
|
||||
@@ -126,6 +127,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(&o.UploaderType, "uploader-type", o.UploaderType, fmt.Sprintf("The type of uploader to transfer the data of pod volumes, the supported values are '%s', '%s'", uploader.ResticType, uploader.KopiaType))
|
||||
flags.BoolVar(&o.DefaultSnapshotMoveData, "default-snapshot-move-data", o.DefaultSnapshotMoveData, "Bool flag to configure Velero server to move data by default for all snapshots supporting data movement. Optional.")
|
||||
flags.BoolVar(&o.DisableInformerCache, "disable-informer-cache", o.DisableInformerCache, "Disable informer cache for Get calls on restore. With this enabled, it will speed up restore in cases where there are backup resources which already exist in the cluster, but for very large clusters this will increase velero memory usage. Default is false (don't disable). Optional.")
|
||||
flags.BoolVar(&o.ScheduleSkipImmediately, "schedule-skip-immediately", o.ScheduleSkipImmediately, "Skip the first scheduled backup immediately after creating a schedule. Default is false (don't skip).")
|
||||
}
|
||||
|
||||
// NewInstallOptions instantiates a new, default InstallOptions struct.
|
||||
@@ -154,6 +156,7 @@ func NewInstallOptions() *Options {
|
||||
UploaderType: uploader.KopiaType,
|
||||
DefaultSnapshotMoveData: false,
|
||||
DisableInformerCache: true,
|
||||
ScheduleSkipImmediately: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +223,7 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) {
|
||||
UploaderType: o.UploaderType,
|
||||
DefaultSnapshotMoveData: o.DefaultSnapshotMoveData,
|
||||
DisableInformerCache: o.DisableInformerCache,
|
||||
ScheduleSkipImmediately: o.ScheduleSkipImmediately,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ example: "@every 2h30m".`,
|
||||
|
||||
type CreateOptions struct {
|
||||
BackupOptions *backup.CreateOptions
|
||||
SkipOptions *SkipOptions
|
||||
Schedule string
|
||||
UseOwnerReferencesInBackup bool
|
||||
Paused bool
|
||||
@@ -90,11 +91,13 @@ type CreateOptions struct {
|
||||
func NewCreateOptions() *CreateOptions {
|
||||
return &CreateOptions{
|
||||
BackupOptions: backup.NewCreateOptions(),
|
||||
SkipOptions: NewSkipOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
o.BackupOptions.BindFlags(flags)
|
||||
o.SkipOptions.BindFlags(flags)
|
||||
flags.StringVar(&o.Schedule, "schedule", o.Schedule, "A cron expression specifying a recurring schedule for this backup to run")
|
||||
flags.BoolVar(&o.UseOwnerReferencesInBackup, "use-owner-references-in-backup", o.UseOwnerReferencesInBackup, "Specifies whether to use OwnerReferences on backups created by this Schedule. Notice: if set to true, when schedule is deleted, backups will be deleted too.")
|
||||
flags.BoolVar(&o.Paused, "paused", o.Paused, "Specifies whether the newly created schedule is paused or not.")
|
||||
@@ -160,6 +163,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
Schedule: o.Schedule,
|
||||
UseOwnerReferencesInBackup: &o.UseOwnerReferencesInBackup,
|
||||
Paused: o.Paused,
|
||||
SkipImmediately: o.SkipOptions.SkipImmediately.Value,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
// NewPauseCommand creates the command for pause
|
||||
func NewPauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
o := cli.NewSelectOptions("pause", "schedule")
|
||||
pauseOpts := NewPauseOptions()
|
||||
|
||||
c := &cobra.Command{
|
||||
Use: use,
|
||||
@@ -54,16 +56,31 @@ func NewPauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cmd.CheckError(o.Complete(args))
|
||||
cmd.CheckError(o.Validate())
|
||||
cmd.CheckError(runPause(f, o, true))
|
||||
cmd.CheckError(runPause(f, o, true, pauseOpts.SkipOptions.SkipImmediately.Value))
|
||||
},
|
||||
}
|
||||
|
||||
o.BindFlags(c.Flags())
|
||||
pauseOpts.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
type PauseOptions struct {
|
||||
SkipOptions *SkipOptions
|
||||
}
|
||||
|
||||
func NewPauseOptions() *PauseOptions {
|
||||
return &PauseOptions{
|
||||
SkipOptions: NewSkipOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PauseOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
o.SkipOptions.BindFlags(flags)
|
||||
}
|
||||
|
||||
func runPause(f client.Factory, o *cli.SelectOptions, paused bool, skipImmediately *bool) error {
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -120,6 +137,7 @@ func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
continue
|
||||
}
|
||||
schedule.Spec.Paused = paused
|
||||
schedule.Spec.SkipImmediately = skipImmediately
|
||||
if err := crClient.Update(context.TODO(), schedule); err != nil {
|
||||
return errors.Wrapf(err, "failed to update schedule %s", schedule.Name)
|
||||
}
|
||||
|
||||
20
pkg/cmd/cli/schedule/skip_options.go
Normal file
20
pkg/cmd/cli/schedule/skip_options.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
)
|
||||
|
||||
type SkipOptions struct {
|
||||
SkipImmediately flag.OptionalBool
|
||||
}
|
||||
|
||||
func NewSkipOptions() *SkipOptions {
|
||||
return &SkipOptions{}
|
||||
}
|
||||
|
||||
func (o *SkipOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
f := flags.VarPF(&o.SkipImmediately, "skip-immediately", "", "Skip the next scheduled backup immediately")
|
||||
f.NoOptDefVal = "" // default to nil so server options can take precedence
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// NewUnpauseCommand creates the command for unpause
|
||||
func NewUnpauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
o := cli.NewSelectOptions("pause", "schedule")
|
||||
|
||||
pauseOpts := NewPauseOptions()
|
||||
c := &cobra.Command{
|
||||
Use: use,
|
||||
Short: "Unpause schedules",
|
||||
@@ -45,11 +45,12 @@ func NewUnpauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cmd.CheckError(o.Complete(args))
|
||||
cmd.CheckError(o.Validate())
|
||||
cmd.CheckError(runPause(f, o, false))
|
||||
cmd.CheckError(runPause(f, o, false, pauseOpts.SkipOptions.SkipImmediately.Value))
|
||||
},
|
||||
}
|
||||
|
||||
o.BindFlags(c.Flags())
|
||||
pauseOpts.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ type serverConfig struct {
|
||||
maxConcurrentK8SConnections int
|
||||
defaultSnapshotMoveData bool
|
||||
disableInformerCache bool
|
||||
scheduleSkipImmediately bool
|
||||
}
|
||||
|
||||
func NewCommand(f client.Factory) *cobra.Command {
|
||||
@@ -163,6 +164,7 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
maxConcurrentK8SConnections: defaultMaxConcurrentK8SConnections,
|
||||
defaultSnapshotMoveData: false,
|
||||
disableInformerCache: defaultDisableInformerCache,
|
||||
scheduleSkipImmediately: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -235,6 +237,7 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
command.Flags().IntVar(&config.maxConcurrentK8SConnections, "max-concurrent-k8s-connections", config.maxConcurrentK8SConnections, "Max concurrent connections number that Velero can create with kube-apiserver. Default is 30.")
|
||||
command.Flags().BoolVar(&config.defaultSnapshotMoveData, "default-snapshot-move-data", config.defaultSnapshotMoveData, "Move data by default for all snapshots supporting data movement.")
|
||||
command.Flags().BoolVar(&config.disableInformerCache, "disable-informer-cache", config.disableInformerCache, "Disable informer cache for Get calls on restore. With this enabled, it will speed up restore in cases where there are backup resources which already exist in the cluster, but for very large clusters this will increase velero memory usage. Default is false (don't disable).")
|
||||
command.Flags().BoolVar(&config.scheduleSkipImmediately, "schedule-skip-immediately", config.scheduleSkipImmediately, "Skip the first scheduled backup immediately after creating a schedule. Default is false (don't skip).")
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -915,7 +918,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
}
|
||||
|
||||
if _, ok := enabledRuntimeControllers[controller.Schedule]; ok {
|
||||
if err := controller.NewScheduleReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.metrics).SetupWithManager(s.mgr); err != nil {
|
||||
if err := controller.NewScheduleReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.metrics, s.config.scheduleSkipImmediately).SetupWithManager(s.mgr); err != nil {
|
||||
s.logger.Fatal(err, "unable to create controller", "controller", controller.Schedule)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,11 @@ const (
|
||||
|
||||
type scheduleReconciler struct {
|
||||
client.Client
|
||||
namespace string
|
||||
logger logrus.FieldLogger
|
||||
clock clocks.WithTickerAndDelayedExecution
|
||||
metrics *metrics.ServerMetrics
|
||||
namespace string
|
||||
logger logrus.FieldLogger
|
||||
clock clocks.WithTickerAndDelayedExecution
|
||||
metrics *metrics.ServerMetrics
|
||||
skipImmediately bool
|
||||
}
|
||||
|
||||
func NewScheduleReconciler(
|
||||
@@ -55,13 +56,15 @@ func NewScheduleReconciler(
|
||||
logger logrus.FieldLogger,
|
||||
client client.Client,
|
||||
metrics *metrics.ServerMetrics,
|
||||
skipImmediately bool,
|
||||
) *scheduleReconciler {
|
||||
return &scheduleReconciler{
|
||||
Client: client,
|
||||
namespace: namespace,
|
||||
logger: logger,
|
||||
clock: clocks.RealClock{},
|
||||
metrics: metrics,
|
||||
Client: client,
|
||||
namespace: namespace,
|
||||
logger: logger,
|
||||
clock: clocks.RealClock{},
|
||||
metrics: metrics,
|
||||
skipImmediately: skipImmediately,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +102,18 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
}
|
||||
return ctrl.Result{}, errors.Wrapf(err, "error getting schedule %s", req.String())
|
||||
}
|
||||
|
||||
c.metrics.InitSchedule(schedule.Name)
|
||||
|
||||
original := schedule.DeepCopy()
|
||||
|
||||
if schedule.Spec.SkipImmediately == nil {
|
||||
schedule.Spec.SkipImmediately = &c.skipImmediately
|
||||
}
|
||||
if schedule.Spec.SkipImmediately != nil && *schedule.Spec.SkipImmediately {
|
||||
*schedule.Spec.SkipImmediately = false
|
||||
schedule.Status.LastSkipped = &metav1.Time{Time: c.clock.Now()}
|
||||
}
|
||||
|
||||
// validation - even if the item is Enabled, we can't trust it
|
||||
// so re-validate
|
||||
currentPhase := schedule.Status.Phase
|
||||
@@ -116,10 +126,25 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
schedule.Status.Phase = velerov1.SchedulePhaseEnabled
|
||||
}
|
||||
|
||||
// update status if it's changed
|
||||
scheduleNeedsPatch := false
|
||||
errStringArr := make([]string, 0)
|
||||
if currentPhase != schedule.Status.Phase {
|
||||
scheduleNeedsPatch = true
|
||||
errStringArr = append(errStringArr, fmt.Sprintf("phase to %s", schedule.Status.Phase))
|
||||
}
|
||||
// update spec.SkipImmediately if it's changed
|
||||
if original.Spec.SkipImmediately != schedule.Spec.SkipImmediately {
|
||||
scheduleNeedsPatch = true
|
||||
errStringArr = append(errStringArr, fmt.Sprintf("spec.skipImmediately to %v", schedule.Spec.SkipImmediately))
|
||||
}
|
||||
// update status if it's changed
|
||||
if original.Status.LastSkipped != schedule.Status.LastSkipped {
|
||||
scheduleNeedsPatch = true
|
||||
errStringArr = append(errStringArr, fmt.Sprintf("last skipped to %v", schedule.Status.LastSkipped))
|
||||
}
|
||||
if scheduleNeedsPatch {
|
||||
if err := c.Patch(ctx, schedule, client.MergeFrom(original)); err != nil {
|
||||
return ctrl.Result{}, errors.Wrapf(err, "error updating phase of schedule %s to %s", req.String(), schedule.Status.Phase)
|
||||
return ctrl.Result{}, errors.Wrapf(err, "error updating %v for schedule %s", errStringArr, req.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +274,9 @@ func getNextRunTime(schedule *velerov1.Schedule, cronSchedule cron.Schedule, asO
|
||||
} else {
|
||||
lastBackupTime = schedule.CreationTimestamp.Time
|
||||
}
|
||||
if schedule.Status.LastSkipped != nil && schedule.Status.LastSkipped.After(lastBackupTime) {
|
||||
lastBackupTime = schedule.Status.LastSkipped.Time
|
||||
}
|
||||
|
||||
nextRunTime := cronSchedule.Next(lastBackupTime)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
testclocks "k8s.io/utils/clock/testing"
|
||||
"k8s.io/utils/pointer"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
// Test reconcile function of schedule controller. Pause is not covered as event filter will not allow it through
|
||||
func TestReconcileOfSchedule(t *testing.T) {
|
||||
require.Nil(t, velerov1.AddToScheme(scheme.Scheme))
|
||||
|
||||
@@ -44,15 +46,17 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scheduleKey string
|
||||
schedule *velerov1.Schedule
|
||||
fakeClockTime string
|
||||
expectedPhase string
|
||||
expectedValidationErrors []string
|
||||
expectedBackupCreate *velerov1.Backup
|
||||
expectedLastBackup string
|
||||
backup *velerov1.Backup
|
||||
name string
|
||||
scheduleKey string
|
||||
schedule *velerov1.Schedule
|
||||
fakeClockTime string
|
||||
expectedPhase string
|
||||
expectedValidationErrors []string
|
||||
expectedBackupCreate *velerov1.Backup
|
||||
expectedLastBackup string
|
||||
expectedLastSkipped string
|
||||
backup *velerov1.Backup
|
||||
reconcilerSkipImmediately bool
|
||||
}{
|
||||
{
|
||||
name: "missing schedule triggers no backup",
|
||||
@@ -88,6 +92,13 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1.ScheduleNameLabel, "name")).Result(),
|
||||
expectedLastBackup: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule with phase New and SkipImmediately gets validated and does not trigger a backup",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseNew).CronSchedule("@every 5m").SkipImmediately(pointer.Bool(true)).Result(),
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedPhase: string(velerov1.SchedulePhaseEnabled),
|
||||
expectedLastSkipped: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule with phase Enabled gets re-validated and triggers a backup if valid",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").Result(),
|
||||
@@ -103,6 +114,35 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1.ScheduleNameLabel, "name")).Result(),
|
||||
expectedLastBackup: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule that's already run but has SkippedImmediately=nil gets LastBackup updated",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").SkipImmediately(nil).Result(),
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1.ScheduleNameLabel, "name")).Result(),
|
||||
expectedLastBackup: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule that's already run but has SkippedImmediately=false gets LastBackup updated",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").SkipImmediately(pointer.Bool(false)).Result(),
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedBackupCreate: builder.ForBackup("ns", "name-20170101120000").ObjectMeta(builder.WithLabels(velerov1.ScheduleNameLabel, "name")).Result(),
|
||||
expectedLastBackup: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule that's already run, server has skipImmediately set to true, and Schedule has SkippedImmediately=nil do not get LastBackup updated",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").SkipImmediately(nil).Result(),
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedLastBackup: "2000-01-01 00:00:00",
|
||||
expectedLastSkipped: "2017-01-01 12:00:00",
|
||||
reconcilerSkipImmediately: true,
|
||||
},
|
||||
{
|
||||
name: "schedule that's already run but has SkippedImmediately=true do not get LastBackup updated",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").SkipImmediately(pointer.Bool(true)).Result(),
|
||||
fakeClockTime: "2017-01-01 12:00:00",
|
||||
expectedLastBackup: "2000-01-01 00:00:00",
|
||||
expectedLastSkipped: "2017-01-01 12:00:00",
|
||||
},
|
||||
{
|
||||
name: "schedule already has backup in New state.",
|
||||
schedule: newScheduleBuilder(velerov1.SchedulePhaseEnabled).CronSchedule("@every 5m").LastBackupTime("2000-01-01 00:00:00").Result(),
|
||||
@@ -120,7 +160,7 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
err error
|
||||
)
|
||||
|
||||
reconciler := NewScheduleReconciler("namespace", logger, client, metrics.NewServerMetrics())
|
||||
reconciler := NewScheduleReconciler("namespace", logger, client, metrics.NewServerMetrics(), test.reconcilerSkipImmediately)
|
||||
|
||||
if test.fakeClockTime != "" {
|
||||
testTime, err = time.Parse("2006-01-02 15:04:05", test.fakeClockTime)
|
||||
@@ -136,6 +176,12 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
require.Nil(t, client.Create(ctx, test.backup))
|
||||
}
|
||||
|
||||
scheduleb4reconcile := &velerov1.Schedule{}
|
||||
err = client.Get(ctx, types.NamespacedName{Namespace: "ns", Name: "name"}, scheduleb4reconcile)
|
||||
if test.schedule != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
_, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "ns", Name: "name"}})
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -151,8 +197,20 @@ func TestReconcileOfSchedule(t *testing.T) {
|
||||
}
|
||||
if len(test.expectedLastBackup) > 0 {
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, schedule.Status.LastBackup)
|
||||
assert.Equal(t, parseTime(test.expectedLastBackup).Unix(), schedule.Status.LastBackup.Unix())
|
||||
}
|
||||
if len(test.expectedLastSkipped) > 0 {
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, schedule.Status.LastSkipped)
|
||||
assert.Equal(t, parseTime(test.expectedLastSkipped).Unix(), schedule.Status.LastSkipped.Unix())
|
||||
}
|
||||
|
||||
// we expect reconcile to flip SkipImmediately to false if it's true or the server is configured to skip immediately and the schedule doesn't have it set
|
||||
if scheduleb4reconcile.Spec.SkipImmediately != nil && *scheduleb4reconcile.Spec.SkipImmediately ||
|
||||
test.reconcilerSkipImmediately && scheduleb4reconcile.Spec.SkipImmediately == nil {
|
||||
assert.Equal(t, schedule.Spec.SkipImmediately, pointer.Bool(false))
|
||||
}
|
||||
|
||||
backups := &velerov1.BackupList{}
|
||||
require.Nil(t, client.List(ctx, backups))
|
||||
@@ -403,7 +461,7 @@ func TestCheckIfBackupInNewOrProgress(t *testing.T) {
|
||||
err = client.Create(ctx, newBackup)
|
||||
require.NoError(t, err, "fail to create backup in New phase in TestCheckIfBackupInNewOrProgress: %v", err)
|
||||
|
||||
reconciler := NewScheduleReconciler("ns", logger, client, metrics.NewServerMetrics())
|
||||
reconciler := NewScheduleReconciler("ns", logger, client, metrics.NewServerMetrics(), false)
|
||||
result := reconciler.checkIfBackupInNewOrProgress(testSchedule)
|
||||
assert.True(t, result)
|
||||
|
||||
@@ -418,7 +476,7 @@ func TestCheckIfBackupInNewOrProgress(t *testing.T) {
|
||||
err = client.Create(ctx, inProgressBackup)
|
||||
require.NoError(t, err, "fail to create backup in InProgress phase in TestCheckIfBackupInNewOrProgress: %v", err)
|
||||
|
||||
reconciler = NewScheduleReconciler("namespace", logger, client, metrics.NewServerMetrics())
|
||||
reconciler = NewScheduleReconciler("namespace", logger, client, metrics.NewServerMetrics(), false)
|
||||
result = reconciler.checkIfBackupInNewOrProgress(testSchedule)
|
||||
assert.True(t, result)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ type podTemplateConfig struct {
|
||||
defaultSnapshotMoveData bool
|
||||
privilegedNodeAgent bool
|
||||
disableInformerCache bool
|
||||
scheduleSkipImmediately bool
|
||||
}
|
||||
|
||||
func WithImage(image string) podTemplateOption {
|
||||
@@ -170,6 +171,12 @@ func WithPrivilegedNodeAgent() podTemplateOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithScheduleSkipImmediately(b bool) podTemplateOption {
|
||||
return func(c *podTemplateConfig) {
|
||||
c.scheduleSkipImmediately = b
|
||||
}
|
||||
}
|
||||
|
||||
func Deployment(namespace string, opts ...podTemplateOption) *appsv1.Deployment {
|
||||
// TODO: Add support for server args
|
||||
c := &podTemplateConfig{
|
||||
@@ -203,6 +210,10 @@ func Deployment(namespace string, opts ...podTemplateOption) *appsv1.Deployment
|
||||
args = append(args, "--disable-informer-cache=true")
|
||||
}
|
||||
|
||||
if c.scheduleSkipImmediately {
|
||||
args = append(args, "--schedule-skip-immediately=true")
|
||||
}
|
||||
|
||||
if len(c.uploaderType) > 0 {
|
||||
args = append(args, fmt.Sprintf("--uploader-type=%s", c.uploaderType))
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ type VeleroOptions struct {
|
||||
UploaderType string
|
||||
DefaultSnapshotMoveData bool
|
||||
DisableInformerCache bool
|
||||
ScheduleSkipImmediately bool
|
||||
}
|
||||
|
||||
func AllCRDs() *unstructured.UnstructuredList {
|
||||
@@ -338,6 +339,7 @@ func AllResources(o *VeleroOptions) *unstructured.UnstructuredList {
|
||||
WithGarbageCollectionFrequency(o.GarbageCollectionFrequency),
|
||||
WithPodVolumeOperationTimeout(o.PodVolumeOperationTimeout),
|
||||
WithUploaderType(o.UploaderType),
|
||||
WithScheduleSkipImmediately(o.ScheduleSkipImmediately),
|
||||
}
|
||||
|
||||
if len(o.Features) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user