From cbe52b505a3c9ef7ef52af0f8c638407b21d2128 Mon Sep 17 00:00:00 2001 From: Alan Hollis Date: Wed, 6 Oct 2021 14:14:47 +0100 Subject: [PATCH] Add opsgenie provider Signed-off-by: Alan Hollis --- api/v1beta1/provider_types.go | 3 +- ...ification.toolkit.fluxcd.io_providers.yaml | 1 + docs/spec/v1beta1/provider.md | 48 ++++++++++- internal/notifier/factory.go | 2 + internal/notifier/opsgenie.go | 82 +++++++++++++++++++ internal/notifier/opsgenie_test.go | 45 ++++++++++ 6 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 internal/notifier/opsgenie.go create mode 100644 internal/notifier/opsgenie_test.go diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go index b5eb1ddd1..76a560c39 100644 --- a/api/v1beta1/provider_types.go +++ b/api/v1beta1/provider_types.go @@ -28,7 +28,7 @@ const ( // ProviderSpec defines the desired state of Provider type ProviderSpec struct { // Type of provider - // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix; + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie; // +required Type string `json:"type"` @@ -80,6 +80,7 @@ const ( TelegramProvider string = "telegram" LarkProvider string = "lark" Matrix string = "matrix" + OpsgenieProvider string = "opsgenie" ) // ProviderStatus defines the observed state of Provider diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 091f1d5e8..33829f00b 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -90,6 +90,7 @@ spec: - telegram - lark - matrix + - opsgenie type: string username: description: Bot username for this provider diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md index ca191f495..e556e3b93 100644 --- a/docs/spec/v1beta1/provider.md +++ b/docs/spec/v1beta1/provider.md @@ -9,7 +9,7 @@ Spec: ```go type ProviderSpec struct { // Type of provider - // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix; + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie // +required Type string `json:"type"` @@ -56,6 +56,7 @@ Notification providers: * Matrix * Azure Event Hub * Generic webhook +* Opsgenie Git commit status providers: @@ -225,7 +226,7 @@ Note that `spec.channel` holds the room id. ### Lark -For sending notifications to Lark, You will have to +For sending notifications to Lark, you will have to [add a bot to the group](https://www.larksuite.com/hc/en-US/articles/360048487736-Bot-Use-bots-in-groups#III.%20How%20to%20configure%20custom%20bots%20in%20a%20group%C2%A0) and set up a webhook for the bot. This serves as the address field in the secret: @@ -248,6 +249,36 @@ spec: name: lark-token ``` + +### Opsgenie + +For sending notifications to Opsgenie, you will have to +[add a REST api integration](https://support.atlassian.com/opsgenie/docs/create-a-default-api-integration/) +and setup a api integration for notification provider. + +A secret needs to be generated with the api key given by Opsgenie for the integration + +```shell +kubectl create secret generic opsgenie-token \ +--from-literal=token= +``` + +Then reference the secret in `spec.secretRef`: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta1 +kind: Provider +metadata: + name: opsgenie + namespace: default +spec: + type: opsgenie + address: https://api.opsgenie.com/v2/alerts + secretRef: + name: opsgenie-token +``` + + ### Git commit status The GitHub, GitLab, Bitbucket, and Azure DevOps provider will write to the @@ -309,6 +340,19 @@ data: token: : ``` + +Opsgenie uses an api key to authenticate [api key](https://support.atlassian.com/opsgenie/docs/api-key-management/). +The providers require a secret in the same format, with the api key as the value for the token key: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: api-token + namespace: default +data: + token: +``` ### Azure Event Hub The Azure Event Hub supports two authentication methods, [JWT](https://docs.microsoft.com/en-us/azure/event-hubs/authenticate-application) diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index 95e3c94ae..8093b8fc5 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -83,6 +83,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewLark(f.URL) case v1beta1.Matrix: n, err = NewMatrix(f.URL, f.Token, f.Channel) + case v1beta1.OpsgenieProvider: + n, err = NewOpsgenie(f.URL, f.ProxyURL, f.CertPool, f.Token) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/internal/notifier/opsgenie.go b/internal/notifier/opsgenie.go new file mode 100644 index 000000000..2cbe52610 --- /dev/null +++ b/internal/notifier/opsgenie.go @@ -0,0 +1,82 @@ +/* +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 ( + "crypto/x509" + "errors" + "fmt" + "net/url" + + "github.com/fluxcd/pkg/runtime/events" + "github.com/hashicorp/go-retryablehttp" +) + +type Opsgenie struct { + URL string + ProxyURL string + CertPool *x509.CertPool + ApiKey string +} + +type OpsgenieAlert struct { + Message string `json:"message"` + Description string `json:"description"` + Details map[string]string `json:"details"` +} + +// NewSlack validates the Slack URL and returns a Slack object +func NewOpsgenie(hookURL string, proxyURL string, certPool *x509.CertPool, token string) (*Opsgenie, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid Opsgenie hook URL %s", hookURL) + } + + if token == "" { + return nil, errors.New("empty Opsgenie apikey/token") + } + + return &Opsgenie{ + URL: hookURL, + ProxyURL: proxyURL, + CertPool: certPool, + ApiKey: token, + }, nil +} + +// Post opsgenie alert message +func (s *Opsgenie) Post(event events.Event) error { + // Skip any update events + if isCommitStatus(event.Metadata, "update") { + return nil + } + + payload := OpsgenieAlert{ + Message: event.InvolvedObject.Kind + "/" + event.InvolvedObject.Name, + Description: event.Message, + Details: event.Metadata, + } + + err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(req *retryablehttp.Request) { + req.Header.Set("Authorization", "GenieKey "+s.ApiKey) + }) + + if err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + return nil +} diff --git a/internal/notifier/opsgenie_test.go b/internal/notifier/opsgenie_test.go new file mode 100644 index 000000000..506d76340 --- /dev/null +++ b/internal/notifier/opsgenie_test.go @@ -0,0 +1,45 @@ +/* +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 TestOpsgenie_Post(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + var payload OpsgenieAlert + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + + })) + defer ts.Close() + + opsgenie, err := NewOpsgenie(ts.URL, "", nil, "token") + require.NoError(t, err) + + err = opsgenie.Post(testEvent()) + require.NoError(t, err) +}