Improve notification mechanism
This commit is contained in:
		
							parent
							
								
									61d5311ac2
								
							
						
					
					
						commit
						75f95be65a
					
				|  | @ -3,8 +3,14 @@ package notifier | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/mailgun/mailgun-go/v3" |  | ||||||
| 	"time" | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mailgun/mailgun-go/v3" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Mailgun is service for sending emails
 | // Mailgun is service for sending emails
 | ||||||
|  | @ -15,8 +21,19 @@ type Mailgun struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Send is function for sending directly to API
 | // Send is function for sending directly to API
 | ||||||
| func (m Mailgun) Send(secret string, i *Information) error { | func (m Mailgun) Send(n *Notification) error { | ||||||
| 	mg := mailgun.NewMailgun(m.Domain, secret) | 	var selector v1alpha2.SecretKeySelector | ||||||
|  | 	secret := &corev1.Secret{} | ||||||
|  | 
 | ||||||
|  | 	i := n.Information | ||||||
|  | 
 | ||||||
|  | 	err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mg := mailgun.NewMailgun(m.Domain, secret.StringData[selector.Name]) | ||||||
| 
 | 
 | ||||||
| 	content := ` | 	content := ` | ||||||
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | ||||||
|  | @ -45,14 +62,14 @@ func (m Mailgun) Send(secret string, i *Information) error { | ||||||
| </html> | </html> | ||||||
| 	` | 	` | ||||||
| 
 | 
 | ||||||
| 	content = fmt.Sprintf(content, getStatusColor(i.Status, m), i.CrName, i.ConfigurationType, getStatusColor(i.Status, m), getStatusName(i.Status)) | 	content = fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) | ||||||
| 
 | 
 | ||||||
| 	msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) | 	msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) | ||||||
| 	msg.SetHtml(content) | 	msg.SetHtml(content) | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
| 
 | 
 | ||||||
| 	_, _, err := mg.Send(ctx, msg) | 	_, _, err = mg.Send(ctx, msg) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  |  | ||||||
|  | @ -2,12 +2,22 @@ package notifier | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
|  | 
 | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Teams is Microsoft Teams Service
 | // Teams is Microsoft Teams Service
 | ||||||
| type Teams struct{} | type Teams struct { | ||||||
|  | 	apiURL string | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // TeamsMessage is representation of json message structure
 | // TeamsMessage is representation of json message structure
 | ||||||
| type TeamsMessage struct { | type TeamsMessage struct { | ||||||
|  | @ -31,20 +41,16 @@ type TeamsFact struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Send is function for sending directly to API
 | // Send is function for sending directly to API
 | ||||||
| func (t Teams) Send(secret string, i *Information) error { | func (t Teams) Send(n *Notification) error { | ||||||
| 	err := i.Error | 	var selector v1alpha2.SecretKeySelector | ||||||
| 	var errMessage string | 	secret := &corev1.Secret{} | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	i := n.Information | ||||||
| 		errMessage = err.Error() |  | ||||||
| 	} else { |  | ||||||
| 		errMessage = noErrorMessage |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	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.Status, t), | 		ThemeColor: getStatusColor(i.LogLevel, t), | ||||||
| 		Title:      titleText, | 		Title:      titleText, | ||||||
| 		Sections: []TeamsSection{ | 		Sections: []TeamsSection{ | ||||||
| 			{ | 			{ | ||||||
|  | @ -58,20 +64,33 @@ func (t Teams) Send(secret string, i *Information) error { | ||||||
| 						Value: i.ConfigurationType, | 						Value: i.ConfigurationType, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Name:  statusFieldName, | 						Name:  loggingLevelFieldName, | ||||||
| 						Value: getStatusName(i.Status), | 						Value: string(i.LogLevel), | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Name:  namespaceFieldName, | ||||||
|  | 						Value: i.Namespace, | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 				Text: errMessage, | 				Text: i.Message, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
|  | 	if t.apiURL == "" { | ||||||
|  | 		err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | ||||||
|  | 		if err != nil { | ||||||
|  | 			n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		t.apiURL = secret.StringData[selector.Name] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	request, err := http.NewRequest("POST", secret, bytes.NewBuffer(msg)) | 	request, err := http.NewRequest("POST", t.apiURL, bytes.NewBuffer(msg)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -81,10 +100,7 @@ func (t Teams) Send(secret string, i *Information) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = resp.Body.Close() | 	defer func() { _ = resp.Body.Close() }() | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,20 +2,25 @@ package notifier | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestTeams_Send(t *testing.T) { | func TestTeams_Send(t *testing.T) { | ||||||
| 	teams := Teams{} |  | ||||||
| 
 |  | ||||||
| 	i := &Information{ | 	i := &Information{ | ||||||
| 		ConfigurationType: testConfigurationType, | 		ConfigurationType: testConfigurationType, | ||||||
| 		CrName:            testCrName, | 		CrName:            testCrName, | ||||||
| 		Status:            testStatus, | 		Message:           testMessage, | ||||||
| 		Error:             testError, | 		MessageVerbose:    testMessageVerbose, | ||||||
|  | 		Namespace:         testNamespace, | ||||||
|  | 		LogLevel:          testLoggingLevel, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notification := &Notification{ | ||||||
|  | 		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) { | ||||||
|  | @ -28,32 +33,28 @@ func TestTeams_Send(t *testing.T) { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, message.Title, titleText) | 		assert.Equal(t, message.Title, titleText) | ||||||
| 		assert.Equal(t, message.ThemeColor, getStatusColor(i.Status, teams)) | 		assert.Equal(t, message.ThemeColor, getStatusColor(i.LogLevel, Teams{})) | ||||||
| 
 | 
 | ||||||
| 		mainSection := message.Sections[0] | 		mainSection := message.Sections[0] | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, mainSection.Text, noErrorMessage) | 		assert.Equal(t, mainSection.Text, i.Message) | ||||||
| 
 | 
 | ||||||
| 		for _, fact := range mainSection.Facts { | 		for _, fact := range mainSection.Facts { | ||||||
| 			switch fact.Name { | 			switch fact.Name { | ||||||
| 			case configurationTypeFieldName: | 			case configurationTypeFieldName: | ||||||
| 				if fact.Value != i.ConfigurationType { | 				assert.Equal(t, fact.Value, i.ConfigurationType) | ||||||
| 					t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) |  | ||||||
| 				} |  | ||||||
| 			case crNameFieldName: | 			case crNameFieldName: | ||||||
| 				if fact.Value != i.CrName { | 				assert.Equal(t, fact.Value, i.CrName) | ||||||
| 					t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) | 			case messageFieldName: | ||||||
| 				} | 				assert.Equal(t, fact.Value, i.Message) | ||||||
| 			case statusFieldName: | 			case loggingLevelFieldName: | ||||||
| 				if fact.Value != getStatusName(i.Status) { | 				assert.Equal(t, fact.Value, string(i.LogLevel)) | ||||||
| 					t.Fatalf("%s is not equal! Must be %s", statusFieldName, getStatusName(i.Status)) |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
|  | 	teams := Teams{apiURL: server.URL} | ||||||
|  | 
 | ||||||
| 	defer server.Close() | 	defer server.Close() | ||||||
| 	if err := teams.Send(server.URL, i); err != nil { | 	assert.NoError(t, teams.Send(notification)) | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,57 +1,57 @@ | ||||||
| package notifier | package notifier | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/go-logr/logr" | 	"github.com/go-logr/logr" | ||||||
|  | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
| 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 
 | ||||||
| 	"k8s.io/apimachinery/pkg/types" |  | ||||||
| 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | 	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 ( | var ( | ||||||
| 	testConfigurationType = "test-configuration" | 	testConfigurationType = "test-configuration" | ||||||
| 	testCrName            = "test-cr" | 	testCrName            = "test-cr" | ||||||
| 	testStatus            Status = 1 | 	testNamespace         = "test-namespace" | ||||||
| 	testError             error | 	testMessage           = "test-message" | ||||||
|  | 	testMessageVerbose    = "detail-test-message" | ||||||
|  | 	testLoggingLevel      = LogWarn | ||||||
| 
 | 
 | ||||||
| 	client = http.Client{} | 	client = http.Client{} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( |  | ||||||
| 	// StatusSuccess contains value for success state
 |  | ||||||
| 	StatusSuccess = 0 |  | ||||||
| 
 |  | ||||||
| 	// StatusError contains value for error state
 |  | ||||||
| 	StatusError = 1 |  | ||||||
| 
 |  | ||||||
| 	noErrorMessage = "No errors has found." |  | ||||||
| 
 |  | ||||||
| 	titleText                  = "Operator reconciled." |  | ||||||
| 	statusMessageFieldName     = "Status message" |  | ||||||
| 	statusFieldName            = "Status" |  | ||||||
| 	crNameFieldName            = "CR Name" |  | ||||||
| 	configurationTypeFieldName = "Configuration Type" |  | ||||||
| 	footerContent              = "Powered by Jenkins Operator <3" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Status represents the state of operator
 |  | ||||||
| type Status int |  | ||||||
| 
 |  | ||||||
| // StatusColor is useful for better UX
 | // StatusColor is useful for better UX
 | ||||||
| type StatusColor string | type StatusColor string | ||||||
| 
 | 
 | ||||||
|  | // LoggingLevel is type for selecting different logging levels
 | ||||||
|  | type LoggingLevel string | ||||||
|  | 
 | ||||||
| // Information represents details about operator status
 | // Information represents details about operator status
 | ||||||
| type Information struct { | type Information struct { | ||||||
| 	ConfigurationType string | 	ConfigurationType string | ||||||
|  | 	Namespace         string | ||||||
| 	CrName            string | 	CrName            string | ||||||
| 	Status            Status | 	LogLevel          LoggingLevel | ||||||
| 	Error             error | 	Message           string | ||||||
|  | 	MessageVerbose    string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Notification contains message which will be sent
 | // Notification contains message which will be sent
 | ||||||
|  | @ -59,102 +59,74 @@ type Notification struct { | ||||||
| 	Jenkins     *v1alpha2.Jenkins | 	Jenkins     *v1alpha2.Jenkins | ||||||
| 	K8sClient   k8sclient.Client | 	K8sClient   k8sclient.Client | ||||||
| 	Logger      logr.Logger | 	Logger      logr.Logger | ||||||
| 
 |  | ||||||
| 	// Recipient is mobile number or email address
 |  | ||||||
| 	// It's not used in Slack or Microsoft Teams
 |  | ||||||
| 	Recipient string |  | ||||||
| 
 |  | ||||||
| 	Information *Information | 	Information *Information | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Service is skeleton for additional services
 | // Service is skeleton for additional services
 | ||||||
| type Service interface { | type service interface { | ||||||
| 	Send(secret string, i *Information) error | 	Send(i *Notification) error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Listen is goroutine that listens for incoming messages and sends it
 | // Listen is goroutine that listens for incoming messages and sends it
 | ||||||
| func Listen(notification chan *Notification) { | func Listen(notification chan *Notification) { | ||||||
| 	n := <-notification | 	<-notification | ||||||
|  | 	for n := range notification { | ||||||
| 		if len(n.Jenkins.Spec.Notification) > 0 { | 		if len(n.Jenkins.Spec.Notification) > 0 { | ||||||
| 			for _, endpoint := range n.Jenkins.Spec.Notification { | 			for _, endpoint := range n.Jenkins.Spec.Notification { | ||||||
| 				var err error | 				var err error | ||||||
| 			var service Service | 				var svc service | ||||||
| 			var selector v1alpha2.SecretKeySelector |  | ||||||
| 			secret := &corev1.Secret{} |  | ||||||
| 
 | 
 | ||||||
| 				if endpoint.Slack != (v1alpha2.Slack{}) { | 				if endpoint.Slack != (v1alpha2.Slack{}) { | ||||||
| 				n.Logger.V(log.VDebug).Info("Slack detected") | 					svc = Slack{} | ||||||
| 				service = Slack{} |  | ||||||
| 				selector = endpoint.Slack.URLSecretKeySelector |  | ||||||
| 				} else if endpoint.Teams != (v1alpha2.Teams{}) { | 				} else if endpoint.Teams != (v1alpha2.Teams{}) { | ||||||
| 				n.Logger.V(log.VDebug).Info("Microsoft Teams detected") | 					svc = Teams{} | ||||||
| 				service = Teams{} |  | ||||||
| 				selector = endpoint.Teams.URLSecretKeySelector |  | ||||||
| 				} else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { | 				} else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { | ||||||
| 				n.Logger.V(log.VDebug).Info("Mailgun detected") | 					svc = Mailgun{ | ||||||
| 				service = Mailgun{ |  | ||||||
| 						Domain:    endpoint.Mailgun.Domain, | 						Domain:    endpoint.Mailgun.Domain, | ||||||
| 						Recipient: endpoint.Mailgun.Recipient, | 						Recipient: endpoint.Mailgun.Recipient, | ||||||
| 						From:      endpoint.Mailgun.From, | 						From:      endpoint.Mailgun.From, | ||||||
| 					} | 					} | ||||||
| 				selector = endpoint.Mailgun.APIKeySecretKeySelector |  | ||||||
| 				} else { | 				} else { | ||||||
| 				n.Logger.Info("Notification service not found or not defined") | 					n.Logger.V(log.VWarn).Info("Notification service not found or not defined") | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 			err = n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | 				err = notify(svc, n) | ||||||
| 			if err != nil { |  | ||||||
| 				n.Logger.Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			n.Logger.V(log.VDebug).Info(fmt.Sprintf("Endpoint URL: %s", string(secret.Data[selector.Key]))) |  | ||||||
| 			err = notify(service, string(secret.Data[selector.Key]), n.Information) |  | ||||||
| 
 | 
 | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 				n.Logger.Info(fmt.Sprintf("Failed to send notifications. %+v", err)) | 					n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) | ||||||
| 				} else { | 				} else { | ||||||
| 				n.Logger.Info("Sent notification") | 					n.Logger.V(log.VDebug).Info("Sent notification") | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func getStatusName(status Status) string { | func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { | ||||||
| 	switch status { | 	switch svc.(type) { | ||||||
| 	case StatusSuccess: |  | ||||||
| 		return "Success" |  | ||||||
| 	case StatusError: |  | ||||||
| 		return "Error" |  | ||||||
| 	default: |  | ||||||
| 		return "Undefined" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getStatusColor(status Status, service Service) StatusColor { |  | ||||||
| 	switch service.(type) { |  | ||||||
| 	case Slack: | 	case Slack: | ||||||
| 		switch status { | 		switch logLevel { | ||||||
| 		case StatusSuccess: | 		case LogInfo: | ||||||
| 			return "good" | 			return "#439FE0" | ||||||
| 		case StatusError: | 		case LogWarn: | ||||||
| 			return "danger" | 			return "danger" | ||||||
| 		default: | 		default: | ||||||
| 			return "#c8c8c8" | 			return "#c8c8c8" | ||||||
| 		} | 		} | ||||||
| 	case Teams: | 	case Teams: | ||||||
| 		switch status { | 		switch logLevel { | ||||||
| 		case StatusSuccess: | 		case LogInfo: | ||||||
| 			return "54A254" | 			return "439FE0" | ||||||
| 		case StatusError: | 		case LogWarn: | ||||||
| 			return "E81123" | 			return "E81123" | ||||||
| 		default: | 		default: | ||||||
| 			return "C8C8C8" | 			return "C8C8C8" | ||||||
| 		} | 		} | ||||||
| 	case Mailgun: | 	case Mailgun: | ||||||
| 		switch status { | 		switch logLevel { | ||||||
| 		case StatusSuccess: | 		case LogInfo: | ||||||
| 			return "green" | 			return "blue" | ||||||
| 		case StatusError: | 		case LogWarn: | ||||||
| 			return "red" | 			return "red" | ||||||
| 		default: | 		default: | ||||||
| 			return "gray" | 			return "gray" | ||||||
|  | @ -164,15 +136,15 @@ func getStatusColor(status Status, service Service) StatusColor { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func notify(service Service, secret string, i *Information) error { | func notify(svc service, n *Notification) error { | ||||||
| 	var err error | 	var err error | ||||||
| 	switch svc := service.(type) { | 	switch s := svc.(type) { | ||||||
| 	case Slack: | 	case Slack: | ||||||
| 		err = svc.Send(secret, i) | 		err = s.Send(n) | ||||||
| 	case Teams: | 	case Teams: | ||||||
| 		err = svc.Send(secret, i) | 		err = s.Send(n) | ||||||
| 	case Mailgun: | 	case Mailgun: | ||||||
| 		err = svc.Send(secret, i) | 		err = s.Send(n) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return err | 	return err | ||||||
|  |  | ||||||
|  | @ -2,12 +2,22 @@ package notifier | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/log" | ||||||
|  | 
 | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Slack is messaging service
 | // Slack is messaging service
 | ||||||
| type Slack struct{} | type Slack struct { | ||||||
|  | 	apiURL string | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // SlackMessage is representation of json message
 | // SlackMessage is representation of json message
 | ||||||
| type SlackMessage struct { | type SlackMessage struct { | ||||||
|  | @ -34,26 +44,31 @@ type SlackField struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Send is function for sending directly to API
 | // Send is function for sending directly to API
 | ||||||
| func (s Slack) Send(secret string, i *Information) error { | func (s Slack) Send(n *Notification) error { | ||||||
| 	err := i.Error | 	var selector v1alpha2.SecretKeySelector | ||||||
| 	var errMessage string | 	secret := &corev1.Secret{} | ||||||
| 
 | 
 | ||||||
|  | 	i := n.Information | ||||||
|  | 
 | ||||||
|  | 	if s.apiURL == "" { | ||||||
|  | 		err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 		errMessage = err.Error() | 			n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) | ||||||
| 	} else { | 		} | ||||||
| 		errMessage = noErrorMessage | 
 | ||||||
|  | 		s.apiURL = secret.StringData[selector.Name] | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	slackMessage, err := json.Marshal(SlackMessage{ | 	slackMessage, err := json.Marshal(SlackMessage{ | ||||||
| 		Attachments: []SlackAttachment{ | 		Attachments: []SlackAttachment{ | ||||||
| 			{ | 			{ | ||||||
| 				Fallback: "", | 				Fallback: "", | ||||||
| 				Color:    getStatusColor(i.Status, s), | 				Color:    getStatusColor(i.LogLevel, s), | ||||||
| 				Text:     titleText, | 				Text:     titleText, | ||||||
| 				Fields: []SlackField{ | 				Fields: []SlackField{ | ||||||
| 					{ | 					{ | ||||||
| 						Title: statusMessageFieldName, | 						Title: messageFieldName, | ||||||
| 						Value: errMessage, | 						Value: i.Message, | ||||||
| 						Short: false, | 						Short: false, | ||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
|  | @ -66,6 +81,16 @@ func (s Slack) Send(secret string, i *Information) error { | ||||||
| 						Value: i.ConfigurationType, | 						Value: i.ConfigurationType, | ||||||
| 						Short: true, | 						Short: true, | ||||||
| 					}, | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Title: loggingLevelFieldName, | ||||||
|  | 						Value: string(i.LogLevel), | ||||||
|  | 						Short: true, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Title: namespaceFieldName, | ||||||
|  | 						Value: i.Namespace, | ||||||
|  | 						Short: true, | ||||||
|  | 					}, | ||||||
| 				}, | 				}, | ||||||
| 				Footer: footerContent, | 				Footer: footerContent, | ||||||
| 			}, | 			}, | ||||||
|  | @ -76,7 +101,7 @@ func (s Slack) Send(secret string, i *Information) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	request, err := http.NewRequest("POST", secret, bytes.NewBuffer(slackMessage)) | 	request, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(slackMessage)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -86,10 +111,6 @@ func (s Slack) Send(secret string, i *Information) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = resp.Body.Close() | 	defer func() { _ = resp.Body.Close() }() | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,20 +2,25 @@ package notifier | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestSlack_Send(t *testing.T) { | func TestSlack_Send(t *testing.T) { | ||||||
| 	slack := Slack{} |  | ||||||
| 
 |  | ||||||
| 	i := &Information{ | 	i := &Information{ | ||||||
| 		ConfigurationType: testConfigurationType, | 		ConfigurationType: testConfigurationType, | ||||||
| 		CrName:            testCrName, | 		CrName:            testCrName, | ||||||
| 		Status:            testStatus, | 		Message:           testMessage, | ||||||
| 		Error:             testError, | 		MessageVerbose:    testMessageVerbose, | ||||||
|  | 		Namespace:         testNamespace, | ||||||
|  | 		LogLevel:          testLoggingLevel, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	notification := &Notification{ | ||||||
|  | 		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) { | ||||||
|  | @ -34,27 +39,23 @@ 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: | ||||||
| 				if field.Value != i.ConfigurationType { | 				assert.Equal(t, field.Value, i.ConfigurationType) | ||||||
| 					t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) |  | ||||||
| 				} |  | ||||||
| 			case crNameFieldName: | 			case crNameFieldName: | ||||||
| 				if field.Value != i.CrName { | 				assert.Equal(t, field.Value, i.CrName) | ||||||
| 					t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) | 			case messageFieldName: | ||||||
| 				} | 				assert.Equal(t, field.Value, i.Message) | ||||||
| 			case statusMessageFieldName: | 			case loggingLevelFieldName: | ||||||
| 				if field.Value != noErrorMessage { | 				assert.Equal(t, field.Value, string(i.LogLevel)) | ||||||
| 					t.Fatalf("Error thrown but not expected") |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		assert.Equal(t, mainAttachment.Footer, footerContent) | 		assert.Equal(t, mainAttachment.Footer, footerContent) | ||||||
| 		assert.Equal(t, mainAttachment.Color, getStatusColor(i.Status, slack)) | 		assert.Equal(t, mainAttachment.Color, getStatusColor(i.LogLevel, Slack{})) | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	defer server.Close() | 	defer server.Close() | ||||||
| 
 | 
 | ||||||
| 	if err := slack.Send(server.URL, i); err != nil { | 	slack := Slack{apiURL: server.URL} | ||||||
| 		t.Fatal(err) | 
 | ||||||
| 	} | 	assert.NoError(t, slack.Send(notification)) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue