diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index acc07dcad..d037f8a29 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -1393,6 +1393,7 @@ spec: - discord - rocket - gchat + - ringcentral channel: description: Alert channel for this provider type: string diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index acc07dcad..d037f8a29 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -1393,6 +1393,7 @@ spec: - discord - rocket - gchat + - ringcentral channel: description: Alert channel for this provider type: string diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index acc07dcad..d037f8a29 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -1393,6 +1393,7 @@ spec: - discord - rocket - gchat + - ringcentral channel: description: Alert channel for this provider type: string diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index 5b7a05ef8..40385fa9a 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -56,6 +56,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewMSTeams(f.URL, f.ProxyURL) case "gchat": n, err = NewGChat(f.URL, f.ProxyURL) + case "ringcentral": + n, err = NewRingCentral(f.URL, f.ProxyURL) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/pkg/notifier/ringcentral.go b/pkg/notifier/ringcentral.go new file mode 100644 index 000000000..3adc51fd6 --- /dev/null +++ b/pkg/notifier/ringcentral.go @@ -0,0 +1,111 @@ +/* +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" + "strings" +) + +// RingCentral holds the incoming webhook URL +type RingCentral struct { + URL string + ProxyURL string +} + +// RingCentralPayload holds the message card data +type RingCentralPayload struct { + Activity string `json:"activity"` + Attachments []RingCentralAttachment `json:"attachments"` +} + +// RingCentralAttachment holds the canary analysis result +type RingCentralAttachment struct { + Schema string `json:"$schema"` + Type string `json:"type"` + Version string `json:"version"` + Body []map[string]interface{} `json:"body"` +} + +// NewRingCentral validates the RingCentral URL and returns a RingCentral object +func NewRingCentral(hookURL string, proxyURL string) (*RingCentral, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid RingCentral webhook URL %s", hookURL) + } + + return &RingCentral{ + URL: hookURL, + ProxyURL: proxyURL, + }, nil +} + +// Post RingCentral message +func (s *RingCentral) Post(workload string, namespace string, message string, fields []Field, severity string) error { + payload := RingCentralPayload{ + Activity: fmt.Sprintf("%s.%s", workload, namespace), + } + + var statusEmoji string + switch strings.ToLower(severity) { + case "info": + statusEmoji = "\u2705" // Check Mark + case "error": + statusEmoji = "\u274C" // Cross Mark + default: + statusEmoji = "" // No emoji for other severities + } + + body := []map[string]interface{}{ + { + "type": "TextBlock", + "text": fmt.Sprintf("%s %s", statusEmoji, message), + "wrap": true, + "weight": "bolder", + "size": "medium", + }, + } + + for _, f := range fields { + body = append(body, map[string]interface{}{ + "type": "FactSet", + "facts": []map[string]string{ + { + "title": f.Name, + "value": f.Value, + }, + }, + }) + } + + attachment := RingCentralAttachment{ + Schema: "http://adaptivecards.io/schemas/adaptive-card.json", + Type: "AdaptiveCard", + Version: "1.0", + Body: body, + } + + payload.Attachments = []RingCentralAttachment{attachment} + + err := postMessage(s.URL, "", s.ProxyURL, payload) + if err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + + return nil +} diff --git a/pkg/notifier/ringcentral_test.go b/pkg/notifier/ringcentral_test.go new file mode 100644 index 000000000..488304429 --- /dev/null +++ b/pkg/notifier/ringcentral_test.go @@ -0,0 +1,69 @@ +/* +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" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRingCentral_NewRingCentral(t *testing.T) { + _, err := NewRingCentral("invalid-url", "") + require.Error(t, err) + + ringCentral, err := NewRingCentral("http://localhost", "") + require.NoError(t, err) + require.Equal(t, "http://localhost", ringCentral.URL) +} + +func TestRingCentral_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 := io.ReadAll(r.Body) + require.NoError(t, err) + + var payload = RingCentralPayload{} + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + require.Equal(t, "podinfo.test", payload.Activity) + require.Equal(t, len(fields)+1, len(payload.Attachments[0].Body)) + require.Equal(t, "http://adaptivecards.io/schemas/adaptive-card.json", payload.Attachments[0].Schema) + require.Equal(t, "AdaptiveCard", payload.Attachments[0].Type) + require.Equal(t, "1.0", payload.Attachments[0].Version) + })) + defer ts.Close() + + ringCentral, err := NewRingCentral(ts.URL, "") + require.NoError(t, err) + + err = ringCentral.Post("podinfo", "test", "test", fields, "info") + require.NoError(t, err) + err = ringCentral.Post("podinfo", "test", "test", fields, "error") + require.NoError(t, err) + err = ringCentral.Post("podinfo", "test", "test", fields, "warn") + require.NoError(t, err) + +}