parent
							
								
									8182858b75
								
							
						
					
					
						commit
						6d32ee71e4
					
				|  | @ -14,6 +14,7 @@ import ( | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins" | 	"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/constants" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" | ||||||
|  | 	e "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/event" | 	"github.com/jenkinsci/kubernetes-operator/pkg/event" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/version" | 	"github.com/jenkinsci/kubernetes-operator/version" | ||||||
|  | @ -119,7 +120,7 @@ func main() { | ||||||
| 		fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug) | 		fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	c := make(chan notifications.Event) | 	c := make(chan e.Event) | ||||||
| 	go notifications.Listen(c, events, mgr.GetClient()) | 	go notifications.Listen(c, events, mgr.GetClient()) | ||||||
| 
 | 
 | ||||||
| 	// setup Jenkins controller
 | 	// setup Jenkins controller
 | ||||||
|  |  | ||||||
|  | @ -54,20 +54,20 @@ type JenkinsSpec struct { | ||||||
| 	ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` | 	ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NotificationLogLevel defines logging level of Notification
 | // NotificationLevel defines the level of a Notification
 | ||||||
| type NotificationLogLevel string | type NotificationLevel string | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// NotificationLogLevelWarning - Only Warnings
 | 	// NotificationLevelWarning - Only Warnings
 | ||||||
| 	NotificationLogLevelWarning NotificationLogLevel = "warning" | 	NotificationLevelWarning NotificationLevel = "warning" | ||||||
| 
 | 
 | ||||||
| 	// NotificationLogLevelInfo - Only info
 | 	// NotificationLevelInfo - Only info
 | ||||||
| 	NotificationLogLevelInfo NotificationLogLevel = "info" | 	NotificationLevelInfo NotificationLevel = "info" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Notification is a service configuration used to send notifications about Jenkins status
 | // Notification is a service configuration used to send notifications about Jenkins status
 | ||||||
| type Notification struct { | type Notification struct { | ||||||
| 	LoggingLevel NotificationLogLevel `json:"loggingLevel"` | 	LoggingLevel NotificationLevel `json:"level"` | ||||||
| 	Verbose      bool              `json:"verbose"` | 	Verbose      bool              `json:"verbose"` | ||||||
| 	Name         string            `json:"name"` | 	Name         string            `json:"name"` | ||||||
| 	Slack        *Slack            `json:"slack,omitempty"` | 	Slack        *Slack            `json:"slack,omitempty"` | ||||||
|  |  | ||||||
|  | @ -15,7 +15,8 @@ import ( | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/version" | 	"github.com/jenkinsci/kubernetes-operator/version" | ||||||
|  | @ -106,8 +107,14 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki | ||||||
| 		return reconcile.Result{}, nil, err | 		return reconcile.Result{}, nil, err | ||||||
| 	} | 	} | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		r.logger.Info("Some plugins have changed, restarting Jenkins") | 		message := "Some plugins have changed, restarting Jenkins" | ||||||
| 		return reconcile.Result{Requeue: true}, nil, r.Configuration.RestartJenkinsMasterPod() | 		r.logger.Info(message) | ||||||
|  | 
 | ||||||
|  | 		restartReason := reason.NewPodRestart( | ||||||
|  | 			reason.OperatorSource, | ||||||
|  | 			[]string{message}, | ||||||
|  | 		) | ||||||
|  | 		return reconcile.Result{Requeue: true}, nil, r.Configuration.RestartJenkinsMasterPod(restartReason) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	result, err = r.ensureBaseConfiguration(jenkinsClient) | 	result, err = r.ensureBaseConfiguration(jenkinsClient) | ||||||
|  | @ -402,11 +409,11 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.O | ||||||
| 				resources.JenkinsMasterContainerName, []string{"bash", "-c", fmt.Sprintf("%s/%s && <custom-command-here> && /sbin/tini -s -- /usr/local/bin/jenkins.sh", | 				resources.JenkinsMasterContainerName, []string{"bash", "-c", fmt.Sprintf("%s/%s && <custom-command-here> && /sbin/tini -s -- /usr/local/bin/jenkins.sh", | ||||||
| 					resources.JenkinsScriptsVolumePath, resources.InitScriptName)})) | 					resources.JenkinsScriptsVolumePath, resources.InitScriptName)})) | ||||||
| 		} | 		} | ||||||
| 		*r.Notifications <- notifications.Event{ | 		*r.Notifications <- event.Event{ | ||||||
| 			Jenkins: *r.Configuration.Jenkins, | 			Jenkins: *r.Configuration.Jenkins, | ||||||
| 			Phase:    notifications.PhaseBase, | 			Phase:   event.PhaseBase, | ||||||
| 			LogLevel: v1alpha2.NotificationLogLevelInfo, | 			Level:   v1alpha2.NotificationLevelInfo, | ||||||
| 			Message:  "Creating a new Jenkins Master Pod", | 			Reason:  reason.NewPodCreation(reason.OperatorSource, []string{"Creating a new Jenkins Master Pod"}), | ||||||
| 		} | 		} | ||||||
| 		r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name)) | 		r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name)) | ||||||
| 		err = r.createResource(jenkinsMasterPod) | 		err = r.createResource(jenkinsMasterPod) | ||||||
|  | @ -452,12 +459,13 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.O | ||||||
| 		return reconcile.Result{Requeue: true}, nil | 		return reconcile.Result{Requeue: true}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	messages := r.isRecreatePodNeeded(*currentJenkinsMasterPod, userAndPasswordHash) | 	restartReason := r.checkForPodRecreation(*currentJenkinsMasterPod, userAndPasswordHash) | ||||||
| 	if hasMessages := len(messages) > 0; hasMessages { | 	if restartReason.HasMessages() { | ||||||
| 		for _, msg := range messages { | 		for _, msg := range restartReason.Verbose() { | ||||||
| 			r.logger.Info(msg) | 			r.logger.Info(msg) | ||||||
| 		} | 		} | ||||||
| 		return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod() | 
 | ||||||
|  | 		return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod(restartReason) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return reconcile.Result{}, nil | 	return reconcile.Result{}, nil | ||||||
|  | @ -480,59 +488,76 @@ func isPodTerminating(pod corev1.Pod) bool { | ||||||
| 	return pod.ObjectMeta.DeletionTimestamp != nil | 	return pod.ObjectMeta.DeletionTimestamp != nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMasterPod corev1.Pod, userAndPasswordHash string) []string { | func (r *ReconcileJenkinsBaseConfiguration) checkForPodRecreation(currentJenkinsMasterPod corev1.Pod, userAndPasswordHash string) reason.Reason { | ||||||
| 	var messages []string | 	var messages []string | ||||||
| 	if userAndPasswordHash != r.Configuration.Jenkins.Status.UserAndPasswordHash { | 	var verbose []string | ||||||
| 		messages = append(messages, "User or password have changed, recreating pod") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if r.Configuration.Jenkins.Spec.Restore.RecoveryOnce != 0 && r.Configuration.Jenkins.Status.RestoredBackup != 0 { |  | ||||||
| 		messages = append(messages, "spec.restore.recoveryOnce is set, recreating pod") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if version.Version != r.Configuration.Jenkins.Status.OperatorVersion { |  | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins Operator version has changed, actual '%+v' new '%+v', recreating pod", |  | ||||||
| 			r.Configuration.Jenkins.Status.OperatorVersion, version.Version)) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if currentJenkinsMasterPod.Status.Phase == corev1.PodFailed || | 	if currentJenkinsMasterPod.Status.Phase == corev1.PodFailed || | ||||||
| 		currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded || | 		currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded || | ||||||
| 		currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown { | 		currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown { | ||||||
| 		messages = append(messages, fmt.Sprintf("Invalid Jenkins pod phase '%+v', recreating pod", currentJenkinsMasterPod.Status)) | 		messages = append(messages, fmt.Sprintf("Invalid Jenkins pod phase '%s'", currentJenkinsMasterPod.Status.Phase)) | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Invalid Jenkins pod phase '%+v'", currentJenkinsMasterPod.Status)) | ||||||
|  | 		return reason.NewPodRestart(reason.KubernetesSource, messages, verbose...) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if userAndPasswordHash != r.Configuration.Jenkins.Status.UserAndPasswordHash { | ||||||
|  | 		messages = append(messages, "User or password have changed") | ||||||
|  | 		verbose = append(verbose, "User or password have changed, recreating pod") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if r.Configuration.Jenkins.Spec.Restore.RecoveryOnce != 0 && r.Configuration.Jenkins.Status.RestoredBackup != 0 { | ||||||
|  | 		messages = append(messages, "spec.restore.recoveryOnce is set") | ||||||
|  | 		verbose = append(verbose, "spec.restore.recoveryOnce is set, recreating pod") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if version.Version != r.Configuration.Jenkins.Status.OperatorVersion { | ||||||
|  | 		messages = append(messages, "Jenkins Operator version has changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins Operator version has changed, actual '%+v' new '%+v'", | ||||||
|  | 			r.Configuration.Jenkins.Status.OperatorVersion, version.Version)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.SecurityContext, currentJenkinsMasterPod.Spec.SecurityContext) { | 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.SecurityContext, currentJenkinsMasterPod.Spec.SecurityContext) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins pod security context has changed, actual '%+v' required '%+v', recreating pod", | 		messages = append(messages, fmt.Sprintf("Jenkins pod security context has changed")) | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins pod security context has changed, actual '%+v' required '%+v'", | ||||||
| 			currentJenkinsMasterPod.Spec.SecurityContext, r.Configuration.Jenkins.Spec.Master.SecurityContext)) | 			currentJenkinsMasterPod.Spec.SecurityContext, r.Configuration.Jenkins.Spec.Master.SecurityContext)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) { | 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v', recreating pod", | 		messages = append(messages, "Jenkins Pod ImagePullSecrets has changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v'", | ||||||
| 			currentJenkinsMasterPod.Spec.ImagePullSecrets, r.Configuration.Jenkins.Spec.Master.ImagePullSecrets)) | 			currentJenkinsMasterPod.Spec.ImagePullSecrets, r.Configuration.Jenkins.Spec.Master.ImagePullSecrets)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) { | 	if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v', recreating pod", | 		messages = append(messages, "Jenkins pod node selector has changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v'", | ||||||
| 			currentJenkinsMasterPod.Spec.NodeSelector, r.Configuration.Jenkins.Spec.Master.NodeSelector)) | 			currentJenkinsMasterPod.Spec.NodeSelector, r.Configuration.Jenkins.Spec.Master.NodeSelector)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(r.Configuration.Jenkins.Spec.Master.Annotations) > 0 && | 	if len(r.Configuration.Jenkins.Spec.Master.Annotations) > 0 && | ||||||
| 		!reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.Annotations, currentJenkinsMasterPod.ObjectMeta.Annotations) { | 		!reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.Annotations, currentJenkinsMasterPod.ObjectMeta.Annotations) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins pod annotations have changed to '%+v', recreating pod", r.Configuration.Jenkins.Spec.Master.Annotations)) | 		messages = append(messages, "Jenkins pod annotations have changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins pod annotations have changed, actual '%+v' required '%+v'", | ||||||
|  | 			currentJenkinsMasterPod.ObjectMeta.Annotations, r.Configuration.Jenkins.Spec.Master.Annotations)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !r.compareVolumes(currentJenkinsMasterPod) { | 	if !r.compareVolumes(currentJenkinsMasterPod) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins pod volumes have changed, actual '%v' required '%v', recreating pod", | 		messages = append(messages, "Jenkins pod volumes have changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins pod volumes have changed, actual '%v' required '%v'", | ||||||
| 			currentJenkinsMasterPod.Spec.Volumes, r.Configuration.Jenkins.Spec.Master.Volumes)) | 			currentJenkinsMasterPod.Spec.Volumes, r.Configuration.Jenkins.Spec.Master.Volumes)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(r.Configuration.Jenkins.Spec.Master.Containers) != len(currentJenkinsMasterPod.Spec.Containers) { | 	if len(r.Configuration.Jenkins.Spec.Master.Containers) != len(currentJenkinsMasterPod.Spec.Containers) { | ||||||
| 		messages = append(messages, fmt.Sprintf("Jenkins amount of containers has changed to '%+v', recreating pod", len(r.Configuration.Jenkins.Spec.Master.Containers))) | 		messages = append(messages, "Jenkins amount of containers has changed") | ||||||
|  | 		verbose = append(verbose, fmt.Sprintf("Jenkins amount of containers has changed, actual '%+v' required '%+v'", | ||||||
|  | 			len(currentJenkinsMasterPod.Spec.Containers), len(r.Configuration.Jenkins.Spec.Master.Containers))) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, actualContainer := range currentJenkinsMasterPod.Spec.Containers { | 	for _, actualContainer := range currentJenkinsMasterPod.Spec.Containers { | ||||||
| 		if actualContainer.Name == resources.JenkinsMasterContainerName { | 		if actualContainer.Name == resources.JenkinsMasterContainerName { | ||||||
| 			messages = append(messages, r.compareContainers(resources.NewJenkinsMasterContainer(r.Configuration.Jenkins), actualContainer)...) | 			containerMessages, verboseMessages := r.compareContainers(resources.NewJenkinsMasterContainer(r.Configuration.Jenkins), actualContainer) | ||||||
|  | 			messages = append(messages, containerMessages...) | ||||||
|  | 			verbose = append(verbose, verboseMessages...) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -545,76 +570,79 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if expectedContainer == nil { | 		if expectedContainer == nil { | ||||||
| 			messages = append(messages, fmt.Sprintf("Container '%+v' not found in pod, recreating pod", actualContainer)) | 			messages = append(messages, fmt.Sprintf("Container '%s' not found in pod", actualContainer.Name)) | ||||||
|  | 			verbose = append(verbose, fmt.Sprintf("Container '%+v' not found in pod", actualContainer)) | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		messages = append(messages, r.compareContainers(*expectedContainer, actualContainer)...) | 		containerMessages, verboseMessages := r.compareContainers(*expectedContainer, actualContainer) | ||||||
|  | 
 | ||||||
|  | 		messages = append(messages, containerMessages...) | ||||||
|  | 		verbose = append(verbose, verboseMessages...) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return messages | 	return reason.NewPodRestart(reason.KubernetesSource, messages, verbose...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *ReconcileJenkinsBaseConfiguration) compareContainers(expected corev1.Container, actual corev1.Container) []string { | func (r *ReconcileJenkinsBaseConfiguration) compareContainers(expected corev1.Container, actual corev1.Container) (messages []string, verbose []string) { | ||||||
| 	var messages []string |  | ||||||
| 
 |  | ||||||
| 	if !reflect.DeepEqual(expected.Args, actual.Args) { | 	if !reflect.DeepEqual(expected.Args, actual.Args) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Arguments have changed to '%+v' in container '%s', recreating pod", expected.Args, expected.Name)) | 		messages = append(messages, "Arguments have changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Arguments have changed to '%+v' in container '%s', recreating pod", expected.Args, expected.Name)) | 		verbose = append(messages, fmt.Sprintf("Arguments have changed to '%+v' in container '%s'", expected.Args, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.Command, actual.Command) { | 	if !reflect.DeepEqual(expected.Command, actual.Command) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Command has changed to '%+v' in container '%s', recreating pod", expected.Command, expected.Name)) | 		messages = append(messages, "Command has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Command has changed to '%+v' in container '%s', recreating pod", expected.Command, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Command has changed to '%+v' in container '%s'", expected.Command, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !compareEnv(expected.Env, actual.Env) { | 	if !compareEnv(expected.Env, actual.Env) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Env has changed to '%+v' in container '%s', recreating pod", expected.Env, expected.Name)) | 		messages = append(messages, "Env has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Env has changed to '%+v' in container '%s', recreating pod", expected.Env, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Env has changed to '%+v' in container '%s'", expected.Env, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.EnvFrom, actual.EnvFrom) { | 	if !reflect.DeepEqual(expected.EnvFrom, actual.EnvFrom) { | ||||||
| 		r.logger.Info(fmt.Sprintf("EnvFrom has changed to '%+v' in container '%s', recreating pod", expected.EnvFrom, expected.Name)) | 		messages = append(messages, "EnvFrom has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("EnvFrom has changed to '%+v' in container '%s', recreating pod", expected.EnvFrom, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("EnvFrom has changed to '%+v' in container '%s'", expected.EnvFrom, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.Image, actual.Image) { | 	if !reflect.DeepEqual(expected.Image, actual.Image) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Image has changed to '%+v' in container '%s', recreating pod", expected.Image, expected.Name)) | 		messages = append(messages, "Image has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Image has changed to '%+v' in container '%s', recreating pod", expected.Image, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Image has changed to '%+v' in container '%s'", expected.Image, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.ImagePullPolicy, actual.ImagePullPolicy) { | 	if !reflect.DeepEqual(expected.ImagePullPolicy, actual.ImagePullPolicy) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Image pull policy has changed to '%+v' in container '%s', recreating pod", expected.ImagePullPolicy, expected.Name)) | 		messages = append(messages, "Image pull policy has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Image pull policy has changed to '%+v' in container '%s', recreating pod", expected.ImagePullPolicy, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Image pull policy has changed to '%+v' in container '%s'", expected.ImagePullPolicy, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.Lifecycle, actual.Lifecycle) { | 	if !reflect.DeepEqual(expected.Lifecycle, actual.Lifecycle) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Lifecycle has changed to '%+v' in container '%s', recreating pod", expected.Lifecycle, expected.Name)) | 		messages = append(messages, "Lifecycle has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Lifecycle has changed to '%+v' in container '%s', recreating pod", expected.Lifecycle, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Lifecycle has changed to '%+v' in container '%s'", expected.Lifecycle, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.LivenessProbe, actual.LivenessProbe) { | 	if !reflect.DeepEqual(expected.LivenessProbe, actual.LivenessProbe) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Liveness probe has changed to '%+v' in container '%s', recreating pod", expected.LivenessProbe, expected.Name)) | 		messages = append(messages, "Liveness probe has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Liveness probe has changed to '%+v' in container '%s', recreating pod", expected.LivenessProbe, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Liveness probe has changed to '%+v' in container '%s'", expected.LivenessProbe, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.Ports, actual.Ports) { | 	if !reflect.DeepEqual(expected.Ports, actual.Ports) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Ports have changed to '%+v' in container '%s', recreating pod", expected.Ports, expected.Name)) | 		messages = append(messages, "Ports have changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Ports have changed to '%+v' in container '%s', recreating pod", expected.Ports, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Ports have changed to '%+v' in container '%s'", expected.Ports, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.ReadinessProbe, actual.ReadinessProbe) { | 	if !reflect.DeepEqual(expected.ReadinessProbe, actual.ReadinessProbe) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Readiness probe has changed to '%+v' in container '%s', recreating pod", expected.ReadinessProbe, expected.Name)) | 		messages = append(messages, "Readiness probe has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Readiness probe has changed to '%+v' in container '%s', recreating pod", expected.ReadinessProbe, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Readiness probe has changed to '%+v' in container '%s'", expected.ReadinessProbe, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.Resources, actual.Resources) { | 	if !reflect.DeepEqual(expected.Resources, actual.Resources) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Resources have changed to '%+v' in container '%s', recreating pod", expected.Resources, expected.Name)) | 		messages = append(messages, "Resources have changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Resources have changed to '%+v' in container '%s', recreating pod", expected.Resources, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Resources have changed to '%+v' in container '%s'", expected.Resources, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.SecurityContext, actual.SecurityContext) { | 	if !reflect.DeepEqual(expected.SecurityContext, actual.SecurityContext) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Security context has changed to '%+v' in container '%s', recreating pod", expected.SecurityContext, expected.Name)) | 		messages = append(messages, "Security context has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Security context has changed to '%+v' in container '%s', recreating pod", expected.SecurityContext, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Security context has changed to '%+v' in container '%s'", expected.SecurityContext, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !reflect.DeepEqual(expected.WorkingDir, actual.WorkingDir) { | 	if !reflect.DeepEqual(expected.WorkingDir, actual.WorkingDir) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Working directory has changed to '%+v' in container '%s', recreating pod", expected.WorkingDir, expected.Name)) | 		messages = append(messages, "Working directory has changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Working directory has changed to '%+v' in container '%s', recreating pod", expected.WorkingDir, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Working directory has changed to '%+v' in container '%s'", expected.WorkingDir, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 	if !CompareContainerVolumeMounts(expected, actual) { | 	if !CompareContainerVolumeMounts(expected, actual) { | ||||||
| 		r.logger.Info(fmt.Sprintf("Volume mounts have changed to '%+v' in container '%s', recreating pod", expected.VolumeMounts, expected.Name)) | 		messages = append(messages, "Volume mounts have changed") | ||||||
| 		messages = append(messages, fmt.Sprintf("Volume mounts have changed to '%+v' in container '%s', recreating pod", expected.VolumeMounts, expected.Name)) | 		verbose = append(verbose, fmt.Sprintf("Volume mounts have changed to '%+v' in container '%s'", expected.VolumeMounts, expected.Name)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return messages | 	return messages, verbose | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func compareEnv(expected, actual []corev1.EnvVar) bool { | func compareEnv(expected, actual []corev1.EnvVar) bool { | ||||||
|  | @ -723,8 +751,14 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMet | ||||||
| 	containersReadyCount := 0 | 	containersReadyCount := 0 | ||||||
| 	for _, containerStatus := range jenkinsMasterPod.Status.ContainerStatuses { | 	for _, containerStatus := range jenkinsMasterPod.Status.ContainerStatuses { | ||||||
| 		if containerStatus.State.Terminated != nil { | 		if containerStatus.State.Terminated != nil { | ||||||
| 			r.logger.Info(fmt.Sprintf("Container '%s' is terminated, status '%+v', recreating pod", containerStatus.Name, containerStatus)) | 			message := fmt.Sprintf("Container '%s' is terminated, status '%+v'", containerStatus.Name, containerStatus) | ||||||
| 			return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod() | 			r.logger.Info(message) | ||||||
|  | 
 | ||||||
|  | 			restartReason := reason.NewPodRestart( | ||||||
|  | 				reason.KubernetesSource, | ||||||
|  | 				[]string{message}, | ||||||
|  | 			) | ||||||
|  | 			return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod(restartReason) | ||||||
| 		} | 		} | ||||||
| 		if !containerStatus.Ready { | 		if !containerStatus.Ready { | ||||||
| 			r.logger.V(log.VDebug).Info(fmt.Sprintf("Container '%s' not ready, readiness probe failed", containerStatus.Name)) | 			r.logger.V(log.VDebug).Info(fmt.Sprintf("Container '%s' not ready, readiness probe failed", containerStatus.Name)) | ||||||
|  |  | ||||||
|  | @ -2,11 +2,11 @@ package configuration | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
| 
 | 
 | ||||||
| 	stackerr "github.com/pkg/errors" | 	stackerr "github.com/pkg/errors" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | @ -19,23 +19,22 @@ import ( | ||||||
| type Configuration struct { | type Configuration struct { | ||||||
| 	Client        client.Client | 	Client        client.Client | ||||||
| 	ClientSet     kubernetes.Clientset | 	ClientSet     kubernetes.Clientset | ||||||
| 	Notifications *chan notifications.Event | 	Notifications *chan event.Event | ||||||
| 	Jenkins       *v1alpha2.Jenkins | 	Jenkins       *v1alpha2.Jenkins | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RestartJenkinsMasterPod terminate Jenkins master pod and notifies about it
 | // RestartJenkinsMasterPod terminate Jenkins master pod and notifies about it
 | ||||||
| func (c *Configuration) RestartJenkinsMasterPod() error { | func (c *Configuration) RestartJenkinsMasterPod(reason reason.Reason) error { | ||||||
| 	currentJenkinsMasterPod, err := c.getJenkinsMasterPod() | 	currentJenkinsMasterPod, err := c.getJenkinsMasterPod() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	*c.Notifications <- notifications.Event{ | 	*c.Notifications <- event.Event{ | ||||||
| 		Jenkins: *c.Jenkins, | 		Jenkins: *c.Jenkins, | ||||||
| 		Phase:           notifications.PhaseBase, | 		Phase:   event.PhaseBase, | ||||||
| 		LogLevel:        v1alpha2.NotificationLogLevelInfo, | 		Level:   v1alpha2.NotificationLevelInfo, | ||||||
| 		Message:         fmt.Sprintf("Terminating Jenkins Master Pod %s/%s.", currentJenkinsMasterPod.Namespace, currentJenkinsMasterPod.Name), | 		Reason:  reason, | ||||||
| 		MessagesVerbose: []string{}, |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return stackerr.WithStack(c.Client.Delete(context.TODO(), currentJenkinsMasterPod)) | 	return stackerr.WithStack(c.Client.Delete(context.TODO(), currentJenkinsMasterPod)) | ||||||
|  |  | ||||||
|  | @ -3,7 +3,6 @@ package user | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 	jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" | 	jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import ( | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
| 
 | 
 | ||||||
| 	"github.com/go-logr/logr" | 	"github.com/go-logr/logr" | ||||||
| 	stackerr "github.com/pkg/errors" | 	stackerr "github.com/pkg/errors" | ||||||
|  | @ -148,8 +149,14 @@ func New(jenkinsClient jenkinsclient.Jenkins, config configuration.Configuration | ||||||
| // EnsureSeedJobs configures seed job and runs it for every entry from Jenkins.Spec.SeedJobs
 | // EnsureSeedJobs configures seed job and runs it for every entry from Jenkins.Spec.SeedJobs
 | ||||||
| func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err error) { | func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err error) { | ||||||
| 	if s.isRecreatePodNeeded(*jenkins) { | 	if s.isRecreatePodNeeded(*jenkins) { | ||||||
| 		s.logger.Info("Some seed job has been deleted, recreating pod") | 		message := "Some seed job has been deleted, recreating pod" | ||||||
| 		return false, s.RestartJenkinsMasterPod() | 		s.logger.Info(message) | ||||||
|  | 
 | ||||||
|  | 		restartReason := reason.NewPodRestart( | ||||||
|  | 			reason.OperatorSource, | ||||||
|  | 			[]string{message}, | ||||||
|  | 		) | ||||||
|  | 		return false, s.RestartJenkinsMasterPod(restartReason) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(jenkins.Spec.SeedJobs) > 0 { | 	if len(jenkins.Spec.SeedJobs) > 0 { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,8 @@ import ( | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/version" | 	"github.com/jenkinsci/kubernetes-operator/version" | ||||||
|  | @ -44,12 +45,12 @@ 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
 | // 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.
 | // and Start it when the Manager is Started.
 | ||||||
| func Add(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) error { | func Add(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan event.Event) error { | ||||||
| 	return add(mgr, newReconciler(mgr, local, minikube, clientSet, config, notificationEvents)) | 	return add(mgr, newReconciler(mgr, local, minikube, clientSet, config, notificationEvents)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newReconciler returns a new reconcile.Reconciler
 | // newReconciler returns a new reconcile.Reconciler
 | ||||||
| func newReconciler(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) reconcile.Reconciler { | func newReconciler(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan event.Event) reconcile.Reconciler { | ||||||
| 	return &ReconcileJenkins{ | 	return &ReconcileJenkins{ | ||||||
| 		client:             mgr.GetClient(), | 		client:             mgr.GetClient(), | ||||||
| 		scheme:             mgr.GetScheme(), | 		scheme:             mgr.GetScheme(), | ||||||
|  | @ -116,7 +117,7 @@ type ReconcileJenkins struct { | ||||||
| 	local, minikube    bool | 	local, minikube    bool | ||||||
| 	clientSet          kubernetes.Clientset | 	clientSet          kubernetes.Clientset | ||||||
| 	config             rest.Config | 	config             rest.Config | ||||||
| 	notificationEvents *chan notifications.Event | 	notificationEvents *chan event.Event | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec
 | // Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec
 | ||||||
|  | @ -151,12 +152,14 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul | ||||||
| 				logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, 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{ | 			*r.notificationEvents <- event.Event{ | ||||||
| 				Jenkins: *jenkins, | 				Jenkins: *jenkins, | ||||||
| 				Phase:           notifications.PhaseBase, | 				Phase:   event.PhaseBase, | ||||||
| 				LogLevel:        v1alpha2.NotificationLogLevelWarning, | 				Level:   v1alpha2.NotificationLevelWarning, | ||||||
| 				Message:         fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, err), | 				Reason: reason.NewReconcileLoopFailed( | ||||||
| 				MessagesVerbose: []string{fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %+v", reconcileFailLimit, err)}, | 					reason.OperatorSource, | ||||||
|  | 					[]string{fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, err)}, | ||||||
|  | 				), | ||||||
| 			} | 			} | ||||||
| 			return reconcile.Result{Requeue: false}, nil | 			return reconcile.Result{Requeue: false}, nil | ||||||
| 		} | 		} | ||||||
|  | @ -170,12 +173,15 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if groovyErr, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { | 		if groovyErr, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { | ||||||
| 			*r.notificationEvents <- notifications.Event{ | 			*r.notificationEvents <- event.Event{ | ||||||
| 				Jenkins: *jenkins, | 				Jenkins: *jenkins, | ||||||
| 				Phase:           notifications.PhaseBase, | 				Phase:   event.PhaseBase, | ||||||
| 				LogLevel:        v1alpha2.NotificationLogLevelWarning, | 				Level:   v1alpha2.NotificationLevelWarning, | ||||||
| 				Message:         fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs:", groovyErr.ConfigurationType, groovyErr.Source, groovyErr.Name), | 				Reason: reason.NewGroovyScriptExecutionFailed( | ||||||
| 				MessagesVerbose: []string{groovyErr.Logs}, | 					reason.OperatorSource, | ||||||
|  | 					[]string{fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed", groovyErr.ConfigurationType, groovyErr.Source, groovyErr.Name)}, | ||||||
|  | 					[]string{fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs: %+v", groovyErr.ConfigurationType, groovyErr.Source, groovyErr.Name, groovyErr.Logs)}..., | ||||||
|  | 				), | ||||||
| 			} | 			} | ||||||
| 			return reconcile.Result{Requeue: false}, nil | 			return reconcile.Result{Requeue: false}, nil | ||||||
| 		} | 		} | ||||||
|  | @ -213,21 +219,20 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg | ||||||
| 	// Reconcile base configuration
 | 	// Reconcile base configuration
 | ||||||
| 	baseConfiguration := base.New(config, r.scheme, logger, r.local, r.minikube, &r.config) | 	baseConfiguration := base.New(config, r.scheme, logger, r.local, r.minikube, &r.config) | ||||||
| 
 | 
 | ||||||
| 	messages, err := baseConfiguration.Validate(jenkins) | 	baseMessages, err := baseConfiguration.Validate(jenkins) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return reconcile.Result{}, jenkins, err | 		return reconcile.Result{}, jenkins, err | ||||||
| 	} | 	} | ||||||
| 	if len(messages) > 0 { | 	if len(baseMessages) > 0 { | ||||||
| 		message := "Validation of base configuration failed, please correct Jenkins CR." | 		message := "Validation of base configuration failed, please correct Jenkins CR." | ||||||
| 		*r.notificationEvents <- notifications.Event{ | 		*r.notificationEvents <- event.Event{ | ||||||
| 			Jenkins: *jenkins, | 			Jenkins: *jenkins, | ||||||
| 			Phase:           notifications.PhaseBase, | 			Phase:   event.PhaseBase, | ||||||
| 			LogLevel:        v1alpha2.NotificationLogLevelWarning, | 			Level:   v1alpha2.NotificationLevelWarning, | ||||||
| 			Message:         message, | 			Reason:  reason.NewBaseConfigurationFailed(reason.HumanSource, []string{message}, append([]string{message}, baseMessages...)...), | ||||||
| 			MessagesVerbose: messages, |  | ||||||
| 		} | 		} | ||||||
| 		logger.V(log.VWarn).Info(message) | 		logger.V(log.VWarn).Info(message) | ||||||
| 		for _, msg := range messages { | 		for _, msg := range baseMessages { | ||||||
| 			logger.V(log.VWarn).Info(msg) | 			logger.V(log.VWarn).Info(msg) | ||||||
| 		} | 		} | ||||||
| 		return reconcile.Result{}, jenkins, nil // don't requeue
 | 		return reconcile.Result{}, jenkins, nil // don't requeue
 | ||||||
|  | @ -254,30 +259,28 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg | ||||||
| 
 | 
 | ||||||
| 		message := fmt.Sprintf("Base configuration phase is complete, took %s", | 		message := fmt.Sprintf("Base configuration phase is complete, took %s", | ||||||
| 			jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) | 			jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) | ||||||
| 		*r.notificationEvents <- notifications.Event{ | 		*r.notificationEvents <- event.Event{ | ||||||
| 			Jenkins: *jenkins, | 			Jenkins: *jenkins, | ||||||
| 			Phase:           notifications.PhaseBase, | 			Phase:   event.PhaseBase, | ||||||
| 			LogLevel:        v1alpha2.NotificationLogLevelInfo, | 			Level:   v1alpha2.NotificationLevelInfo, | ||||||
| 			Message:         message, | 			Reason:  reason.NewBaseConfigurationComplete(reason.OperatorSource, []string{message}), | ||||||
| 			MessagesVerbose: messages, |  | ||||||
| 		} | 		} | ||||||
| 		logger.Info(message) | 		logger.Info(message) | ||||||
| 	} | 	} | ||||||
| 	// Reconcile user configuration
 | 	// Reconcile user configuration
 | ||||||
| 	userConfiguration := user.New(config, jenkinsClient, logger, r.config) | 	userConfiguration := user.New(config, jenkinsClient, logger, r.config) | ||||||
| 
 | 
 | ||||||
| 	messages, err = userConfiguration.Validate(jenkins) | 	messages, err := userConfiguration.Validate(jenkins) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return reconcile.Result{}, jenkins, err | 		return reconcile.Result{}, jenkins, err | ||||||
| 	} | 	} | ||||||
| 	if len(messages) > 0 { | 	if len(messages) > 0 { | ||||||
| 		message := fmt.Sprintf("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{ | 		*r.notificationEvents <- event.Event{ | ||||||
| 			Jenkins: *jenkins, | 			Jenkins: *jenkins, | ||||||
| 			Phase:           notifications.PhaseUser, | 			Phase:   event.PhaseUser, | ||||||
| 			LogLevel:        v1alpha2.NotificationLogLevelWarning, | 			Level:   v1alpha2.NotificationLevelWarning, | ||||||
| 			Message:         message, | 			Reason:  reason.NewUserConfigurationFailed(reason.HumanSource, []string{message}, append([]string{message}, messages...)...), | ||||||
| 			MessagesVerbose: messages, |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		logger.V(log.VWarn).Info(message) | 		logger.V(log.VWarn).Info(message) | ||||||
|  | @ -304,12 +307,11 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg | ||||||
| 		} | 		} | ||||||
| 		message := fmt.Sprintf("User configuration phase is complete, took %s", | 		message := fmt.Sprintf("User configuration phase is complete, took %s", | ||||||
| 			jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) | 			jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time)) | ||||||
| 		*r.notificationEvents <- notifications.Event{ | 		*r.notificationEvents <- event.Event{ | ||||||
| 			Jenkins: *jenkins, | 			Jenkins: *jenkins, | ||||||
| 			Phase:           notifications.PhaseUser, | 			Phase:   event.PhaseUser, | ||||||
| 			LogLevel:        v1alpha2.NotificationLogLevelInfo, | 			Level:   v1alpha2.NotificationLevelInfo, | ||||||
| 			Message:         message, | 			Reason:  reason.NewUserConfigurationComplete(reason.OperatorSource, []string{message}), | ||||||
| 			MessagesVerbose: messages, |  | ||||||
| 		} | 		} | ||||||
| 		logger.Info(message) | 		logger.Info(message) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | package event | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Phase defines the context where notification has been generated: base or user.
 | ||||||
|  | type Phase string | ||||||
|  | 
 | ||||||
|  | // StatusColor is useful for better UX
 | ||||||
|  | type StatusColor string | ||||||
|  | 
 | ||||||
|  | // LoggingLevel is type for selecting different logging levels
 | ||||||
|  | type LoggingLevel string | ||||||
|  | 
 | ||||||
|  | // Event contains event details which will be sent as a notification
 | ||||||
|  | type Event struct { | ||||||
|  | 	Jenkins v1alpha2.Jenkins | ||||||
|  | 	Phase   Phase | ||||||
|  | 	Level   v1alpha2.NotificationLevel | ||||||
|  | 	Reason  reason.Reason | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// PhaseBase is core configuration of Jenkins provided by the Operator
 | ||||||
|  | 	PhaseBase Phase = "base" | ||||||
|  | 
 | ||||||
|  | 	// PhaseUser is user-defined configuration of Jenkins
 | ||||||
|  | 	PhaseUser Phase = "user" | ||||||
|  | 
 | ||||||
|  | 	// PhaseUnknown is untraceable type of configuration
 | ||||||
|  | 	PhaseUnknown Phase = "unknown" | ||||||
|  | ) | ||||||
|  | @ -1,96 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mailgun/mailgun-go/v3" |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const content = ` |  | ||||||
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" |  | ||||||
|         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |  | ||||||
| <html> |  | ||||||
| <head></head> |  | ||||||
| <body> |  | ||||||
| 		<h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1> |  | ||||||
| 		<h3>%s</h3> |  | ||||||
| 		<table> |  | ||||||
| 			<tr> |  | ||||||
| 				<td><b>CR name:</b></td> |  | ||||||
| 				<td>%s</td> |  | ||||||
| 			</tr> |  | ||||||
| 			<tr> |  | ||||||
| 				<td><b>Phase:</b></td> |  | ||||||
| 				<td>%s</td> |  | ||||||
| 			</tr> |  | ||||||
| 		</table> |  | ||||||
| 		<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6> |  | ||||||
| </body> |  | ||||||
| </html>` |  | ||||||
| 
 |  | ||||||
| // MailGun is a sending emails notification service
 |  | ||||||
| type MailGun struct { |  | ||||||
| 	k8sClient k8sclient.Client |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m MailGun) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor { |  | ||||||
| 	switch logLevel { |  | ||||||
| 	case v1alpha2.NotificationLogLevelInfo: |  | ||||||
| 		return "blue" |  | ||||||
| 	case v1alpha2.NotificationLogLevelWarning: |  | ||||||
| 		return "red" |  | ||||||
| 	default: |  | ||||||
| 		return "gray" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Send is function for sending directly to API
 |  | ||||||
| func (m MailGun) Send(event Event, config v1alpha2.Notification) error { |  | ||||||
| 	secret := &corev1.Secret{} |  | ||||||
| 
 |  | ||||||
| 	selector := config.Mailgun.APIKeySecretKeySelector |  | ||||||
| 
 |  | ||||||
| 	err := m.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.WithStack(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	secretValue := string(secret.Data[selector.Key]) |  | ||||||
| 	if secretValue == "" { |  | ||||||
| 		return errors.Errorf("Mailgun API secret is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) |  | ||||||
| 
 |  | ||||||
| 	var statusMessage string |  | ||||||
| 
 |  | ||||||
| 	if config.Verbose { |  | ||||||
| 		message := event.Message + "<ul>" |  | ||||||
| 		for _, msg := range event.MessagesVerbose { |  | ||||||
| 			message = message + "<li>" + msg + "</li>" |  | ||||||
| 		} |  | ||||||
| 		message = message + "</ul>" |  | ||||||
| 		statusMessage = message |  | ||||||
| 	} else { |  | ||||||
| 		statusMessage = event.Message |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	htmlMessage := fmt.Sprintf(content, m.getStatusColor(event.LogLevel), notificationTitle(event), statusMessage, event.Jenkins.Name, event.Phase) |  | ||||||
| 
 |  | ||||||
| 	msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), notificationTitle(event), "", config.Mailgun.Recipient) |  | ||||||
| 	msg.SetHtml(htmlMessage) |  | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) |  | ||||||
| 	defer cancel() |  | ||||||
| 
 |  | ||||||
| 	_, _, err = mg.Send(ctx, msg) |  | ||||||
| 
 |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,123 @@ | ||||||
|  | package mailgun | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mailgun/mailgun-go/v3" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	infoColor    = "blue" | ||||||
|  | 	warningColor = "red" | ||||||
|  | 	defaultColor = "gray" | ||||||
|  | 
 | ||||||
|  | 	content = ` | ||||||
|  | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | ||||||
|  |         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||||||
|  | <html> | ||||||
|  | <head></head> | ||||||
|  | <body> | ||||||
|  | 		<h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1> | ||||||
|  | 		<h3>%s</h3> | ||||||
|  | 		<table> | ||||||
|  | 			<tr> | ||||||
|  | 				<td><b>CR name:</b></td> | ||||||
|  | 				<td>%s</td> | ||||||
|  | 			</tr> | ||||||
|  | 			<tr> | ||||||
|  | 				<td><b>Phase:</b></td> | ||||||
|  | 				<td>%s</td> | ||||||
|  | 			</tr> | ||||||
|  | 		</table> | ||||||
|  | 		<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6> | ||||||
|  | </body> | ||||||
|  | </html>` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MailGun is a sending emails notification service
 | ||||||
|  | type MailGun struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | 	config    v1alpha2.Notification | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns instance of MailGun
 | ||||||
|  | func New(k8sClient k8sclient.Client, config v1alpha2.Notification) *MailGun { | ||||||
|  | 	return &MailGun{k8sClient: k8sClient, config: config} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m MailGun) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case v1alpha2.NotificationLevelInfo: | ||||||
|  | 		return infoColor | ||||||
|  | 	case v1alpha2.NotificationLevelWarning: | ||||||
|  | 		return warningColor | ||||||
|  | 	default: | ||||||
|  | 		return defaultColor | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m MailGun) generateMessage(event event.Event) string { | ||||||
|  | 	var statusMessage strings.Builder | ||||||
|  | 	var reasons string | ||||||
|  | 
 | ||||||
|  | 	if m.config.Verbose { | ||||||
|  | 		reasons = strings.TrimRight(strings.Join(event.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 	} else { | ||||||
|  | 		reasons = strings.TrimRight(strings.Join(event.Reason.Short(), "</li><li>"), "<li>") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	statusMessage.WriteString("<ul><li>") | ||||||
|  | 	statusMessage.WriteString(reasons) | ||||||
|  | 	statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 	statusColor := m.getStatusColor(event.Level) | ||||||
|  | 	messageTitle := provider.NotificationTitle(event) | ||||||
|  | 	message := statusMessage.String() | ||||||
|  | 	crName := event.Jenkins.Name | ||||||
|  | 	phase := event.Phase | ||||||
|  | 
 | ||||||
|  | 	return fmt.Sprintf(content, statusColor, messageTitle, message, crName, phase) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send is function for sending directly to API
 | ||||||
|  | func (m MailGun) Send(event event.Event) error { | ||||||
|  | 	secret := &corev1.Secret{} | ||||||
|  | 	selector := m.config.Mailgun.APIKeySecretKeySelector | ||||||
|  | 
 | ||||||
|  | 	err := m.k8sClient.Get(context.TODO(), | ||||||
|  | 		types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.WithStack(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	secretValue := string(secret.Data[selector.Key]) | ||||||
|  | 	if secretValue == "" { | ||||||
|  | 		return errors.Errorf("Mailgun API secret is empty in secret '%s/%s[%s]", | ||||||
|  | 			event.Jenkins.Namespace, selector.Name, selector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mg := mailgun.NewMailgun(m.config.Mailgun.Domain, secretValue) | ||||||
|  | 	from := fmt.Sprintf("Jenkins Operator Notifier <%s>", m.config.Mailgun.From) | ||||||
|  | 	subject := provider.NotificationTitle(event) | ||||||
|  | 	recipient := m.config.Mailgun.Recipient | ||||||
|  | 
 | ||||||
|  | 	msg := mg.NewMessage(from, subject, "", recipient) | ||||||
|  | 	msg.SetHtml(m.generateMessage(event)) | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	_, _, err = mg.Send(ctx, msg) | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | @ -0,0 +1,182 @@ | ||||||
|  | package mailgun | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestGenerateMessages(t *testing.T) { | ||||||
|  | 	t.Run("happy", func(t *testing.T) { | ||||||
|  | 		crName := "test-jenkins" | ||||||
|  | 		crNamespace := "test-namespace" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose") | ||||||
|  | 
 | ||||||
|  | 		s := MailGun{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var statusMessage strings.Builder | ||||||
|  | 		r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 
 | ||||||
|  | 		statusMessage.WriteString("<ul><li>") | ||||||
|  | 		statusMessage.WriteString(r) | ||||||
|  | 		statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 		want := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		got := fmt.Sprintf(content, s.getStatusColor(e.Level), | ||||||
|  | 			provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, want, got) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with nils", func(t *testing.T) { | ||||||
|  | 		crName := "nil" | ||||||
|  | 		crNamespace := "nil" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil") | ||||||
|  | 
 | ||||||
|  | 		s := MailGun{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var statusMessage strings.Builder | ||||||
|  | 		r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 
 | ||||||
|  | 		statusMessage.WriteString("<ul><li>") | ||||||
|  | 		statusMessage.WriteString(r) | ||||||
|  | 		statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 		want := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		got := fmt.Sprintf(content, s.getStatusColor(e.Level), | ||||||
|  | 			provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, want, got) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with empty strings", func(t *testing.T) { | ||||||
|  | 		crName := "" | ||||||
|  | 		crNamespace := "" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "") | ||||||
|  | 
 | ||||||
|  | 		s := MailGun{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var statusMessage strings.Builder | ||||||
|  | 		r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 
 | ||||||
|  | 		statusMessage.WriteString("<ul><li>") | ||||||
|  | 		statusMessage.WriteString(r) | ||||||
|  | 		statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 		want := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		got := fmt.Sprintf(content, s.getStatusColor(e.Level), | ||||||
|  | 			provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, want, got) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with utf-8 characters", func(t *testing.T) { | ||||||
|  | 		crName := "ąśćńółżź" | ||||||
|  | 		crNamespace := "ąśćńółżź" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź") | ||||||
|  | 
 | ||||||
|  | 		s := MailGun{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var statusMessage strings.Builder | ||||||
|  | 		r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 
 | ||||||
|  | 		statusMessage.WriteString("<ul><li>") | ||||||
|  | 		statusMessage.WriteString(r) | ||||||
|  | 		statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 		want := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		got := fmt.Sprintf(content, s.getStatusColor(e.Level), | ||||||
|  | 			provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, want, got) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -1,133 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Teams is a Microsoft MicrosoftTeams notification service
 |  | ||||||
| type Teams struct { |  | ||||||
| 	k8sClient k8sclient.Client |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TeamsMessage is representation of json message structure
 |  | ||||||
| type TeamsMessage struct { |  | ||||||
| 	Type       string         `json:"@type"` |  | ||||||
| 	Context    string         `json:"@context"` |  | ||||||
| 	ThemeColor StatusColor    `json:"themeColor"` |  | ||||||
| 	Title      string         `json:"title"` |  | ||||||
| 	Sections   []TeamsSection `json:"sections"` |  | ||||||
| 	Summary    string         `json:"summary"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TeamsSection is MS Teams message section
 |  | ||||||
| type TeamsSection struct { |  | ||||||
| 	Facts []TeamsFact `json:"facts"` |  | ||||||
| 	Text  string      `json:"text"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TeamsFact is field where we can put content
 |  | ||||||
| type TeamsFact struct { |  | ||||||
| 	Name  string `json:"name"` |  | ||||||
| 	Value string `json:"value"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (t Teams) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor { |  | ||||||
| 	switch logLevel { |  | ||||||
| 	case v1alpha2.NotificationLogLevelInfo: |  | ||||||
| 		return "439FE0" |  | ||||||
| 	case v1alpha2.NotificationLogLevelWarning: |  | ||||||
| 		return "E81123" |  | ||||||
| 	default: |  | ||||||
| 		return "C8C8C8" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Send is function for sending directly to API
 |  | ||||||
| func (t Teams) Send(event Event, config v1alpha2.Notification) error { |  | ||||||
| 	secret := &corev1.Secret{} |  | ||||||
| 
 |  | ||||||
| 	selector := config.Teams.WebHookURLSecretKeySelector |  | ||||||
| 
 |  | ||||||
| 	err := t.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.WithStack(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	secretValue := string(secret.Data[selector.Key]) |  | ||||||
| 	if secretValue == "" { |  | ||||||
| 		return errors.Errorf("Microsoft Teams WebHook URL is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	tm := &TeamsMessage{ |  | ||||||
| 		Type:       "MessageCard", |  | ||||||
| 		Context:    "https://schema.org/extensions", |  | ||||||
| 		ThemeColor: t.getStatusColor(event.LogLevel), |  | ||||||
| 		Sections: []TeamsSection{ |  | ||||||
| 			{ |  | ||||||
| 				Facts: []TeamsFact{ |  | ||||||
| 					{ |  | ||||||
| 						Name:  crNameFieldName, |  | ||||||
| 						Value: event.Jenkins.Name, |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						Name:  namespaceFieldName, |  | ||||||
| 						Value: event.Jenkins.Namespace, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 				Text: event.Message, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		Summary: event.Message, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	tm.Title = notificationTitle(event) |  | ||||||
| 
 |  | ||||||
| 	if config.Verbose { |  | ||||||
| 		message := event.Message |  | ||||||
| 		for _, msg := range event.MessagesVerbose { |  | ||||||
| 			message = message + "\n\n - " + msg |  | ||||||
| 		} |  | ||||||
| 		tm.Sections[0].Text += message |  | ||||||
| 		tm.Summary = message |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if event.Phase != PhaseUnknown { |  | ||||||
| 		tm.Sections[0].Facts = append(tm.Sections[0].Facts, TeamsFact{ |  | ||||||
| 			Name:  phaseFieldName, |  | ||||||
| 			Value: string(event.Phase), |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	msg, err := json.Marshal(tm) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.WithStack(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.WithStack(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	resp, err := client.Do(request) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.WithStack(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if resp.StatusCode != http.StatusOK { |  | ||||||
| 		return errors.New(fmt.Sprintf("Invalid response from server: %s", resp.Status)) |  | ||||||
| 	} |  | ||||||
| 	defer func() { _ = resp.Body.Close() }() |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,147 @@ | ||||||
|  | package msteams | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 
 | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	infoColor    = "439FE0" | ||||||
|  | 	warningColor = "E81123" | ||||||
|  | 	defaultColor = "C8C8C8" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Teams is a Microsoft MicrosoftTeams notification service
 | ||||||
|  | type Teams struct { | ||||||
|  | 	httpClient http.Client | ||||||
|  | 	k8sClient  k8sclient.Client | ||||||
|  | 	config     v1alpha2.Notification | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns instance of Teams
 | ||||||
|  | func New(k8sClient k8sclient.Client, config v1alpha2.Notification, httpClient http.Client) *Teams { | ||||||
|  | 	return &Teams{k8sClient: k8sClient, config: config, httpClient: httpClient} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Message is representation of json message structure
 | ||||||
|  | type Message struct { | ||||||
|  | 	Type       string            `json:"@type"` | ||||||
|  | 	Context    string            `json:"@context"` | ||||||
|  | 	ThemeColor event.StatusColor `json:"themeColor"` | ||||||
|  | 	Title      string            `json:"title"` | ||||||
|  | 	Sections   []Section         `json:"sections"` | ||||||
|  | 	Summary    string            `json:"summary"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Section is MS Teams message section
 | ||||||
|  | type Section struct { | ||||||
|  | 	Facts []Fact `json:"facts"` | ||||||
|  | 	Text  string `json:"text"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Fact is field where we can put content
 | ||||||
|  | type Fact struct { | ||||||
|  | 	Name  string `json:"name"` | ||||||
|  | 	Value string `json:"value"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t Teams) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case v1alpha2.NotificationLevelInfo: | ||||||
|  | 		return infoColor | ||||||
|  | 	case v1alpha2.NotificationLevelWarning: | ||||||
|  | 		return warningColor | ||||||
|  | 	default: | ||||||
|  | 		return defaultColor | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t Teams) generateMessage(e event.Event) Message { | ||||||
|  | 	var reason string | ||||||
|  | 	if t.config.Verbose { | ||||||
|  | 		reason = strings.Join(e.Reason.Verbose(), "\n\n - ") | ||||||
|  | 	} else { | ||||||
|  | 		reason = strings.Join(e.Reason.Short(), "\n\n - ") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tm := Message{ | ||||||
|  | 		Title:      provider.NotificationTitle(e), | ||||||
|  | 		Type:       "MessageCard", | ||||||
|  | 		Context:    "https://schema.org/extensions", | ||||||
|  | 		ThemeColor: t.getStatusColor(e.Level), | ||||||
|  | 		Sections: []Section{ | ||||||
|  | 			{ | ||||||
|  | 				Facts: []Fact{ | ||||||
|  | 					{ | ||||||
|  | 						Name:  provider.CrNameFieldName, | ||||||
|  | 						Value: e.Jenkins.Name, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Name:  provider.NamespaceFieldName, | ||||||
|  | 						Value: e.Jenkins.Namespace, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Name:  provider.PhaseFieldName, | ||||||
|  | 						Value: string(e.Phase), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Text: reason, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		Summary: reason, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return tm | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send is function for sending directly to API
 | ||||||
|  | func (t Teams) Send(e event.Event) error { | ||||||
|  | 	secret := &corev1.Secret{} | ||||||
|  | 
 | ||||||
|  | 	selector := t.config.Teams.WebHookURLSecretKeySelector | ||||||
|  | 
 | ||||||
|  | 	err := t.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: e.Jenkins.Namespace}, secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.WithStack(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	secretValue := string(secret.Data[selector.Key]) | ||||||
|  | 	if secretValue == "" { | ||||||
|  | 		return errors.Errorf("Microsoft Teams WebHook URL is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, selector.Name, selector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	msg, err := json.Marshal(t.generateMessage(e)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.WithStack(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.WithStack(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resp, err := t.httpClient.Do(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.WithStack(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		return errors.New(fmt.Sprintf("Invalid response from server: %s", resp.Status)) | ||||||
|  | 	} | ||||||
|  | 	defer func() { _ = resp.Body.Close() }() | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,287 @@ | ||||||
|  | package msteams | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	testPhase     = event.PhaseUser | ||||||
|  | 	testCrName    = "test-cr" | ||||||
|  | 	testNamespace = "default" | ||||||
|  | 	testReason    = reason.NewPodRestart( | ||||||
|  | 		reason.KubernetesSource, | ||||||
|  | 		[]string{"test-reason-1"}, | ||||||
|  | 		[]string{"test-verbose-1"}..., | ||||||
|  | 	) | ||||||
|  | 	testLevel = v1alpha2.NotificationLevelWarning | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestTeams_Send(t *testing.T) { | ||||||
|  | 	fakeClient := fake.NewFakeClient() | ||||||
|  | 	testURLSelectorKeyName := "test-url-selector" | ||||||
|  | 	testSecretName := "test-secret" | ||||||
|  | 
 | ||||||
|  | 	event := event.Event{ | ||||||
|  | 		Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      testCrName, | ||||||
|  | 				Namespace: testNamespace, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		Phase:  testPhase, | ||||||
|  | 		Level:  testLevel, | ||||||
|  | 		Reason: testReason, | ||||||
|  | 	} | ||||||
|  | 	teams := Teams{k8sClient: fakeClient, config: v1alpha2.Notification{ | ||||||
|  | 		Teams: &v1alpha2.MicrosoftTeams{ | ||||||
|  | 			WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testURLSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}} | ||||||
|  | 
 | ||||||
|  | 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		var message Message | ||||||
|  | 		decoder := json.NewDecoder(r.Body) | ||||||
|  | 		err := decoder.Decode(&message) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, message.Title, provider.NotificationTitle(event)) | ||||||
|  | 		assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.Level)) | ||||||
|  | 
 | ||||||
|  | 		mainSection := message.Sections[0] | ||||||
|  | 
 | ||||||
|  | 		reason := strings.Join(event.Reason.Short(), "\n\n - ") | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainSection.Text, reason) | ||||||
|  | 
 | ||||||
|  | 		for _, fact := range mainSection.Facts { | ||||||
|  | 			switch fact.Name { | ||||||
|  | 			case provider.PhaseFieldName: | ||||||
|  | 				assert.Equal(t, fact.Value, string(event.Phase)) | ||||||
|  | 			case provider.CrNameFieldName: | ||||||
|  | 				assert.Equal(t, fact.Value, event.Jenkins.Name) | ||||||
|  | 			case provider.MessageFieldName: | ||||||
|  | 				assert.Equal(t, fact.Value, reason) | ||||||
|  | 			case provider.LevelFieldName: | ||||||
|  | 				assert.Equal(t, fact.Value, string(event.Level)) | ||||||
|  | 			case provider.NamespaceFieldName: | ||||||
|  | 				assert.Equal(t, fact.Value, event.Jenkins.Namespace) | ||||||
|  | 			default: | ||||||
|  | 				t.Errorf("Found unexpected '%+v' fact", fact) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	})) | ||||||
|  | 
 | ||||||
|  | 	defer server.Close() | ||||||
|  | 
 | ||||||
|  | 	secret := &corev1.Secret{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      testSecretName, | ||||||
|  | 			Namespace: testNamespace, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		Data: map[string][]byte{ | ||||||
|  | 			testURLSelectorKeyName: []byte(server.URL), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := fakeClient.Create(context.TODO(), secret) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	err = teams.Send(event) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGenerateMessages(t *testing.T) { | ||||||
|  | 	t.Run("happy", func(t *testing.T) { | ||||||
|  | 		crName := "test-jenkins" | ||||||
|  | 		crNamespace := "test-namespace" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose") | ||||||
|  | 
 | ||||||
|  | 		s := Teams{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		msg := strings.Join(e.Reason.Verbose(), "\n\n - ") | ||||||
|  | 
 | ||||||
|  | 		mainSection := message.Sections[0] | ||||||
|  | 
 | ||||||
|  | 		crNameFact := mainSection.Facts[0] | ||||||
|  | 		namespaceFact := mainSection.Facts[1] | ||||||
|  | 		phaseFact := mainSection.Facts[2] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainSection.Text, msg) | ||||||
|  | 		assert.Equal(t, crNameFact.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseFact.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with nils", func(t *testing.T) { | ||||||
|  | 		crName := "nil" | ||||||
|  | 		crNamespace := "nil" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil") | ||||||
|  | 
 | ||||||
|  | 		s := Teams{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		msg := strings.Join(e.Reason.Verbose(), "\n\n - ") | ||||||
|  | 
 | ||||||
|  | 		mainSection := message.Sections[0] | ||||||
|  | 
 | ||||||
|  | 		crNameFact := mainSection.Facts[0] | ||||||
|  | 		namespaceFact := mainSection.Facts[1] | ||||||
|  | 		phaseFact := mainSection.Facts[2] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainSection.Text, msg) | ||||||
|  | 		assert.Equal(t, crNameFact.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseFact.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with empty strings", func(t *testing.T) { | ||||||
|  | 		crName := "" | ||||||
|  | 		crNamespace := "" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "") | ||||||
|  | 
 | ||||||
|  | 		s := Teams{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		msg := strings.Join(e.Reason.Verbose(), "\n\n - ") | ||||||
|  | 
 | ||||||
|  | 		mainSection := message.Sections[0] | ||||||
|  | 
 | ||||||
|  | 		crNameFact := mainSection.Facts[0] | ||||||
|  | 		namespaceFact := mainSection.Facts[1] | ||||||
|  | 		phaseFact := mainSection.Facts[2] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainSection.Text, msg) | ||||||
|  | 		assert.Equal(t, crNameFact.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseFact.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with utf-8 characters", func(t *testing.T) { | ||||||
|  | 		crName := "ąśćńółżź" | ||||||
|  | 		crNamespace := "ąśćńółżź" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź") | ||||||
|  | 		s := Teams{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		msg := strings.Join(e.Reason.Verbose(), "\n\n - ") | ||||||
|  | 
 | ||||||
|  | 		mainSection := message.Sections[0] | ||||||
|  | 
 | ||||||
|  | 		crNameFact := mainSection.Facts[0] | ||||||
|  | 		namespaceFact := mainSection.Facts[1] | ||||||
|  | 		phaseFact := mainSection.Facts[2] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainSection.Text, msg) | ||||||
|  | 		assert.Equal(t, crNameFact.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseFact.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -1,97 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestTeams_Send(t *testing.T) { |  | ||||||
| 	fakeClient := fake.NewFakeClient() |  | ||||||
| 	testURLSelectorKeyName := "test-url-selector" |  | ||||||
| 	testSecretName := "test-secret" |  | ||||||
| 
 |  | ||||||
| 	event := Event{ |  | ||||||
| 		Jenkins: v1alpha2.Jenkins{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      testCrName, |  | ||||||
| 				Namespace: testNamespace, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		Phase:           testPhase, |  | ||||||
| 		Message:         testMessage, |  | ||||||
| 		MessagesVerbose: testMessageVerbose, |  | ||||||
| 		LogLevel:        testLoggingLevel, |  | ||||||
| 	} |  | ||||||
| 	teams := Teams{k8sClient: fakeClient} |  | ||||||
| 
 |  | ||||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		var message TeamsMessage |  | ||||||
| 		decoder := json.NewDecoder(r.Body) |  | ||||||
| 		err := decoder.Decode(&message) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatal(err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		assert.Equal(t, message.Title, notificationTitle(event)) |  | ||||||
| 		assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.LogLevel)) |  | ||||||
| 
 |  | ||||||
| 		mainSection := message.Sections[0] |  | ||||||
| 
 |  | ||||||
| 		assert.Equal(t, mainSection.Text, event.Message) |  | ||||||
| 
 |  | ||||||
| 		for _, fact := range mainSection.Facts { |  | ||||||
| 			switch fact.Name { |  | ||||||
| 			case phaseFieldName: |  | ||||||
| 				assert.Equal(t, fact.Value, string(event.Phase)) |  | ||||||
| 			case crNameFieldName: |  | ||||||
| 				assert.Equal(t, fact.Value, event.Jenkins.Name) |  | ||||||
| 			case messageFieldName: |  | ||||||
| 				assert.Equal(t, fact.Value, event.Message) |  | ||||||
| 			case loggingLevelFieldName: |  | ||||||
| 				assert.Equal(t, fact.Value, string(event.LogLevel)) |  | ||||||
| 			case namespaceFieldName: |  | ||||||
| 				assert.Equal(t, fact.Value, event.Jenkins.Namespace) |  | ||||||
| 			default: |  | ||||||
| 				t.Errorf("Found unexpected '%+v' fact", fact) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	defer server.Close() |  | ||||||
| 
 |  | ||||||
| 	secret := &corev1.Secret{ |  | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 			Name:      testSecretName, |  | ||||||
| 			Namespace: testNamespace, |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		Data: map[string][]byte{ |  | ||||||
| 			testURLSelectorKeyName: []byte(server.URL), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := fakeClient.Create(context.TODO(), secret) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = teams.Send(event, v1alpha2.Notification{ |  | ||||||
| 		Teams: &v1alpha2.MicrosoftTeams{ |  | ||||||
| 			WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{ |  | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ |  | ||||||
| 					Name: testSecretName, |  | ||||||
| 				}, |  | ||||||
| 				Key: testURLSelectorKeyName, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | package provider | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// InfoTitleText is info header of notification
 | ||||||
|  | 	InfoTitleText = "Jenkins Operator reconciliation info" | ||||||
|  | 
 | ||||||
|  | 	// WarnTitleText is warning header of notification
 | ||||||
|  | 	WarnTitleText = "Jenkins Operator reconciliation warning" | ||||||
|  | 
 | ||||||
|  | 	// MessageFieldName is field title for message content
 | ||||||
|  | 	MessageFieldName = "Message" | ||||||
|  | 
 | ||||||
|  | 	// LevelFieldName is field title for level enum
 | ||||||
|  | 	LevelFieldName = "Level" | ||||||
|  | 
 | ||||||
|  | 	// CrNameFieldName is field title for CR Name string
 | ||||||
|  | 	CrNameFieldName = "CR Name" | ||||||
|  | 
 | ||||||
|  | 	// PhaseFieldName is field title for Phase enum
 | ||||||
|  | 	PhaseFieldName = "Phase" | ||||||
|  | 
 | ||||||
|  | 	// NamespaceFieldName is field title for Namespace string
 | ||||||
|  | 	NamespaceFieldName = "Namespace" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NotificationTitle converts NotificationLevel enum to string
 | ||||||
|  | func NotificationTitle(event event.Event) string { | ||||||
|  | 	if event.Level == v1alpha2.NotificationLevelInfo { | ||||||
|  | 		return InfoTitleText | ||||||
|  | 	} else if event.Level == v1alpha2.NotificationLevelWarning { | ||||||
|  | 		return WarnTitleText | ||||||
|  | 	} else { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,192 @@ | ||||||
|  | package reason | ||||||
|  | 
 | ||||||
|  | import "fmt" | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// OperatorSource defines that notification concerns operator
 | ||||||
|  | 	OperatorSource Source = "operator" | ||||||
|  | 	// KubernetesSource defines that notification concerns kubernetes
 | ||||||
|  | 	KubernetesSource Source = "kubernetes" | ||||||
|  | 	// HumanSource defines that notification concerns human
 | ||||||
|  | 	HumanSource Source = "human" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Reason is interface that let us know why operator sent notification
 | ||||||
|  | type Reason interface { | ||||||
|  | 	Short() []string | ||||||
|  | 	Verbose() []string | ||||||
|  | 	HasMessages() bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Undefined is base or untraceable reason
 | ||||||
|  | type Undefined struct { | ||||||
|  | 	source  Source | ||||||
|  | 	short   []string | ||||||
|  | 	verbose []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PodRestart defines the reason why Jenkins master pod restarted
 | ||||||
|  | type PodRestart struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PodCreation informs that pod is being created
 | ||||||
|  | type PodCreation struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ReconcileLoopFailed defines the reason why the reconcile loop failed
 | ||||||
|  | type ReconcileLoopFailed struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GroovyScriptExecutionFailed defines the reason why the groovy script execution failed
 | ||||||
|  | type GroovyScriptExecutionFailed struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // BaseConfigurationFailed defines the reason why base configuration phase failed
 | ||||||
|  | type BaseConfigurationFailed struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // BaseConfigurationComplete informs that base configuration is valid and complete
 | ||||||
|  | type BaseConfigurationComplete struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UserConfigurationFailed defines the reason why user configuration phase failed
 | ||||||
|  | type UserConfigurationFailed struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // UserConfigurationComplete informs that user configuration is valid and complete
 | ||||||
|  | type UserConfigurationComplete struct { | ||||||
|  | 	Undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewUndefined returns new instance of Undefined
 | ||||||
|  | func NewUndefined(source Source, short []string, verbose ...string) *Undefined { | ||||||
|  | 	return &Undefined{source: source, short: short, verbose: checkIfVerboseEmpty(short, verbose)} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewPodRestart returns new instance of PodRestart
 | ||||||
|  | func NewPodRestart(source Source, short []string, verbose ...string) *PodRestart { | ||||||
|  | 	restartPodMessage := "Jenkins master pod restarted by:" | ||||||
|  | 	if len(short) == 1 { | ||||||
|  | 		short[0] = fmt.Sprintf("%s %s", restartPodMessage, short[0]) | ||||||
|  | 	} else if len(short) > 1 { | ||||||
|  | 		short = append([]string{restartPodMessage}, short...) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &PodRestart{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewPodCreation returns new instance of PodCreation
 | ||||||
|  | func NewPodCreation(source Source, short []string, verbose ...string) *PodCreation { | ||||||
|  | 	return &PodCreation{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewReconcileLoopFailed returns new instance of ReconcileLoopFailed
 | ||||||
|  | func NewReconcileLoopFailed(source Source, short []string, verbose ...string) *ReconcileLoopFailed { | ||||||
|  | 	return &ReconcileLoopFailed{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewGroovyScriptExecutionFailed returns new instance of GroovyScriptExecutionFailed
 | ||||||
|  | func NewGroovyScriptExecutionFailed(source Source, short []string, verbose ...string) *GroovyScriptExecutionFailed { | ||||||
|  | 	return &GroovyScriptExecutionFailed{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewBaseConfigurationFailed returns new instance of BaseConfigurationFailed
 | ||||||
|  | func NewBaseConfigurationFailed(source Source, short []string, verbose ...string) *BaseConfigurationFailed { | ||||||
|  | 	return &BaseConfigurationFailed{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewBaseConfigurationComplete returns new instance of BaseConfigurationComplete
 | ||||||
|  | func NewBaseConfigurationComplete(source Source, short []string, verbose ...string) *BaseConfigurationComplete { | ||||||
|  | 	return &BaseConfigurationComplete{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: verbose, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewUserConfigurationFailed returns new instance of UserConfigurationFailed
 | ||||||
|  | func NewUserConfigurationFailed(source Source, short []string, verbose ...string) *UserConfigurationFailed { | ||||||
|  | 	return &UserConfigurationFailed{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewUserConfigurationComplete returns new instance of UserConfigurationComplete
 | ||||||
|  | func NewUserConfigurationComplete(source Source, short []string, verbose ...string) *UserConfigurationComplete { | ||||||
|  | 	return &UserConfigurationComplete{ | ||||||
|  | 		Undefined{ | ||||||
|  | 			source:  source, | ||||||
|  | 			short:   short, | ||||||
|  | 			verbose: checkIfVerboseEmpty(short, verbose), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Source is enum type that informs us what triggered notification
 | ||||||
|  | type Source string | ||||||
|  | 
 | ||||||
|  | // Short is list of reasons
 | ||||||
|  | func (p Undefined) Short() []string { | ||||||
|  | 	return p.short | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Verbose is list of reasons with details
 | ||||||
|  | func (p Undefined) Verbose() []string { | ||||||
|  | 	return p.verbose | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasMessages checks if there is any message
 | ||||||
|  | func (p Undefined) HasMessages() bool { | ||||||
|  | 	return len(p.short) > 0 || len(p.verbose) > 0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkIfVerboseEmpty(short []string, verbose []string) []string { | ||||||
|  | 	if len(verbose) == 0 { | ||||||
|  | 		return short | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return verbose | ||||||
|  | } | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | package reason | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestCopyToVerboseIfNil(t *testing.T) { | ||||||
|  | 	t.Run("happy", func(t *testing.T) { | ||||||
|  | 		var verbose []string | ||||||
|  | 		short := []string{"test", "string"} | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, checkIfVerboseEmpty(short, verbose), short) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("check with invalid slice", func(t *testing.T) { | ||||||
|  | 		valid := []string{"valid", "string"} | ||||||
|  | 		invalid := []string{"invalid", "string"} | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, checkIfVerboseEmpty(invalid, valid), valid) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("check with two empty slices", func(t *testing.T) { | ||||||
|  | 		var short []string | ||||||
|  | 		var verbose []string | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, checkIfVerboseEmpty(short, verbose), verbose) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with nils", func(t *testing.T) { | ||||||
|  | 		assert.Equal(t, checkIfVerboseEmpty(nil, nil), []string(nil)) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestUndefined_HasMessages(t *testing.T) { | ||||||
|  | 	t.Run("short full", func(t *testing.T) { | ||||||
|  | 		podRestart := NewUndefined(KubernetesSource, []string{"test", "another-test"}) | ||||||
|  | 		assert.True(t, podRestart.HasMessages()) | ||||||
|  | 	}) | ||||||
|  | 	 | ||||||
|  | 	t.Run("verbose full", func(t *testing.T) { | ||||||
|  | 		podRestart := NewUndefined(KubernetesSource, []string{}, []string{"test", "another-test"}...) | ||||||
|  | 		assert.True(t, podRestart.HasMessages()) | ||||||
|  | 	}) | ||||||
|  | 	 | ||||||
|  | 	t.Run("short empty", func(t *testing.T) { | ||||||
|  | 		podRestart := NewUndefined(KubernetesSource, []string{}) | ||||||
|  | 		assert.False(t, podRestart.HasMessages()) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("verbose and short full", func(t *testing.T) { | ||||||
|  | 		podRestart := NewUndefined(KubernetesSource, []string{"test", "another-test"}, []string{"test", "another-test"}...) | ||||||
|  | 		assert.True(t, podRestart.HasMessages()) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("verbose and short empty", func(t *testing.T) { | ||||||
|  | 		podRestart := NewUndefined(KubernetesSource, []string{}, []string{}...) | ||||||
|  | 		assert.False(t, podRestart.HasMessages()) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestPodRestartPrepend(t *testing.T) { | ||||||
|  | 	t.Run("happy with one message", func(t *testing.T) { | ||||||
|  | 		res := "test-reason" | ||||||
|  | 		podRestart := NewPodRestart(KubernetesSource, []string{res}) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, podRestart.short[0], fmt.Sprintf("Jenkins master pod restarted by: %s", res)) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("happy with multiple message", func(t *testing.T) { | ||||||
|  | 		podRestart := NewPodRestart(KubernetesSource, []string{"first-reason", "second-reason", "third-reason"}) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, podRestart.short[0], "Jenkins master pod restarted by:") | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -3,130 +3,90 @@ package notifications | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/event" | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/mailgun" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/msteams" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/slack" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/smtp" | ||||||
|  | 	k8sevent "github.com/jenkinsci/kubernetes-operator/pkg/event" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
| 
 | 
 | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | // Provider is the communication service handler
 | ||||||
| 	infoTitleText         = "Jenkins Operator reconciliation info" | type Provider interface { | ||||||
| 	warnTitleText         = "Jenkins Operator reconciliation warning" | 	Send(event event.Event) error | ||||||
| 	messageFieldName      = "Message" |  | ||||||
| 	loggingLevelFieldName = "Logging Level" |  | ||||||
| 	crNameFieldName       = "CR Name" |  | ||||||
| 	phaseFieldName        = "Phase" |  | ||||||
| 	namespaceFieldName    = "Namespace" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// PhaseBase is core configuration of Jenkins provided by the Operator
 |  | ||||||
| 	PhaseBase Phase = "base" |  | ||||||
| 
 |  | ||||||
| 	// PhaseUser is user-defined configuration of Jenkins
 |  | ||||||
| 	PhaseUser Phase = "user" |  | ||||||
| 
 |  | ||||||
| 	// PhaseUnknown is untraceable type of configuration
 |  | ||||||
| 	PhaseUnknown Phase = "unknown" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	testPhase          = PhaseUser |  | ||||||
| 	testCrName         = "test-cr" |  | ||||||
| 	testNamespace      = "default" |  | ||||||
| 	testMessage        = "test-message" |  | ||||||
| 	testMessageVerbose = []string{"detail-test-message"} |  | ||||||
| 	testLoggingLevel   = v1alpha2.NotificationLogLevelWarning |  | ||||||
| 
 |  | ||||||
| 	client = http.Client{} |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Phase defines the type of configuration
 |  | ||||||
| type Phase string |  | ||||||
| 
 |  | ||||||
| // StatusColor is useful for better UX
 |  | ||||||
| type StatusColor string |  | ||||||
| 
 |  | ||||||
| // LoggingLevel is type for selecting different logging levels
 |  | ||||||
| type LoggingLevel string |  | ||||||
| 
 |  | ||||||
| // Event contains event details which will be sent as a notification
 |  | ||||||
| type Event struct { |  | ||||||
| 	Jenkins         v1alpha2.Jenkins |  | ||||||
| 	Phase           Phase |  | ||||||
| 	LogLevel        v1alpha2.NotificationLogLevel |  | ||||||
| 	Message         string |  | ||||||
| 	MessagesVerbose []string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type service interface { |  | ||||||
| 	Send(event Event, notificationConfig v1alpha2.Notification) error |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Listen listens for incoming events and send it as notifications
 | // Listen listens for incoming events and send it as notifications
 | ||||||
| func Listen(events chan Event, k8sEvent event.Recorder, k8sClient k8sclient.Client) { | func Listen(events chan event.Event, k8sEvent k8sevent.Recorder, k8sClient k8sclient.Client) { | ||||||
| 	for evt := range events { | 	httpClient := http.Client{} | ||||||
| 		logger := log.Log.WithValues("cr", evt.Jenkins.Name) | 	for e := range events { | ||||||
| 		for _, notificationConfig := range evt.Jenkins.Spec.Notifications { | 		logger := log.Log.WithValues("cr", e.Jenkins.Name) | ||||||
|  | 
 | ||||||
|  | 		if !e.Reason.HasMessages() { | ||||||
|  | 			logger.V(log.VWarn).Info("Reason has no messages, this should not happen") | ||||||
|  | 			continue // skip empty messages
 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		k8sEvent.Emit(&e.Jenkins, | ||||||
|  | 			eventLevelToKubernetesEventType(e.Level), | ||||||
|  | 			k8sevent.Reason(reflect.TypeOf(e.Reason).Name()), | ||||||
|  | 			strings.Join(e.Reason.Short(), "; "), | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		for _, notificationConfig := range e.Jenkins.Spec.Notifications { | ||||||
| 			var err error | 			var err error | ||||||
| 			var svc service | 			var provider Provider | ||||||
| 
 | 
 | ||||||
| 			if notificationConfig.Slack != nil { | 			if notificationConfig.Slack != nil { | ||||||
| 				svc = Slack{k8sClient: k8sClient} | 				provider = slack.New(k8sClient, notificationConfig, httpClient) | ||||||
| 			} else if notificationConfig.Teams != nil { | 			} else if notificationConfig.Teams != nil { | ||||||
| 				svc = Teams{k8sClient: k8sClient} | 				provider = msteams.New(k8sClient, notificationConfig, httpClient) | ||||||
| 			} else if notificationConfig.Mailgun != nil { | 			} else if notificationConfig.Mailgun != nil { | ||||||
| 				svc = MailGun{k8sClient: k8sClient} | 				provider = mailgun.New(k8sClient, notificationConfig) | ||||||
| 			} else if notificationConfig.SMTP != nil { | 			} else if notificationConfig.SMTP != nil { | ||||||
| 				svc = SMTP{k8sClient: k8sClient} | 				provider = smtp.New(k8sClient, notificationConfig) | ||||||
| 			} else { | 			} else { | ||||||
| 				logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig)) | 				logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig)) | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			go func(notificationConfig v1alpha2.Notification) { | 			isInfoEvent := e.Level == v1alpha2.NotificationLevelInfo | ||||||
| 				err = notify(svc, evt, notificationConfig) | 			wantsWarning := notificationConfig.LoggingLevel == v1alpha2.NotificationLevelWarning | ||||||
|  | 			if isInfoEvent && wantsWarning { | ||||||
|  | 				continue // skip the event
 | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
|  | 			go func(notificationConfig v1alpha2.Notification) { | ||||||
|  | 				err = provider.Send(e) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
|  | 					wrapped := errors.WithMessage(err, | ||||||
|  | 						fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name)) | ||||||
| 					if log.Debug { | 					if log.Debug { | ||||||
| 						logger.Error(nil, fmt.Sprintf("%+v", errors.WithMessage(err, fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name)))) | 						logger.Error(nil, fmt.Sprintf("%+v", wrapped)) | ||||||
| 					} else { | 					} else { | ||||||
| 						logger.Error(nil, fmt.Sprintf("%s", errors.WithMessage(err, fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name)))) | 						logger.Error(nil, fmt.Sprintf("%s", wrapped)) | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			}(notificationConfig) | 			}(notificationConfig) | ||||||
| 		} | 		} | ||||||
| 		k8sEvent.Emit(&evt.Jenkins, logLevelEventType(evt.LogLevel), "NotificationSent", evt.Message) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func logLevelEventType(level v1alpha2.NotificationLogLevel) event.Type { | func eventLevelToKubernetesEventType(level v1alpha2.NotificationLevel) k8sevent.Type { | ||||||
| 	switch level { | 	switch level { | ||||||
| 	case v1alpha2.NotificationLogLevelWarning: | 	case v1alpha2.NotificationLevelWarning: | ||||||
| 		return event.TypeWarning | 		return k8sevent.TypeWarning | ||||||
| 	case v1alpha2.NotificationLogLevelInfo: | 	case v1alpha2.NotificationLevelInfo: | ||||||
| 		return event.TypeNormal | 		return k8sevent.TypeNormal | ||||||
| 	default: | 	default: | ||||||
| 		return event.TypeNormal | 		return k8sevent.TypeNormal | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func notify(svc service, event Event, manifest v1alpha2.Notification) error { |  | ||||||
| 	if event.LogLevel == v1alpha2.NotificationLogLevelInfo && manifest.LoggingLevel == v1alpha2.NotificationLogLevelWarning { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return svc.Send(event, manifest) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func notificationTitle(event Event) string { |  | ||||||
| 	if event.LogLevel == v1alpha2.NotificationLogLevelInfo { |  | ||||||
| 		return infoTitleText |  | ||||||
| 	} else if event.LogLevel == v1alpha2.NotificationLogLevelWarning { |  | ||||||
| 		return warnTitleText |  | ||||||
| 	} else { |  | ||||||
| 		return "" |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,136 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Slack is a Slack notification service
 |  | ||||||
| type Slack struct { |  | ||||||
| 	k8sClient k8sclient.Client |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SlackMessage is representation of json message
 |  | ||||||
| type SlackMessage struct { |  | ||||||
| 	Text        string            `json:"text"` |  | ||||||
| 	Attachments []SlackAttachment `json:"attachments"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SlackAttachment is representation of json attachment
 |  | ||||||
| type SlackAttachment struct { |  | ||||||
| 	Fallback string       `json:"fallback"` |  | ||||||
| 	Color    StatusColor  `json:"color"` |  | ||||||
| 	Pretext  string       `json:"pretext"` |  | ||||||
| 	Title    string       `json:"title"` |  | ||||||
| 	Text     string       `json:"text"` |  | ||||||
| 	Fields   []SlackField `json:"fields"` |  | ||||||
| 	Footer   string       `json:"footer"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SlackField is representation of json field.
 |  | ||||||
| type SlackField struct { |  | ||||||
| 	Title string `json:"title"` |  | ||||||
| 	Value string `json:"value"` |  | ||||||
| 	Short bool   `json:"short"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s Slack) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor { |  | ||||||
| 	switch logLevel { |  | ||||||
| 	case v1alpha2.NotificationLogLevelInfo: |  | ||||||
| 		return "#439FE0" |  | ||||||
| 	case v1alpha2.NotificationLogLevelWarning: |  | ||||||
| 		return "danger" |  | ||||||
| 	default: |  | ||||||
| 		return "#c8c8c8" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Send is function for sending directly to API
 |  | ||||||
| func (s Slack) Send(event Event, config v1alpha2.Notification) error { |  | ||||||
| 	secret := &corev1.Secret{} |  | ||||||
| 	selector := config.Slack.WebHookURLSecretKeySelector |  | ||||||
| 
 |  | ||||||
| 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sm := &SlackMessage{ |  | ||||||
| 		Attachments: []SlackAttachment{ |  | ||||||
| 			{ |  | ||||||
| 				Fallback: "", |  | ||||||
| 				Color:    s.getStatusColor(event.LogLevel), |  | ||||||
| 				Fields: []SlackField{ |  | ||||||
| 					{ |  | ||||||
| 						Title: "", |  | ||||||
| 						Value: event.Message, |  | ||||||
| 						Short: false, |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						Title: namespaceFieldName, |  | ||||||
| 						Value: event.Jenkins.Namespace, |  | ||||||
| 						Short: true, |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						Title: crNameFieldName, |  | ||||||
| 						Value: event.Jenkins.Name, |  | ||||||
| 						Short: true, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mainAttachment := &sm.Attachments[0] |  | ||||||
| 
 |  | ||||||
| 	mainAttachment.Title = notificationTitle(event) |  | ||||||
| 
 |  | ||||||
| 	if config.Verbose { |  | ||||||
| 		// TODO: or for title == message
 |  | ||||||
| 		message := event.Message |  | ||||||
| 		for _, msg := range event.MessagesVerbose { |  | ||||||
| 			message = message + "\n - " + msg |  | ||||||
| 		} |  | ||||||
| 		mainAttachment.Fields[0].Value = message |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if event.Phase != PhaseUnknown { |  | ||||||
| 		mainAttachment.Fields = append(mainAttachment.Fields, SlackField{ |  | ||||||
| 			Title: phaseFieldName, |  | ||||||
| 			Value: string(event.Phase), |  | ||||||
| 			Short: true, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	slackMessage, err := json.Marshal(sm) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	secretValue := string(secret.Data[selector.Key]) |  | ||||||
| 	if secretValue == "" { |  | ||||||
| 		return errors.Errorf("Slack WebHook URL is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	resp, err := client.Do(request) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	defer func() { _ = resp.Body.Close() }() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,152 @@ | ||||||
|  | package slack | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 
 | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	infoColor    = "#439FE0" | ||||||
|  | 	warningColor = "danger" | ||||||
|  | 	defaultColor = "#c8c8c8" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Slack is a Slack notification service
 | ||||||
|  | type Slack struct { | ||||||
|  | 	httpClient http.Client | ||||||
|  | 	k8sClient  k8sclient.Client | ||||||
|  | 	config     v1alpha2.Notification | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns instance of Slack
 | ||||||
|  | func New(k8sClient k8sclient.Client, config v1alpha2.Notification, httpClient http.Client) *Slack { | ||||||
|  | 	return &Slack{k8sClient: k8sClient, config: config, httpClient: httpClient} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Message is representation of json message
 | ||||||
|  | type Message struct { | ||||||
|  | 	Text        string       `json:"text"` | ||||||
|  | 	Attachments []Attachment `json:"attachments"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Attachment is representation of json attachment
 | ||||||
|  | type Attachment struct { | ||||||
|  | 	Fallback string            `json:"fallback"` | ||||||
|  | 	Color    event.StatusColor `json:"color"` | ||||||
|  | 	Pretext  string            `json:"pretext"` | ||||||
|  | 	Title    string            `json:"title"` | ||||||
|  | 	Text     string            `json:"text"` | ||||||
|  | 	Fields   []Field           `json:"fields"` | ||||||
|  | 	Footer   string            `json:"footer"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Field is representation of json field.
 | ||||||
|  | type Field struct { | ||||||
|  | 	Title string `json:"title"` | ||||||
|  | 	Value string `json:"value"` | ||||||
|  | 	Short bool   `json:"short"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s Slack) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case v1alpha2.NotificationLevelInfo: | ||||||
|  | 		return infoColor | ||||||
|  | 	case v1alpha2.NotificationLevelWarning: | ||||||
|  | 		return warningColor | ||||||
|  | 	default: | ||||||
|  | 		return defaultColor | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s Slack) generateMessage(e event.Event) Message { | ||||||
|  | 	var messageStringBuilder strings.Builder | ||||||
|  | 	if s.config.Verbose { | ||||||
|  | 		for _, msg := range e.Reason.Verbose() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		for _, msg := range e.Reason.Short() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sm := Message{ | ||||||
|  | 		Attachments: []Attachment{ | ||||||
|  | 			{ | ||||||
|  | 				Title:    provider.NotificationTitle(e), | ||||||
|  | 				Fallback: "", | ||||||
|  | 				Color:    s.getStatusColor(e.Level), | ||||||
|  | 				Fields: []Field{ | ||||||
|  | 					{ | ||||||
|  | 						Title: "", | ||||||
|  | 						Value: messageStringBuilder.String(), | ||||||
|  | 						Short: false, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Title: provider.NamespaceFieldName, | ||||||
|  | 						Value: e.Jenkins.Namespace, | ||||||
|  | 						Short: true, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Title: provider.CrNameFieldName, | ||||||
|  | 						Value: e.Jenkins.Name, | ||||||
|  | 						Short: true, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Title: provider.PhaseFieldName, | ||||||
|  | 						Value: string(e.Phase), | ||||||
|  | 						Short: true, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return sm | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send is function for sending directly to API
 | ||||||
|  | func (s Slack) Send(e event.Event) error { | ||||||
|  | 	secret := &corev1.Secret{} | ||||||
|  | 	selector := s.config.Slack.WebHookURLSecretKeySelector | ||||||
|  | 
 | ||||||
|  | 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: e.Jenkins.Namespace}, secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	slackMessage, err := json.Marshal(s.generateMessage(e)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	secretValue := string(secret.Data[selector.Key]) | ||||||
|  | 	if secretValue == "" { | ||||||
|  | 		return errors.Errorf("Slack WebHook URL is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, selector.Name, selector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resp, err := s.httpClient.Do(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	defer func() { _ = resp.Body.Close() }() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,303 @@ | ||||||
|  | package slack | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	testPhase     = event.PhaseUser | ||||||
|  | 	testCrName    = "test-cr" | ||||||
|  | 	testNamespace = "default" | ||||||
|  | 	testReason    = reason.NewPodRestart( | ||||||
|  | 		reason.KubernetesSource, | ||||||
|  | 		[]string{"test-reason-1"}, | ||||||
|  | 		[]string{"test-verbose-1"}..., | ||||||
|  | 	) | ||||||
|  | 	testLevel = v1alpha2.NotificationLevelWarning | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestSlack_Send(t *testing.T) { | ||||||
|  | 	fakeClient := fake.NewFakeClient() | ||||||
|  | 	testURLSelectorKeyName := "test-url-selector" | ||||||
|  | 	testSecretName := "test-secret" | ||||||
|  | 
 | ||||||
|  | 	e := event.Event{ | ||||||
|  | 		Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      testCrName, | ||||||
|  | 				Namespace: testNamespace, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		Phase:  testPhase, | ||||||
|  | 		Level:  testLevel, | ||||||
|  | 		Reason: testReason, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	slack := Slack{k8sClient: fakeClient, config: v1alpha2.Notification{ | ||||||
|  | 		Slack: &v1alpha2.Slack{ | ||||||
|  | 			WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testURLSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}} | ||||||
|  | 
 | ||||||
|  | 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		var message Message | ||||||
|  | 		decoder := json.NewDecoder(r.Body) | ||||||
|  | 		err := decoder.Decode(&message) | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mainAttachment := message.Attachments[0] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainAttachment.Title, provider.NotificationTitle(e)) | ||||||
|  | 		for _, field := range mainAttachment.Fields { | ||||||
|  | 			switch field.Title { | ||||||
|  | 			case provider.PhaseFieldName: | ||||||
|  | 				assert.Equal(t, field.Value, string(e.Phase)) | ||||||
|  | 			case provider.CrNameFieldName: | ||||||
|  | 				assert.Equal(t, field.Value, e.Jenkins.Name) | ||||||
|  | 			case "": | ||||||
|  | 				message := "" | ||||||
|  | 				for _, msg := range e.Reason.Short() { | ||||||
|  | 					message = message + "\n - " + msg + "\n" | ||||||
|  | 				} | ||||||
|  | 				assert.Equal(t, field.Value, message) | ||||||
|  | 			case provider.LevelFieldName: | ||||||
|  | 				assert.Equal(t, field.Value, string(e.Level)) | ||||||
|  | 			case provider.NamespaceFieldName: | ||||||
|  | 				assert.Equal(t, field.Value, e.Jenkins.Namespace) | ||||||
|  | 			default: | ||||||
|  | 				t.Errorf("Unexpected field %+v", field) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, mainAttachment.Footer, "") | ||||||
|  | 		assert.Equal(t, mainAttachment.Color, slack.getStatusColor(e.Level)) | ||||||
|  | 	})) | ||||||
|  | 
 | ||||||
|  | 	defer server.Close() | ||||||
|  | 
 | ||||||
|  | 	secret := &corev1.Secret{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      testSecretName, | ||||||
|  | 			Namespace: testNamespace, | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		Data: map[string][]byte{ | ||||||
|  | 			testURLSelectorKeyName: []byte(server.URL), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := fakeClient.Create(context.TODO(), secret) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	err = slack.Send(e) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGenerateMessage(t *testing.T) { | ||||||
|  | 	t.Run("happy", func(t *testing.T) { | ||||||
|  | 		crName := "test-jenkins" | ||||||
|  | 		crNamespace := "test-namespace" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose") | ||||||
|  | 
 | ||||||
|  | 		s := Slack{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		var messageStringBuilder strings.Builder | ||||||
|  | 		for _, msg := range e.Reason.Verbose() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mainAttachment := message.Attachments[0] | ||||||
|  | 		messageField := mainAttachment.Fields[0] | ||||||
|  | 		namespaceField := mainAttachment.Fields[1] | ||||||
|  | 		crNameField := mainAttachment.Fields[2] | ||||||
|  | 		phaseField := mainAttachment.Fields[3] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, messageField.Value, messageStringBuilder.String()) | ||||||
|  | 		assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, crNameField.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseField.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with nils", func(t *testing.T) { | ||||||
|  | 		crName := "nil" | ||||||
|  | 		crNamespace := "nil" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil") | ||||||
|  | 
 | ||||||
|  | 		s := Slack{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		var messageStringBuilder strings.Builder | ||||||
|  | 		for _, msg := range e.Reason.Verbose() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mainAttachment := message.Attachments[0] | ||||||
|  | 		messageField := mainAttachment.Fields[0] | ||||||
|  | 		namespaceField := mainAttachment.Fields[1] | ||||||
|  | 		crNameField := mainAttachment.Fields[2] | ||||||
|  | 		phaseField := mainAttachment.Fields[3] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, messageField.Value, messageStringBuilder.String()) | ||||||
|  | 		assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, crNameField.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseField.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with empty strings", func(t *testing.T) { | ||||||
|  | 		crName := "" | ||||||
|  | 		crNamespace := "" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "") | ||||||
|  | 
 | ||||||
|  | 		s := Slack{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		var messageStringBuilder strings.Builder | ||||||
|  | 		for _, msg := range e.Reason.Verbose() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mainAttachment := message.Attachments[0] | ||||||
|  | 		messageField := mainAttachment.Fields[0] | ||||||
|  | 		namespaceField := mainAttachment.Fields[1] | ||||||
|  | 		crNameField := mainAttachment.Fields[2] | ||||||
|  | 		phaseField := mainAttachment.Fields[3] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, messageField.Value, messageStringBuilder.String()) | ||||||
|  | 		assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, crNameField.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseField.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with utf-8 characters", func(t *testing.T) { | ||||||
|  | 		crName := "ąśćńółżź" | ||||||
|  | 		crNamespace := "ąśćńółżź" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź") | ||||||
|  | 
 | ||||||
|  | 		s := Slack{ | ||||||
|  | 			httpClient: http.Client{}, | ||||||
|  | 			k8sClient:  fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				Verbose: true, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name:      crName, | ||||||
|  | 					Namespace: crNamespace, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 
 | ||||||
|  | 		var messageStringBuilder strings.Builder | ||||||
|  | 		for _, msg := range e.Reason.Verbose() { | ||||||
|  | 			messageStringBuilder.WriteString("\n - " + msg + "\n") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		mainAttachment := message.Attachments[0] | ||||||
|  | 		messageField := mainAttachment.Fields[0] | ||||||
|  | 		namespaceField := mainAttachment.Fields[1] | ||||||
|  | 		crNameField := mainAttachment.Fields[2] | ||||||
|  | 		phaseField := mainAttachment.Fields[3] | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, messageField.Value, messageStringBuilder.String()) | ||||||
|  | 		assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace) | ||||||
|  | 		assert.Equal(t, crNameField.Value, e.Jenkins.Name) | ||||||
|  | 		assert.Equal(t, event.Phase(phaseField.Value), e.Phase) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -1,97 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |  | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestSlack_Send(t *testing.T) { |  | ||||||
| 	fakeClient := fake.NewFakeClient() |  | ||||||
| 	testURLSelectorKeyName := "test-url-selector" |  | ||||||
| 	testSecretName := "test-secret" |  | ||||||
| 
 |  | ||||||
| 	event := Event{ |  | ||||||
| 		Jenkins: v1alpha2.Jenkins{ |  | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 				Name:      testCrName, |  | ||||||
| 				Namespace: testNamespace, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		Phase:           testPhase, |  | ||||||
| 		Message:         testMessage, |  | ||||||
| 		MessagesVerbose: testMessageVerbose, |  | ||||||
| 		LogLevel:        testLoggingLevel, |  | ||||||
| 	} |  | ||||||
| 	slack := Slack{k8sClient: fakeClient} |  | ||||||
| 
 |  | ||||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		var message SlackMessage |  | ||||||
| 		decoder := json.NewDecoder(r.Body) |  | ||||||
| 		err := decoder.Decode(&message) |  | ||||||
| 
 |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatal(err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		mainAttachment := message.Attachments[0] |  | ||||||
| 
 |  | ||||||
| 		assert.Equal(t, mainAttachment.Title, notificationTitle(event)) |  | ||||||
| 		for _, field := range mainAttachment.Fields { |  | ||||||
| 			switch field.Title { |  | ||||||
| 			case phaseFieldName: |  | ||||||
| 				assert.Equal(t, field.Value, string(event.Phase)) |  | ||||||
| 			case crNameFieldName: |  | ||||||
| 				assert.Equal(t, field.Value, event.Jenkins.Name) |  | ||||||
| 			case "": |  | ||||||
| 				assert.Equal(t, field.Value, event.Message) |  | ||||||
| 			case loggingLevelFieldName: |  | ||||||
| 				assert.Equal(t, field.Value, string(event.LogLevel)) |  | ||||||
| 			case namespaceFieldName: |  | ||||||
| 				assert.Equal(t, field.Value, event.Jenkins.Namespace) |  | ||||||
| 			default: |  | ||||||
| 				t.Errorf("Unexpected field %+v", field) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		assert.Equal(t, mainAttachment.Footer, "") |  | ||||||
| 		assert.Equal(t, mainAttachment.Color, slack.getStatusColor(event.LogLevel)) |  | ||||||
| 	})) |  | ||||||
| 
 |  | ||||||
| 	defer server.Close() |  | ||||||
| 
 |  | ||||||
| 	secret := &corev1.Secret{ |  | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ |  | ||||||
| 			Name:      testSecretName, |  | ||||||
| 			Namespace: testNamespace, |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		Data: map[string][]byte{ |  | ||||||
| 			testURLSelectorKeyName: []byte(server.URL), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := fakeClient.Create(context.TODO(), secret) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	err = slack.Send(event, v1alpha2.Notification{ |  | ||||||
| 		Slack: &v1alpha2.Slack{ |  | ||||||
| 			WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{ |  | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ |  | ||||||
| 					Name: testSecretName, |  | ||||||
| 				}, |  | ||||||
| 				Key: testURLSelectorKeyName, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| } |  | ||||||
|  | @ -1,94 +0,0 @@ | ||||||
| package notifications |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 
 |  | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	"gopkg.in/gomail.v2" |  | ||||||
| 	corev1 "k8s.io/api/core/v1" |  | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	mailSubject = "Jenkins Operator Notification" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // SMTP is Simple Mail Transport Protocol used for sending emails
 |  | ||||||
| type SMTP struct { |  | ||||||
| 	k8sClient k8sclient.Client |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Send is function for sending notification by SMTP server
 |  | ||||||
| func (s SMTP) Send(event Event, config v1alpha2.Notification) error { |  | ||||||
| 	usernameSecret := &corev1.Secret{} |  | ||||||
| 	passwordSecret := &corev1.Secret{} |  | ||||||
| 
 |  | ||||||
| 	usernameSelector := config.SMTP.UsernameSecretKeySelector |  | ||||||
| 	passwordSelector := config.SMTP.PasswordSecretKeySelector |  | ||||||
| 
 |  | ||||||
| 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: usernameSelector.Name, Namespace: event.Jenkins.Namespace}, usernameSecret) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: passwordSelector.Name, Namespace: event.Jenkins.Namespace}, passwordSecret) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	usernameSecretValue := string(usernameSecret.Data[usernameSelector.Key]) |  | ||||||
| 	if usernameSecretValue == "" { |  | ||||||
| 		return errors.Errorf("SMTP username is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, usernameSelector.Name, usernameSelector.Key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	passwordSecretValue := string(passwordSecret.Data[passwordSelector.Key]) |  | ||||||
| 	if passwordSecretValue == "" { |  | ||||||
| 		return errors.Errorf("SMTP password is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, passwordSelector.Name, passwordSelector.Key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mailer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, usernameSecretValue, passwordSecretValue) |  | ||||||
| 	mailer.TLSConfig = &tls.Config{InsecureSkipVerify: config.SMTP.TLSInsecureSkipVerify} |  | ||||||
| 
 |  | ||||||
| 	var statusMessage string |  | ||||||
| 
 |  | ||||||
| 	if config.Verbose { |  | ||||||
| 		message := event.Message + "<ul>" |  | ||||||
| 		for _, msg := range event.MessagesVerbose { |  | ||||||
| 			message = message + "<li>" + msg + "</li>" |  | ||||||
| 		} |  | ||||||
| 		message = message + "</ul>" |  | ||||||
| 		statusMessage = message |  | ||||||
| 	} else { |  | ||||||
| 		statusMessage = event.Message |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	htmlMessage := fmt.Sprintf(content, s.getStatusColor(event.LogLevel), notificationTitle(event), statusMessage, event.Jenkins.Name, event.Phase) |  | ||||||
| 	message := gomail.NewMessage() |  | ||||||
| 
 |  | ||||||
| 	message.SetHeader("From", config.SMTP.From) |  | ||||||
| 	message.SetHeader("To", config.SMTP.To) |  | ||||||
| 	message.SetHeader("Subject", mailSubject) |  | ||||||
| 	message.SetBody("text/html", htmlMessage) |  | ||||||
| 
 |  | ||||||
| 	if err := mailer.DialAndSend(message); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s SMTP) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor { |  | ||||||
| 	switch logLevel { |  | ||||||
| 	case v1alpha2.NotificationLogLevelInfo: |  | ||||||
| 		return "blue" |  | ||||||
| 	case v1alpha2.NotificationLogLevelWarning: |  | ||||||
| 		return "red" |  | ||||||
| 	default: |  | ||||||
| 		return "gray" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,134 @@ | ||||||
|  | package smtp | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider" | ||||||
|  | 
 | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"gopkg.in/gomail.v2" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	mailSubject = "Jenkins Operator Notification" | ||||||
|  | 
 | ||||||
|  | 	infoColor    = "blue" | ||||||
|  | 	warningColor = "red" | ||||||
|  | 	defaultColor = "gray" | ||||||
|  | 
 | ||||||
|  | 	content = ` | ||||||
|  | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | ||||||
|  |         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||||||
|  | <html> | ||||||
|  | <head></head> | ||||||
|  | <body> | ||||||
|  | 		<h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1> | ||||||
|  | 		<h3>%s</h3> | ||||||
|  | 		<table> | ||||||
|  | 			<tr> | ||||||
|  | 				<td><b>CR name:</b></td> | ||||||
|  | 				<td>%s</td> | ||||||
|  | 			</tr> | ||||||
|  | 			<tr> | ||||||
|  | 				<td><b>Phase:</b></td> | ||||||
|  | 				<td>%s</td> | ||||||
|  | 			</tr> | ||||||
|  | 		</table> | ||||||
|  | 		<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6> | ||||||
|  | </body> | ||||||
|  | </html>` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // SMTP is Simple Mail Transport Protocol used for sending emails
 | ||||||
|  | type SMTP struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | 	config    v1alpha2.Notification | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // New returns instance of SMTP
 | ||||||
|  | func New(k8sClient k8sclient.Client, config v1alpha2.Notification) *SMTP { | ||||||
|  | 	return &SMTP{k8sClient: k8sClient, config: config} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s SMTP) generateMessage(e event.Event) *gomail.Message { | ||||||
|  | 	var statusMessage strings.Builder | ||||||
|  | 	var reasons string | ||||||
|  | 
 | ||||||
|  | 	if s.config.Verbose { | ||||||
|  | 		reasons = strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>") | ||||||
|  | 	} else { | ||||||
|  | 		reasons = strings.TrimRight(strings.Join(e.Reason.Short(), "</li><li>"), "<li>") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	statusMessage.WriteString("<ul><li>") | ||||||
|  | 	statusMessage.WriteString(reasons) | ||||||
|  | 	statusMessage.WriteString("</ul>") | ||||||
|  | 
 | ||||||
|  | 	htmlMessage := fmt.Sprintf(content, s.getStatusColor(e.Level), provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase) | ||||||
|  | 	message := gomail.NewMessage() | ||||||
|  | 
 | ||||||
|  | 	message.SetHeader("From", s.config.SMTP.From) | ||||||
|  | 	message.SetHeader("To", s.config.SMTP.To) | ||||||
|  | 	message.SetHeader("Subject", mailSubject) | ||||||
|  | 	message.SetBody("text/html", htmlMessage) | ||||||
|  | 
 | ||||||
|  | 	return message | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send is function for sending notification by SMTP server
 | ||||||
|  | func (s SMTP) Send(e event.Event) error { | ||||||
|  | 	usernameSecret := &corev1.Secret{} | ||||||
|  | 	passwordSecret := &corev1.Secret{} | ||||||
|  | 
 | ||||||
|  | 	usernameSelector := s.config.SMTP.UsernameSecretKeySelector | ||||||
|  | 	passwordSelector := s.config.SMTP.PasswordSecretKeySelector | ||||||
|  | 
 | ||||||
|  | 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: usernameSelector.Name, Namespace: e.Jenkins.Namespace}, usernameSecret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: passwordSelector.Name, Namespace: e.Jenkins.Namespace}, passwordSecret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	usernameSecretValue := string(usernameSecret.Data[usernameSelector.Key]) | ||||||
|  | 	if usernameSecretValue == "" { | ||||||
|  | 		return errors.Errorf("SMTP username is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, usernameSelector.Name, usernameSelector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	passwordSecretValue := string(passwordSecret.Data[passwordSelector.Key]) | ||||||
|  | 	if passwordSecretValue == "" { | ||||||
|  | 		return errors.Errorf("SMTP password is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, passwordSelector.Name, passwordSelector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mailer := gomail.NewDialer(s.config.SMTP.Server, s.config.SMTP.Port, usernameSecretValue, passwordSecretValue) | ||||||
|  | 	mailer.TLSConfig = &tls.Config{InsecureSkipVerify: s.config.SMTP.TLSInsecureSkipVerify} | ||||||
|  | 
 | ||||||
|  | 	message := s.generateMessage(e) | ||||||
|  | 	if err := mailer.DialAndSend(message); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s SMTP) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case v1alpha2.NotificationLevelInfo: | ||||||
|  | 		return infoColor | ||||||
|  | 	case v1alpha2.NotificationLevelWarning: | ||||||
|  | 		return warningColor | ||||||
|  | 	default: | ||||||
|  | 		return defaultColor | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package notifications | package smtp | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | @ -13,6 +13,8 @@ import ( | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason" | ||||||
| 
 | 
 | ||||||
| 	"github.com/emersion/go-smtp" | 	"github.com/emersion/go-smtp" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | @ -37,8 +39,20 @@ const ( | ||||||
| 	subjectHeader = "Subject" | 	subjectHeader = "Subject" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var ( | ||||||
|  | 	testPhase     = event.PhaseUser | ||||||
|  | 	testCrName    = "test-cr" | ||||||
|  | 	testNamespace = "default" | ||||||
|  | 	testReason    = reason.NewPodRestart( | ||||||
|  | 		reason.KubernetesSource, | ||||||
|  | 		[]string{"test-reason-1"}, | ||||||
|  | 		[]string{"test-verbose-1"}..., | ||||||
|  | 	) | ||||||
|  | 	testLevel = v1alpha2.NotificationLevelWarning | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| type testServer struct { | type testServer struct { | ||||||
| 	event Event | 	event event.Event | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Login handles a login command with username and password.
 | // Login handles a login command with username and password.
 | ||||||
|  | @ -56,7 +70,7 @@ func (bkd *testServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session | ||||||
| 
 | 
 | ||||||
| // A Session is returned after successful login.
 | // A Session is returned after successful login.
 | ||||||
| type testSession struct { | type testSession struct { | ||||||
| 	event Event | 	event event.Event | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *testSession) Mail(from string) error { | func (s *testSession) Mail(from string) error { | ||||||
|  | @ -111,7 +125,7 @@ func (s *testSession) Logout() error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSMTP_Send(t *testing.T) { | func TestSMTP_Send(t *testing.T) { | ||||||
| 	event := Event{ | 	event := event.Event{ | ||||||
| 		Jenkins: v1alpha2.Jenkins{ | 		Jenkins: v1alpha2.Jenkins{ | ||||||
| 			ObjectMeta: metav1.ObjectMeta{ | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
| 				Name:      testCrName, | 				Name:      testCrName, | ||||||
|  | @ -119,9 +133,8 @@ func TestSMTP_Send(t *testing.T) { | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Phase:  testPhase, | 		Phase:  testPhase, | ||||||
| 		Message:         testMessage, | 		Level:  testLevel, | ||||||
| 		MessagesVerbose: testMessageVerbose, | 		Reason: testReason, | ||||||
| 		LogLevel:        testLoggingLevel, |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fakeClient := fake.NewFakeClient() | 	fakeClient := fake.NewFakeClient() | ||||||
|  | @ -129,7 +142,27 @@ func TestSMTP_Send(t *testing.T) { | ||||||
| 	testPasswordSelectorKeyName := "test-password-selector" | 	testPasswordSelectorKeyName := "test-password-selector" | ||||||
| 	testSecretName := "test-secret" | 	testSecretName := "test-secret" | ||||||
| 
 | 
 | ||||||
| 	smtpClient := SMTP{k8sClient: fakeClient} | 	smtpClient := SMTP{k8sClient: fakeClient, config: v1alpha2.Notification{ | ||||||
|  | 		SMTP: &v1alpha2.SMTP{ | ||||||
|  | 			Server:                "localhost", | ||||||
|  | 			From:                  testFrom, | ||||||
|  | 			To:                    testTo, | ||||||
|  | 			TLSInsecureSkipVerify: true, | ||||||
|  | 			Port:                  testSMTPPort, | ||||||
|  | 			UsernameSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testUsernameSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 			PasswordSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testPasswordSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}} | ||||||
| 
 | 
 | ||||||
| 	ts := &testServer{event: event} | 	ts := &testServer{event: event} | ||||||
| 
 | 
 | ||||||
|  | @ -169,27 +202,108 @@ func TestSMTP_Send(t *testing.T) { | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
| 	err = smtpClient.Send(event, v1alpha2.Notification{ | 	err = smtpClient.Send(event) | ||||||
| 		SMTP: &v1alpha2.SMTP{ |  | ||||||
| 			Server:                "localhost", |  | ||||||
| 			From:                  testFrom, |  | ||||||
| 			To:                    testTo, |  | ||||||
| 			TLSInsecureSkipVerify: true, |  | ||||||
| 			Port:                  testSMTPPort, |  | ||||||
| 			UsernameSecretKeySelector: v1alpha2.SecretKeySelector{ |  | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ |  | ||||||
| 					Name: testSecretName, |  | ||||||
| 				}, |  | ||||||
| 				Key: testUsernameSelectorKeyName, |  | ||||||
| 			}, |  | ||||||
| 			PasswordSecretKeySelector: v1alpha2.SecretKeySelector{ |  | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ |  | ||||||
| 					Name: testSecretName, |  | ||||||
| 				}, |  | ||||||
| 				Key: testPasswordSelectorKeyName, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
| 
 | 
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestGenerateMessage(t *testing.T) { | ||||||
|  | 	t.Run("happy", func(t *testing.T) { | ||||||
|  | 		crName := "test-jenkins" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"test"}, []string{"test-verbose"}...) | ||||||
|  | 
 | ||||||
|  | 		from := "from@jenkins.local" | ||||||
|  | 		to := "to@jenkins.local" | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: crName, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 		s := SMTP{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				LoggingLevel: level, | ||||||
|  | 				SMTP: &v1alpha2.SMTP{ | ||||||
|  | 					From: from, | ||||||
|  | 					To:   to, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 		assert.NotNil(t, message) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with nils", func(t *testing.T) { | ||||||
|  | 		crName := "nil" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, []string{"nil"}...) | ||||||
|  | 
 | ||||||
|  | 		from := "nil" | ||||||
|  | 		to := "nil" | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: crName, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 		s := SMTP{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				LoggingLevel: level, | ||||||
|  | 				SMTP: &v1alpha2.SMTP{ | ||||||
|  | 					From: from, | ||||||
|  | 					To:   to, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 		assert.NotNil(t, message) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("with empty strings", func(t *testing.T) { | ||||||
|  | 		crName := "" | ||||||
|  | 		phase := event.PhaseBase | ||||||
|  | 		level := v1alpha2.NotificationLevelInfo | ||||||
|  | 		res := reason.NewUndefined(reason.KubernetesSource, []string{""}, []string{""}...) | ||||||
|  | 
 | ||||||
|  | 		from := "" | ||||||
|  | 		to := "" | ||||||
|  | 
 | ||||||
|  | 		e := event.Event{ | ||||||
|  | 			Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: crName, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Phase:  phase, | ||||||
|  | 			Level:  level, | ||||||
|  | 			Reason: res, | ||||||
|  | 		} | ||||||
|  | 		s := SMTP{ | ||||||
|  | 			k8sClient: fake.NewFakeClient(), | ||||||
|  | 			config: v1alpha2.Notification{ | ||||||
|  | 				LoggingLevel: level, | ||||||
|  | 				SMTP: &v1alpha2.SMTP{ | ||||||
|  | 					From: from, | ||||||
|  | 					To:   to, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		message := s.generateMessage(e) | ||||||
|  | 		assert.NotNil(t, message) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue