diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 47f440e6..ac5634e2 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -13,6 +13,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" "github.com/jenkinsci/kubernetes-operator/pkg/event" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/version" @@ -118,8 +119,11 @@ func main() { fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug) } + c := make(chan notifications.Event) + go notifications.Listen(c, mgr.GetClient()) + // setup Jenkins controller - if err := jenkins.Add(mgr, *local, *minikube, events, *clientSet, *cfg); err != nil { + if err := jenkins.Add(mgr, *local, *minikube, events, *clientSet, *cfg, &c); err != nil { fatal(errors.Wrap(err, "failed to setup controllers"), *debug) } diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index cdf98c23..0ae0c136 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,9 +17,9 @@ type JenkinsSpec struct { // +optional SeedJobs []SeedJob `json:"seedJobs,omitempty"` - /* // Notifications defines list of a services which are used to inform about Jenkins status - // Can be used to integrate chat services like Slack, Microsoft MicrosoftTeams or Mailgun - Notifications []Notification `json:"notifications,omitempty"`*/ + // Notifications defines list of a services which are used to inform about Jenkins status + // Can be used to integrate chat services like Slack, Microsoft Teams or Mailgun + Notifications []Notification `json:"notifications,omitempty"` // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 932750f5..88c845c6 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -342,6 +342,13 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } + if in.Notifications != nil { + in, out := &in.Notifications, &out.Notifications + *out = make([]Notification, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 41c0c1ed..4a7cd7bf 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -124,6 +124,7 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } result, err = r.ensureBaseConfiguration(jenkinsClient) + return result, jenkinsClient, err } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 0e6361e2..7cf371cb 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -3,6 +3,7 @@ package jenkins import ( "context" "fmt" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" "reflect" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" @@ -52,20 +53,21 @@ var reconcileErrors = map[string]reconcileError{} // 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, clientSet kubernetes.Clientset, config rest.Config) error { - return add(mgr, newReconciler(mgr, local, minikube, events, clientSet, config)) +func Add(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) error { + return add(mgr, newReconciler(mgr, local, minikube, events, clientSet, config, notificationEvents)) } // newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config) reconcile.Reconciler { +func newReconciler(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) reconcile.Reconciler { return &ReconcileJenkins{ - client: mgr.GetClient(), - scheme: mgr.GetScheme(), - local: local, - minikube: minikube, - events: events, - clientSet: clientSet, - config: config, + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + local: local, + minikube: minikube, + events: events, + clientSet: clientSet, + config: config, + notificationEvents: notificationEvents, } } @@ -119,19 +121,34 @@ var _ reconcile.Reconciler = &ReconcileJenkins{} // ReconcileJenkins reconciles a Jenkins object type ReconcileJenkins struct { - client client.Client - scheme *runtime.Scheme - local, minikube bool - events event.Recorder - clientSet kubernetes.Clientset - config rest.Config + client client.Client + scheme *runtime.Scheme + local, minikube bool + events event.Recorder + clientSet kubernetes.Clientset + config rest.Config + notificationEvents *chan notifications.Event } // Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) { + reconcileFailLimit := uint64(10) logger := r.buildLogger(request.Name) logger.V(log.VDebug).Info("Reconciling Jenkins") + jenkins := &v1alpha2.Jenkins{} + err := r.client.Get(context.TODO(), request.NamespacedName, jenkins) + if err != nil { + if apierrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, errors.WithStack(err) + } + result, err := r.reconcile(request, logger) if err != nil && apierrors.IsConflict(err) { return reconcile.Result{Requeue: true}, nil @@ -151,11 +168,19 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul } } reconcileErrors[request.Name] = lastErrors - if lastErrors.counter >= 15 { + if lastErrors.counter >= reconcileFailLimit { if log.Debug { - logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %+v", err)) + logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %+v", reconcileFailLimit, err)) } else { - logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %s", err)) + logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, err)) + } + + *r.notificationEvents <- notifications.Event{ + Jenkins: *jenkins, + ConfigurationType: notifications.ConfigurationTypeUnknown, + LogLevel: v1alpha2.NotificationLogLevelWarning, + Message: fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %s", err), + MessageVerbose: fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %+v", err), } return reconcile.Result{Requeue: false}, nil } @@ -203,8 +228,16 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg return reconcile.Result{}, err } if !valid { + message := "Validation of base configuration failed, please correct Jenkins CR" + *r.notificationEvents <- notifications.Event{ + Jenkins: *jenkins, + ConfigurationType: notifications.ConfigurationTypeBase, + LogLevel: v1alpha2.NotificationLogLevelWarning, + Message: message, + MessageVerbose: message, + } r.events.Emit(jenkins, event.TypeWarning, reasonCRValidationFailure, "Base CR validation failed") - logger.V(log.VWarn).Info("Validation of base configuration failed, please correct Jenkins CR") + logger.V(log.VWarn).Info(message) return reconcile.Result{}, nil // don't requeue } @@ -226,8 +259,17 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg if err != nil { return reconcile.Result{}, errors.WithStack(err) } - logger.Info(fmt.Sprintf("Base configuration phase is complete, took %s", - jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))) + + message := fmt.Sprintf("Base configuration phase is complete, took %s", + jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) + *r.notificationEvents <- notifications.Event{ + Jenkins: *jenkins, + ConfigurationType: notifications.ConfigurationTypeBase, + LogLevel: v1alpha2.NotificationLogLevelInfo, + Message: message, + MessageVerbose: message, + } + logger.Info(message) r.events.Emit(jenkins, event.TypeNormal, reasonBaseConfigurationSuccess, "Base configuration completed") } // Reconcile user configuration @@ -238,7 +280,15 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg return reconcile.Result{}, err } if !valid { - logger.V(log.VWarn).Info("Validation of user configuration failed, please correct Jenkins CR") + message := fmt.Sprintf("Validation of user configuration failed, please correct Jenkins CR") + *r.notificationEvents <- notifications.Event{ + Jenkins: *jenkins, + ConfigurationType: notifications.ConfigurationTypeUser, + LogLevel: v1alpha2.NotificationLogLevelWarning, + Message: message, + MessageVerbose: message, + } + logger.V(log.VWarn).Info(message) r.events.Emit(jenkins, event.TypeWarning, reasonCRValidationFailure, "User CR validation failed") return reconcile.Result{}, nil // don't requeue } @@ -258,8 +308,16 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg if err != nil { return reconcile.Result{}, errors.WithStack(err) } - logger.Info(fmt.Sprintf("User configuration phase is complete, took %s", - jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))) + message := fmt.Sprintf("User configuration phase is complete, took %s", + jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) + *r.notificationEvents <- notifications.Event{ + Jenkins: *jenkins, + ConfigurationType: notifications.ConfigurationTypeUser, + LogLevel: v1alpha2.NotificationLogLevelInfo, + Message: message, + MessageVerbose: message, + } + logger.Info(message) r.events.Emit(jenkins, event.TypeNormal, reasonUserConfigurationSuccess, "User configuration completed") } diff --git a/pkg/controller/jenkins/notifications/mailgun.go b/pkg/controller/jenkins/notifications/mailgun.go index 4aec5ff6..8e705c56 100644 --- a/pkg/controller/jenkins/notifications/mailgun.go +++ b/pkg/controller/jenkins/notifications/mailgun.go @@ -20,8 +20,8 @@ const content = `
-| CR name: | @@ -31,10 +31,6 @@ const content = `Configuration type: | %s |
| Status: | -%s | -