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= diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 0ae0c136..47489ac4 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,17 @@ 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"` +} + // 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/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 fad1219f..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 ( @@ -81,6 +80,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..02f78c3b --- /dev/null +++ b/pkg/controller/jenkins/notifications/smtp.go @@ -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 + "