From 2d501b00d59af5af5c2487abb35b5fb14b8de949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 16 Jun 2019 08:21:23 +0200 Subject: [PATCH] #4 [WIP] Backup and restore --- cmd/manager/main.go | 8 +- go.mod | 1 + go.sum | 2 + pkg/apis/jenkins/v1alpha2/jenkins_types.go | 27 ++- pkg/apis/jenkins/v1alpha2/register.go | 4 + .../jenkins/configuration/base/reconcile.go | 11 +- .../configuration/base/resources/pod.go | 2 +- .../user/backuprestore/backuprestore.go | 191 ++++++++++++++++++ .../jenkins/configuration/user/reconcile.go | 21 +- .../jenkins/configuration/user/validate.go | 6 + pkg/controller/jenkins/jenkins_controller.go | 40 +++- 11 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 pkg/controller/jenkins/configuration/user/backuprestore/backuprestore.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 40d159ac..8b40b54e 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -25,6 +25,7 @@ import ( sdkVersion "github.com/operator-framework/operator-sdk/version" "github.com/pkg/errors" "github.com/spf13/pflag" + "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -107,8 +108,13 @@ func main() { fatal(errors.Wrap(err, "failed to create manager"), *debug) } + clientSet, err := kubernetes.NewForConfig(cfg) + if err != nil { + fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug) + } + // setup Jenkins controller - if err := jenkins.Add(mgr, *local, *minikube, events); err != nil { + if err := jenkins.Add(mgr, *local, *minikube, events, *clientSet, *cfg); err != nil { fatal(errors.Wrap(err, "failed to setup controllers"), *debug) } diff --git a/go.mod b/go.mod index 27840d08..5690bdc1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/coreos/prometheus-operator v0.26.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible + github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/emicklei/go-restful v2.8.1+incompatible // indirect github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.0 diff --git a/go.sum b/go.sum index 798f4ae4..6a041cfd 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index df07bbbb..612f4cff 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,6 +17,8 @@ type JenkinsSpec struct { SeedJobs []SeedJob `json:"seedJobs,omitempty"` Service Service `json:"service,omitempty"` SlaveService Service `json:"slaveService,omitempty"` + Backup Backup `json:"backup,omitempty"` + Restore Restore `json:"restore,omitempty"` } // Container defines Kubernetes container attributes @@ -80,6 +82,9 @@ type JenkinsStatus struct { BaseConfigurationCompletedTime *metav1.Time `json:"baseConfigurationCompletedTime,omitempty"` UserConfigurationCompletedTime *metav1.Time `json:"userConfigurationCompletedTime,omitempty"` Builds []Build `json:"builds,omitempty"` + RestoredBackup uint64 `json:"restoredBackup,omitempty"` + LastBackup uint64 `json:"lastBackup,omitempty"` + PendingBackup uint64 `json:"pendingBackup,omitempty"` } // BuildStatus defines type of Jenkins build job status @@ -154,7 +159,7 @@ var AllowedJenkinsCredentialMap = map[string]string{ string(UsernamePasswordCredentialType): "", } -// SeedJob defined configuration for seed jobs and deploy keys +// SeedJob defines configuration for seed jobs and deploy keys type SeedJob struct { ID string `json:"id,omitempty"` CredentialID string `json:"credentialID,omitempty"` @@ -165,6 +170,22 @@ type SeedJob struct { JenkinsCredentialType JenkinsCredentialType `json:"credentialType,omitempty"` } -func init() { - SchemeBuilder.Register(&Jenkins{}, &JenkinsList{}) +// Handler defines a specific action that should be taken +type Handler struct { + // Exec specifies the action to take. + Exec *corev1.ExecAction `json:"exec,omitempty"` +} + +// Backup defines configuration of Jenkins backup +type Backup struct { + ContainerName string `json:"containerName"` + Action Handler `json:"action"` + Interval uint64 `json:"interval"` +} + +// Restore defines configuration of Jenkins backup restore +type Restore struct { + ContainerName string `json:"containerName"` + Action Handler `json:"action"` + RecoveryOnce uint64 `json:"recoveryOnce,omitempty"` } diff --git a/pkg/apis/jenkins/v1alpha2/register.go b/pkg/apis/jenkins/v1alpha2/register.go index 16a6256e..53e87d4a 100644 --- a/pkg/apis/jenkins/v1alpha2/register.go +++ b/pkg/apis/jenkins/v1alpha2/register.go @@ -25,3 +25,7 @@ var ( // GetObjectKind returns Jenkins object kind func (in *Jenkins) GetObjectKind() schema.ObjectKind { return in } + +func init() { + SchemeBuilder.Register(&Jenkins{}, &JenkinsList{}) +} diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index e0a20e44..11097c8e 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -360,9 +360,9 @@ func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta } func (r *ReconcileJenkinsBaseConfiguration) getJenkinsMasterPod(meta metav1.ObjectMeta) (*corev1.Pod, error) { - jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.jenkins) + jenkinsMasterPodName := resources.GetJenkinsMasterPodName(*r.jenkins) currentJenkinsMasterPod := &corev1.Pod{} - err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: jenkinsMasterPod.Name, Namespace: jenkinsMasterPod.Namespace}, currentJenkinsMasterPod) + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: jenkinsMasterPodName, Namespace: r.jenkins.Namespace}, currentJenkinsMasterPod) if err != nil { return nil, err // don't wrap error } @@ -382,6 +382,8 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.O now := metav1.Now() r.jenkins.Status = v1alpha2.JenkinsStatus{ ProvisionStartTime: &now, + LastBackup: r.jenkins.Status.LastBackup, + PendingBackup: r.jenkins.Status.LastBackup, } err = r.updateResource(r.jenkins) if err != nil { @@ -407,6 +409,11 @@ func isPodTerminating(pod corev1.Pod) bool { } func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMasterPod corev1.Pod) bool { + if r.jenkins.Spec.Restore.RecoveryOnce != 0 { + r.logger.Info(fmt.Sprintf("spec.restore.recoveryOnce is set, recreating pod")) + return true + } + if version.Version != r.jenkins.Status.OperatorVersion { r.logger.Info(fmt.Sprintf("Jenkins Operator version has changed, actual '%+v' new '%+v', recreating pod", r.jenkins.Status.OperatorVersion, version.Version)) diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index c53df583..faadfcbc 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -14,7 +14,7 @@ const ( // JenkinsMasterContainerName is the Jenkins master container name in pod JenkinsMasterContainerName = "jenkins-master" // JenkinsHomeVolumeName is the Jenkins home volume name - JenkinsHomeVolumeName = "home" + JenkinsHomeVolumeName = "jenkins-home" jenkinsPath = "/var/jenkins" jenkinsHomePath = jenkinsPath + "/home" diff --git a/pkg/controller/jenkins/configuration/user/backuprestore/backuprestore.go b/pkg/controller/jenkins/configuration/user/backuprestore/backuprestore.go new file mode 100644 index 00000000..9192d14a --- /dev/null +++ b/pkg/controller/jenkins/configuration/user/backuprestore/backuprestore.go @@ -0,0 +1,191 @@ +package backuprestore + +import ( + "bytes" + "context" + "fmt" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + + "github.com/go-logr/logr" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + k8s "sigs.k8s.io/controller-runtime/pkg/client" +) + +// BackupAndRestore represents Jenkins backup and restore client +type BackupAndRestore struct { + config rest.Config + k8sClient k8s.Client + clientSet kubernetes.Clientset + + jenkinsClient jenkinsclient.Jenkins + logger logr.Logger + jenkins *v1alpha2.Jenkins +} + +// New returns Jenkins backup and restore client +func New(k8sClient k8s.Client, clientSet kubernetes.Clientset, jenkinsClient jenkinsclient.Jenkins, + logger logr.Logger, jenkins *v1alpha2.Jenkins, config rest.Config) *BackupAndRestore { + return &BackupAndRestore{k8sClient: k8sClient, clientSet: clientSet, jenkinsClient: jenkinsClient, logger: logger, jenkins: jenkins, config: config} +} + +// Validate validates backup and restore configuration +func (bar *BackupAndRestore) Validate() bool { + valid := true + allContainers := map[string]v1alpha2.Container{} + for _, container := range bar.jenkins.Spec.Master.Containers { + allContainers[container.Name] = container + } + + restore := bar.jenkins.Spec.Restore + if len(restore.ContainerName) > 0 { + _, found := allContainers[restore.ContainerName] + if !found { + valid = false + bar.logger.V(log.VWarn).Info(fmt.Sprintf("restore container '%s' not found in CR spec.master.containers", restore.ContainerName)) + } + if restore.Action.Exec == nil { + valid = false + bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.restore.action.exec is not configured")) + } + } + + backup := bar.jenkins.Spec.Backup + if len(backup.ContainerName) > 0 { + _, found := allContainers[backup.ContainerName] + if !found { + valid = false + bar.logger.V(log.VWarn).Info(fmt.Sprintf("backup container '%s' not found in CR spec.master.containers", backup.ContainerName)) + } + if backup.Action.Exec == nil { + valid = false + bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.backup.action.exec is not configured")) + } + if backup.Interval == 0 { + valid = false + bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.backup.interval is not configured")) + } + } + + if len(restore.ContainerName) > 0 && len(backup.ContainerName) == 0 { + valid = false + bar.logger.V(log.VWarn).Info("spec.backup.containerName is not configured") + } + if len(backup.ContainerName) > 0 && len(restore.ContainerName) == 0 { + valid = false + bar.logger.V(log.VWarn).Info("spec.restore.containerName is not configured") + } + + return valid +} + +// Restore performs Jenkins restore backup operation +func (bar *BackupAndRestore) Restore() error { + jenkins := bar.jenkins + if jenkins.Status.RestoredBackup != 0 { + bar.logger.V(log.VDebug).Info("Skipping restore backup, backup already restored") + return nil + } + if jenkins.Status.LastBackup == 0 { + bar.logger.Info("Skipping restore backup") + if jenkins.Status.PendingBackup == 0 { + jenkins.Status.PendingBackup = 1 + return bar.k8sClient.Update(context.TODO(), jenkins) + } + return nil + } + + var backupNumber uint64 + if jenkins.Spec.Restore.RecoveryOnce != 0 { + backupNumber = jenkins.Spec.Restore.RecoveryOnce + } else { + backupNumber = jenkins.Status.LastBackup + } + bar.logger.Info(fmt.Sprintf("Restoring backup '%d'", backupNumber)) + podName := resources.GetJenkinsMasterPodName(*jenkins) + 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 { + jenkins.Spec.Restore.RecoveryOnce = 0 + jenkins.Status.RestoredBackup = backupNumber + jenkins.Status.PendingBackup = backupNumber + 1 + return bar.k8sClient.Update(context.TODO(), jenkins) + } + + //TODO reload? + //TODO after 3 fails stop + + return err +} + +// Backup performs Jenkins backup operation +func (bar *BackupAndRestore) Backup() error { + jenkins := bar.jenkins + 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 { + if jenkins.Status.RestoredBackup == 0 { + jenkins.Status.RestoredBackup = backupNumber + } + jenkins.Status.LastBackup = backupNumber + jenkins.Status.PendingBackup = backupNumber + return bar.k8sClient.Update(context.TODO(), jenkins) + } + + //TODO after 3 fails stop + + return err +} + +func (bar *BackupAndRestore) exec(podName, containerName string, command []string) (stdout, stderr bytes.Buffer, err error) { + req := bar.clientSet.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(bar.jenkins.Namespace). + SubResource("exec") + req.VersionedParams(&corev1.PodExecOptions{ + Command: command, + Container: containerName, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(&bar.config, "POST", req.URL()) + if err != nil { + return stdout, stderr, errors.Wrap(err, "pod exec error while creating Executor") + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + bar.logger.V(log.VDebug).Info(fmt.Sprintf("pod exec: stdout '%s' stderr '%s'", stdout.String(), stderr.String())) + if err != nil { + return stdout, stderr, errors.Wrapf(err, "pod exec error operation on stream: stdout '%s' stderr '%s'", stdout.String(), stderr.String()) + } + + return +} diff --git a/pkg/controller/jenkins/configuration/user/reconcile.go b/pkg/controller/jenkins/configuration/user/reconcile.go index 268965f5..8b0583ff 100644 --- a/pkg/controller/jenkins/configuration/user/reconcile.go +++ b/pkg/controller/jenkins/configuration/user/reconcile.go @@ -7,6 +7,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/backuprestore" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/casc" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" @@ -17,6 +18,8 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" k8s "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -27,22 +30,27 @@ type ReconcileUserConfiguration struct { jenkinsClient jenkinsclient.Jenkins logger logr.Logger jenkins *v1alpha2.Jenkins + clientSet kubernetes.Clientset + config rest.Config } // New create structure which takes care of user configuration func New(k8sClient k8s.Client, jenkinsClient jenkinsclient.Jenkins, logger logr.Logger, - jenkins *v1alpha2.Jenkins) *ReconcileUserConfiguration { + jenkins *v1alpha2.Jenkins, clientSet kubernetes.Clientset, config rest.Config) *ReconcileUserConfiguration { return &ReconcileUserConfiguration{ k8sClient: k8sClient, jenkinsClient: jenkinsClient, logger: logger, jenkins: jenkins, + clientSet: clientSet, + config: config, } } // Reconcile it's a main reconciliation loop for user supplied configuration func (r *ReconcileUserConfiguration) Reconcile() (reconcile.Result, error) { - // reconcile seed jobs + backupAndRestore := backuprestore.New(r.k8sClient, r.clientSet, r.jenkinsClient, r.logger, r.jenkins, r.config) + result, err := r.ensureSeedJobs() if err != nil { return reconcile.Result{}, err @@ -51,6 +59,10 @@ func (r *ReconcileUserConfiguration) Reconcile() (reconcile.Result, error) { return result, nil } + if err := backupAndRestore.Restore(); err != nil { + return reconcile.Result{}, err + } + result, err = r.ensureUserConfiguration(r.jenkinsClient) if err != nil { return reconcile.Result{}, err @@ -59,6 +71,11 @@ func (r *ReconcileUserConfiguration) Reconcile() (reconcile.Result, error) { return result, nil } + if err := backupAndRestore.Backup(); err != nil { + return reconcile.Result{}, err + } + //TODO backup Goroutine + return reconcile.Result{}, nil } diff --git a/pkg/controller/jenkins/configuration/user/validate.go b/pkg/controller/jenkins/configuration/user/validate.go index 36e590ed..232890d2 100644 --- a/pkg/controller/jenkins/configuration/user/validate.go +++ b/pkg/controller/jenkins/configuration/user/validate.go @@ -2,11 +2,17 @@ package user import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/backuprestore" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs" ) // Validate validates Jenkins CR Spec section func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha2.Jenkins) (bool, error) { + backupAndRestore := backuprestore.New(r.k8sClient, r.clientSet, r.jenkinsClient, r.logger, r.jenkins, r.config) + if ok := backupAndRestore.Validate(); !ok { + return false, nil + } + seedJobs := seedjobs.New(r.jenkinsClient, r.k8sClient, r.logger) return seedJobs.ValidateSeedJobs(*jenkins) } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 0d83cb06..689a35e1 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -23,6 +23,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -42,18 +44,20 @@ const ( // Add creates a new Jenkins Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. -func Add(mgr manager.Manager, local, minikube bool, events event.Recorder) error { - return add(mgr, newReconciler(mgr, local, minikube, events)) +func Add(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config) error { + return add(mgr, newReconciler(mgr, local, minikube, events, clientSet, config)) } // newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager, local, minikube bool, events event.Recorder) reconcile.Reconciler { +func newReconciler(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config) reconcile.Reconciler { return &ReconcileJenkins{ - client: mgr.GetClient(), - scheme: mgr.GetScheme(), - local: local, - minikube: minikube, - events: events, + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + local: local, + minikube: minikube, + events: events, + clientSet: clientSet, + config: config, } } @@ -103,6 +107,8 @@ type ReconcileJenkins struct { scheme *runtime.Scheme local, minikube bool events event.Recorder + clientSet kubernetes.Clientset + config rest.Config } // Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec @@ -181,7 +187,7 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg r.events.Emit(jenkins, event.TypeNormal, reasonBaseConfigurationSuccess, "Base configuration completed") } // Reconcile user configuration - userConfiguration := user.New(r.client, jenkinsClient, logger, jenkins) + userConfiguration := user.New(r.client, jenkinsClient, logger, jenkins, r.clientSet, r.config) valid, err = userConfiguration.Validate(jenkins) if err != nil { @@ -334,13 +340,25 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo } if len(jenkins.Spec.Master.Containers) > 1 { for i, container := range jenkins.Spec.Master.Containers[1:] { - if setDefaultsForContainer(jenkins, i, logger.WithValues("container", container.Name)) { + if setDefaultsForContainer(jenkins, i+1, logger.WithValues("container", container.Name)) { changed = true } } } + if len(jenkins.Spec.Backup.ContainerName) > 0 && jenkins.Spec.Backup.Interval == 0 { + logger.Info("Setting default backup interval") + changed = true + jenkins.Spec.Backup.Interval = 30 + } - jenkins.Spec.Master.Containers = []v1alpha2.Container{jenkinsContainer} + if len(jenkins.Spec.Master.Containers) == 0 || len(jenkins.Spec.Master.Containers) == 1 { + jenkins.Spec.Master.Containers = []v1alpha2.Container{jenkinsContainer} + } else { + noJenkinsContainers := jenkins.Spec.Master.Containers[1:] + containers := []v1alpha2.Container{jenkinsContainer} + containers = append(containers, noJenkinsContainers...) + jenkins.Spec.Master.Containers = containers + } if changed { return errors.WithStack(r.client.Update(context.TODO(), jenkins))