2017-08-02 17:27:17 +00:00
|
|
|
/*
|
2019-03-20 19:32:48 +00:00
|
|
|
Copyright 2017 the Velero contributors.
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
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 controller
|
|
|
|
|
|
|
|
import (
|
k8s 1.18 import (#2651)
* k8s 1.18 import wip
backup, cmd, controller, generated, restic, restore, serverstatusrequest, test and util
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* go mod tidy
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* add changelog file
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* go fmt
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* update code-generator and controller-gen in CI
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* checkout proper code-generator version, regen
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* fix remaining calls
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* regenerate CRDs with ./hack/update-generated-crd-code.sh
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* use existing context in restic and server
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* fix test cases by resetting resource version
also use main library go context, not golang.org/x/net/context, in pkg/restore/restore.go
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* clarify changelog message
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* use github.com/kubernetes-csi/external-snapshotter/v2@v2.2.0-rc1
Signed-off-by: Andrew Lavery <laverya@umich.edu>
* run 'go mod tidy' to remove old external-snapshotter version
Signed-off-by: Andrew Lavery <laverya@umich.edu>
2020-07-16 16:21:37 +00:00
|
|
|
"context"
|
2017-08-02 17:27:17 +00:00
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
2017-09-14 21:27:31 +00:00
|
|
|
"github.com/pkg/errors"
|
2017-08-02 17:27:17 +00:00
|
|
|
"github.com/robfig/cron"
|
2017-09-14 21:27:31 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2022-04-19 01:23:40 +00:00
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
2017-08-02 17:27:17 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/util/clock"
|
2022-03-11 02:16:41 +00:00
|
|
|
"sigs.k8s.io/cluster-api/util/patch"
|
|
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
2019-09-30 21:26:56 +00:00
|
|
|
"github.com/vmware-tanzu/velero/pkg/builder"
|
|
|
|
"github.com/vmware-tanzu/velero/pkg/metrics"
|
2022-03-11 02:16:41 +00:00
|
|
|
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
2019-09-30 21:26:56 +00:00
|
|
|
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
2017-08-02 17:27:17 +00:00
|
|
|
)
|
|
|
|
|
2018-08-09 16:54:48 +00:00
|
|
|
const (
|
|
|
|
scheduleSyncPeriod = time.Minute
|
|
|
|
)
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
type scheduleReconciler struct {
|
|
|
|
client.Client
|
|
|
|
namespace string
|
|
|
|
logger logrus.FieldLogger
|
|
|
|
clock clock.Clock
|
|
|
|
metrics *metrics.ServerMetrics
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func NewScheduleReconciler(
|
2017-12-22 14:43:44 +00:00
|
|
|
namespace string,
|
2017-12-11 22:10:52 +00:00
|
|
|
logger logrus.FieldLogger,
|
2022-03-11 02:16:41 +00:00
|
|
|
client client.Client,
|
2018-07-20 13:03:44 +00:00
|
|
|
metrics *metrics.ServerMetrics,
|
2022-03-11 02:16:41 +00:00
|
|
|
) *scheduleReconciler {
|
|
|
|
return &scheduleReconciler{
|
|
|
|
Client: client,
|
|
|
|
namespace: namespace,
|
|
|
|
logger: logger,
|
|
|
|
clock: clock.RealClock{},
|
|
|
|
metrics: metrics,
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
2022-03-11 02:16:41 +00:00
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func (c *scheduleReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|
|
|
s := kube.NewPeriodicalEnqueueSource(c.logger, mgr.GetClient(), &velerov1.ScheduleList{}, scheduleSyncPeriod)
|
|
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
|
|
For(&velerov1.Schedule{}).
|
|
|
|
Watches(s, nil).
|
|
|
|
Complete(c)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
// +kubebuilder:rbac:groups=velero.io,resources=schedules,verbs=get;list;watch;create;update;patch;delete
|
|
|
|
// +kubebuilder:rbac:groups=velero.io,resources=schedules/status,verbs=get;update;patch
|
|
|
|
// +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=create
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
|
|
|
log := c.logger.WithField("schedule", req.String())
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
log.Debug("Getting schedule")
|
|
|
|
schedule := &velerov1.Schedule{}
|
|
|
|
if err := c.Get(ctx, req.NamespacedName, schedule); err != nil {
|
2022-04-19 01:23:40 +00:00
|
|
|
if apierrors.IsNotFound(err) {
|
|
|
|
log.WithError(err).Error("schedule not found")
|
|
|
|
return ctrl.Result{}, nil
|
|
|
|
}
|
|
|
|
return ctrl.Result{}, errors.Wrapf(err, "error getting schedule %s", req.String())
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
if schedule.Status.Phase != "" &&
|
|
|
|
schedule.Status.Phase != velerov1.SchedulePhaseNew &&
|
|
|
|
schedule.Status.Phase != velerov1.SchedulePhaseEnabled {
|
|
|
|
log.Debugf("the schedule phase is %s, isn't %s or %s, skip", schedule.Status.Phase, velerov1.SchedulePhaseNew, velerov1.SchedulePhaseEnabled)
|
|
|
|
return ctrl.Result{}, nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
c.metrics.InitSchedule(schedule.Name)
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
patchHelper, err := patch.NewHelper(schedule, c.Client)
|
|
|
|
if err != nil {
|
2022-04-19 01:23:40 +00:00
|
|
|
return ctrl.Result{}, errors.Wrapf(err, "error new patch helper for schedule %s", req.String())
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// validation - even if the item is Enabled, we can't trust it
|
|
|
|
// so re-validate
|
|
|
|
currentPhase := schedule.Status.Phase
|
|
|
|
|
2018-08-29 19:52:09 +00:00
|
|
|
cronSchedule, errs := parseCronSchedule(schedule, c.logger)
|
2017-08-02 17:27:17 +00:00
|
|
|
if len(errs) > 0 {
|
2022-03-11 02:16:41 +00:00
|
|
|
schedule.Status.Phase = velerov1.SchedulePhaseFailedValidation
|
2017-08-02 17:27:17 +00:00
|
|
|
schedule.Status.ValidationErrors = errs
|
|
|
|
} else {
|
2022-03-11 02:16:41 +00:00
|
|
|
schedule.Status.Phase = velerov1.SchedulePhaseEnabled
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// update status if it's changed
|
|
|
|
if currentPhase != schedule.Status.Phase {
|
2022-03-11 02:16:41 +00:00
|
|
|
if err = patchHelper.Patch(ctx, schedule); err != nil {
|
2022-04-19 01:23:40 +00:00
|
|
|
return ctrl.Result{}, errors.Wrapf(err, "error updating phase of schedule %s to %s", req.String(), schedule.Status.Phase)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
if schedule.Status.Phase != velerov1.SchedulePhaseEnabled {
|
|
|
|
log.Debugf("the schedule's phase is %s, isn't %s, skip", schedule.Status.Phase, velerov1.SchedulePhaseEnabled)
|
|
|
|
return ctrl.Result{}, nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// check for the schedule being due to run, and submit a Backup if so
|
2022-03-11 02:16:41 +00:00
|
|
|
if err := c.submitBackupIfDue(ctx, schedule, cronSchedule); err != nil {
|
2022-04-19 01:23:40 +00:00
|
|
|
return ctrl.Result{}, errors.Wrapf(err, "error running submitBackupIfDue for schedule %s", req.String())
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
return ctrl.Result{}, nil
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func parseCronSchedule(itm *velerov1.Schedule, logger logrus.FieldLogger) (cron.Schedule, []string) {
|
2017-08-02 17:27:17 +00:00
|
|
|
var validationErrors []string
|
|
|
|
var schedule cron.Schedule
|
|
|
|
|
|
|
|
// cron.Parse panics if schedule is empty
|
|
|
|
if len(itm.Spec.Schedule) == 0 {
|
|
|
|
validationErrors = append(validationErrors, "Schedule must be a non-empty valid Cron expression")
|
|
|
|
return nil, validationErrors
|
|
|
|
}
|
|
|
|
|
2018-08-29 19:52:09 +00:00
|
|
|
log := logger.WithField("schedule", kubeutil.NamespaceAndName(itm))
|
2017-09-14 21:27:31 +00:00
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
// adding a recover() around cron.Parse because it panics on empty string and is possible
|
|
|
|
// that it panics under other scenarios as well.
|
|
|
|
func() {
|
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
2018-08-29 19:52:09 +00:00
|
|
|
log.WithFields(logrus.Fields{
|
2017-09-14 21:27:31 +00:00
|
|
|
"schedule": itm.Spec.Schedule,
|
|
|
|
"recover": r,
|
|
|
|
}).Debug("Panic parsing schedule")
|
2017-08-02 17:27:17 +00:00
|
|
|
validationErrors = append(validationErrors, fmt.Sprintf("invalid schedule: %v", r))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2017-08-10 16:44:00 +00:00
|
|
|
if res, err := cron.ParseStandard(itm.Spec.Schedule); err != nil {
|
2018-08-29 19:52:09 +00:00
|
|
|
log.WithError(errors.WithStack(err)).WithField("schedule", itm.Spec.Schedule).Debug("Error parsing schedule")
|
2017-08-02 17:27:17 +00:00
|
|
|
validationErrors = append(validationErrors, fmt.Sprintf("invalid schedule: %v", err))
|
|
|
|
} else {
|
|
|
|
schedule = res
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if len(validationErrors) > 0 {
|
|
|
|
return nil, validationErrors
|
|
|
|
}
|
|
|
|
|
|
|
|
return schedule, nil
|
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func (c *scheduleReconciler) submitBackupIfDue(ctx context.Context, item *velerov1.Schedule, cronSchedule cron.Schedule) error {
|
2017-09-14 21:27:31 +00:00
|
|
|
var (
|
2018-08-29 19:52:09 +00:00
|
|
|
now = c.clock.Now()
|
2017-09-14 21:27:31 +00:00
|
|
|
isDue, nextRunTime = getNextRunTime(item, cronSchedule, now)
|
2018-08-29 19:52:09 +00:00
|
|
|
log = c.logger.WithField("schedule", kubeutil.NamespaceAndName(item))
|
2017-09-14 21:27:31 +00:00
|
|
|
)
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
if !isDue {
|
2018-11-02 21:55:47 +00:00
|
|
|
log.WithField("nextRunTime", nextRunTime).Debug("Schedule is not due, skipping")
|
2017-08-02 17:27:17 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't attempt to "catch up" if there are any missed or failed runs - simply
|
|
|
|
// trigger a Backup if it's time.
|
|
|
|
//
|
|
|
|
// It might also make sense in the future to explicitly check for currently-running
|
|
|
|
// backups so that we don't overlap runs (for disk snapshots in particular, this can
|
|
|
|
// lead to performance issues).
|
2018-08-29 19:52:09 +00:00
|
|
|
log.WithField("nextRunTime", nextRunTime).Info("Schedule is due, submitting Backup")
|
2017-08-02 17:27:17 +00:00
|
|
|
backup := getBackup(item, now)
|
2022-03-11 02:16:41 +00:00
|
|
|
if err := c.Create(ctx, backup); err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return errors.Wrap(err, "error creating Backup")
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
patchHelper, err := patch.NewHelper(item, c.Client)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error creating patch helper")
|
|
|
|
}
|
|
|
|
item.Status.LastBackup = &metav1.Time{Time: now}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
if err := patchHelper.Patch(ctx, item); err != nil {
|
|
|
|
return errors.Wrapf(err, "error updating Schedule's LastBackup time to %v", item.Status.LastBackup)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func getNextRunTime(schedule *velerov1.Schedule, cronSchedule cron.Schedule, asOf time.Time) (bool, time.Time) {
|
2019-10-14 16:20:28 +00:00
|
|
|
var lastBackupTime time.Time
|
|
|
|
if schedule.Status.LastBackup != nil {
|
|
|
|
lastBackupTime = schedule.Status.LastBackup.Time
|
2021-10-26 23:31:58 +00:00
|
|
|
} else {
|
|
|
|
lastBackupTime = schedule.CreationTimestamp.Time
|
2019-10-14 16:20:28 +00:00
|
|
|
}
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
nextRunTime := cronSchedule.Next(lastBackupTime)
|
|
|
|
|
|
|
|
return asOf.After(nextRunTime), nextRunTime
|
|
|
|
}
|
|
|
|
|
2022-03-11 02:16:41 +00:00
|
|
|
func getBackup(item *velerov1.Schedule, timestamp time.Time) *velerov1.Backup {
|
2020-05-28 19:31:25 +00:00
|
|
|
name := item.TimestampedName(timestamp)
|
2019-08-23 20:03:51 +00:00
|
|
|
backup := builder.
|
|
|
|
ForBackup(item.Namespace, name).
|
|
|
|
FromSchedule(item).
|
|
|
|
Result()
|
2018-09-03 13:01:03 +00:00
|
|
|
|
2017-08-02 17:27:17 +00:00
|
|
|
return backup
|
|
|
|
}
|