diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 4c92e7fd..74f524c5 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -13,29 +13,7 @@ import ( "k8s.io/apimachinery/pkg/types" ) -// Mailgun is service for sending emails -type Mailgun struct { - Domain string - Recipient string - From string -} - -// Send is function for sending directly to API -func (m Mailgun) Send(n *Notification) error { - 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 := ` +const content = ` @@ -59,21 +37,40 @@ func (m Mailgun) Send(n *Notification) error {
Powered by Jenkins Operator <3
- - ` +` - content = fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) +// Mailgun is service for sending emails +type Mailgun struct{} - msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) - msg.SetHtml(content) +// Send is function for sending directly to API +func (m Mailgun) Send(n *Notification, config v1alpha2.Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} + i := n.Information + + selector = config.Mailgun.APIKeySecretKeySelector + + 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 + } + + secretValue := string(secret.Data[selector.Name]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) + } + + 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)) + + msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient) + msg.SetHtml(htmlMessage) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, _, err = mg.Send(ctx, msg) - if err != nil { - return err - } - - return nil + return err } diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 596664be..7ebea378 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -15,9 +15,7 @@ import ( ) // Teams is Microsoft Teams Service -type Teams struct { - apiURL string -} +type Teams struct{} // TeamsMessage is representation of json message structure type TeamsMessage struct { @@ -41,12 +39,18 @@ type TeamsFact struct { } // Send is function for sending directly to API -func (t Teams) Send(n *Notification) error { +func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} - i := n.Information + selector = config.Teams.URLSecretKeySelector + + 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)) + } + msg, err := json.Marshal(TeamsMessage{ Type: "MessageCard", Context: "https://schema.org/extensions", @@ -77,20 +81,16 @@ func (t Teams) Send(n *Notification) error { }, }) - 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] + secretValue := string(secret.Data[selector.Key]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { return err } - request, err := http.NewRequest("POST", t.apiURL, bytes.NewBuffer(msg)) + request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) if err != nil { return err } diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index cda82db2..2205eff2 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -1,16 +1,26 @@ package notifier 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) { - i := &Information{ + fakeClient := fake.NewFakeClient() + testURLSelectorKeyName := "test-url-selector" + testSecretName := "test-secret" + + i := Information{ ConfigurationType: testConfigurationType, CrName: testCrName, Message: testMessage, @@ -20,6 +30,7 @@ func TestTeams_Send(t *testing.T) { } notification := &Notification{ + K8sClient: fakeClient, Information: i, } @@ -49,12 +60,39 @@ func TestTeams_Send(t *testing.T) { assert.Equal(t, fact.Value, i.Message) case loggingLevelFieldName: assert.Equal(t, fact.Value, string(i.LogLevel)) + case namespaceFieldName: + assert.Equal(t, fact.Value, i.Namespace) + default: + t.Fail() } } })) - teams := Teams{apiURL: server.URL} + teams := Teams{} defer server.Close() - assert.NoError(t, teams.Send(notification)) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + }, + + Data: map[string][]byte{ + testURLSelectorKeyName: []byte(server.URL), + }, + } + + err := notification.K8sClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + assert.NoError(t, teams.Send(notification, v1alpha2.Notification{ + Teams: v1alpha2.Teams{ + URLSecretKeySelector: v1alpha2.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: testSecretName, + }, + Key: testURLSelectorKeyName, + }, + }, + })) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index a8b802aa..81e70ca5 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -56,48 +56,41 @@ type Information struct { // Notification contains message which will be sent type Notification struct { - Jenkins *v1alpha2.Jenkins + Jenkins v1alpha2.Jenkins K8sClient k8sclient.Client Logger logr.Logger - Information *Information + Information Information } // Service is skeleton for additional services type service interface { - Send(i *Notification) error + Send(i *Notification, config v1alpha2.Notification) error } // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { - <-notification for n := range notification { - if len(n.Jenkins.Spec.Notification) > 0 { - for _, endpoint := range n.Jenkins.Spec.Notification { - var err error - var svc service + notificationConfig := n.Jenkins.Spec.Notification + var err error + var svc service - if endpoint.Slack != (v1alpha2.Slack{}) { - svc = Slack{} - } else if endpoint.Teams != (v1alpha2.Teams{}) { - svc = Teams{} - } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { - svc = Mailgun{ - Domain: endpoint.Mailgun.Domain, - Recipient: endpoint.Mailgun.Recipient, - From: endpoint.Mailgun.From, - } - } else { - n.Logger.V(log.VWarn).Info("Notification service not found or not defined") - } + 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) + 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") - } - } + 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") } } } @@ -136,15 +129,15 @@ func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { } } -func notify(svc service, n *Notification) error { +func notify(svc service, n *Notification, nc v1alpha2.Notification) error { var err error switch s := svc.(type) { case Slack: - err = s.Send(n) + err = s.Send(n, nc) case Teams: - err = s.Send(n) + err = s.Send(n, nc) case Mailgun: - err = s.Send(n) + err = s.Send(n, nc) } return err diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 1222d80d..0a009514 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -15,9 +15,7 @@ import ( ) // Slack is messaging service -type Slack struct { - apiURL string -} +type Slack struct{} // SlackMessage is representation of json message type SlackMessage struct { @@ -44,19 +42,16 @@ type SlackField struct { } // Send is function for sending directly to API -func (s Slack) Send(n *Notification) error { +func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { var selector v1alpha2.SecretKeySelector 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 { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - } + selector = config.Slack.URLSecretKeySelector - s.apiURL = secret.StringData[selector.Name] + 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)) } slackMessage, err := json.Marshal(SlackMessage{ @@ -97,11 +92,16 @@ func (s Slack) Send(n *Notification) error { }, }) + secretValue := string(secret.Data[selector.Key]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) + } + if err != nil { return err } - request, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(slackMessage)) + request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage)) if err != nil { return err } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index b1776bd6..06da7aae 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -1,16 +1,26 @@ package notifier 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) { - i := &Information{ + fakeClient := fake.NewFakeClient() + testURLSelectorKeyName := "test-url-selector" + testSecretName := "test-secret" + + i := Information{ ConfigurationType: testConfigurationType, CrName: testCrName, Message: testMessage, @@ -20,6 +30,7 @@ func TestSlack_Send(t *testing.T) { } notification := &Notification{ + K8sClient: fakeClient, Information: i, } @@ -35,7 +46,6 @@ func TestSlack_Send(t *testing.T) { mainAttachment := message.Attachments[0] assert.Equal(t, mainAttachment.Text, titleText) - for _, field := range mainAttachment.Fields { switch field.Title { case configurationTypeFieldName: @@ -46,6 +56,10 @@ func TestSlack_Send(t *testing.T) { assert.Equal(t, field.Value, i.Message) case loggingLevelFieldName: assert.Equal(t, field.Value, string(i.LogLevel)) + case namespaceFieldName: + assert.Equal(t, field.Value, i.Namespace) + default: + t.Fail() } } @@ -55,7 +69,29 @@ func TestSlack_Send(t *testing.T) { defer server.Close() - slack := Slack{apiURL: server.URL} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + }, - assert.NoError(t, slack.Send(notification)) + Data: map[string][]byte{ + testURLSelectorKeyName: []byte(server.URL), + }, + } + + err := notification.K8sClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + slack := Slack{} + + assert.NoError(t, slack.Send(notification, v1alpha2.Notification{ + Slack: v1alpha2.Slack{ + URLSecretKeySelector: v1alpha2.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: testSecretName, + }, + Key: testURLSelectorKeyName, + }, + }, + })) }