From cf8783ea379db43c45c90a64ee6c4448476b55e0 Mon Sep 17 00:00:00 2001 From: Fabio Pinna Date: Tue, 24 Aug 2021 05:26:00 -0300 Subject: [PATCH] Add support for Google Chat alerts (#953) Add gchat alerting support Signed-off-by: fpinna --- README.md | 2 +- artifacts/flagger/crd.yaml | 1 + charts/flagger/crds/crd.yaml | 1 + go.sum | 1 + kustomize/base/flagger/crd.yaml | 1 + pkg/notifier/factory.go | 2 + pkg/notifier/gchat.go | 120 ++++++++++++++++++++++++++++++++ pkg/notifier/gchat_test.go | 53 ++++++++++++++ 8 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 pkg/notifier/gchat.go create mode 100644 pkg/notifier/gchat_test.go diff --git a/README.md b/README.md index 80bb1ce7..e8f493fa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Flagger implements several deployment strategies (Canary releases, A/B testing, using a service mesh (App Mesh, Istio, Linkerd, Open Service Mesh) or an ingress controller (Contour, Gloo, NGINX, Skipper, Traefik) for traffic routing. For release analysis, Flagger can query Prometheus, Datadog, New Relic or CloudWatch -and for alerting it uses Slack, MS Teams, Discord and Rocket. +and for alerting it uses Slack, MS Teams, Discord, Rocket and Google Chat. Flagger is a [Cloud Native Computing Foundation](https://cncf.io/) project and part of [Flux](https://fluxcd.io) family of GitOps tools. diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index e4a1c3a9..e8a2db02 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -1191,6 +1191,7 @@ spec: - msteams - discord - rocket + - gchat channel: description: Alert channel for this provider type: string diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index e4a1c3a9..e8a2db02 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -1191,6 +1191,7 @@ spec: - msteams - discord - rocket + - gchat channel: description: Alert channel for this provider type: string diff --git a/go.sum b/go.sum index 03ea1910..657f3393 100644 --- a/go.sum +++ b/go.sum @@ -283,6 +283,7 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index e4a1c3a9..e8a2db02 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -1191,6 +1191,7 @@ spec: - msteams - discord - rocket + - gchat channel: description: Alert channel for this provider type: string diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index 16b833e0..32c029bb 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -52,6 +52,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewRocket(f.URL, f.ProxyURL, f.Username, f.Channel) case "msteams": n, err = NewMSTeams(f.URL, f.ProxyURL) + case "gchat": + n, err = NewGChat(f.URL, f.ProxyURL) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/pkg/notifier/gchat.go b/pkg/notifier/gchat.go new file mode 100644 index 00000000..f5906bde --- /dev/null +++ b/pkg/notifier/gchat.go @@ -0,0 +1,120 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package notifier + +import ( + "fmt" + "net/url" +) + +// Google Chat holds the incoming webhook URL +type GChat struct { + URL string + ProxyURL string +} + +// GChatPayload holds the message +type GChatPayload struct { + Cards []GChatCards `json:"cards"` +} + +// Start - GChatCards holds the canary analysis result +type GChatCards struct { + Header GChatHeader `json:"header"` + Sections []*GChatSections `json:"sections"` +} + +type GChatHeader struct { + Title string `json:"title"` + SubTitle string `json:"subtitle"` + ImageUrl string `json:"imageUrl"` +} + +type GChatSections struct { + Widgets []GChatWidgets `json:"widgets"` +} + +type GChatWidgets struct { + TextParagraph GChatText `json:"textParagraph"` +} + +type GChatField struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type GChatText struct { + Text string `json:"text"` +} + +// NewGChat validates the GChat URL and returns a GChat object +func NewGChat(hookURL string, proxyURL string) (*GChat, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid Google Chat webhook URL %s", hookURL) + } + + return &GChat{ + URL: hookURL, + ProxyURL: proxyURL, + }, nil +} + +// Post Google Chat message +func (s *GChat) Post(workload string, namespace string, message string, fields []Field, severity string) error { + facts := make([]*GChatSections, 0, len(fields)) + facts = append(facts, &GChatSections{ + Widgets: []GChatWidgets{ + { + TextParagraph: GChatText{ + Text: "" + message + "", + }, + }, + }, + }) + for _, f := range fields { + facts = append(facts, &GChatSections{ + Widgets: []GChatWidgets{ + { + TextParagraph: GChatText{ + Text: f.Name + "
" + f.Value + "", + }, + }, + }, + }) + } + + payload := GChatPayload{ + Cards: []GChatCards{ + { + Header: GChatHeader{ + Title: "Flagger", + SubTitle: fmt.Sprintf("%s.%s", workload, namespace), + ImageUrl: "https://flagger.app/favicon.png", + }, + Sections: facts, + }, + }, + } + + err := postMessage(s.URL, s.ProxyURL, payload) + if err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + + return nil +} diff --git a/pkg/notifier/gchat_test.go b/pkg/notifier/gchat_test.go new file mode 100644 index 00000000..9037fefa --- /dev/null +++ b/pkg/notifier/gchat_test.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package notifier + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGChat_Post(t *testing.T) { + + fields := []Field{ + {Name: "name1", Value: "value1"}, + {Name: "name2", Value: "value2"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var payload = GChatPayload{} + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + require.Equal(t, "podinfo.test", payload.Cards[0].Header.SubTitle) + require.Equal(t, len(fields), len(payload.Cards[0].Sections)-1) + })) + defer ts.Close() + + GChat, err := NewGChat(ts.URL, "") + require.NoError(t, err) + + err = GChat.Post("podinfo", "test", "test", fields, "info") + require.NoError(t, err) +}