From 5cbd21b03c69bdad15a8932fa68e90058fb35539 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 14:51:00 +0200 Subject: [PATCH 01/10] Add notification library --- go.mod | 4 + go.sum | 31 ++- internal/notifier/mailgun.go | 62 ++++++ internal/notifier/msteams.go | 90 +++++++++ internal/notifier/msteams_test.go | 59 ++++++ internal/notifier/sender.go | 179 ++++++++++++++++++ internal/notifier/slack.go | 95 ++++++++++ internal/notifier/slack_test.go | 60 ++++++ pkg/apis/jenkins/v1alpha2/jenkins_types.go | 55 ++++++ .../jenkins/v1alpha2/zz_generated.deepcopy.go | 91 +++++++++ .../jenkins/configuration/base/reconcile.go | 16 ++ 11 files changed, 739 insertions(+), 3 deletions(-) create mode 100644 internal/notifier/mailgun.go create mode 100644 internal/notifier/msteams.go create mode 100644 internal/notifier/msteams_test.go create mode 100644 internal/notifier/sender.go create mode 100644 internal/notifier/slack.go create mode 100644 internal/notifier/slack_test.go diff --git a/go.mod b/go.mod index 7c5a3bdd..d998e0c2 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect + github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect github.com/emicklei/go-restful v2.8.1+incompatible // indirect github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.0 @@ -22,12 +24,14 @@ require ( github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/mailgun/mailgun-go/v3 v3.6.0 github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/operator-framework/operator-sdk v0.8.2-0.20190522220659-031d71ef8154 github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.8.1 + github.com/sergi/go-diff v1.0.0 // indirect github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.3.0 go.opencensus.io v0.19.2 // indirect diff --git a/go.sum b/go.sum index 5cee9964..59c1ac8d 100644 --- a/go.sum +++ b/go.sum @@ -40,13 +40,26 @@ github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 h1:aZtFdDNWY/yH86JPR2WX/PN63635VsE/f/nXNPAbYxY= +github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +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/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/evanphx/json-patch v4.0.0+incompatible h1:xregGRMLBeuRcwiOTHRCsPPuzCQlqhxUPbqdw+zNkLc= github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= +github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= @@ -83,6 +96,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= @@ -101,6 +115,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJ github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -116,10 +131,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailgun/mailgun-go/v3 v3.6.0 h1:oQWhyDTFjSiuO6vx1PRlfLZ7Fu+oK0Axn0UTREh3k/g= +github.com/mailgun/mailgun-go/v3 v3.6.0/go.mod h1:E81I5Agcfi/u1szdehi6p6ttdRX/UD3Rq2SrUzwyFIU= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g= @@ -134,7 +153,9 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -165,9 +186,12 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densiiieafo= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= @@ -203,8 +227,6 @@ golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -259,7 +281,6 @@ golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190213015956-f7e1b50d2251/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKYtBNhox8WtulQlOr3jIDk= @@ -282,12 +303,15 @@ google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -316,6 +340,7 @@ sigs.k8s.io/controller-runtime v0.1.10 h1:amLOmcekVdnsD1uIpmgRqfTbQWJ2qxvQkcdeFh sigs.k8s.io/controller-runtime v0.1.10/go.mod h1:HFAYoOh6XMV+jKF1UjFwrknPbowfyHEHHRdJMf2jMX8= sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde h1:ZkaHf5rNYzIB6CB82keKMQNv7xxkqT0ylOBdfJPfi+k= sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde/go.mod h1:ATWLRP3WGxuAN9HcT2LaKHReXIH+EZGzRuMHuxjXfhQ= +sigs.k8s.io/testing_frameworks v0.1.0 h1:2hBE1sDhKWALoqvhi2i/mnQOFZVfWtQFtsfH0QBTI0U= sigs.k8s.io/testing_frameworks v0.1.0/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go new file mode 100644 index 00000000..049f0c49 --- /dev/null +++ b/internal/notifier/mailgun.go @@ -0,0 +1,62 @@ +package notifier + +import ( + "context" + "fmt" + "github.com/mailgun/mailgun-go/v3" + "time" +) + +// 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(secret string, i *Information) error { + mg := mailgun.NewMailgun(m.Domain, secret) + + content := ` + + + + +

Jenkins Operator Reconciled

+

Failed to do something

+ + + + + + + + + + + + + +
CR name:%s
Configuration type:%s
Status:%s
+
Powered by Jenkins Operator <3
+ + + ` + + content = fmt.Sprintf(content, getStatusColor(i.Status, m), i.CrName, i.ConfigurationType, getStatusColor(i.Status, m), getStatusName(i.Status)) + + msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) + msg.SetHtml(content) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + _, _, err := mg.Send(ctx, msg) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go new file mode 100644 index 00000000..9128d149 --- /dev/null +++ b/internal/notifier/msteams.go @@ -0,0 +1,90 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "net/http" +) + +// Teams is Microsoft Teams Service +type Teams struct{} + +// TeamsMessage is representation of json message structure +type TeamsMessage struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor StatusColor `json:"themeColor"` + Title string `json:"title"` + Sections []TeamsSection `json:"sections"` +} + +// TeamsSection is MS Teams message section +type TeamsSection struct { + Facts []TeamsFact `json:"facts"` + Text string `json:"text"` +} + +// TeamsFact is field where we can put content +type TeamsFact struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Send is function for sending directly to API +func (t Teams) Send(secret string, i *Information) error { + err := i.Error + var errMessage string + + if err != nil { + errMessage = err.Error() + } else { + errMessage = noErrorMessage + } + + msg, err := json.Marshal(TeamsMessage{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: getStatusColor(i.Status, t), + Title: titleText, + Sections: []TeamsSection{ + { + Facts: []TeamsFact{ + { + Name: crNameFieldName, + Value: i.CrName, + }, + { + Name: configurationTypeFieldName, + Value: i.ConfigurationType, + }, + { + Name: statusFieldName, + Value: getStatusName(i.Status), + }, + }, + Text: errMessage, + }, + }, + }) + + if err != nil { + return err + } + + request, err := http.NewRequest("POST", secret, bytes.NewBuffer(msg)) + if err != nil { + return err + } + + resp, err := client.Do(request) + if err != nil { + return err + } + + err = resp.Body.Close() + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go new file mode 100644 index 00000000..33d7af6c --- /dev/null +++ b/internal/notifier/msteams_test.go @@ -0,0 +1,59 @@ +package notifier + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestTeams_Send(t *testing.T) { + teams := Teams{} + + i := &Information{ + ConfigurationType: testConfigurationType, + CrName: testCrName, + Status: testStatus, + Error: testError, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var message TeamsMessage + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&message) + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, message.Title, titleText) + assert.Equal(t, message.ThemeColor, getStatusColor(i.Status, teams)) + + mainSection := message.Sections[0] + + assert.Equal(t, mainSection.Text, noErrorMessage) + + for _, fact := range mainSection.Facts { + switch fact.Name { + case configurationTypeFieldName: + if fact.Value != i.ConfigurationType { + t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) + } + case crNameFieldName: + if fact.Value != i.CrName { + t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) + } + case statusFieldName: + if fact.Value != getStatusName(i.Status) { + t.Fatalf("%s is not equal! Must be %s", statusFieldName, getStatusName(i.Status)) + } + } + } + })) + + defer server.Close() + if err := teams.Send(server.URL, i); err != nil { + t.Fatal(err) + } +} diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go new file mode 100644 index 00000000..5c12669a --- /dev/null +++ b/internal/notifier/sender.go @@ -0,0 +1,179 @@ +package notifier + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-logr/logr" + + "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" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + testConfigurationType = "test-configuration" + testCrName = "test-cr" + testStatus Status = 1 + testError error + + 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 +type StatusColor string + +// Information represents details about operator status +type Information struct { + ConfigurationType string + CrName string + Status Status + Error error +} + +// Notification contains message which will be sent +type Notification struct { + Jenkins *v1alpha2.Jenkins + K8sClient k8sclient.Client + Logger logr.Logger + + // Recipient is mobile number or email address + // It's not used in Slack or Microsoft Teams + Recipient string + + Information *Information +} + +// Service is skeleton for additional services +type Service interface { + Send(secret string, i *Information) error +} + +// Listen is goroutine that listens for incoming messages and sends it +func Listen(notification chan *Notification) { + n := <-notification + if len(n.Jenkins.Spec.Notification) > 0 { + for _, endpoint := range n.Jenkins.Spec.Notification { + var err error + var service Service + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} + + if endpoint.Slack != (v1alpha2.Slack{}) { + n.Logger.V(log.VDebug).Info("Slack detected") + service = Slack{} + selector = endpoint.Slack.URLSecretKeySelector + } else if endpoint.Teams != (v1alpha2.Teams{}) { + n.Logger.V(log.VDebug).Info("Microsoft Teams detected") + service = Teams{} + selector = endpoint.Teams.URLSecretKeySelector + } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { + n.Logger.V(log.VDebug).Info("Mailgun detected") + service = Mailgun{ + Domain: endpoint.Mailgun.Domain, + Recipient: endpoint.Mailgun.Recipient, + From: endpoint.Mailgun.From, + } + selector = endpoint.Mailgun.APIKeySecretKeySelector + } else { + n.Logger.Info("Notification service not found or not defined") + } + + err = n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + 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 { + n.Logger.Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.Info("Sent notification") + } + } + } +} + +func getStatusName(status Status) string { + switch status { + case StatusSuccess: + return "Success" + case StatusError: + return "Error" + default: + return "Undefined" + } +} + +func getStatusColor(status Status, service Service) StatusColor { + switch service.(type) { + case Slack: + switch status { + case StatusSuccess: + return "good" + case StatusError: + return "danger" + default: + return "#c8c8c8" + } + case Teams: + switch status { + case StatusSuccess: + return "54A254" + case StatusError: + return "E81123" + default: + return "C8C8C8" + } + case Mailgun: + switch status { + case StatusSuccess: + return "green" + case StatusError: + return "red" + default: + return "gray" + } + default: + return "#c8c8c8" + } +} + +func notify(service Service, secret string, i *Information) error { + var err error + switch svc := service.(type) { + case Slack: + err = svc.Send(secret, i) + case Teams: + err = svc.Send(secret, i) + case Mailgun: + err = svc.Send(secret, i) + } + + return err +} diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go new file mode 100644 index 00000000..ca4308ee --- /dev/null +++ b/internal/notifier/slack.go @@ -0,0 +1,95 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "net/http" +) + +// Slack is messaging service +type Slack struct{} + +// SlackMessage is representation of json message +type SlackMessage struct { + Text string `json:"text"` + Attachments []SlackAttachment `json:"attachments"` +} + +// SlackAttachment is representation of json attachment +type SlackAttachment struct { + Fallback string `json:"fallback"` + Color StatusColor `json:"color"` + Pretext string `json:"pretext"` + Title string `json:"title"` + Text string `json:"text"` + Fields []SlackField `json:"fields"` + Footer string `json:"footer"` +} + +// SlackField is representation of json field. +type SlackField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// Send is function for sending directly to API +func (s Slack) Send(secret string, i *Information) error { + err := i.Error + var errMessage string + + if err != nil { + errMessage = err.Error() + } else { + errMessage = noErrorMessage + } + + slackMessage, err := json.Marshal(SlackMessage{ + Attachments: []SlackAttachment{ + { + Fallback: "", + Color: getStatusColor(i.Status, s), + Text: titleText, + Fields: []SlackField{ + { + Title: statusMessageFieldName, + Value: errMessage, + Short: false, + }, + { + Title: crNameFieldName, + Value: i.CrName, + Short: true, + }, + { + Title: configurationTypeFieldName, + Value: i.ConfigurationType, + Short: true, + }, + }, + Footer: footerContent, + }, + }, + }) + + if err != nil { + return err + } + + request, err := http.NewRequest("POST", secret, bytes.NewBuffer(slackMessage)) + if err != nil { + return err + } + + resp, err := client.Do(request) + if err != nil { + return err + } + + err = resp.Body.Close() + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go new file mode 100644 index 00000000..98c9181b --- /dev/null +++ b/internal/notifier/slack_test.go @@ -0,0 +1,60 @@ +package notifier + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSlack_Send(t *testing.T) { + slack := Slack{} + + i := &Information{ + ConfigurationType: testConfigurationType, + CrName: testCrName, + Status: testStatus, + Error: testError, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var message SlackMessage + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&message) + + if err != nil { + t.Fatal(err) + } + + mainAttachment := message.Attachments[0] + + assert.Equal(t, mainAttachment.Text, titleText) + + for _, field := range mainAttachment.Fields { + switch field.Title { + case configurationTypeFieldName: + if field.Value != i.ConfigurationType { + t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) + } + case crNameFieldName: + if field.Value != i.CrName { + t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) + } + case statusMessageFieldName: + if field.Value != noErrorMessage { + t.Fatalf("Error thrown but not expected") + } + } + } + + assert.Equal(t, mainAttachment.Footer, footerContent) + assert.Equal(t, mainAttachment.Color, getStatusColor(i.Status, slack)) + })) + + defer server.Close() + + if err := slack.Send(server.URL, i); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index a961b7a9..25698f18 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,6 +17,9 @@ type JenkinsSpec struct { // +optional SeedJobs []SeedJob `json:"seedJobs,omitempty"` + // Notification defines services which are used to inform about Jenkins behavior + Notification []Notification `json:"notification,omitempty"` + // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : // port: 8080 @@ -50,6 +53,44 @@ type JenkinsSpec struct { ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` } +// Notification is info sending service about Jenkins Operator +type Notification struct { + LoggingLevel JenkinsNotificationLogLevel `json:"loggingLevel"` + Verbose bool `json:"verbose"` + Name string `json:"name"` + Slack Slack `json:"slack,omitempty"` + Teams Teams `json:"teams,omitempty"` + Mailgun Mailgun `json:"mailgun,omitempty"` +} + +// Slack is handler for Slack +type Slack struct { + // The web hook url to Slack App + URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` +} + +// Teams is handler for Microsoft Teams +type Teams struct { + // The web hook url to Teams App + URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` +} + +// Mailgun is handler for Mailgun email service +type Mailgun struct { + Domain string `json:"domain"` + APIKeySecretKeySelector SecretKeySelector `json:"apiKeySecretKeySelector"` + Recipient string `json:"recipient"` + From string `json:"from"` +} + +// SecretKeySelector selects a key of a Secret. +type SecretKeySelector struct { + // The name of the secret in the pod's namespace to select from. + corev1.LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` + // The key of the secret to select from. Must be a valid secret key. + Key string `json:"key" protobuf:"bytes,2,opt,name=key"` +} + // Container defines Kubernetes container attributes type Container struct { // Name of the container specified as a DNS_LABEL. @@ -443,6 +484,20 @@ const ( UsernamePasswordCredentialType JenkinsCredentialType = "usernamePassword" ) +// JenkinsNotificationLogLevel defines type of Notification feature frequency of sending logger entries +type JenkinsNotificationLogLevel string + +const ( + // LogLevelNone - No logs + LogLevelNone JenkinsNotificationLogLevel = "" + + // LogLevelWarning - Only Warnings + LogLevelWarning JenkinsNotificationLogLevel = "warning" + + // LogLevelInfo - Only info + LogLevelInfo JenkinsNotificationLogLevel = "info" +) + // AllowedJenkinsCredentialMap contains all allowed Jenkins credentials types var AllowedJenkinsCredentialMap = map[string]string{ string(NoJenkinsCredentialCredentialType): "", diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 33b5ff35..96caca6f 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -366,6 +366,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } + if in.Notification != nil { + in, out := &in.Notification, &out.Notification + *out = make([]Notification, len(*in)) + copy(*out, *in) + } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) @@ -430,6 +435,41 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mailgun) DeepCopyInto(out *Mailgun) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mailgun. +func (in *Mailgun) DeepCopy() *Mailgun { + if in == nil { + return nil + } + out := new(Mailgun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Notification) DeepCopyInto(out *Notification) { + *out = *in + out.Slack = in.Slack + out.Teams = in.Teams + out.Mailgun = in.Mailgun + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Notification. +func (in *Notification) DeepCopy() *Notification { + if in == nil { + return nil + } + out := new(Notification) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Plugin) DeepCopyInto(out *Plugin) { *out = *in @@ -463,6 +503,23 @@ 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 *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { + *out = *in + out.LocalObjectReference = in.LocalObjectReference + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector. +func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { + if in == nil { + return nil + } + out := new(SecretKeySelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretRef) DeepCopyInto(out *SecretRef) { *out = *in @@ -529,3 +586,37 @@ func (in *Service) DeepCopy() *Service { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Slack) DeepCopyInto(out *Slack) { + *out = *in + out.URLSecretKeySelector = in.URLSecretKeySelector + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Slack. +func (in *Slack) DeepCopy() *Slack { + if in == nil { + return nil + } + out := new(Slack) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Teams) DeepCopyInto(out *Teams) { + *out = *in + out.URLSecretKeySelector = in.URLSecretKeySelector + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Teams. +func (in *Teams) DeepCopy() *Teams { + if in == nil { + return nil + } + out := new(Teams) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index ed8ac2f1..489332c9 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -4,7 +4,9 @@ import ( "context" "crypto/sha256" "encoding/base64" + er "errors" "fmt" + "github.com/jenkinsci/kubernetes-operator/internal/notifier" "reflect" "strings" "time" @@ -124,6 +126,20 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } result, err = r.ensureBaseConfiguration(jenkinsClient) + notificationChannel := make(chan *notifier.Notification) + go notifier.Listen(notificationChannel) + + notificationChannel <- ¬ifier.Notification{ + K8sClient: r.k8sClient, + Jenkins: r.jenkins, + Logger: r.logger, + Information: ¬ifier.Information{ + CrName: r.jenkins.Name, + ConfigurationType: "base", + Status: notifier.StatusError, + Error: er.New("failed to do something"), + }, + } return result, jenkinsClient, err } From 61d5311ac2c84aff97abfa300a270dbd91c017a2 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 14:52:32 +0200 Subject: [PATCH 02/10] Disabled notification feature --- .../jenkins/configuration/base/reconcile.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 489332c9..ed8ac2f1 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -4,9 +4,7 @@ import ( "context" "crypto/sha256" "encoding/base64" - er "errors" "fmt" - "github.com/jenkinsci/kubernetes-operator/internal/notifier" "reflect" "strings" "time" @@ -126,20 +124,6 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } result, err = r.ensureBaseConfiguration(jenkinsClient) - notificationChannel := make(chan *notifier.Notification) - go notifier.Listen(notificationChannel) - - notificationChannel <- ¬ifier.Notification{ - K8sClient: r.k8sClient, - Jenkins: r.jenkins, - Logger: r.logger, - Information: ¬ifier.Information{ - CrName: r.jenkins.Name, - ConfigurationType: "base", - Status: notifier.StatusError, - Error: er.New("failed to do something"), - }, - } return result, jenkinsClient, err } From 75f95be65afd2d7edfd5730b72b3a85e5f34d0ad Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 11:16:17 +0200 Subject: [PATCH 03/10] Improve notification mechanism --- internal/notifier/mailgun.go | 27 ++++- internal/notifier/msteams.go | 52 ++++++--- internal/notifier/msteams_test.go | 41 +++---- internal/notifier/sender.go | 186 +++++++++++++----------------- internal/notifier/slack.go | 55 ++++++--- internal/notifier/slack_test.go | 39 ++++--- 6 files changed, 214 insertions(+), 186 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 049f0c49..4c92e7fd 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -3,8 +3,14 @@ package notifier import ( "context" "fmt" - "github.com/mailgun/mailgun-go/v3" "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 @@ -15,8 +21,19 @@ type Mailgun struct { } // Send is function for sending directly to API -func (m Mailgun) Send(secret string, i *Information) error { - mg := mailgun.NewMailgun(m.Domain, secret) +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 := ` ` - 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.SetHtml(content) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - _, _, err := mg.Send(ctx, msg) + _, _, err = mg.Send(ctx, msg) if err != nil { return err diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 9128d149..596664be 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -2,12 +2,22 @@ package notifier import ( "bytes" + "context" "encoding/json" + "fmt" "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 -type Teams struct{} +type Teams struct { + apiURL string +} // TeamsMessage is representation of json message structure type TeamsMessage struct { @@ -31,20 +41,16 @@ type TeamsFact struct { } // Send is function for sending directly to API -func (t Teams) Send(secret string, i *Information) error { - err := i.Error - var errMessage string +func (t Teams) Send(n *Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} - if err != nil { - errMessage = err.Error() - } else { - errMessage = noErrorMessage - } + i := n.Information msg, err := json.Marshal(TeamsMessage{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: getStatusColor(i.Status, t), + ThemeColor: getStatusColor(i.LogLevel, t), Title: titleText, Sections: []TeamsSection{ { @@ -58,20 +64,33 @@ func (t Teams) Send(secret string, i *Information) error { Value: i.ConfigurationType, }, { - Name: statusFieldName, - Value: getStatusName(i.Status), + Name: loggingLevelFieldName, + 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 { return err } - request, err := http.NewRequest("POST", secret, bytes.NewBuffer(msg)) + request, err := http.NewRequest("POST", t.apiURL, bytes.NewBuffer(msg)) if err != nil { return err } @@ -81,10 +100,7 @@ func (t Teams) Send(secret string, i *Information) error { return err } - err = resp.Body.Close() - if err != nil { - return err - } + defer func() { _ = resp.Body.Close() }() return nil } diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index 33d7af6c..cda82db2 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -2,20 +2,25 @@ package notifier import ( "encoding/json" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestTeams_Send(t *testing.T) { - teams := Teams{} - i := &Information{ ConfigurationType: testConfigurationType, CrName: testCrName, - Status: testStatus, - Error: testError, + Message: testMessage, + MessageVerbose: testMessageVerbose, + Namespace: testNamespace, + LogLevel: testLoggingLevel, + } + + notification := &Notification{ + Information: i, } 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.ThemeColor, getStatusColor(i.Status, teams)) + assert.Equal(t, message.ThemeColor, getStatusColor(i.LogLevel, Teams{})) mainSection := message.Sections[0] - assert.Equal(t, mainSection.Text, noErrorMessage) + assert.Equal(t, mainSection.Text, i.Message) for _, fact := range mainSection.Facts { switch fact.Name { case configurationTypeFieldName: - if fact.Value != i.ConfigurationType { - t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) - } + assert.Equal(t, fact.Value, i.ConfigurationType) case crNameFieldName: - if fact.Value != i.CrName { - t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) - } - case statusFieldName: - if fact.Value != getStatusName(i.Status) { - t.Fatalf("%s is not equal! Must be %s", statusFieldName, getStatusName(i.Status)) - } + assert.Equal(t, fact.Value, i.CrName) + case messageFieldName: + assert.Equal(t, fact.Value, i.Message) + case loggingLevelFieldName: + assert.Equal(t, fact.Value, string(i.LogLevel)) } } })) + teams := Teams{apiURL: server.URL} + defer server.Close() - if err := teams.Send(server.URL, i); err != nil { - t.Fatal(err) - } + assert.NoError(t, teams.Send(notification)) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 5c12669a..a8b802aa 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -1,160 +1,132 @@ package notifier import ( - "context" "fmt" - "net/http" - "github.com/go-logr/logr" + "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" + 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 ( - testConfigurationType = "test-configuration" - testCrName = "test-cr" - testStatus Status = 1 - testError error + testConfigurationType = "test-configuration" + testCrName = "test-cr" + testNamespace = "test-namespace" + testMessage = "test-message" + testMessageVerbose = "detail-test-message" + testLoggingLevel = LogWarn 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 type StatusColor string +// LoggingLevel is type for selecting different logging levels +type LoggingLevel string + // Information represents details about operator status type Information struct { ConfigurationType string + Namespace string CrName string - Status Status - Error error + LogLevel LoggingLevel + Message string + MessageVerbose string } // Notification contains message which will be sent type Notification struct { - Jenkins *v1alpha2.Jenkins - K8sClient k8sclient.Client - Logger logr.Logger - - // Recipient is mobile number or email address - // It's not used in Slack or Microsoft Teams - Recipient string - + Jenkins *v1alpha2.Jenkins + K8sClient k8sclient.Client + Logger logr.Logger Information *Information } // Service is skeleton for additional services -type Service interface { - Send(secret string, i *Information) error +type service interface { + Send(i *Notification) error } // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { - n := <-notification - if len(n.Jenkins.Spec.Notification) > 0 { - for _, endpoint := range n.Jenkins.Spec.Notification { - var err error - var service Service - var selector v1alpha2.SecretKeySelector - secret := &corev1.Secret{} + <-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 - if endpoint.Slack != (v1alpha2.Slack{}) { - n.Logger.V(log.VDebug).Info("Slack detected") - service = Slack{} - selector = endpoint.Slack.URLSecretKeySelector - } else if endpoint.Teams != (v1alpha2.Teams{}) { - n.Logger.V(log.VDebug).Info("Microsoft Teams detected") - service = Teams{} - selector = endpoint.Teams.URLSecretKeySelector - } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { - n.Logger.V(log.VDebug).Info("Mailgun detected") - service = Mailgun{ - Domain: endpoint.Mailgun.Domain, - Recipient: endpoint.Mailgun.Recipient, - From: endpoint.Mailgun.From, + 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") } - selector = endpoint.Mailgun.APIKeySecretKeySelector - } else { - n.Logger.Info("Notification service not found or not defined") - } - err = n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) - if err != nil { - n.Logger.Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - } + err = notify(svc, n) - 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 { - n.Logger.Info(fmt.Sprintf("Failed to send notifications. %+v", err)) - } else { - n.Logger.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") + } } } } } -func getStatusName(status Status) string { - switch status { - case StatusSuccess: - return "Success" - case StatusError: - return "Error" - default: - return "Undefined" - } -} - -func getStatusColor(status Status, service Service) StatusColor { - switch service.(type) { +func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { + switch svc.(type) { case Slack: - switch status { - case StatusSuccess: - return "good" - case StatusError: + switch logLevel { + case LogInfo: + return "#439FE0" + case LogWarn: return "danger" default: return "#c8c8c8" } case Teams: - switch status { - case StatusSuccess: - return "54A254" - case StatusError: + switch logLevel { + case LogInfo: + return "439FE0" + case LogWarn: return "E81123" default: return "C8C8C8" } case Mailgun: - switch status { - case StatusSuccess: - return "green" - case StatusError: + switch logLevel { + case LogInfo: + return "blue" + case LogWarn: return "red" default: 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 - switch svc := service.(type) { + switch s := svc.(type) { case Slack: - err = svc.Send(secret, i) + err = s.Send(n) case Teams: - err = svc.Send(secret, i) + err = s.Send(n) case Mailgun: - err = svc.Send(secret, i) + err = s.Send(n) } return err diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index ca4308ee..1222d80d 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -2,12 +2,22 @@ package notifier import ( "bytes" + "context" "encoding/json" + "fmt" "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 -type Slack struct{} +type Slack struct { + apiURL string +} // SlackMessage is representation of json message type SlackMessage struct { @@ -34,26 +44,31 @@ type SlackField struct { } // Send is function for sending directly to API -func (s Slack) Send(secret string, i *Information) error { - err := i.Error - var errMessage string +func (s Slack) Send(n *Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} - if err != nil { - errMessage = err.Error() - } else { - errMessage = noErrorMessage + 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)) + } + + s.apiURL = secret.StringData[selector.Name] } slackMessage, err := json.Marshal(SlackMessage{ Attachments: []SlackAttachment{ { Fallback: "", - Color: getStatusColor(i.Status, s), + Color: getStatusColor(i.LogLevel, s), Text: titleText, Fields: []SlackField{ { - Title: statusMessageFieldName, - Value: errMessage, + Title: messageFieldName, + Value: i.Message, Short: false, }, { @@ -66,6 +81,16 @@ func (s Slack) Send(secret string, i *Information) error { Value: i.ConfigurationType, Short: true, }, + { + Title: loggingLevelFieldName, + Value: string(i.LogLevel), + Short: true, + }, + { + Title: namespaceFieldName, + Value: i.Namespace, + Short: true, + }, }, Footer: footerContent, }, @@ -76,7 +101,7 @@ func (s Slack) Send(secret string, i *Information) error { return err } - request, err := http.NewRequest("POST", secret, bytes.NewBuffer(slackMessage)) + request, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(slackMessage)) if err != nil { return err } @@ -86,10 +111,6 @@ func (s Slack) Send(secret string, i *Information) error { return err } - err = resp.Body.Close() - if err != nil { - return err - } - + defer func() { _ = resp.Body.Close() }() return nil } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 98c9181b..b1776bd6 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -2,20 +2,25 @@ package notifier import ( "encoding/json" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestSlack_Send(t *testing.T) { - slack := Slack{} - i := &Information{ ConfigurationType: testConfigurationType, CrName: testCrName, - Status: testStatus, - Error: testError, + Message: testMessage, + MessageVerbose: testMessageVerbose, + Namespace: testNamespace, + LogLevel: testLoggingLevel, + } + + notification := &Notification{ + Information: i, } 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 { switch field.Title { case configurationTypeFieldName: - if field.Value != i.ConfigurationType { - t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) - } + assert.Equal(t, field.Value, i.ConfigurationType) case crNameFieldName: - if field.Value != i.CrName { - t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) - } - case statusMessageFieldName: - if field.Value != noErrorMessage { - t.Fatalf("Error thrown but not expected") - } + assert.Equal(t, field.Value, i.CrName) + case messageFieldName: + assert.Equal(t, field.Value, i.Message) + case loggingLevelFieldName: + assert.Equal(t, field.Value, string(i.LogLevel)) } } 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() - if err := slack.Send(server.URL, i); err != nil { - t.Fatal(err) - } + slack := Slack{apiURL: server.URL} + + assert.NoError(t, slack.Send(notification)) } From da31b3b7dd96393ea16ada4207eca1741d34fc46 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 16:13:18 +0200 Subject: [PATCH 04/10] Enhance notification services mechanism --- internal/notifier/mailgun.go | 63 +++++++++++++++---------------- internal/notifier/msteams.go | 26 ++++++------- internal/notifier/msteams_test.go | 44 +++++++++++++++++++-- internal/notifier/sender.go | 57 ++++++++++++---------------- internal/notifier/slack.go | 24 ++++++------ internal/notifier/slack_test.go | 44 +++++++++++++++++++-- 6 files changed, 161 insertions(+), 97 deletions(-) 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, + }, + }, + })) } From 16fd981703b99cc1716bcac575e81235953862b7 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 16:36:26 +0200 Subject: [PATCH 05/10] Add notification to api --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 25698f18..d9634fd6 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -18,7 +18,8 @@ type JenkinsSpec struct { SeedJobs []SeedJob `json:"seedJobs,omitempty"` // Notification defines services which are used to inform about Jenkins behavior - Notification []Notification `json:"notification,omitempty"` + // Can be used to integrate chat services like Slack or Email services like Mailgun + Notification Notification `json:"notifications,omitempty"` // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : @@ -488,9 +489,6 @@ const ( type JenkinsNotificationLogLevel string const ( - // LogLevelNone - No logs - LogLevelNone JenkinsNotificationLogLevel = "" - // LogLevelWarning - Only Warnings LogLevelWarning JenkinsNotificationLogLevel = "warning" From dc727f45d71586ae857b565e15f98fb32ed33c1d Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 09:38:17 +0200 Subject: [PATCH 06/10] Deepcopy fix --- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 96caca6f..ce270cab 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -260,6 +260,23 @@ func (in *Jenkins) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsAgent) DeepCopyInto(out *JenkinsAgent) { + *out = *in + in.Container.DeepCopyInto(&out.Container) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsAgent. +func (in *JenkinsAgent) DeepCopy() *JenkinsAgent { + if in == nil { + return nil + } + out := new(JenkinsAgent) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsList) DeepCopyInto(out *JenkinsList) { *out = *in @@ -366,11 +383,7 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } - if in.Notification != nil { - in, out := &in.Notification, &out.Notification - *out = make([]Notification, len(*in)) - copy(*out, *in) - } + out.Notification = in.Notification in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) @@ -438,6 +451,7 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Mailgun) DeepCopyInto(out *Mailgun) { *out = *in + out.APIKeySecretKeySelector = in.APIKeySecretKeySelector return } From 364ce8ad8afe1aa36906412cede6fe968eb89e98 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 09:51:42 +0200 Subject: [PATCH 07/10] Fix deepcopy --- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index ce270cab..0ed66359 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -260,23 +260,6 @@ func (in *Jenkins) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *JenkinsAgent) DeepCopyInto(out *JenkinsAgent) { - *out = *in - in.Container.DeepCopyInto(&out.Container) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsAgent. -func (in *JenkinsAgent) DeepCopy() *JenkinsAgent { - if in == nil { - return nil - } - out := new(JenkinsAgent) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsList) DeepCopyInto(out *JenkinsList) { *out = *in From 9bde4cb59fc49e68a92d5724ad08d14d559e8f1a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:07:37 +0200 Subject: [PATCH 08/10] Improve notification mechanism --- internal/notifier/mailgun.go | 9 +-- internal/notifier/msteams.go | 11 ++-- internal/notifier/sender.go | 55 +++++++++---------- internal/notifier/slack.go | 11 ++-- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 8 +-- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 6 +- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 74f524c5..3bf190b9 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -3,11 +3,10 @@ package notifier import ( "context" "fmt" + "github.com/pkg/errors" "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" @@ -44,21 +43,19 @@ type Mailgun struct{} // 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 + 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) + return errors.Errorf("SecretValue %s is empty", selector.Name) } mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 7ebea378..f1a1b290 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/json" - "fmt" + "github.com/pkg/errors" "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" ) @@ -40,15 +38,14 @@ type TeamsFact struct { // Send is function for sending directly to API func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { - var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} i := n.Information - selector = config.Teams.URLSecretKeySelector + 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)) + return err } msg, err := json.Marshal(TeamsMessage{ @@ -83,7 +80,7 @@ func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { secretValue := string(secret.Data[selector.Key]) if secretValue == "" { - return fmt.Errorf("SecretValue %s is empty", selector.Name) + return errors.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 81e70ca5..9735fcc5 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -2,9 +2,9 @@ package notifier import ( "fmt" - "github.com/go-logr/logr" "net/http" + "github.com/go-logr/logr" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/log" @@ -70,27 +70,30 @@ type service interface { // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { for n := range notification { - notificationConfig := n.Jenkins.Spec.Notification - var err error - var svc service + if len(n.Jenkins.Spec.Notifications) > 0 { + for _, notificationConfig := range n.Jenkins.Spec.Notifications { + var err error + var svc service - 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 - } + 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, notificationConfig) + 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") + } + } } } } @@ -129,16 +132,10 @@ func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { } } -func notify(svc service, n *Notification, nc v1alpha2.Notification) error { - var err error - switch s := svc.(type) { - case Slack: - err = s.Send(n, nc) - case Teams: - err = s.Send(n, nc) - case Mailgun: - err = s.Send(n, nc) +func notify(svc service, n *Notification, manifest v1alpha2.Notification) error { + if n.Information.LogLevel == LogInfo && string(manifest.LoggingLevel) == string(LogWarn) { + return nil } - return err + return svc.Send(n, manifest) } diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 0a009514..185d3393 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/json" - "fmt" + "github.com/pkg/errors" "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" ) @@ -43,15 +41,14 @@ type SlackField struct { // Send is function for sending directly to API func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { - var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} i := n.Information - selector = config.Slack.URLSecretKeySelector + selector := config.Slack.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)) + return err } slackMessage, err := json.Marshal(SlackMessage{ @@ -94,7 +91,7 @@ func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { secretValue := string(secret.Data[selector.Key]) if secretValue == "" { - return fmt.Errorf("SecretValue %s is empty", selector.Name) + return errors.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index d9634fd6..4afb27b2 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,9 +17,9 @@ type JenkinsSpec struct { // +optional SeedJobs []SeedJob `json:"seedJobs,omitempty"` - // Notification defines services which are used to inform about Jenkins behavior + // Notifications defines services which are used to inform about Jenkins status // Can be used to integrate chat services like Slack or Email services like Mailgun - Notification Notification `json:"notifications,omitempty"` + Notifications []Notification `json:"notifications,omitempty"` // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : @@ -66,13 +66,13 @@ type Notification struct { // Slack is handler for Slack type Slack struct { - // The web hook url to Slack App + // The web hook URL to Slack App URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` } // Teams is handler for Microsoft Teams type Teams struct { - // The web hook url to Teams App + // The web hook URL to Teams App URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` } diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 0ed66359..5d6d2d18 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -366,7 +366,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } - out.Notification = in.Notification + if in.Notifications != nil { + in, out := &in.Notifications, &out.Notifications + *out = make([]Notification, len(*in)) + copy(*out, *in) + } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) From e641a9fa1202d248b3341b9304baab49e915fd7b Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:24:36 +0200 Subject: [PATCH 09/10] Organize code structure --- internal/notifier/mailgun.go | 1 + internal/notifier/msteams.go | 1 + internal/notifier/msteams_test.go | 5 ++-- internal/notifier/sender.go | 41 +++++++++++++++---------------- internal/notifier/slack.go | 3 ++- internal/notifier/slack_test.go | 6 +++-- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 3bf190b9..7f7e5e9f 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -7,6 +7,7 @@ import ( "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/mailgun/mailgun-go/v3" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index f1a1b290..39ca3ae0 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index 2205eff2..f19bcf2e 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -85,7 +85,7 @@ func TestTeams_Send(t *testing.T) { err := notification.K8sClient.Create(context.TODO(), secret) assert.NoError(t, err) - assert.NoError(t, teams.Send(notification, v1alpha2.Notification{ + err := teams.Send(notification, v1alpha2.Notification{ Teams: v1alpha2.Teams{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -94,5 +94,6 @@ func TestTeams_Send(t *testing.T) { Key: testURLSelectorKeyName, }, }, - })) + }) + assert.NoError(t, err) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 9735fcc5..b67bc407 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -4,10 +4,10 @@ import ( "fmt" "net/http" - "github.com/go-logr/logr" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/log" + "github.com/go-logr/logr" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -70,31 +70,30 @@ type service interface { // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { for n := range notification { - if len(n.Jenkins.Spec.Notifications) > 0 { - for _, notificationConfig := range n.Jenkins.Spec.Notifications { - var err error - var svc service + for _, notificationConfig := range n.Jenkins.Spec.Notifications { + var err error + var svc service - 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 - } + 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, notificationConfig) + 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") } } + } } diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 185d3393..c8c3ba94 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "encoding/json" - "github.com/pkg/errors" "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 06da7aae..67cf751f 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -84,7 +84,7 @@ func TestSlack_Send(t *testing.T) { slack := Slack{} - assert.NoError(t, slack.Send(notification, v1alpha2.Notification{ + err := slack.Send(notification, v1alpha2.Notification{ Slack: v1alpha2.Slack{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -93,5 +93,7 @@ func TestSlack_Send(t *testing.T) { Key: testURLSelectorKeyName, }, }, - })) + }) + + assert.NoError(t, err) } From aecfd500f15ba52a1e0fc4a0abb0af8b178a1118 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:37:52 +0200 Subject: [PATCH 10/10] Fix err --- internal/notifier/msteams_test.go | 2 +- internal/notifier/slack_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index f19bcf2e..bc52715d 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -85,7 +85,7 @@ func TestTeams_Send(t *testing.T) { err := notification.K8sClient.Create(context.TODO(), secret) assert.NoError(t, err) - err := teams.Send(notification, v1alpha2.Notification{ + err = teams.Send(notification, v1alpha2.Notification{ Teams: v1alpha2.Teams{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 67cf751f..cb5f3221 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -84,7 +84,7 @@ func TestSlack_Send(t *testing.T) { slack := Slack{} - err := slack.Send(notification, v1alpha2.Notification{ + err = slack.Send(notification, v1alpha2.Notification{ Slack: v1alpha2.Slack{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{