From ba4a2406ba78c01b854db465cc9a34372166930d Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 6 Jul 2019 15:47:12 +0300 Subject: [PATCH 1/6] Refactor notifier to allow more implementations --- cmd/flagger/main.go | 23 ++++++++++++++++++----- pkg/controller/controller.go | 23 +++++++++++------------ pkg/notifier/factory.go | 28 ++++++++++++++++++++++++++++ pkg/notifier/notifier.go | 10 ++++++++++ pkg/notifier/slack.go | 9 +++++++-- 5 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 pkg/notifier/factory.go create mode 100644 pkg/notifier/notifier.go diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index c84da585..d2d77e4a 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -34,6 +34,7 @@ var ( controlLoopInterval time.Duration logLevel string port string + msteamsURL string slackURL string slackUser string slackChannel string @@ -53,6 +54,7 @@ func init() { flag.DurationVar(&controlLoopInterval, "control-loop-interval", 10*time.Second, "Kubernetes API sync interval.") flag.StringVar(&logLevel, "log-level", "debug", "Log level can be: debug, info, warning, error.") flag.StringVar(&port, "port", "8080", "Port to listen on.") + flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") flag.StringVar(&slackURL, "slack-url", "", "Slack hook URL.") flag.StringVar(&slackUser, "slack-user", "flagger", "Slack user name.") flag.StringVar(&slackChannel, "slack-channel", "", "Slack channel.") @@ -158,13 +160,25 @@ func main() { logger.Errorf("Metrics server %s unreachable %v", metricsServer, err) } - var slack *notifier.Slack + var notifierClient notifier.Interface if slackURL != "" { - slack, err = notifier.NewSlack(slackURL, slackUser, slackChannel) + f := notifier.NewFactory(slackURL, slackUser, slackChannel) + var err error + notifierClient, err = f.Notifier() if err != nil { logger.Errorf("Notifier %v", err) } else { - logger.Infof("Slack notifications enabled for channel %s", slack.Channel) + logger.Infof("Slack notifications enabled for channel %s", slackChannel) + } + } + if msteamsURL != "" { + f := notifier.NewFactory(slackURL, slackUser, slackChannel) + var err error + notifierClient, err = f.Notifier() + if err != nil { + logger.Errorf("Notifier %v", err) + } else { + logger.Infof("Slack notifications enabled for channel %s", slackChannel) } } @@ -179,9 +193,8 @@ func main() { flaggerClient, canaryInformer, controlLoopInterval, - metricsServer, logger, - slack, + notifierClient, routerFactory, observerFactory, meshProvider, diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 4f44adee..f15c6158 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -46,7 +46,7 @@ type Controller struct { jobs map[string]CanaryJob deployer canary.Deployer recorder metrics.Recorder - notifier *notifier.Slack + notifier notifier.Interface routerFactory *router.Factory observerFactory *metrics.Factory meshProvider string @@ -58,9 +58,8 @@ func NewController( flaggerClient clientset.Interface, flaggerInformer flaggerinformers.CanaryInformer, flaggerWindow time.Duration, - metricServer string, logger *zap.SugaredLogger, - notifier *notifier.Slack, + notifier notifier.Interface, routerFactory *router.Factory, observerFactory *metrics.Factory, meshProvider string, @@ -271,26 +270,26 @@ func (c *Controller) sendNotification(cd *flaggerv1.Canary, message string, meta return } - var fields []notifier.SlackField + var fields []notifier.Field if metadata { fields = append(fields, - notifier.SlackField{ - Title: "Target", + notifier.Field{ + Name: "Target", Value: fmt.Sprintf("%s/%s.%s", cd.Spec.TargetRef.Kind, cd.Spec.TargetRef.Name, cd.Namespace), }, - notifier.SlackField{ - Title: "Traffic routing", + notifier.Field{ + Name: "Traffic routing", Value: fmt.Sprintf("Weight step: %v max: %v", cd.Spec.CanaryAnalysis.StepWeight, cd.Spec.CanaryAnalysis.MaxWeight), }, - notifier.SlackField{ - Title: "Failed checks threshold", + notifier.Field{ + Name: "Failed checks threshold", Value: fmt.Sprintf("%v", cd.Spec.CanaryAnalysis.Threshold), }, - notifier.SlackField{ - Title: "Progress deadline", + notifier.Field{ + Name: "Progress deadline", Value: fmt.Sprintf("%vs", cd.GetProgressDeadlineSeconds()), }, ) diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go new file mode 100644 index 00000000..482fd736 --- /dev/null +++ b/pkg/notifier/factory.go @@ -0,0 +1,28 @@ +package notifier + +import ( + "strings" +) + +type Factory struct { + URL string + Username string + Channel string +} + +func NewFactory(URL string, username string, channel string) *Factory { + return &Factory{ + URL: URL, + Channel: channel, + Username: username, + } +} + +func (f Factory) Notifier() (Interface, error) { + switch { + case strings.Contains(f.URL, "slack.com"): + return NewSlack(f.URL, f.Username, f.Channel) + } + + return nil, nil +} diff --git a/pkg/notifier/notifier.go b/pkg/notifier/notifier.go new file mode 100644 index 00000000..c014b030 --- /dev/null +++ b/pkg/notifier/notifier.go @@ -0,0 +1,10 @@ +package notifier + +type Interface interface { + Post(workload string, namespace string, message string, fields []Field, warn bool) error +} + +type Field struct { + Name string + Value string +} diff --git a/pkg/notifier/slack.go b/pkg/notifier/slack.go index a97fc0ad..8f247fbb 100644 --- a/pkg/notifier/slack.go +++ b/pkg/notifier/slack.go @@ -67,7 +67,7 @@ func NewSlack(hookURL string, username string, channel string) (*Slack, error) { } // Post Slack message -func (s *Slack) Post(workload string, namespace string, message string, fields []SlackField, warn bool) error { +func (s *Slack) Post(workload string, namespace string, message string, fields []Field, warn bool) error { payload := SlackPayload{ Channel: s.Channel, Username: s.Username, @@ -78,12 +78,17 @@ func (s *Slack) Post(workload string, namespace string, message string, fields [ color = "danger" } + sfields := make([]SlackField, len(fields)) + for _, f := range fields { + sfields = append(sfields, SlackField{f.Name, f.Value, false}) + } + a := SlackAttachment{ Color: color, AuthorName: fmt.Sprintf("%s.%s", workload, namespace), Text: message, MrkdwnIn: []string{"text"}, - Fields: fields, + Fields: sfields, } payload.Attachments = []SlackAttachment{a} From 61d0216c217ff39e5b2228de23ae33f0f53ba557 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 6 Jul 2019 17:13:09 +0300 Subject: [PATCH 2/6] Add traffic routing to notifications --- pkg/controller/controller.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index f15c6158..80dec4a8 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -278,12 +278,6 @@ func (c *Controller) sendNotification(cd *flaggerv1.Canary, message string, meta Name: "Target", Value: fmt.Sprintf("%s/%s.%s", cd.Spec.TargetRef.Kind, cd.Spec.TargetRef.Name, cd.Namespace), }, - notifier.Field{ - Name: "Traffic routing", - Value: fmt.Sprintf("Weight step: %v max: %v", - cd.Spec.CanaryAnalysis.StepWeight, - cd.Spec.CanaryAnalysis.MaxWeight), - }, notifier.Field{ Name: "Failed checks threshold", Value: fmt.Sprintf("%v", cd.Spec.CanaryAnalysis.Threshold), @@ -293,6 +287,25 @@ func (c *Controller) sendNotification(cd *flaggerv1.Canary, message string, meta Value: fmt.Sprintf("%vs", cd.GetProgressDeadlineSeconds()), }, ) + + if cd.Spec.CanaryAnalysis.StepWeight > 0 { + fields = append(fields, notifier.Field{ + Name: "Traffic routing", + Value: fmt.Sprintf("Weight step: %v max: %v", + cd.Spec.CanaryAnalysis.StepWeight, + cd.Spec.CanaryAnalysis.MaxWeight), + }) + } else if len(cd.Spec.CanaryAnalysis.Match) > 0 { + fields = append(fields, notifier.Field{ + Name: "Traffic routing", + Value: "A/B Testing", + }) + } else if cd.Spec.CanaryAnalysis.Iterations > 0 { + fields = append(fields, notifier.Field{ + Name: "Traffic routing", + Value: "Blue/Green", + }) + } } err := c.notifier.Post(cd.Name, cd.Namespace, message, fields, warn) if err != nil { From 8ca9cf24bbb0870058c7649970232b5a26ca4146 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 6 Jul 2019 17:14:21 +0300 Subject: [PATCH 3/6] Implement MS Teams notifier --- cmd/flagger/main.go | 26 +++++------- pkg/notifier/factory.go | 2 + pkg/notifier/teams.go | 94 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 pkg/notifier/teams.go diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index d2d77e4a..e0b3ecfc 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -54,10 +54,10 @@ func init() { flag.DurationVar(&controlLoopInterval, "control-loop-interval", 10*time.Second, "Kubernetes API sync interval.") flag.StringVar(&logLevel, "log-level", "debug", "Log level can be: debug, info, warning, error.") flag.StringVar(&port, "port", "8080", "Port to listen on.") - flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") flag.StringVar(&slackURL, "slack-url", "", "Slack hook URL.") flag.StringVar(&slackUser, "slack-user", "flagger", "Slack user name.") flag.StringVar(&slackChannel, "slack-channel", "", "Slack channel.") + flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") flag.IntVar(&threadiness, "threadiness", 2, "Worker concurrency.") flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.") flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.") @@ -160,25 +160,21 @@ func main() { logger.Errorf("Metrics server %s unreachable %v", metricsServer, err) } - var notifierClient notifier.Interface - if slackURL != "" { - f := notifier.NewFactory(slackURL, slackUser, slackChannel) - var err error - notifierClient, err = f.Notifier() - if err != nil { - logger.Errorf("Notifier %v", err) - } else { - logger.Infof("Slack notifications enabled for channel %s", slackChannel) - } - } + // setup Slack or MS Teams notifications + notifierURL := slackURL if msteamsURL != "" { - f := notifier.NewFactory(slackURL, slackUser, slackChannel) + notifierURL = msteamsURL + } + notifierFactory := notifier.NewFactory(notifierURL, slackUser, slackChannel) + + var notifierClient notifier.Interface + if notifierURL != "" { var err error - notifierClient, err = f.Notifier() + notifierClient, err = notifierFactory.Notifier() if err != nil { logger.Errorf("Notifier %v", err) } else { - logger.Infof("Slack notifications enabled for channel %s", slackChannel) + logger.Infof("Notifications enabled for %s", notifierURL[0:30]) } } diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index 482fd736..54716973 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -22,6 +22,8 @@ func (f Factory) Notifier() (Interface, error) { switch { case strings.Contains(f.URL, "slack.com"): return NewSlack(f.URL, f.Username, f.Channel) + case strings.Contains(f.URL, "office.com"): + return NewMSTeams(f.URL) } return nil, nil diff --git a/pkg/notifier/teams.go b/pkg/notifier/teams.go new file mode 100644 index 00000000..8545837e --- /dev/null +++ b/pkg/notifier/teams.go @@ -0,0 +1,94 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +// MS Teams holds the incoming webhook URL +type MSTeams struct { + URL string +} + +// MSTeamsPayload holds the message card data +type MSTeamsPayload struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor string `json:"themeColor"` + Summary string `json:"summary"` + Sections []MSTeamsSection `json:"sections"` +} + +// MSTeamsSection holds the canary analysis result +type MSTeamsSection struct { + ActivityTitle string `json:"activityTitle"` + ActivitySubtitle string `json:"activitySubtitle"` + Facts []MSTeamsField `json:"facts"` +} + +type MSTeamsField struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// NewMSTeams validates the MS Teams URL and returns a MSTeams object +func NewMSTeams(hookURL string) (*MSTeams, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid MS Teams webhook URL %s", hookURL) + } + + return &MSTeams{ + URL: hookURL, + }, nil +} + +// Post MS Teams message +func (s *MSTeams) Post(workload string, namespace string, message string, fields []Field, warn bool) error { + facts := make([]MSTeamsField, len(fields)) + for _, f := range fields { + facts = append(facts, MSTeamsField{f.Name, f.Value}) + } + + payload := MSTeamsPayload{ + Type: "MessageCard", + Context: "http://schema.org/extensions", + ThemeColor: "0076D7", + Summary: fmt.Sprintf("%s.%s", workload, namespace), + Sections: []MSTeamsSection{ + { + ActivityTitle: message, + ActivitySubtitle: fmt.Sprintf("%s.%s", workload, namespace), + Facts: facts, + }, + }, + } + + if warn { + payload.ThemeColor = "FF0000" + } + + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshalling slack payload failed %v", err) + } + + b := bytes.NewBuffer(data) + + if res, err := http.Post(s.URL, "application/json", b); err != nil { + return fmt.Errorf("sending data to MS Teams failed %v", err) + } else { + defer res.Body.Close() + statusCode := res.StatusCode + if statusCode != 200 { + body, _ := ioutil.ReadAll(res.Body) + return fmt.Errorf("sending data to MS Teams failed %v", string(body)) + } + } + + return nil +} From 4f49aa57602a50de42e97025f876ed4bf74ee33a Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 6 Jul 2019 17:14:50 +0300 Subject: [PATCH 4/6] Add MS Teams webhook field to chart --- charts/flagger/README.md | 18 +++++++++++++++--- charts/flagger/templates/deployment.yaml | 3 +++ charts/flagger/values.yaml | 4 ++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/charts/flagger/README.md b/charts/flagger/README.md index b5edc50d..3641cac2 100644 --- a/charts/flagger/README.md +++ b/charts/flagger/README.md @@ -19,13 +19,24 @@ Add Flagger Helm repository: helm repo add flagger https://flagger.app ``` -To install the chart with the release name `flagger`: +To install the chart with the release name `flagger` for Istio: ```console -$ helm install --name flagger --namespace istio-system flagger/flagger +$ helm upgrade -i flagger flagger/flagger \ + --namespace=istio-system \ + --set meshProvider=istio \ + --set metricsServer=http://prometheus:9090 +``` + +To install the chart with the release name `flagger` for Linkerd: + +```console +$ helm upgrade -i flagger flagger/flagger \ + --namespace=linkerd \ + --set meshProvider=linkerd \ + --set metricsServer=http://linkerd-prometheus:9090 ``` -The command deploys Flagger on the Kubernetes cluster in the istio-system namespace. The [configuration](#configuration) section lists the parameters that can be configured during installation. ## Uninstalling the Chart @@ -52,6 +63,7 @@ Parameter | Description | Default `slack.url` | Slack incoming webhook | None `slack.channel` | Slack channel | None `slack.user` | Slack username | `flagger` +`msteams.url` | Microsoft Teams incoming webhook | None `rbac.create` | if `true`, create and use RBAC resources | `true` `rbac.pspEnabled` | If `true`, create and use a restricted pod security policy | `false` `crd.create` | if `true`, create Flagger's CRDs | `true` diff --git a/charts/flagger/templates/deployment.yaml b/charts/flagger/templates/deployment.yaml index 0df390b7..24f59b01 100644 --- a/charts/flagger/templates/deployment.yaml +++ b/charts/flagger/templates/deployment.yaml @@ -51,6 +51,9 @@ spec: - -slack-user={{ .Values.slack.user }} - -slack-channel={{ .Values.slack.channel }} {{- end }} + {{- if .Values.msteams.url }} + - -msteams-url={{ .Values.msteams.url }} + {{- end }} livenessProbe: exec: command: diff --git a/charts/flagger/values.yaml b/charts/flagger/values.yaml index 1b6e90dd..d99717dd 100644 --- a/charts/flagger/values.yaml +++ b/charts/flagger/values.yaml @@ -19,6 +19,10 @@ slack: # incoming webhook https://api.slack.com/incoming-webhooks url: +msteams: + # MS Teams incoming webhook URL + url: + serviceAccount: # serviceAccount.create: Whether to create a service account or not create: true From 85e683446f8677e720d40b6343ac225257e38818 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 6 Jul 2019 17:16:30 +0300 Subject: [PATCH 5/6] Add MS Teams to docs --- Makefile | 1 + docs/gitbook/README.md | 4 ++-- .../install/flagger-install-on-kubernetes.md | 8 ++++++++ docs/gitbook/usage/alerting.md | 18 +++++++++++++++++- docs/screens/flagger-ms-teams-failed.png | Bin 0 -> 22015 bytes .../flagger-ms-teams-notifications.png | Bin 0 -> 30370 bytes 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 docs/screens/flagger-ms-teams-failed.png create mode 100644 docs/screens/flagger-ms-teams-notifications.png diff --git a/Makefile b/Makefile index fbca7d9c..398e13b6 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ TS=$(shell date +%Y-%m-%d_%H-%M-%S) run: GO111MODULE=on go run cmd/flagger/* -kubeconfig=$$HOME/.kube/config -log-level=info -mesh-provider=istio -namespace=test \ + -msteams-url=https://outlook.office.com/webhook/3ec1691c-c86d-41f7-af77-fb198f1e449c@512d0320-35b0-485e-8f12-39f8e36a7325/IncomingWebhook/f89b33444d3b4b17bb0c9f677bceb361/4a76478d-c25c-4200-9153-dcb9af901b75 \ -metrics-server=https://prometheus.istio.weavedx.com run-appmesh: diff --git a/docs/gitbook/README.md b/docs/gitbook/README.md index db6be6d6..946105a7 100644 --- a/docs/gitbook/README.md +++ b/docs/gitbook/README.md @@ -5,12 +5,12 @@ description: Flagger is a progressive delivery Kubernetes operator # Introduction [Flagger](https://github.com/weaveworks/flagger) is a **Kubernetes** operator that automates the promotion of canary -deployments using **Istio**, **App Mesh**, **NGINX** or **Gloo** routing for traffic shifting and **Prometheus** metrics for canary analysis. +deployments using **Istio**, **Linkerd**, **App Mesh**, **NGINX** or **Gloo** routing for traffic shifting and **Prometheus** metrics for canary analysis. The canary analysis can be extended with webhooks for running system integration/acceptance tests, load tests, or any other custom validation. Flagger implements a control loop that gradually shifts traffic to the canary while measuring key performance indicators like HTTP requests success rate, requests average duration and pods health. -Based on analysis of the **KPIs** a canary is promoted or aborted, and the analysis result is published to **Slack**. +Based on analysis of the **KPIs** a canary is promoted or aborted, and the analysis result is published to **Slack** or **MS Teams**. ![Flagger overview diagram](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/diagrams/flagger-canary-overview.png) diff --git a/docs/gitbook/install/flagger-install-on-kubernetes.md b/docs/gitbook/install/flagger-install-on-kubernetes.md index 659face1..2a827bfd 100644 --- a/docs/gitbook/install/flagger-install-on-kubernetes.md +++ b/docs/gitbook/install/flagger-install-on-kubernetes.md @@ -47,6 +47,14 @@ helm upgrade -i flagger flagger/flagger \ --set slack.user=flagger ``` +Enable **Microsoft Teams** notifications: + +```bash +helm upgrade -i flagger flagger/flagger \ +--namespace=istio-system \ +--set msteams.url=https://outlook.office.com/webhook/YOUR/TEAMS/WEBHOOK +``` + If you don't have Tiller you can use the helm template command and apply the generated yaml with kubectl: ```bash diff --git a/docs/gitbook/usage/alerting.md b/docs/gitbook/usage/alerting.md index 42189ef5..17fe1531 100644 --- a/docs/gitbook/usage/alerting.md +++ b/docs/gitbook/usage/alerting.md @@ -6,7 +6,6 @@ Flagger can be configured to send Slack notifications: ```bash helm upgrade -i flagger flagger/flagger \ ---namespace=istio-system \ --set slack.url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ --set slack.channel=general \ --set slack.user=flagger @@ -22,6 +21,23 @@ maximum number of failed checks: ![Slack Notifications](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/screens/slack-canary-failed.png) +### Microsoft Teams + +Flagger can be configured to send notifications to Microsoft Teams: + +```bash +helm upgrade -i flagger flagger/flagger \ +--set msteams.url=https://outlook.office.com/webhook/YOUR/TEAMS/WEBHOOK +``` + +Flagger will post a message card to MS Teams when a new revision has been detected and if the canary analysis failed or succeeded: + +![MS Teams Notifications](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/screens/flagger-ms-teams-notifications.png) + +And you'll get a notification on rollback: + +![MS Teams Notifications](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/screens/flagger-ms-teams-failed.png) + ### Prometheus Alert Manager Besides Slack, you can use Alertmanager to trigger alerts when a canary deployment failed: diff --git a/docs/screens/flagger-ms-teams-failed.png b/docs/screens/flagger-ms-teams-failed.png new file mode 100644 index 0000000000000000000000000000000000000000..5155877fd2ed466bc744d1ec8e329b8d7e7b2862 GIT binary patch literal 22015 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 G_x}LUGuCtf literal 0 HcmV?d00001 diff --git a/docs/screens/flagger-ms-teams-notifications.png b/docs/screens/flagger-ms-teams-notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..a592efd3b46c3245982f97deb6b8f5a29d537387 GIT binary patch literal 30370 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 Date: Sat, 6 Jul 2019 18:02:45 +0300 Subject: [PATCH 6/6] Add 5 seconds timeout to notifier --- Makefile | 1 - cmd/flagger/main.go | 38 +++++++++++++++++++++--------------- pkg/notifier/client.go | 43 +++++++++++++++++++++++++++++++++++++++++ pkg/notifier/factory.go | 10 +++------- pkg/notifier/slack.go | 21 ++------------------ pkg/notifier/teams.go | 21 ++------------------ 6 files changed, 72 insertions(+), 62 deletions(-) create mode 100644 pkg/notifier/client.go diff --git a/Makefile b/Makefile index 398e13b6..fbca7d9c 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,6 @@ TS=$(shell date +%Y-%m-%d_%H-%M-%S) run: GO111MODULE=on go run cmd/flagger/* -kubeconfig=$$HOME/.kube/config -log-level=info -mesh-provider=istio -namespace=test \ - -msteams-url=https://outlook.office.com/webhook/3ec1691c-c86d-41f7-af77-fb198f1e449c@512d0320-35b0-485e-8f12-39f8e36a7325/IncomingWebhook/f89b33444d3b4b17bb0c9f677bceb361/4a76478d-c25c-4200-9153-dcb9af901b75 \ -metrics-server=https://prometheus.istio.weavedx.com run-appmesh: diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index e0b3ecfc..f7a308d0 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -161,22 +161,7 @@ func main() { } // setup Slack or MS Teams notifications - notifierURL := slackURL - if msteamsURL != "" { - notifierURL = msteamsURL - } - notifierFactory := notifier.NewFactory(notifierURL, slackUser, slackChannel) - - var notifierClient notifier.Interface - if notifierURL != "" { - var err error - notifierClient, err = notifierFactory.Notifier() - if err != nil { - logger.Errorf("Notifier %v", err) - } else { - logger.Infof("Notifications enabled for %s", notifierURL[0:30]) - } - } + notifierClient := initNotifier(logger) // start HTTP server go server.ListenAndServe(port, 3*time.Second, logger, stopCh) @@ -218,3 +203,24 @@ func main() { <-stopCh } + +func initNotifier(logger *zap.SugaredLogger) (client notifier.Interface) { + provider := "slack" + notifierURL := slackURL + if msteamsURL != "" { + provider = "msteams" + notifierURL = msteamsURL + } + notifierFactory := notifier.NewFactory(notifierURL, slackUser, slackChannel) + + if notifierURL != "" { + var err error + client, err = notifierFactory.Notifier(provider) + if err != nil { + logger.Errorf("Notifier %v", err) + } else { + logger.Infof("Notifications enabled for %s", notifierURL[0:30]) + } + } + return +} diff --git a/pkg/notifier/client.go b/pkg/notifier/client.go new file mode 100644 index 00000000..72d0b1bd --- /dev/null +++ b/pkg/notifier/client.go @@ -0,0 +1,43 @@ +package notifier + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func postMessage(address string, payload interface{}) error { + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshalling notification payload failed %v", err) + } + + b := bytes.NewBuffer(data) + + req, err := http.NewRequest("POST", address, b) + if err != nil { + return err + } + req.Header.Set("Content-type", "application/json") + + ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) + defer cancel() + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return fmt.Errorf("sending notification failed %v", err) + } + + defer res.Body.Close() + statusCode := res.StatusCode + if statusCode != 200 { + body, _ := ioutil.ReadAll(res.Body) + return fmt.Errorf("sending notification failed %v", string(body)) + } + + return nil +} diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index 54716973..d7ef2ec1 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -1,9 +1,5 @@ package notifier -import ( - "strings" -) - type Factory struct { URL string Username string @@ -18,11 +14,11 @@ func NewFactory(URL string, username string, channel string) *Factory { } } -func (f Factory) Notifier() (Interface, error) { +func (f Factory) Notifier(provider string) (Interface, error) { switch { - case strings.Contains(f.URL, "slack.com"): + case provider == "slack": return NewSlack(f.URL, f.Username, f.Channel) - case strings.Contains(f.URL, "office.com"): + case provider == "msteams": return NewMSTeams(f.URL) } diff --git a/pkg/notifier/slack.go b/pkg/notifier/slack.go index 8f247fbb..1ca4f9d2 100644 --- a/pkg/notifier/slack.go +++ b/pkg/notifier/slack.go @@ -1,12 +1,8 @@ package notifier import ( - "bytes" - "encoding/json" "errors" "fmt" - "io/ioutil" - "net/http" "net/url" ) @@ -93,22 +89,9 @@ func (s *Slack) Post(workload string, namespace string, message string, fields [ payload.Attachments = []SlackAttachment{a} - data, err := json.Marshal(payload) + err := postMessage(s.URL, payload) if err != nil { - return fmt.Errorf("marshalling slack payload failed %v", err) - } - - b := bytes.NewBuffer(data) - - if res, err := http.Post(s.URL, "application/json", b); err != nil { - return fmt.Errorf("sending data to slack failed %v", err) - } else { - defer res.Body.Close() - statusCode := res.StatusCode - if statusCode != 200 { - body, _ := ioutil.ReadAll(res.Body) - return fmt.Errorf("sending data to slack failed %v", string(body)) - } + return err } return nil diff --git a/pkg/notifier/teams.go b/pkg/notifier/teams.go index 8545837e..e463b908 100644 --- a/pkg/notifier/teams.go +++ b/pkg/notifier/teams.go @@ -1,11 +1,7 @@ package notifier import ( - "bytes" - "encoding/json" "fmt" - "io/ioutil" - "net/http" "net/url" ) @@ -72,22 +68,9 @@ func (s *MSTeams) Post(workload string, namespace string, message string, fields payload.ThemeColor = "FF0000" } - data, err := json.Marshal(payload) + err := postMessage(s.URL, payload) if err != nil { - return fmt.Errorf("marshalling slack payload failed %v", err) - } - - b := bytes.NewBuffer(data) - - if res, err := http.Post(s.URL, "application/json", b); err != nil { - return fmt.Errorf("sending data to MS Teams failed %v", err) - } else { - defer res.Body.Close() - statusCode := res.StatusCode - if statusCode != 200 { - body, _ := ioutil.ReadAll(res.Body) - return fmt.Errorf("sending data to MS Teams failed %v", string(body)) - } + return err } return nil