pkg/controller/jenkins_jobbuildrun/controller.go (169 lines of code) (raw):
package jenkins_jobbuildrun
import (
"context"
"fmt"
"reflect"
"time"
"github.com/bndr/gojenkins"
"github.com/go-logr/logr"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
jenkinsApi "github.com/epam/edp-jenkins-operator/v2/pkg/apis/v2/v1"
"github.com/epam/edp-jenkins-operator/v2/pkg/client/jenkins"
"github.com/epam/edp-jenkins-operator/v2/pkg/controller/helper"
"github.com/epam/edp-jenkins-operator/v2/pkg/service/platform"
)
const (
retryInterval = 10 * time.Second
)
type Reconcile struct {
client client.Client
log logr.Logger
jenkinsClientFactory jenkins.ClientFactory
}
func NewReconciler(k8sCl client.Client, logf logr.Logger, ps platform.PlatformService) *Reconcile {
return &Reconcile{
client: k8sCl,
log: logf.WithName("controller_jenkins_jobbuildrun"),
jenkinsClientFactory: jenkins.MakeClientBuilder(ps, k8sCl),
}
}
func (r *Reconcile) SetupWithManager(mgr ctrl.Manager) error {
p := predicate.Funcs{
UpdateFunc: specUpdated,
}
err := ctrl.NewControllerManagedBy(mgr).
For(&jenkinsApi.JenkinsJobBuildRun{}, builder.WithPredicates(p)).
Complete(r)
if err != nil {
return fmt.Errorf("failed to create new managed controller: %w", err)
}
return nil
}
func specUpdated(e event.UpdateEvent) bool {
oldObject, ok := e.ObjectOld.(*jenkinsApi.JenkinsJobBuildRun)
if !ok {
return false
}
newObject, ok := e.ObjectNew.(*jenkinsApi.JenkinsJobBuildRun)
if !ok {
return false
}
return !reflect.DeepEqual(oldObject.Spec, newObject.Spec) ||
(oldObject.GetDeletionTimestamp().IsZero() && !newObject.GetDeletionTimestamp().IsZero())
}
func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
var result reconcile.Result
reqLogger := r.log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.V(2).Info("Reconciling JenkinsJobBuildRun has been started")
var instance jenkinsApi.JenkinsJobBuildRun
if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
if k8serrors.IsNotFound(err) {
reqLogger.Info("instance not found")
return result, nil
}
return result, fmt.Errorf("failed to get JenkinsJobBuildRun instance: %w", err)
}
if instance.Status.Status == jenkinsApi.JobBuildRunStatusCompleted {
reqLogger.V(2).Info("Reconciling JenkinsJobBuildRun has been finished, job already completed")
if err := r.deleteExpiredBuilds(&instance); err != nil {
return result, fmt.Errorf("failed to delete expired builds: %w", err)
}
return result, nil
}
jc, err := r.jenkinsClientFactory.MakeNewClient(&instance.ObjectMeta, instance.Spec.OwnerName)
if err != nil {
return result,
fmt.Errorf("failed to create gojenkins client: %w", err)
}
requeue, err := tryToReconcile(&instance, jc)
if err != nil {
r.log.Error(err, "error during reconciliation", "instance", instance)
result.RequeueAfter = helper.DefaultRequeueTime * time.Second
return result, nil
}
result.RequeueAfter = requeue
instance.Status.LastUpdated = metav1.NewTime(time.Now())
if err := r.client.Status().Update(context.Background(), &instance); err != nil {
r.log.Error(err, "unable to update status", "instance", instance)
}
reqLogger.V(2).Info("Reconciling JenkinsJobBuildRun has been finished")
return result, nil
}
func tryToReconcile(instance *jenkinsApi.JenkinsJobBuildRun, jc jenkins.ClientInterface) (time.Duration, error) {
job, err := jc.GetJobByName(instance.Spec.JobPath) // check if job exists
if err != nil {
if helper.JenkinsIsNotFoundErr(err) {
// job is not found, returning error and setting not found status for CR
instance.Status.Status = jenkinsApi.JobBuildRunStatusNotFound
return 0, nil
}
// unknown error
return 0, fmt.Errorf("failed to get job by name: %s: %w", instance.Spec.JobPath, err)
}
// job exists, and it's already in queue, stop here and check later after specified interval
if job.Raw.InQueue {
return retryInterval, nil
}
// check latest job build
interval, err := checkLastBuild(job, instance, jc)
if err != nil {
return 0, fmt.Errorf("failed to check latest build: %w", err)
}
return interval, nil
}
func checkLastBuild(job *gojenkins.Job, instance *jenkinsApi.JenkinsJobBuildRun,
jc jenkins.ClientInterface,
) (time.Duration, error) {
build, err := jc.GetLastBuild(job)
if err != nil {
// job does not have any builds so we can trigger new one
if helper.JenkinsIsNotFoundErr(err) {
return retryInterval, triggerNewBuild(instance, jc, jenkinsApi.JobBuildRunStatusCreated)
}
// unknown error
return 0, fmt.Errorf("failed to get last build: %w", err)
}
// check if latest build already running
if jc.BuildIsRunning(build) {
return retryInterval, nil // latest build already running, stop here and check later after specified interval
}
// if job has latest build we must check if it was created by this controller
if build.GetBuildNumber() == instance.Status.BuildNumber { // build created by this controller
if build.GetResult() == gojenkins.STATUS_SUCCESS { // build finished with success, so we can set Completed status to CR and exit
instance.Status.Status = jenkinsApi.JobBuildRunStatusCompleted
return instance.GetDeleteAfterCompletionInterval(), nil
}
// build was not finished with success, so we must check how many times we already started it
if instance.Spec.Retry > instance.Status.Launches { // launches is less than amount of specified retries
return retryInterval, triggerNewBuild(instance, jc, jenkinsApi.JobBuildRunStatusRetrying)
}
// we reach amount of specified retries so job is failed, exit
instance.Status.Status = jenkinsApi.JobBuildRunStatusFailed
return 0, nil
}
// latest job was not created by this controller so we can trigger a new one
return retryInterval, triggerNewBuild(instance, jc, jenkinsApi.JobBuildRunStatusCreated)
}
func triggerNewBuild(
instance *jenkinsApi.JenkinsJobBuildRun,
jc jenkins.ClientInterface,
status string,
) error {
buildNumber, err := jc.BuildJob(instance.Spec.JobPath, instance.Spec.Params)
if err != nil {
return fmt.Errorf("failed to build job: %w", err)
}
instance.Status.Status = status
instance.Status.Launches++
instance.Status.BuildNumber = *buildNumber
return nil
}
func (r *Reconcile) deleteExpiredBuilds(instance *jenkinsApi.JenkinsJobBuildRun) error {
cond := time.Now().After(
instance.Status.LastUpdated.Add(
instance.GetDeleteAfterCompletionInterval(),
),
)
if cond {
if err := r.client.Delete(context.Background(), instance); err != nil {
return fmt.Errorf("failed to delete expired build: %w", err)
}
}
return nil
}