313 lines
10 KiB
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)
|
|
}
|