common/controllers/backupschedule_controller.go (239 lines of code) (raw):
// Copyright 2021 Google LLC
//
// 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 controllers
import (
"context"
"encoding/json"
"fmt"
"reflect"
"sort"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/common/api/v1alpha1"
)
const (
defaultTriggerDeadlineSeconds int64 = 30
defaultRetention int32 = 7
defaultMaxHistoryRecords int32 = 7
)
var (
defaultTimeFormat = "20060102-150405"
)
type backupScheduleControl interface {
Get(name, namespace string) (v1alpha1.BackupSchedule, error)
UpdateStatus(backupSchedule v1alpha1.BackupSchedule) error
GetBackupBytes(backupSchedule v1alpha1.BackupSchedule) ([]byte, error)
}
type backupControl interface {
List(cronAnythingName string) ([]v1alpha1.Backup, error)
Delete(backup v1alpha1.Backup) error
}
var _ reconcile.Reconciler = &BackupScheduleReconciler{}
// BackupScheduleReconciler reconciles a BackupSchedule object
type BackupScheduleReconciler struct {
client.Client
Log logr.Logger
scheme *runtime.Scheme
backupScheduleCtrl backupScheduleControl
cronAnythingCtrl cronAnythingControl
backupCtrl backupControl
}
// Reconcile is a generic reconcile function for BackupSchedule resources.
func (r *BackupScheduleReconciler) Reconcile(_ context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("backupschedule", req.NamespacedName)
backupSchedule, err := r.backupScheduleCtrl.Get(req.Name, req.Namespace)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
cron, err := r.lookupCron(backupSchedule)
if err != nil && !errors.IsNotFound(err) {
return ctrl.Result{}, err
}
if errors.IsNotFound(err) {
log.Info("No cron found for backup schedule. Creating new one", "backupSchedule", backupSchedule.GetNamespace()+"/"+backupSchedule.GetName())
err := r.createCron(backupSchedule)
return reconcile.Result{}, err
}
err = r.updateCron(backupSchedule, cron)
if err != nil {
return reconcile.Result{}, err
}
var backups []v1alpha1.Backup
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
backups, err = r.getSortedBackupsForCron(cron)
if err != nil {
return err
}
backupSchedule, err := r.backupScheduleCtrl.Get(req.Name, req.Namespace)
if err != nil {
return err
}
return r.updateHistory(backupSchedule, backups)
})
if err != nil {
return reconcile.Result{}, err
}
return ctrl.Result{}, r.pruneBackups(backupSchedule.BackupScheduleSpec().BackupRetentionPolicy, backups)
}
// NewBackupScheduleReconciler returns a BackupScheduleReconciler object.
func NewBackupScheduleReconciler(mgr manager.Manager, bsCtrl backupScheduleControl, caCtrl cronAnythingControl, backupCtrl backupControl) *BackupScheduleReconciler {
return &BackupScheduleReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("BackupSchedule"),
scheme: mgr.GetScheme(),
backupScheduleCtrl: bsCtrl,
cronAnythingCtrl: caCtrl,
backupCtrl: backupCtrl,
}
}
func (r *BackupScheduleReconciler) lookupCron(backupSchedule v1alpha1.BackupSchedule) (v1alpha1.CronAnything, error) {
cron, err := r.cronAnythingCtrl.Get(types.NamespacedName{
Namespace: backupSchedule.GetNamespace(),
Name: r.getCronName(backupSchedule)})
if err != nil {
return nil, err
}
return cron, nil
}
func (r *BackupScheduleReconciler) createCron(backupSchedule v1alpha1.BackupSchedule) error {
name := r.getCronName(backupSchedule)
triggerDeadlineSeconds := defaultTriggerDeadlineSeconds
if backupSchedule.BackupScheduleSpec().StartingDeadlineSeconds != nil {
triggerDeadlineSeconds = *backupSchedule.BackupScheduleSpec().StartingDeadlineSeconds
}
backupBytes, err := r.backupScheduleCtrl.GetBackupBytes(backupSchedule)
if err != nil {
return err
}
cronAnythingSpec := v1alpha1.CronAnythingSpec{
Schedule: backupSchedule.BackupScheduleSpec().Schedule,
TriggerDeadlineSeconds: &triggerDeadlineSeconds,
ConcurrencyPolicy: v1alpha1.ForbidConcurrent,
FinishableStrategy: &v1alpha1.FinishableStrategy{
Type: v1alpha1.FinishableStrategyStringField,
StringField: &v1alpha1.StringFieldStrategy{
FieldPath: "{.status.phase}",
FinishedValues: []string{
string(v1alpha1.BackupSucceeded),
string(v1alpha1.BackupFailed),
},
},
},
ResourceBaseName: &name,
ResourceTimestampFormat: &defaultTimeFormat,
Template: runtime.RawExtension{
Raw: backupBytes,
},
}
err = r.cronAnythingCtrl.Create(backupSchedule.GetNamespace(), name, cronAnythingSpec, backupSchedule)
if err != nil {
return err
}
return nil
}
func (r *BackupScheduleReconciler) updateCron(backupSchedule v1alpha1.BackupSchedule, cron v1alpha1.CronAnything) error {
backupBytes, err := r.backupScheduleCtrl.GetBackupBytes(backupSchedule)
if err != nil {
return err
}
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
freshCron, err := r.cronAnythingCtrl.Get(types.NamespacedName{
Namespace: cron.GetNamespace(),
Name: cron.GetName()})
if err != nil {
return err
}
templatesEqual, err := r.compareTemplate(freshCron.CronAnythingSpec().Template.Raw, backupBytes)
if err != nil {
return err
}
scheduleEqual := backupSchedule.BackupScheduleSpec().Schedule == freshCron.CronAnythingSpec().Schedule
startingDeadlineSecondsEqual := compareInt64Pointers(backupSchedule.BackupScheduleSpec().StartingDeadlineSeconds, freshCron.CronAnythingSpec().TriggerDeadlineSeconds)
r.Log.Info("backup schedule diff", "templateUnchanged", templatesEqual, "scheduleUnchanged", scheduleEqual, "StartingDeadlineSecondsUnchanged", startingDeadlineSecondsEqual)
if templatesEqual && scheduleEqual && startingDeadlineSecondsEqual {
return nil
}
freshCron.CronAnythingSpec().Schedule = backupSchedule.BackupScheduleSpec().Schedule
freshCron.CronAnythingSpec().Template.Raw = backupBytes
freshCron.CronAnythingSpec().TriggerDeadlineSeconds = backupSchedule.BackupScheduleSpec().StartingDeadlineSeconds
return r.Client.Update(context.TODO(), freshCron)
})
}
func (r *BackupScheduleReconciler) updateHistory(backupSchedule v1alpha1.BackupSchedule, sortedBackups []v1alpha1.Backup) error {
newBackupHistory := []v1alpha1.BackupHistoryRecord{}
for _, backup := range sortedBackups {
newBackupHistory = append(newBackupHistory, v1alpha1.BackupHistoryRecord{
BackupName: backup.GetName(),
CreationTime: backup.GetCreationTimestamp(),
Phase: backup.BackupStatus().Phase,
})
}
backupTotal := int32(len(newBackupHistory))
if backupTotal > defaultMaxHistoryRecords {
newBackupHistory = newBackupHistory[:defaultMaxHistoryRecords]
}
backupSchedule.BackupScheduleStatus().BackupTotal = &backupTotal
backupSchedule.BackupScheduleStatus().BackupHistory = newBackupHistory
return r.backupScheduleCtrl.UpdateStatus(backupSchedule)
}
func (r *BackupScheduleReconciler) pruneBackups(retention *v1alpha1.BackupRetentionPolicy, sortedBackups []v1alpha1.Backup) error {
max := defaultRetention
if retention != nil && retention.BackupRetention != nil {
max = *retention.BackupRetention
}
if max == 0 {
return nil
}
count := max
for _, backup := range sortedBackups {
if count <= 0 {
r.Log.Info("deleting backup", "backup", backup)
if err := r.backupCtrl.Delete(backup); err != nil {
return err
}
}
if backup.BackupStatus().Phase == v1alpha1.BackupSucceeded && count > 0 {
count -= 1
}
}
return nil
}
func (r *BackupScheduleReconciler) compareTemplate(left, right []byte) (bool, error) {
var leftMap map[string]interface{}
err := json.Unmarshal(left, &leftMap)
if err != nil {
return false, err
}
var rightMap map[string]interface{}
err = json.Unmarshal(right, &rightMap)
if err != nil {
return false, err
}
return reflect.DeepEqual(leftMap, rightMap), nil
}
func compareInt64Pointers(i1, i2 *int64) bool {
if i1 == nil && i2 == nil {
return true
}
if i1 == nil || i2 == nil {
return false
}
return *i1 == *i2
}
func (r *BackupScheduleReconciler) getCronName(backupSchedule v1alpha1.BackupSchedule) string {
return fmt.Sprintf("%s-cron", backupSchedule.GetName())
}
func (r *BackupScheduleReconciler) getSortedBackupsForCron(cron v1alpha1.CronAnything) ([]v1alpha1.Backup, error) {
backupList, err := r.backupCtrl.List(cron.GetName())
if err != nil {
return nil, err
}
sort.Slice(backupList, func(i, j int) bool {
iTime := backupList[i].GetCreationTimestamp()
jTime := backupList[j].GetCreationTimestamp()
return jTime.Before(&iTime)
})
return backupList, nil
}