From dfd0b54579f3c3a7ca5dc1d729b3c4611d5a9e1d Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 09:50:40 +0200 Subject: [PATCH 1/7] Add SMTP notification provider --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 11 ++ .../jenkins/v1alpha2/zz_generated.deepcopy.go | 23 +++ .../jenkins/notifications/sender.go | 2 + pkg/controller/jenkins/notifications/smtp.go | 93 +++++++++++ .../jenkins/notifications/smtp_test.go | 152 ++++++++++++++++++ 5 files changed, 281 insertions(+) create mode 100644 pkg/controller/jenkins/notifications/smtp.go create mode 100644 pkg/controller/jenkins/notifications/smtp_test.go diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 0ae0c136..5b829be4 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -73,6 +73,7 @@ type Notification struct { Slack *Slack `json:"slack,omitempty"` Teams *MicrosoftTeams `json:"teams,omitempty"` Mailgun *Mailgun `json:"mailgun,omitempty"` + SMTP *SMTP `json:"smtp,omitempty"` } // Slack is handler for Slack notification channel @@ -81,6 +82,16 @@ type Slack struct { WebHookURLSecretKeySelector SecretKeySelector `json:"webHookURLSecretKeySelector"` } +type SMTP struct { + UsernameSecretKeySelector SecretKeySelector `json:"usernameSecretKeySelector"` + PasswordSecretKeySelector SecretKeySelector `json:"passwordSecretKeySelector"` + Port int `json:"port"` + Server string `json:"server"` + TLSInsecureSkipVerify bool `json:"tlsInsecureSkipVerify,omitempty"` + From string `json:"from"` + To string `json:"to"` +} + // MicrosoftTeams is handler for Microsoft MicrosoftTeams notification channel type MicrosoftTeams struct { // The web hook URL to MicrosoftTeams App diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 88c845c6..0bb8fec6 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -458,6 +458,11 @@ func (in *Notification) DeepCopyInto(out *Notification) { *out = new(Mailgun) **out = **in } + if in.SMTP != nil { + in, out := &in.SMTP, &out.SMTP + *out = new(SMTP) + **out = **in + } return } @@ -504,6 +509,24 @@ func (in *Restore) DeepCopy() *Restore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SMTP) DeepCopyInto(out *SMTP) { + *out = *in + out.UsernameSecretKeySelector = in.UsernameSecretKeySelector + out.PasswordSecretKeySelector = in.PasswordSecretKeySelector + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SMTP. +func (in *SMTP) DeepCopy() *SMTP { + if in == nil { + return nil + } + out := new(SMTP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { *out = *in diff --git a/pkg/controller/jenkins/notifications/sender.go b/pkg/controller/jenkins/notifications/sender.go index fad1219f..d345979b 100644 --- a/pkg/controller/jenkins/notifications/sender.go +++ b/pkg/controller/jenkins/notifications/sender.go @@ -81,6 +81,8 @@ func Listen(events chan Event, k8sEvent event.Recorder, k8sClient k8sclient.Clie svc = Teams{k8sClient: k8sClient} } else if notificationConfig.Mailgun != nil { svc = MailGun{k8sClient: k8sClient} + } else if notificationConfig.SMTP != nil { + svc = SMTP{k8sClient: k8sClient} } else { logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig)) continue diff --git a/pkg/controller/jenkins/notifications/smtp.go b/pkg/controller/jenkins/notifications/smtp.go new file mode 100644 index 00000000..8b8b0f28 --- /dev/null +++ b/pkg/controller/jenkins/notifications/smtp.go @@ -0,0 +1,93 @@ +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" +) + +type SMTP struct { + k8sClient k8sclient.Client +} + +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 + "" + 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 { + fmt.Print(err) + 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" + } +} \ No newline at end of file diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go new file mode 100644 index 00000000..0bfaf7af --- /dev/null +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -0,0 +1,152 @@ +package notifications + +import ( + "context" + "errors" + "fmt" + "github.com/emersion/go-smtp" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/stretchr/testify/assert" + "io" + "io/ioutil" + "log" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testSMTPUsername = "username" + testSMTPPassword = "password" + + testSMTPPort = 1025 +) + +type testServer struct{} + +// Login handles a login command with username and password. +func (bkd *testServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { + if username != testSMTPUsername || password != testSMTPPassword { + return nil, errors.New("invalid username or password") + } + return &testSession{}, nil +} + +// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails +func (bkd *testServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { + return nil, smtp.ErrAuthRequired +} + +// A Session is returned after successful login. +type testSession struct{} + +func (s *testSession) Mail(from string) error { + log.Println("Mail from:", from) + return nil +} + +func (s *testSession) Rcpt(to string) error { + log.Println("Rcpt to:", to) + return nil +} + +func (s *testSession) Data(r io.Reader) error { + if b, err := ioutil.ReadAll(r); err != nil { + return err + } else { + log.Println("Data:", string(b)) + } + return nil +} + +func (s *testSession) Reset() {} + +func (s *testSession) Logout() error { + return nil +} + + +func TestSMTP_Send(t *testing.T) { + fakeClient := fake.NewFakeClient() + testUsernameSelectorKeyName := "test-username-selector" + testPasswordSelectorKeyName := "test-password-selector" + testSecretName := "test-secret" + + event := Event{ + Jenkins: v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: testCrName, + Namespace: testNamespace, + }, + }, + Phase: testPhase, + Message: testMessage, + MessagesVerbose: testMessageVerbose, + LogLevel: testLoggingLevel, + } + + smtpClient := SMTP{k8sClient: fakeClient} + + ts := &testServer{} + + // Create fake SMTP server + + s := smtp.NewServer(ts) + + s.Addr = fmt.Sprintf(":%d", testSMTPPort) + s.Domain = "localhost" + s.ReadTimeout = 10 * time.Second + s.WriteTimeout = 10 * time.Second + s.MaxMessageBytes = 1024 * 1024 + s.MaxRecipients = 50 + s.AllowInsecureAuth = true + + // Create secrets + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + Namespace: testNamespace, + }, + + Data: map[string][]byte{ + testUsernameSelectorKeyName: []byte(testSMTPUsername), + testPasswordSelectorKeyName: []byte(testSMTPPassword), + }, + } + + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + go func() { + if err := s.ListenAndServe(); err != nil { + log.Fatal(err) + } + }() + + err = smtpClient.Send(event, v1alpha2.Notification{ + SMTP: &v1alpha2.SMTP{ + Server: "localhost", + From: "test@localhost", + To: "test@localhost", + 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) +} From 292ae036db2470c2f40d0f7951766fe8e4fd003a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 09:55:23 +0200 Subject: [PATCH 2/7] Add SMTP libraries --- go.mod | 3 +++ go.sum | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/go.mod b/go.mod index 22aa3627..77fbd4c3 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/docker/distribution v2.7.1+incompatible github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect + github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e // indirect + github.com/emersion/go-smtp v0.11.2 github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.1 github.com/go-openapi/spec v0.19.0 @@ -24,6 +26,7 @@ require ( golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b // indirect golang.org/x/text v0.3.2 // indirect golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df k8s.io/api v0.0.0-20190612125737-db0771252981 k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad k8s.io/client-go v11.0.0+incompatible diff --git a/go.sum b/go.sum index 2d411a76..bbb5471b 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,11 @@ github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkg github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emersion/go-sasl v0.0.0-20190704090222-36b50694675c/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= +github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-smtp v0.11.2 h1:5PO2Kwsx+HXuytntCfMvcworC/iq45TPGkwjnaBZFSg= +github.com/emersion/go-smtp v0.11.2/go.mod h1:byi9Y32SuKwjTJt9DO2tTWYjtF3lEh154tE1AcaJQSY= github.com/emicklei/go-restful v2.8.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.8.1+incompatible h1:AyDqLHbJ1quqbWr/OWDw+PlIP8ZFoTmYrGYaxzrLbNg= github.com/emicklei/go-restful v2.8.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -570,6 +575,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= From 4f5391d708015722f3e1a08b682895ad9c402575 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 11:48:07 +0200 Subject: [PATCH 3/7] Improve SMTP provider --- pkg/controller/jenkins/notifications/smtp.go | 1 - .../jenkins/notifications/smtp_test.go | 49 ++++++++++++------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pkg/controller/jenkins/notifications/smtp.go b/pkg/controller/jenkins/notifications/smtp.go index 8b8b0f28..ed06045f 100644 --- a/pkg/controller/jenkins/notifications/smtp.go +++ b/pkg/controller/jenkins/notifications/smtp.go @@ -74,7 +74,6 @@ func (s SMTP) Send(event Event, config v1alpha2.Notification) error { message.SetBody("text/html", htmlMessage) if err := mailer.DialAndSend(message); err != nil { - fmt.Print(err) return err } diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go index 0bfaf7af..55e9492c 100644 --- a/pkg/controller/jenkins/notifications/smtp_test.go +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -5,17 +5,20 @@ import ( "errors" "fmt" "github.com/emersion/go-smtp" - "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/stretchr/testify/assert" "io" "io/ioutil" "log" - "sigs.k8s.io/controller-runtime/pkg/client/fake" + "mime/quotedprintable" + "regexp" "testing" "time" + "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" ) const ( @@ -25,6 +28,19 @@ const ( testSMTPPort = 1025 ) +var smtpEvent = Event{ + Jenkins: v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: testCrName, + Namespace: testNamespace, + }, + }, + Phase: testPhase, + Message: testMessage, + MessagesVerbose: testMessageVerbose, + LogLevel: testLoggingLevel, +} + type testServer struct{} // Login handles a login command with username and password. @@ -54,10 +70,18 @@ func (s *testSession) Rcpt(to string) error { } func (s *testSession) Data(r io.Reader) error { - if b, err := ioutil.ReadAll(r); err != nil { + re := regexp.MustCompile(`\t+\n\t+(.*):\n\t+(.*)\n\t+`) + + if b, err := ioutil.ReadAll(quotedprintable.NewReader(r)); err != nil { return err } else { - log.Println("Data:", string(b)) + res := re.FindAllStringSubmatch(string(b), -1) + + if smtpEvent.Jenkins.Name == res[0][1] { + return errors.New(fmt.Sprintf("jenkins CR not identical: %s, expected: %s", res[0][1], smtpEvent.Jenkins.Name)) + } else if string(smtpEvent.Phase) == res[1][1] { + return errors.New(fmt.Sprintf("phase not identical: %s, expected: %s", res[1][1], smtpEvent.Phase)) + } } return nil } @@ -75,19 +99,6 @@ func TestSMTP_Send(t *testing.T) { testPasswordSelectorKeyName := "test-password-selector" testSecretName := "test-secret" - event := Event{ - Jenkins: v1alpha2.Jenkins{ - ObjectMeta: metav1.ObjectMeta{ - Name: testCrName, - Namespace: testNamespace, - }, - }, - Phase: testPhase, - Message: testMessage, - MessagesVerbose: testMessageVerbose, - LogLevel: testLoggingLevel, - } - smtpClient := SMTP{k8sClient: fakeClient} ts := &testServer{} @@ -126,7 +137,7 @@ func TestSMTP_Send(t *testing.T) { } }() - err = smtpClient.Send(event, v1alpha2.Notification{ + err = smtpClient.Send(smtpEvent, v1alpha2.Notification{ SMTP: &v1alpha2.SMTP{ Server: "localhost", From: "test@localhost", From a5f4c7834691af19815fcb492625c56a4b7db0e3 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 13:21:54 +0200 Subject: [PATCH 4/7] Improve tests and SMTP notification provider --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 13 +++++---- .../configuration/base/validate_test.go | 2 +- .../jenkins/notifications/sender.go | 1 - pkg/controller/jenkins/notifications/smtp.go | 4 ++- .../jenkins/notifications/smtp_test.go | 29 ++++++++++--------- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 5b829be4..47489ac4 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -73,7 +73,7 @@ type Notification struct { Slack *Slack `json:"slack,omitempty"` Teams *MicrosoftTeams `json:"teams,omitempty"` Mailgun *Mailgun `json:"mailgun,omitempty"` - SMTP *SMTP `json:"smtp,omitempty"` + SMTP *SMTP `json:"smtp,omitempty"` } // Slack is handler for Slack notification channel @@ -82,14 +82,15 @@ type Slack struct { WebHookURLSecretKeySelector SecretKeySelector `json:"webHookURLSecretKeySelector"` } +// SMTP is handler for sending emails via this protocol type SMTP struct { UsernameSecretKeySelector SecretKeySelector `json:"usernameSecretKeySelector"` PasswordSecretKeySelector SecretKeySelector `json:"passwordSecretKeySelector"` - Port int `json:"port"` - Server string `json:"server"` - TLSInsecureSkipVerify bool `json:"tlsInsecureSkipVerify,omitempty"` - From string `json:"from"` - To string `json:"to"` + Port int `json:"port"` + Server string `json:"server"` + TLSInsecureSkipVerify bool `json:"tlsInsecureSkipVerify,omitempty"` + From string `json:"from"` + To string `json:"to"` } // MicrosoftTeams is handler for Microsoft MicrosoftTeams notification channel diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 97d21a5a..51c1206f 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -83,7 +83,7 @@ func TestValidatePlugins(t *testing.T) { var userPlugins []v1alpha2.Plugin got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) - + assert.Equal(t, got, []string{"invalid plugin version 'simple-plugin:invalid', must follow pattern '^[0-9\\\\.-]+$'"}) }) t.Run("valid user and base plugin version", func(t *testing.T) { diff --git a/pkg/controller/jenkins/notifications/sender.go b/pkg/controller/jenkins/notifications/sender.go index d345979b..7b299c25 100644 --- a/pkg/controller/jenkins/notifications/sender.go +++ b/pkg/controller/jenkins/notifications/sender.go @@ -20,7 +20,6 @@ const ( crNameFieldName = "CR Name" phaseFieldName = "Phase" namespaceFieldName = "Namespace" - footerContent = "Powered by Jenkins Operator" ) const ( diff --git a/pkg/controller/jenkins/notifications/smtp.go b/pkg/controller/jenkins/notifications/smtp.go index ed06045f..02f78c3b 100644 --- a/pkg/controller/jenkins/notifications/smtp.go +++ b/pkg/controller/jenkins/notifications/smtp.go @@ -18,10 +18,12 @@ 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{} @@ -89,4 +91,4 @@ func (s SMTP) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor default: return "gray" } -} \ No newline at end of file +} diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go index 55e9492c..a46c586a 100644 --- a/pkg/controller/jenkins/notifications/smtp_test.go +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -72,17 +72,19 @@ func (s *testSession) Rcpt(to string) error { func (s *testSession) Data(r io.Reader) error { re := regexp.MustCompile(`\t+\n\t+(.*):\n\t+(.*)\n\t+`) - if b, err := ioutil.ReadAll(quotedprintable.NewReader(r)); err != nil { + b, err := ioutil.ReadAll(quotedprintable.NewReader(r)) + if err != nil { return err - } else { - res := re.FindAllStringSubmatch(string(b), -1) - - if smtpEvent.Jenkins.Name == res[0][1] { - return errors.New(fmt.Sprintf("jenkins CR not identical: %s, expected: %s", res[0][1], smtpEvent.Jenkins.Name)) - } else if string(smtpEvent.Phase) == res[1][1] { - return errors.New(fmt.Sprintf("phase not identical: %s, expected: %s", res[1][1], smtpEvent.Phase)) - } } + + res := re.FindAllStringSubmatch(string(b), -1) + + if smtpEvent.Jenkins.Name == res[0][1] { + return fmt.Errorf("jenkins CR not identical: %s, expected: %s", res[0][1], smtpEvent.Jenkins.Name) + } else if string(smtpEvent.Phase) == res[1][1] { + return fmt.Errorf("phase not identical: %s, expected: %s", res[1][1], smtpEvent.Phase) + } + return nil } @@ -92,7 +94,6 @@ func (s *testSession) Logout() error { return nil } - func TestSMTP_Send(t *testing.T) { fakeClient := fake.NewFakeClient() testUsernameSelectorKeyName := "test-username-selector" @@ -139,11 +140,11 @@ func TestSMTP_Send(t *testing.T) { err = smtpClient.Send(smtpEvent, v1alpha2.Notification{ SMTP: &v1alpha2.SMTP{ - Server: "localhost", - From: "test@localhost", - To: "test@localhost", + Server: "localhost", + From: "test@localhost", + To: "test@localhost", TLSInsecureSkipVerify: true, - Port: testSMTPPort, + Port: testSMTPPort, UsernameSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: testSecretName, From 78a44239e94d137503a84f6394e8b36e27390a40 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 13:49:59 +0200 Subject: [PATCH 5/7] Add sleep in smtp tests --- pkg/controller/jenkins/notifications/smtp_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go index a46c586a..61e2f750 100644 --- a/pkg/controller/jenkins/notifications/smtp_test.go +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -138,6 +138,8 @@ func TestSMTP_Send(t *testing.T) { } }() + time.Sleep(time.Second * 5) + err = smtpClient.Send(smtpEvent, v1alpha2.Notification{ SMTP: &v1alpha2.SMTP{ Server: "localhost", From 73e35fd048aa05d2ee7a53da7cea566c35000221 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 14:40:00 +0200 Subject: [PATCH 6/7] Remove sleep from tests --- pkg/controller/jenkins/notifications/smtp_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go index 61e2f750..cc402dab 100644 --- a/pkg/controller/jenkins/notifications/smtp_test.go +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "mime/quotedprintable" + "net" "regexp" "testing" "time" @@ -77,6 +78,8 @@ func (s *testSession) Data(r io.Reader) error { return err } + fmt.Println(string(b)) + res := re.FindAllStringSubmatch(string(b), -1) if smtpEvent.Jenkins.Name == res[0][1] { @@ -132,13 +135,13 @@ func TestSMTP_Send(t *testing.T) { err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) - go func() { - if err := s.ListenAndServe(); err != nil { - log.Fatal(err) - } - }() + l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", testSMTPPort)) + assert.NoError(t, err) - time.Sleep(time.Second * 5) + go func() { + err := s.Serve(l) + assert.NoError(t, err) + }() err = smtpClient.Send(smtpEvent, v1alpha2.Notification{ SMTP: &v1alpha2.SMTP{ From a198af49a09b90cea1783c8cae0247e96ad9fc16 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 17 Oct 2019 15:06:31 +0200 Subject: [PATCH 7/7] Improve SMTP tests --- .../jenkins/notifications/smtp_test.go | 86 ++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/pkg/controller/jenkins/notifications/smtp_test.go b/pkg/controller/jenkins/notifications/smtp_test.go index cc402dab..5267af19 100644 --- a/pkg/controller/jenkins/notifications/smtp_test.go +++ b/pkg/controller/jenkins/notifications/smtp_test.go @@ -4,10 +4,8 @@ import ( "context" "errors" "fmt" - "github.com/emersion/go-smtp" "io" "io/ioutil" - "log" "mime/quotedprintable" "net" "regexp" @@ -16,6 +14,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/emersion/go-smtp" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,29 +26,27 @@ const ( testSMTPPassword = "password" testSMTPPort = 1025 + + testFrom = "test@localhost" + testTo = "test.to@localhost" + testSubject = "Jenkins Operator Notification" + + // Headers titles + fromHeader = "From" + toHeader = "To" + subjectHeader = "Subject" ) -var smtpEvent = Event{ - Jenkins: v1alpha2.Jenkins{ - ObjectMeta: metav1.ObjectMeta{ - Name: testCrName, - Namespace: testNamespace, - }, - }, - Phase: testPhase, - Message: testMessage, - MessagesVerbose: testMessageVerbose, - LogLevel: testLoggingLevel, +type testServer struct { + event Event } -type testServer struct{} - // Login handles a login command with username and password. func (bkd *testServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { if username != testSMTPUsername || password != testSMTPPassword { return nil, errors.New("invalid username or password") } - return &testSession{}, nil + return &testSession{event: bkd.event}, nil } // AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails @@ -58,34 +55,50 @@ func (bkd *testServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session } // A Session is returned after successful login. -type testSession struct{} +type testSession struct { + event Event +} func (s *testSession) Mail(from string) error { - log.Println("Mail from:", from) + if from != testFrom { + return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", from, testFrom) + } return nil } func (s *testSession) Rcpt(to string) error { - log.Println("Rcpt to:", to) + if to != testTo { + return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", to, testTo) + } return nil } func (s *testSession) Data(r io.Reader) error { - re := regexp.MustCompile(`\t+\n\t+(.*):\n\t+(.*)\n\t+`) + contentRegex := regexp.MustCompile(`\t+\n\t+(.*):\n\t+(.*)\n\t+`) + headersRegex := regexp.MustCompile(`(.*):\s(.*)`) b, err := ioutil.ReadAll(quotedprintable.NewReader(r)) if err != nil { return err } - fmt.Println(string(b)) + content := contentRegex.FindAllStringSubmatch(string(b), -1) + headers := headersRegex.FindAllStringSubmatch(string(b), -1) - res := re.FindAllStringSubmatch(string(b), -1) + if s.event.Jenkins.Name == content[0][1] { + return fmt.Errorf("jenkins CR not identical: %s, expected: %s", content[0][1], s.event.Jenkins.Name) + } else if string(s.event.Phase) == content[1][1] { + return fmt.Errorf("phase not identical: %s, expected: %s", content[1][1], s.event.Phase) + } - if smtpEvent.Jenkins.Name == res[0][1] { - return fmt.Errorf("jenkins CR not identical: %s, expected: %s", res[0][1], smtpEvent.Jenkins.Name) - } else if string(smtpEvent.Phase) == res[1][1] { - return fmt.Errorf("phase not identical: %s, expected: %s", res[1][1], smtpEvent.Phase) + for i := range headers { + if headers[i][1] == fromHeader && headers[i][2] != testFrom { + return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", headers[i][2], testFrom) + } else if headers[i][1] == toHeader && headers[i][2] != testTo { + return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", headers[i][2], testTo) + } else if headers[i][1] == subjectHeader && headers[i][2] != testSubject { + return fmt.Errorf("`Subject` header is not equal: '%s', expected '%s'", headers[i][2], testSubject) + } } return nil @@ -98,6 +111,19 @@ func (s *testSession) Logout() error { } func TestSMTP_Send(t *testing.T) { + event := Event{ + Jenkins: v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: testCrName, + Namespace: testNamespace, + }, + }, + Phase: testPhase, + Message: testMessage, + MessagesVerbose: testMessageVerbose, + LogLevel: testLoggingLevel, + } + fakeClient := fake.NewFakeClient() testUsernameSelectorKeyName := "test-username-selector" testPasswordSelectorKeyName := "test-password-selector" @@ -105,7 +131,7 @@ func TestSMTP_Send(t *testing.T) { smtpClient := SMTP{k8sClient: fakeClient} - ts := &testServer{} + ts := &testServer{event: event} // Create fake SMTP server @@ -143,11 +169,11 @@ func TestSMTP_Send(t *testing.T) { assert.NoError(t, err) }() - err = smtpClient.Send(smtpEvent, v1alpha2.Notification{ + err = smtpClient.Send(event, v1alpha2.Notification{ SMTP: &v1alpha2.SMTP{ Server: "localhost", - From: "test@localhost", - To: "test@localhost", + From: testFrom, + To: testTo, TLSInsecureSkipVerify: true, Port: testSMTPPort, UsernameSecretKeySelector: v1alpha2.SecretKeySelector{