Merge pull request #149 from jakalkhalili/smtp-notification-provider
Add SMTP notification provider
This commit is contained in:
		
						commit
						220f422826
					
				
							
								
								
									
										3
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							|  | @ -7,6 +7,8 @@ require ( | ||||||
| 	github.com/docker/distribution v2.7.1+incompatible | 	github.com/docker/distribution v2.7.1+incompatible | ||||||
| 	github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect | 	github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect | ||||||
| 	github.com/elazarl/goproxy/ext 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/logr v0.1.0 | ||||||
| 	github.com/go-logr/zapr v0.1.1 | 	github.com/go-logr/zapr v0.1.1 | ||||||
| 	github.com/go-openapi/spec v0.19.0 | 	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/sys v0.0.0-20190910064555-bbd175535a8b // indirect | ||||||
| 	golang.org/x/text v0.3.2 // indirect | 	golang.org/x/text v0.3.2 // indirect | ||||||
| 	golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 // 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/api v0.0.0-20190612125737-db0771252981 | ||||||
| 	k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad | 	k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad | ||||||
| 	k8s.io/client-go v11.0.0+incompatible | 	k8s.io/client-go v11.0.0+incompatible | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										7
									
								
								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-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 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= | ||||||
| github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= | 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.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 h1:AyDqLHbJ1quqbWr/OWDw+PlIP8ZFoTmYrGYaxzrLbNg= | ||||||
| github.com/emicklei/go-restful v2.8.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= | 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/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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= | ||||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | 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.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | ||||||
| gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= | ||||||
| gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= | ||||||
|  |  | ||||||
|  | @ -73,6 +73,7 @@ type Notification struct { | ||||||
| 	Slack        *Slack               `json:"slack,omitempty"` | 	Slack        *Slack               `json:"slack,omitempty"` | ||||||
| 	Teams        *MicrosoftTeams      `json:"teams,omitempty"` | 	Teams        *MicrosoftTeams      `json:"teams,omitempty"` | ||||||
| 	Mailgun      *Mailgun             `json:"mailgun,omitempty"` | 	Mailgun      *Mailgun             `json:"mailgun,omitempty"` | ||||||
|  | 	SMTP         *SMTP                `json:"smtp,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Slack is handler for Slack notification channel
 | // Slack is handler for Slack notification channel
 | ||||||
|  | @ -81,6 +82,17 @@ type Slack struct { | ||||||
| 	WebHookURLSecretKeySelector SecretKeySelector `json:"webHookURLSecretKeySelector"` | 	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"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // MicrosoftTeams is handler for Microsoft MicrosoftTeams notification channel
 | // MicrosoftTeams is handler for Microsoft MicrosoftTeams notification channel
 | ||||||
| type MicrosoftTeams struct { | type MicrosoftTeams struct { | ||||||
| 	// The web hook URL to MicrosoftTeams App
 | 	// The web hook URL to MicrosoftTeams App
 | ||||||
|  |  | ||||||
|  | @ -458,6 +458,11 @@ func (in *Notification) DeepCopyInto(out *Notification) { | ||||||
| 		*out = new(Mailgun) | 		*out = new(Mailgun) | ||||||
| 		**out = **in | 		**out = **in | ||||||
| 	} | 	} | ||||||
|  | 	if in.SMTP != nil { | ||||||
|  | 		in, out := &in.SMTP, &out.SMTP | ||||||
|  | 		*out = new(SMTP) | ||||||
|  | 		**out = **in | ||||||
|  | 	} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -504,6 +509,24 @@ func (in *Restore) DeepCopy() *Restore { | ||||||
| 	return out | 	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.
 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 | ||||||
| func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { | func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { | ||||||
| 	*out = *in | 	*out = *in | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ func TestValidatePlugins(t *testing.T) { | ||||||
| 		var userPlugins []v1alpha2.Plugin | 		var userPlugins []v1alpha2.Plugin | ||||||
| 
 | 
 | ||||||
| 		got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) | 		got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) | ||||||
|      | 
 | ||||||
| 		assert.Equal(t, got, []string{"invalid plugin version 'simple-plugin:invalid', must follow pattern '^[0-9\\\\.-]+$'"}) | 		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) { | 	t.Run("valid user and base plugin version", func(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ const ( | ||||||
| 	crNameFieldName       = "CR Name" | 	crNameFieldName       = "CR Name" | ||||||
| 	phaseFieldName        = "Phase" | 	phaseFieldName        = "Phase" | ||||||
| 	namespaceFieldName    = "Namespace" | 	namespaceFieldName    = "Namespace" | ||||||
| 	footerContent         = "Powered by Jenkins Operator" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -81,6 +80,8 @@ func Listen(events chan Event, k8sEvent event.Recorder, k8sClient k8sclient.Clie | ||||||
| 				svc = Teams{k8sClient: k8sClient} | 				svc = Teams{k8sClient: k8sClient} | ||||||
| 			} else if notificationConfig.Mailgun != nil { | 			} else if notificationConfig.Mailgun != nil { | ||||||
| 				svc = MailGun{k8sClient: k8sClient} | 				svc = MailGun{k8sClient: k8sClient} | ||||||
|  | 			} else if notificationConfig.SMTP != nil { | ||||||
|  | 				svc = SMTP{k8sClient: k8sClient} | ||||||
| 			} else { | 			} else { | ||||||
| 				logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig)) | 				logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig)) | ||||||
| 				continue | 				continue | ||||||
|  |  | ||||||
|  | @ -0,0 +1,94 @@ | ||||||
|  | package notifications | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" | ||||||
|  | 
 | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"gopkg.in/gomail.v2" | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	mailSubject = "Jenkins Operator Notification" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // SMTP is Simple Mail Transport Protocol used for sending emails
 | ||||||
|  | type SMTP struct { | ||||||
|  | 	k8sClient k8sclient.Client | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Send is function for sending notification by SMTP server
 | ||||||
|  | func (s SMTP) Send(event Event, config v1alpha2.Notification) error { | ||||||
|  | 	usernameSecret := &corev1.Secret{} | ||||||
|  | 	passwordSecret := &corev1.Secret{} | ||||||
|  | 
 | ||||||
|  | 	usernameSelector := config.SMTP.UsernameSecretKeySelector | ||||||
|  | 	passwordSelector := config.SMTP.PasswordSecretKeySelector | ||||||
|  | 
 | ||||||
|  | 	err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: usernameSelector.Name, Namespace: event.Jenkins.Namespace}, usernameSecret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: passwordSelector.Name, Namespace: event.Jenkins.Namespace}, passwordSecret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	usernameSecretValue := string(usernameSecret.Data[usernameSelector.Key]) | ||||||
|  | 	if usernameSecretValue == "" { | ||||||
|  | 		return errors.Errorf("SMTP username is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, usernameSelector.Name, usernameSelector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	passwordSecretValue := string(passwordSecret.Data[passwordSelector.Key]) | ||||||
|  | 	if passwordSecretValue == "" { | ||||||
|  | 		return errors.Errorf("SMTP password is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, passwordSelector.Name, passwordSelector.Key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mailer := gomail.NewDialer(config.SMTP.Server, config.SMTP.Port, usernameSecretValue, passwordSecretValue) | ||||||
|  | 	mailer.TLSConfig = &tls.Config{InsecureSkipVerify: config.SMTP.TLSInsecureSkipVerify} | ||||||
|  | 
 | ||||||
|  | 	var statusMessage string | ||||||
|  | 
 | ||||||
|  | 	if config.Verbose { | ||||||
|  | 		message := event.Message + "<ul>" | ||||||
|  | 		for _, msg := range event.MessagesVerbose { | ||||||
|  | 			message = message + "<li>" + msg + "</li>" | ||||||
|  | 		} | ||||||
|  | 		message = message + "</ul>" | ||||||
|  | 		statusMessage = message | ||||||
|  | 	} else { | ||||||
|  | 		statusMessage = event.Message | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	htmlMessage := fmt.Sprintf(content, s.getStatusColor(event.LogLevel), notificationTitle(event), statusMessage, event.Jenkins.Name, event.Phase) | ||||||
|  | 	message := gomail.NewMessage() | ||||||
|  | 
 | ||||||
|  | 	message.SetHeader("From", config.SMTP.From) | ||||||
|  | 	message.SetHeader("To", config.SMTP.To) | ||||||
|  | 	message.SetHeader("Subject", mailSubject) | ||||||
|  | 	message.SetBody("text/html", htmlMessage) | ||||||
|  | 
 | ||||||
|  | 	if err := mailer.DialAndSend(message); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s SMTP) getStatusColor(logLevel v1alpha2.NotificationLogLevel) StatusColor { | ||||||
|  | 	switch logLevel { | ||||||
|  | 	case v1alpha2.NotificationLogLevelInfo: | ||||||
|  | 		return "blue" | ||||||
|  | 	case v1alpha2.NotificationLogLevelWarning: | ||||||
|  | 		return "red" | ||||||
|  | 	default: | ||||||
|  | 		return "gray" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,195 @@ | ||||||
|  | package notifications | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"mime/quotedprintable" | ||||||
|  | 	"net" | ||||||
|  | 	"regexp" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"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" | ||||||
|  | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	testSMTPUsername = "username" | ||||||
|  | 	testSMTPPassword = "password" | ||||||
|  | 
 | ||||||
|  | 	testSMTPPort = 1025 | ||||||
|  | 
 | ||||||
|  | 	testFrom    = "test@localhost" | ||||||
|  | 	testTo      = "test.to@localhost" | ||||||
|  | 	testSubject = "Jenkins Operator Notification" | ||||||
|  | 
 | ||||||
|  | 	// Headers titles
 | ||||||
|  | 	fromHeader    = "From" | ||||||
|  | 	toHeader      = "To" | ||||||
|  | 	subjectHeader = "Subject" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type testServer struct { | ||||||
|  | 	event Event | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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{event: bkd.event}, 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 { | ||||||
|  | 	event Event | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *testSession) Mail(from string) error { | ||||||
|  | 	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 { | ||||||
|  | 	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 { | ||||||
|  | 	contentRegex := regexp.MustCompile(`\t+<tr>\n\t+<td><b>(.*):</b></td>\n\t+<td>(.*)</td>\n\t+</tr>`) | ||||||
|  | 	headersRegex := regexp.MustCompile(`(.*):\s(.*)`) | ||||||
|  | 
 | ||||||
|  | 	b, err := ioutil.ReadAll(quotedprintable.NewReader(r)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	content := contentRegex.FindAllStringSubmatch(string(b), -1) | ||||||
|  | 	headers := headersRegex.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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *testSession) Reset() {} | ||||||
|  | 
 | ||||||
|  | func (s *testSession) Logout() error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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" | ||||||
|  | 	testSecretName := "test-secret" | ||||||
|  | 
 | ||||||
|  | 	smtpClient := SMTP{k8sClient: fakeClient} | ||||||
|  | 
 | ||||||
|  | 	ts := &testServer{event: event} | ||||||
|  | 
 | ||||||
|  | 	// 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) | ||||||
|  | 
 | ||||||
|  | 	l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", testSMTPPort)) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	go func() { | ||||||
|  | 		err := s.Serve(l) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	err = smtpClient.Send(event, v1alpha2.Notification{ | ||||||
|  | 		SMTP: &v1alpha2.SMTP{ | ||||||
|  | 			Server:                "localhost", | ||||||
|  | 			From:                  testFrom, | ||||||
|  | 			To:                    testTo, | ||||||
|  | 			TLSInsecureSkipVerify: true, | ||||||
|  | 			Port:                  testSMTPPort, | ||||||
|  | 			UsernameSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testUsernameSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 			PasswordSecretKeySelector: v1alpha2.SecretKeySelector{ | ||||||
|  | 				LocalObjectReference: corev1.LocalObjectReference{ | ||||||
|  | 					Name: testSecretName, | ||||||
|  | 				}, | ||||||
|  | 				Key: testPasswordSelectorKeyName, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue