kubernetes-operator/pkg/configuration/backuprestore/backuprestore.go

313 lines
10 KiB
Go

package backuprestore
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/client"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/go-logr/logr"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
k8s "sigs.k8s.io/controller-runtime/pkg/client"
)
type backupTrigger struct {
interval uint64
ticker *time.Ticker
}
type backupTriggers struct {
triggers map[string]backupTrigger
}
func (t *backupTriggers) stop(logger logr.Logger, namespace string, name string) {
key := t.key(namespace, name)
trigger, found := t.triggers[key]
if found {
logger.Info(fmt.Sprintf("Stopping backup trigger for '%s'", key))
trigger.ticker.Stop()
delete(t.triggers, key)
} else {
logger.V(log.VWarn).Info(fmt.Sprintf("Can't stop backup trigger for '%s', not found, skipping", key))
}
}
func (t *backupTriggers) get(namespace, name string) (backupTrigger, bool) {
trigger, found := t.triggers[t.key(namespace, name)]
return trigger, found
}
func (t *backupTriggers) key(namespace, name string) string {
return namespace + "/" + name
}
func (t *backupTriggers) add(namespace string, name string, trigger backupTrigger) {
t.triggers[t.key(namespace, name)] = trigger
}
var triggers = backupTriggers{triggers: make(map[string]backupTrigger)}
// BackupAndRestore represents Jenkins backup and restore client
type BackupAndRestore struct {
configuration.Configuration
logger logr.Logger
}
// New returns Jenkins backup and restore client
func New(configuration configuration.Configuration, logger logr.Logger) *BackupAndRestore {
return &BackupAndRestore{
Configuration: configuration,
logger: logger,
}
}
// Validate validates backup and restore configuration
func (bar *BackupAndRestore) Validate() []string {
var messages []string
allContainers := map[string]v1alpha2.Container{}
for _, container := range bar.Configuration.Jenkins.Spec.Master.Containers {
allContainers[container.Name] = container
}
restore := bar.Configuration.Jenkins.Spec.Restore
if len(restore.ContainerName) > 0 {
_, found := allContainers[restore.ContainerName]
if !found {
messages = append(messages, fmt.Sprintf("restore container '%s' not found in CR spec.master.containers", restore.ContainerName))
}
if restore.Action.Exec == nil {
messages = append(messages, "spec.restore.action.exec is not configured")
}
}
backup := bar.Configuration.Jenkins.Spec.Backup
if len(backup.ContainerName) > 0 {
_, found := allContainers[backup.ContainerName]
if !found {
messages = append(messages, fmt.Sprintf("backup container '%s' not found in CR spec.master.containers", backup.ContainerName))
}
if backup.Action.Exec == nil {
messages = append(messages, "spec.backup.action.exec is not configured")
}
if backup.Interval == 0 {
messages = append(messages, "spec.backup.interval is not configured")
}
}
if len(restore.ContainerName) > 0 && len(backup.ContainerName) == 0 {
messages = append(messages, "spec.backup.containerName is not configured")
}
if len(backup.ContainerName) > 0 && len(restore.ContainerName) == 0 {
messages = append(messages, "spec.restore.containerName is not configured")
}
return messages
}
// helper value indicating no saved backup
const noBackup = "-1"
// Restore performs Jenkins restore backup operation
func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error {
jenkins := bar.Configuration.Jenkins
if len(jenkins.Spec.Restore.ContainerName) == 0 || jenkins.Spec.Restore.Action.Exec == nil {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured")
return nil
}
if jenkins.Status.RestoredBackup != 0 {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup already restored")
return nil
}
if jenkins.Status.LastBackup == 0 && jenkins.Spec.Restore.GetLatestAction.Exec == nil {
bar.logger.V(log.VDebug).Info("Skipping restore backup")
if jenkins.Status.PendingBackup == 0 {
jenkins.Status.PendingBackup = 1
return bar.Client.Status().Update(context.TODO(), jenkins)
}
return nil
}
podName := resources.GetJenkinsMasterPodName(jenkins)
var backupNumber = jenkins.Status.LastBackup
if jenkins.Spec.Restore.GetLatestAction.Exec != nil {
command := jenkins.Spec.Restore.GetLatestAction.Exec.Command
backupNumberRaw, _, err := bar.Exec(podName, jenkins.Spec.Restore.ContainerName, command)
if err != nil {
return err
}
backupNumberString := strings.TrimSuffix(backupNumberRaw.String(), "\n")
if backupNumberString == noBackup {
bar.logger.V(log.VDebug).Info("Skipping restore backup, get latest action returned -1")
jenkins.Status.LastBackup = 0
jenkins.Status.PendingBackup = 1
return bar.Client.Status().Update(context.TODO(), jenkins)
}
backupNumber, err = strconv.ParseUint(backupNumberString, 10, 64)
if err != nil {
return errors.Wrapf(err, "invalid backup number '%s' returned by get last backup number action", backupNumberString)
}
if backupNumber < 1 {
return errors.Errorf("invalid backup number '%d' returned by get last backup number action", backupNumber)
}
} else {
bar.logger.V(log.VWarn).Info("spec.restore.getLatestAction not set, you may loose backup history when Jenkins CR status will be clear")
}
if jenkins.Spec.Restore.RecoveryOnce != 0 {
backupNumber = jenkins.Spec.Restore.RecoveryOnce
}
bar.logger.Info(fmt.Sprintf("Restoring backup '%d'", backupNumber))
command := jenkins.Spec.Restore.Action.Exec.Command
command = append(command, fmt.Sprintf("%d", backupNumber))
_, _, err := bar.Exec(podName, jenkins.Spec.Restore.ContainerName, command)
if err == nil {
_, err := jenkinsClient.ExecuteScript("Jenkins.instance.reload()")
if err != nil {
return err
}
key := types.NamespacedName{
Namespace: jenkins.Namespace,
Name: jenkins.Name,
}
err = bar.Client.Get(context.TODO(), key, jenkins)
if err != nil {
return err
}
jenkins.Spec.Restore.RecoveryOnce = 0
err = bar.Client.Update(context.TODO(), jenkins)
if err != nil {
return err
}
bar.Configuration.Jenkins = jenkins
jenkins.Status.RestoredBackup = backupNumber
jenkins.Status.PendingBackup = backupNumber + 1
return bar.Client.Status().Update(context.TODO(), jenkins)
}
return err
}
// Backup performs Jenkins backup operation
func (bar *BackupAndRestore) Backup(setBackupDoneBeforePodDeletion bool) error {
jenkins := bar.Configuration.Jenkins
if len(jenkins.Spec.Backup.ContainerName) == 0 || jenkins.Spec.Backup.Action.Exec == nil {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured")
return nil
}
if jenkins.Status.PendingBackup == jenkins.Status.LastBackup {
bar.logger.V(log.VDebug).Info("Skipping backup")
return nil
}
backupNumber := jenkins.Status.PendingBackup
bar.logger.Info(fmt.Sprintf("Performing backup '%d'", backupNumber))
podName := resources.GetJenkinsMasterPodName(jenkins)
command := jenkins.Spec.Backup.Action.Exec.Command
command = append(command, fmt.Sprintf("%d", backupNumber))
_, _, err := bar.Exec(podName, jenkins.Spec.Backup.ContainerName, command)
if err == nil {
key := types.NamespacedName{
Namespace: jenkins.Namespace,
Name: jenkins.Name,
}
err = bar.Client.Get(context.TODO(), key, jenkins)
if err != nil {
return err
}
bar.logger.V(log.VDebug).Info(fmt.Sprintf("Backup completed '%d', updating status", backupNumber))
if jenkins.Status.RestoredBackup == 0 {
jenkins.Status.RestoredBackup = backupNumber
}
jenkins.Status.LastBackup = backupNumber
jenkins.Status.PendingBackup = backupNumber
jenkins.Status.BackupDoneBeforePodDeletion = setBackupDoneBeforePodDeletion
return bar.Client.Status().Update(context.TODO(), jenkins)
}
return err
}
func triggerBackup(ticker *time.Ticker, k8sClient k8s.Client, logger logr.Logger, namespace, name string) {
for range ticker.C {
jenkins := &v1alpha2.Jenkins{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, jenkins)
if err != nil && apierrors.IsNotFound(err) {
triggers.stop(logger, namespace, name)
return // abort
} else if err != nil {
logger.V(log.VWarn).Info(fmt.Sprintf("backup trigger, error when fetching CR: %s", err))
}
if jenkins.Status.LastBackup == jenkins.Status.PendingBackup {
jenkins.Status.PendingBackup++
err = k8sClient.Status().Update(context.TODO(), jenkins)
if err != nil {
logger.V(log.VWarn).Info(fmt.Sprintf("backup trigger, error when updating CR: %s", err))
}
}
}
}
// EnsureBackupTrigger creates or update trigger which update CR to make backup
func (bar *BackupAndRestore) EnsureBackupTrigger() error {
trigger, found := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
isBackupConfigured := len(bar.Configuration.Jenkins.Spec.Backup.ContainerName) > 0 && bar.Configuration.Jenkins.Spec.Backup.Interval > 0
if found && !isBackupConfigured {
bar.StopBackupTrigger()
return nil
}
// configured backup has no trigger
if !found && isBackupConfigured {
bar.startBackupTrigger()
return nil
}
if found && isBackupConfigured && bar.Configuration.Jenkins.Spec.Backup.Interval != trigger.interval {
bar.StopBackupTrigger()
bar.startBackupTrigger()
}
return nil
}
// StopBackupTrigger stops trigger which update CR to make backup
func (bar *BackupAndRestore) StopBackupTrigger() {
triggers.stop(bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
}
// IsBackupTriggerEnabled returns true if the backup trigger is enabled
func (bar *BackupAndRestore) IsBackupTriggerEnabled() bool {
_, enabled := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
return enabled
}
func (bar *BackupAndRestore) startBackupTrigger() {
bar.logger.Info("Starting backup trigger")
ticker := time.NewTicker(time.Duration(bar.Configuration.Jenkins.Spec.Backup.Interval) * time.Second)
triggers.add(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name, backupTrigger{
interval: bar.Configuration.Jenkins.Spec.Backup.Interval,
ticker: ticker,
})
go triggerBackup(ticker, bar.Client, bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
}