Refactor notifications package
This commit is contained in:
		
							parent
							
								
									0c08fbfd8e
								
							
						
					
					
						commit
						8f80fa5bbd
					
				|  | @ -1,140 +0,0 @@ | ||||||
| package notifier |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" |  | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" |  | ||||||
| 
 |  | ||||||
| 	"github.com/go-logr/logr" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// LogWarn is warning log entry
 |  | ||||||
| 	LogWarn LoggingLevel = "warn" |  | ||||||
| 
 |  | ||||||
| 	// LogInfo is info log entry
 |  | ||||||
| 	LogInfo LoggingLevel = "info" |  | ||||||
| 
 |  | ||||||
| 	titleText                  = "Operator reconciled." |  | ||||||
| 	messageFieldName           = "Message" |  | ||||||
| 	loggingLevelFieldName      = "Logging Level" |  | ||||||
| 	crNameFieldName            = "CR Name" |  | ||||||
| 	configurationTypeFieldName = "Configuration Type" |  | ||||||
| 	namespaceFieldName         = "Namespace" |  | ||||||
| 	footerContent              = "Powered by Jenkins Operator <3" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	testConfigurationType = "test-configuration" |  | ||||||
| 	testCrName            = "test-cr" |  | ||||||
| 	testNamespace         = "test-namespace" |  | ||||||
| 	testMessage           = "test-message" |  | ||||||
| 	testMessageVerbose    = "detail-test-message" |  | ||||||
| 	testLoggingLevel      = LogWarn |  | ||||||
| 
 |  | ||||||
| 	client = http.Client{} |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // StatusColor is useful for better UX
 |  | ||||||
| type StatusColor string |  | ||||||
| 
 |  | ||||||
| // LoggingLevel is type for selecting different logging levels
 |  | ||||||
| type LoggingLevel string |  | ||||||
| 
 |  | ||||||
| // Information represents details about operator status
 |  | ||||||
| type Information struct { |  | ||||||
| 	ConfigurationType string |  | ||||||
| 	Namespace         string |  | ||||||
| 	CrName            string |  | ||||||
| 	LogLevel          LoggingLevel |  | ||||||
| 	Message           string |  | ||||||
| 	MessageVerbose    string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Notification contains message which will be sent
 |  | ||||||
| type Notification struct { |  | ||||||
| 	Jenkins     v1alpha2.Jenkins |  | ||||||
| 	K8sClient   k8sclient.Client |  | ||||||
| 	Logger      logr.Logger |  | ||||||
| 	Information Information |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Service is skeleton for additional services
 |  | ||||||
| type service interface { |  | ||||||
| 	Send(i *Notification, config v1alpha2.Notification) error |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Listen is goroutine that listens for incoming messages and sends it
 |  | ||||||
| func Listen(notification chan *Notification) { |  | ||||||
| 	for n := range notification { |  | ||||||
| 		for _, notificationConfig := range n.Jenkins.Spec.Notifications { |  | ||||||
| 			var err error |  | ||||||
| 			var svc service |  | ||||||
| 
 |  | ||||||
| 			if notificationConfig.Slack != (v1alpha2.Slack{}) { |  | ||||||
| 				svc = Slack{} |  | ||||||
| 			} else if notificationConfig.Teams != (v1alpha2.Teams{}) { |  | ||||||
| 				svc = Teams{} |  | ||||||
| 			} else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { |  | ||||||
| 				svc = Mailgun{} |  | ||||||
| 			} else { |  | ||||||
| 				n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			err = notify(svc, n, notificationConfig) |  | ||||||
| 
 |  | ||||||
| 			if err != nil { |  | ||||||
| 				n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) |  | ||||||
| 			} else { |  | ||||||
| 				n.Logger.V(log.VDebug).Info("Sent notification") |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { |  | ||||||
| 	switch svc.(type) { |  | ||||||
| 	case Slack: |  | ||||||
| 		switch logLevel { |  | ||||||
| 		case LogInfo: |  | ||||||
| 			return "#439FE0" |  | ||||||
| 		case LogWarn: |  | ||||||
| 			return "danger" |  | ||||||
| 		default: |  | ||||||
| 			return "#c8c8c8" |  | ||||||
| 		} |  | ||||||
| 	case Teams: |  | ||||||
| 		switch logLevel { |  | ||||||
| 		case LogInfo: |  | ||||||
| 			return "439FE0" |  | ||||||
| 		case LogWarn: |  | ||||||
| 			return "E81123" |  | ||||||
| 		default: |  | ||||||
| 			return "C8C8C8" |  | ||||||
| 		} |  | ||||||
| 	case Mailgun: |  | ||||||
| 		switch logLevel { |  | ||||||
| 		case LogInfo: |  | ||||||
| 			return "blue" |  | ||||||
| 		case LogWarn: |  | ||||||
| 			return "red" |  | ||||||
| 		default: |  | ||||||
| 			return "gray" |  | ||||||
| 		} |  | ||||||
| 	default: |  | ||||||
| 		return "#c8c8c8" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func notify(svc service, n *Notification, manifest v1alpha2.Notification) error { |  | ||||||
| 	if n.Information.LogLevel == LogInfo && string(manifest.LoggingLevel) == string(LogWarn) { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return svc.Send(n, manifest) |  | ||||||
| } |  | ||||||
|  | @ -1,16 +1,17 @@ | ||||||
| package notifier | package notifications | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mailgun/mailgun-go/v3" | 	"github.com/mailgun/mailgun-go/v3" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const content = ` | const content = ` | ||||||
|  | @ -39,29 +40,41 @@ const content = ` | ||||||
| </body> | </body> | ||||||
| </html>` | </html>` | ||||||
| 
 | 
 | ||||||
| // Mailgun is service for sending emails
 | // MailGun is service for sending emails
 | ||||||
| type Mailgun struct{} | type MailGun struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m MailGun) getStatusColor(logLevel LoggingLevel) StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case LogInfo: | ||||||
|  | 		return "blue" | ||||||
|  | 	case LogWarn: | ||||||
|  | 		return "red" | ||||||
|  | 	default: | ||||||
|  | 		return "gray" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // Send is function for sending directly to API
 | // Send is function for sending directly to API
 | ||||||
| func (m Mailgun) Send(n *Notification, config v1alpha2.Notification) error { | func (m MailGun) Send(event Event, config v1alpha2.Notification) error { | ||||||
| 	secret := &corev1.Secret{} | 	secret := &corev1.Secret{} | ||||||
| 	i := n.Information |  | ||||||
| 
 | 
 | ||||||
| 	selector := config.Mailgun.APIKeySecretKeySelector | 	selector := config.Mailgun.APIKeySecretKeySelector | ||||||
| 
 | 
 | ||||||
| 	err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | 	err := m.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return errors.WithStack(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	secretValue := string(secret.Data[selector.Name]) | 	secretValue := string(secret.Data[selector.Key]) | ||||||
| 	if secretValue == "" { | 	if secretValue == "" { | ||||||
| 		return errors.Errorf("SecretValue %s is empty", selector.Name) | 		return errors.Errorf("Mailgun API is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) | 	mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) | ||||||
| 
 | 
 | ||||||
| 	htmlMessage := fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) | 	htmlMessage := fmt.Sprintf(content, m.getStatusColor(event.LogLevel), event.Jenkins.Name, event.ConfigurationType, m.getStatusColor(event.LogLevel), string(event.LogLevel)) | ||||||
| 
 | 
 | ||||||
| 	msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient) | 	msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient) | ||||||
| 	msg.SetHtml(htmlMessage) | 	msg.SetHtml(htmlMessage) | ||||||
|  | @ -1,20 +1,23 @@ | ||||||
| package notifier | package notifications | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/pkg/errors" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/pkg/errors" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Teams is Microsoft Teams Service
 | // Teams is Microsoft Teams Service
 | ||||||
| type Teams struct{} | type Teams struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // TeamsMessage is representation of json message structure
 | // TeamsMessage is representation of json message structure
 | ||||||
| type TeamsMessage struct { | type TeamsMessage struct { | ||||||
|  | @ -37,65 +40,74 @@ type TeamsFact struct { | ||||||
| 	Value string `json:"value"` | 	Value string `json:"value"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (t Teams) getStatusColor(logLevel LoggingLevel) StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case LogInfo: | ||||||
|  | 		return "439FE0" | ||||||
|  | 	case LogWarn: | ||||||
|  | 		return "E81123" | ||||||
|  | 	default: | ||||||
|  | 		return "C8C8C8" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Send is function for sending directly to API
 | // Send is function for sending directly to API
 | ||||||
| func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { | func (t Teams) Send(event Event, config v1alpha2.Notification) error { | ||||||
| 	secret := &corev1.Secret{} | 	secret := &corev1.Secret{} | ||||||
| 	i := n.Information |  | ||||||
| 
 | 
 | ||||||
| 	selector := config.Teams.URLSecretKeySelector | 	selector := config.Teams.URLSecretKeySelector | ||||||
| 
 | 
 | ||||||
| 	err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | 	err := t.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	msg, err := json.Marshal(TeamsMessage{ | 	msg, err := json.Marshal(TeamsMessage{ | ||||||
| 		Type:       "MessageCard", | 		Type:       "MessageCard", | ||||||
| 		Context:    "https://schema.org/extensions", | 		Context:    "https://schema.org/extensions", | ||||||
| 		ThemeColor: getStatusColor(i.LogLevel, t), | 		ThemeColor: t.getStatusColor(event.LogLevel), | ||||||
| 		Title:      titleText, | 		Title:      titleText, | ||||||
| 		Sections: []TeamsSection{ | 		Sections: []TeamsSection{ | ||||||
| 			{ | 			{ | ||||||
| 				Facts: []TeamsFact{ | 				Facts: []TeamsFact{ | ||||||
| 					{ | 					{ | ||||||
| 						Name:  crNameFieldName, | 						Name:  crNameFieldName, | ||||||
| 						Value: i.CrName, | 						Value: event.Jenkins.Name, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Name:  configurationTypeFieldName, | 						Name:  configurationTypeFieldName, | ||||||
| 						Value: i.ConfigurationType, | 						Value: event.ConfigurationType, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Name:  loggingLevelFieldName, | 						Name:  loggingLevelFieldName, | ||||||
| 						Value: string(i.LogLevel), | 						Value: string(event.LogLevel), | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Name:  namespaceFieldName, | 						Name:  namespaceFieldName, | ||||||
| 						Value: i.Namespace, | 						Value: event.Jenkins.Namespace, | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 				Text: i.Message, | 				Text: event.Message, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| 
 |  | ||||||
| 	secretValue := string(secret.Data[selector.Key]) |  | ||||||
| 	if secretValue == "" { |  | ||||||
| 		return errors.Errorf("SecretValue %s is empty", selector.Name) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return errors.WithStack(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) | 	request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return errors.WithStack(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	resp, err := client.Do(request) | 	resp, err := client.Do(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return errors.WithStack(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	defer func() { _ = resp.Body.Close() }() | 	defer func() { _ = resp.Body.Close() }() | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package notifier | package notifications | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | @ -20,61 +20,59 @@ func TestTeams_Send(t *testing.T) { | ||||||
| 	testURLSelectorKeyName := "test-url-selector" | 	testURLSelectorKeyName := "test-url-selector" | ||||||
| 	testSecretName := "test-secret" | 	testSecretName := "test-secret" | ||||||
| 
 | 
 | ||||||
| 	i := Information{ | 	event := Event{ | ||||||
|  | 		Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      testCrName, | ||||||
|  | 				Namespace: testNamespace, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		ConfigurationType: testConfigurationType, | 		ConfigurationType: testConfigurationType, | ||||||
| 		CrName:            testCrName, |  | ||||||
| 		Message:           testMessage, | 		Message:           testMessage, | ||||||
| 		MessageVerbose:    testMessageVerbose, | 		MessageVerbose:    testMessageVerbose, | ||||||
| 		Namespace:         testNamespace, |  | ||||||
| 		LogLevel:          testLoggingLevel, | 		LogLevel:          testLoggingLevel, | ||||||
| 	} | 	} | ||||||
| 
 | 	teams := Teams{k8sClient: fakeClient} | ||||||
| 	notification := &Notification{ |  | ||||||
| 		K8sClient:   fakeClient, |  | ||||||
| 		Information: i, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		var message TeamsMessage | 		var message TeamsMessage | ||||||
| 		decoder := json.NewDecoder(r.Body) | 		decoder := json.NewDecoder(r.Body) | ||||||
| 		err := decoder.Decode(&message) | 		err := decoder.Decode(&message) | ||||||
| 
 |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, message.Title, titleText) | 		assert.Equal(t, message.Title, titleText) | ||||||
| 		assert.Equal(t, message.ThemeColor, getStatusColor(i.LogLevel, Teams{})) | 		assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.LogLevel)) | ||||||
| 
 | 
 | ||||||
| 		mainSection := message.Sections[0] | 		mainSection := message.Sections[0] | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, mainSection.Text, i.Message) | 		assert.Equal(t, mainSection.Text, event.Message) | ||||||
| 
 | 
 | ||||||
| 		for _, fact := range mainSection.Facts { | 		for _, fact := range mainSection.Facts { | ||||||
| 			switch fact.Name { | 			switch fact.Name { | ||||||
| 			case configurationTypeFieldName: | 			case configurationTypeFieldName: | ||||||
| 				assert.Equal(t, fact.Value, i.ConfigurationType) | 				assert.Equal(t, fact.Value, event.ConfigurationType) | ||||||
| 			case crNameFieldName: | 			case crNameFieldName: | ||||||
| 				assert.Equal(t, fact.Value, i.CrName) | 				assert.Equal(t, fact.Value, event.Jenkins.Name) | ||||||
| 			case messageFieldName: | 			case messageFieldName: | ||||||
| 				assert.Equal(t, fact.Value, i.Message) | 				assert.Equal(t, fact.Value, event.Message) | ||||||
| 			case loggingLevelFieldName: | 			case loggingLevelFieldName: | ||||||
| 				assert.Equal(t, fact.Value, string(i.LogLevel)) | 				assert.Equal(t, fact.Value, string(event.LogLevel)) | ||||||
| 			case namespaceFieldName: | 			case namespaceFieldName: | ||||||
| 				assert.Equal(t, fact.Value, i.Namespace) | 				assert.Equal(t, fact.Value, event.Jenkins.Namespace) | ||||||
| 			default: | 			default: | ||||||
| 				t.Fail() | 				t.Errorf("Found unexpected '%+v' fact", fact) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	teams := Teams{} |  | ||||||
| 
 |  | ||||||
| 	defer server.Close() | 	defer server.Close() | ||||||
| 
 | 
 | ||||||
| 	secret := &corev1.Secret{ | 	secret := &corev1.Secret{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name: testSecretName, | 			Name:      testSecretName, | ||||||
|  | 			Namespace: testNamespace, | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		Data: map[string][]byte{ | 		Data: map[string][]byte{ | ||||||
|  | @ -82,10 +80,10 @@ func TestTeams_Send(t *testing.T) { | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err := notification.K8sClient.Create(context.TODO(), secret) | 	err := fakeClient.Create(context.TODO(), secret) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 	err = teams.Send(notification, v1alpha2.Notification{ | 	err = teams.Send(event, v1alpha2.Notification{ | ||||||
| 		Teams: v1alpha2.Teams{ | 		Teams: v1alpha2.Teams{ | ||||||
| 			URLSecretKeySelector: v1alpha2.SecretKeySelector{ | 			URLSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | @ -0,0 +1,101 @@ | ||||||
|  | package notifications | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
|  | 
 | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// LogWarn is warning log entry
 | ||||||
|  | 	LogWarn LoggingLevel = "warn" | ||||||
|  | 
 | ||||||
|  | 	// LogInfo is info log entry
 | ||||||
|  | 	LogInfo LoggingLevel = "info" | ||||||
|  | 
 | ||||||
|  | 	titleText                  = "Operator reconciled." | ||||||
|  | 	messageFieldName           = "Message" | ||||||
|  | 	loggingLevelFieldName      = "Logging Level" | ||||||
|  | 	crNameFieldName            = "CR Name" | ||||||
|  | 	configurationTypeFieldName = "Configuration Type" | ||||||
|  | 	namespaceFieldName         = "Namespace" | ||||||
|  | 	footerContent              = "Powered by Jenkins Operator" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	testConfigurationType = "test-configuration" | ||||||
|  | 	testCrName            = "test-cr" | ||||||
|  | 	testNamespace         = "default" | ||||||
|  | 	testMessage           = "test-message" | ||||||
|  | 	testMessageVerbose    = "detail-test-message" | ||||||
|  | 	testLoggingLevel      = LogWarn | ||||||
|  | 
 | ||||||
|  | 	client = http.Client{} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 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 | ||||||
|  | 	ConfigurationType string | ||||||
|  | 	LogLevel          LoggingLevel | ||||||
|  | 	Message           string | ||||||
|  | 	MessageVerbose    string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type service interface { | ||||||
|  | 	Send(event Event, notificationConfig v1alpha2.Notification) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Listen listens for incoming events and send it as notifications
 | ||||||
|  | func Listen(events chan Event, k8sClient k8sclient.Client) { | ||||||
|  | 	for event := range events { | ||||||
|  | 		logger := log.Log.WithValues("cr", event.Jenkins.Name) | ||||||
|  | 		for _, notificationConfig := range event.Jenkins.Spec.Notifications { | ||||||
|  | 			var err error | ||||||
|  | 			var svc service | ||||||
|  | 
 | ||||||
|  | 			if notificationConfig.Slack != (v1alpha2.Slack{}) { | ||||||
|  | 				svc = Slack{k8sClient: k8sClient} | ||||||
|  | 			} else if notificationConfig.Teams != (v1alpha2.Teams{}) { | ||||||
|  | 				svc = Teams{k8sClient: k8sClient} | ||||||
|  | 			} else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { | ||||||
|  | 				svc = MailGun{k8sClient: k8sClient} | ||||||
|  | 			} else { | ||||||
|  | 				logger.V(log.VWarn).Info(fmt.Sprintf("Unexpected notification `%+v`", notificationConfig)) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			go func(notificationConfig v1alpha2.Notification) { | ||||||
|  | 				err = notify(svc, event, notificationConfig) | ||||||
|  | 
 | ||||||
|  | 				if err != nil { | ||||||
|  | 					if log.Debug { | ||||||
|  | 						logger.Error(nil, fmt.Sprintf("%+v", errors.WithMessage(err, "failed to send notification"))) | ||||||
|  | 					} else { | ||||||
|  | 						logger.Error(nil, fmt.Sprintf("%s", errors.WithMessage(err, "failed to send notification"))) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}(notificationConfig) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func notify(svc service, event Event, manifest v1alpha2.Notification) error { | ||||||
|  | 	if event.LogLevel == LogInfo && string(manifest.LoggingLevel) == string(LogWarn) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return svc.Send(event, manifest) | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package notifier | package notifications | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | @ -11,10 +11,13 @@ import ( | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Slack is messaging service
 | // Slack is messaging service
 | ||||||
| type Slack struct{} | type Slack struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // SlackMessage is representation of json message
 | // SlackMessage is representation of json message
 | ||||||
| type SlackMessage struct { | type SlackMessage struct { | ||||||
|  | @ -40,14 +43,23 @@ type SlackField struct { | ||||||
| 	Short bool   `json:"short"` | 	Short bool   `json:"short"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Send is function for sending directly to API
 | func (s Slack) getStatusColor(logLevel LoggingLevel) StatusColor { | ||||||
| func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { | 	switch logLevel { | ||||||
| 	secret := &corev1.Secret{} | 	case LogInfo: | ||||||
| 	i := n.Information | 		return "#439FE0" | ||||||
|  | 	case LogWarn: | ||||||
|  | 		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.URLSecretKeySelector | 	selector := config.Slack.URLSecretKeySelector | ||||||
| 
 | 
 | ||||||
| 	err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -56,32 +68,32 @@ func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { | ||||||
| 		Attachments: []SlackAttachment{ | 		Attachments: []SlackAttachment{ | ||||||
| 			{ | 			{ | ||||||
| 				Fallback: "", | 				Fallback: "", | ||||||
| 				Color:    getStatusColor(i.LogLevel, s), | 				Color:    s.getStatusColor(event.LogLevel), | ||||||
| 				Text:     titleText, | 				Text:     titleText, | ||||||
| 				Fields: []SlackField{ | 				Fields: []SlackField{ | ||||||
| 					{ | 					{ | ||||||
| 						Title: messageFieldName, | 						Title: messageFieldName, | ||||||
| 						Value: i.Message, | 						Value: event.Message, | ||||||
| 						Short: false, | 						Short: false, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Title: crNameFieldName, | 						Title: crNameFieldName, | ||||||
| 						Value: i.CrName, | 						Value: event.Jenkins.Name, | ||||||
| 						Short: true, | 						Short: true, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Title: configurationTypeFieldName, | 						Title: configurationTypeFieldName, | ||||||
| 						Value: i.ConfigurationType, | 						Value: event.ConfigurationType, | ||||||
| 						Short: true, | 						Short: true, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Title: loggingLevelFieldName, | 						Title: loggingLevelFieldName, | ||||||
| 						Value: string(i.LogLevel), | 						Value: string(event.LogLevel), | ||||||
| 						Short: true, | 						Short: true, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Title: namespaceFieldName, | 						Title: namespaceFieldName, | ||||||
| 						Value: i.Namespace, | 						Value: event.Jenkins.Namespace, | ||||||
| 						Short: true, | 						Short: true, | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package notifier | package notifications | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | @ -20,19 +20,19 @@ func TestSlack_Send(t *testing.T) { | ||||||
| 	testURLSelectorKeyName := "test-url-selector" | 	testURLSelectorKeyName := "test-url-selector" | ||||||
| 	testSecretName := "test-secret" | 	testSecretName := "test-secret" | ||||||
| 
 | 
 | ||||||
| 	i := Information{ | 	event := Event{ | ||||||
|  | 		Jenkins: v1alpha2.Jenkins{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      testCrName, | ||||||
|  | 				Namespace: testNamespace, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		ConfigurationType: testConfigurationType, | 		ConfigurationType: testConfigurationType, | ||||||
| 		CrName:            testCrName, |  | ||||||
| 		Message:           testMessage, | 		Message:           testMessage, | ||||||
| 		MessageVerbose:    testMessageVerbose, | 		MessageVerbose:    testMessageVerbose, | ||||||
| 		Namespace:         testNamespace, |  | ||||||
| 		LogLevel:          testLoggingLevel, | 		LogLevel:          testLoggingLevel, | ||||||
| 	} | 	} | ||||||
| 
 | 	slack := Slack{k8sClient: fakeClient} | ||||||
| 	notification := &Notification{ |  | ||||||
| 		K8sClient:   fakeClient, |  | ||||||
| 		Information: i, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		var message SlackMessage | 		var message SlackMessage | ||||||
|  | @ -49,29 +49,30 @@ func TestSlack_Send(t *testing.T) { | ||||||
| 		for _, field := range mainAttachment.Fields { | 		for _, field := range mainAttachment.Fields { | ||||||
| 			switch field.Title { | 			switch field.Title { | ||||||
| 			case configurationTypeFieldName: | 			case configurationTypeFieldName: | ||||||
| 				assert.Equal(t, field.Value, i.ConfigurationType) | 				assert.Equal(t, field.Value, event.ConfigurationType) | ||||||
| 			case crNameFieldName: | 			case crNameFieldName: | ||||||
| 				assert.Equal(t, field.Value, i.CrName) | 				assert.Equal(t, field.Value, event.Jenkins.Name) | ||||||
| 			case messageFieldName: | 			case messageFieldName: | ||||||
| 				assert.Equal(t, field.Value, i.Message) | 				assert.Equal(t, field.Value, event.Message) | ||||||
| 			case loggingLevelFieldName: | 			case loggingLevelFieldName: | ||||||
| 				assert.Equal(t, field.Value, string(i.LogLevel)) | 				assert.Equal(t, field.Value, string(event.LogLevel)) | ||||||
| 			case namespaceFieldName: | 			case namespaceFieldName: | ||||||
| 				assert.Equal(t, field.Value, i.Namespace) | 				assert.Equal(t, field.Value, event.Jenkins.Namespace) | ||||||
| 			default: | 			default: | ||||||
| 				t.Fail() | 				t.Fail() | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, mainAttachment.Footer, footerContent) | 		assert.Equal(t, mainAttachment.Footer, footerContent) | ||||||
| 		assert.Equal(t, mainAttachment.Color, getStatusColor(i.LogLevel, Slack{})) | 		assert.Equal(t, mainAttachment.Color, slack.getStatusColor(event.LogLevel)) | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	defer server.Close() | 	defer server.Close() | ||||||
| 
 | 
 | ||||||
| 	secret := &corev1.Secret{ | 	secret := &corev1.Secret{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name: testSecretName, | 			Name:      testSecretName, | ||||||
|  | 			Namespace: testNamespace, | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		Data: map[string][]byte{ | 		Data: map[string][]byte{ | ||||||
|  | @ -79,12 +80,10 @@ func TestSlack_Send(t *testing.T) { | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err := notification.K8sClient.Create(context.TODO(), secret) | 	err := fakeClient.Create(context.TODO(), secret) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 	slack := Slack{} | 	err = slack.Send(event, v1alpha2.Notification{ | ||||||
| 
 |  | ||||||
| 	err = slack.Send(notification, v1alpha2.Notification{ |  | ||||||
| 		Slack: v1alpha2.Slack{ | 		Slack: v1alpha2.Slack{ | ||||||
| 			URLSecretKeySelector: v1alpha2.SecretKeySelector{ | 			URLSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
| 				LocalObjectReference: corev1.LocalObjectReference{ | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
		Loading…
	
		Reference in New Issue