diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go index 672a9dc24..6e1507248 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;opsgenie;alertmanager; + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana; // +required Type string `json:"type"` @@ -71,6 +71,7 @@ type ProviderSpec struct { const ( GenericProvider string = "generic" SlackProvider string = "slack" + GrafanaProvider string = "grafana" DiscordProvider string = "discord" MSTeamsProvider string = "msteams" RocketProvider string = "rocket" diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index b75a14bf0..272f6816b 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -102,6 +102,7 @@ spec: - matrix - opsgenie - alertmanager + - grafana type: string username: description: Bot username for this provider diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md index 27e0654a9..f5699018f 100644 --- a/docs/spec/v1beta1/provider.md +++ b/docs/spec/v1beta1/provider.md @@ -52,6 +52,7 @@ Notification providers: | Discord | discord | | Generic webhook | generic | | Google Chat | googlechat | +| Grafana | grafana | | Lark | lark | | Matrix | matrix | | Microsoft Teams | msteams | @@ -122,7 +123,7 @@ kubectl create secret generic webhook-url \ Note that the secret must contain an `address` field. The provider type can be: `slack`, `msteams`, `rocket`, `discord`, `googlechat`, `webex`, `sentry`, -`telegram`, `lark`, `matrix`, `azureeventhub`, `opsgenie`, `alertmanager` or `generic`. +`telegram`, `lark`, `matrix`, `azureeventhub`, `opsgenie`, `alertmanager`, `grafana` or `generic`. When type `generic` is specified, the notification controller will post the incoming [event](event.md) in JSON format to the webhook address. @@ -336,7 +337,6 @@ spec: name: opsgenie-token ``` - ### Prometheus Alertmanager Sends notifications to [alertmanager v2 api](https://github.com/prometheus/alertmanager/blob/main/api/v2/openapi.yaml) if alert manager has basic authentication configured it is recommended to use @@ -370,7 +370,6 @@ The provider will send the following labels for the event. | name | The name of the involved object associated with the event | | namespace | The namespace of the involved object associated with the event | - ### Slack App It is possible to use a Slack App bot integration to send messages. To obtain a bot token, follow @@ -409,6 +408,35 @@ spec: ``` +### Grafana + +To send notifications to [Grafana annotations API](https://grafana.com/docs/grafana/latest/http_api/annotations/), +you have to enable the annotations on a Dashboard like so: + +- Annotations > Query > Enable Match any +- Annotations > Query > Tags (Add Tag: `flux`) + +If Grafana has authentication configured, create a Kubernetes Secret with the API URL and the API token: +```shell +kubectl create secret generic grafana-token \ +--from-literal=token= \ +--from-literal=address=https:///api/annotations +``` + +Then reference the secret in `spec.secretRef`: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1beta1 +kind: Provider +metadata: + name: grafana + namespace: default +spec: + type: grafana + secretRef: + name: grafana-token +``` + ### Git commit status The GitHub, GitLab, Bitbucket, and Azure DevOps provider will write to the @@ -491,6 +519,7 @@ metadata: 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 75f9dc9e5..e94bd2048 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -89,6 +89,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewOpsgenie(f.URL, f.ProxyURL, f.CertPool, f.Token) case v1beta1.AlertManagerProvider: n, err = NewAlertmanager(f.URL, f.ProxyURL, f.CertPool) + case v1beta1.GrafanaProvider: + n, err = NewGrafana(f.URL, f.ProxyURL, f.Token, f.CertPool) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/internal/notifier/grafana.go b/internal/notifier/grafana.go new file mode 100644 index 000000000..6c6f2c010 --- /dev/null +++ b/internal/notifier/grafana.go @@ -0,0 +1,90 @@ +/* +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" + "fmt" + "net/url" + "strings" + + "github.com/fluxcd/pkg/runtime/events" + "github.com/hashicorp/go-retryablehttp" +) + +// Discord holds the hook URL +type Grafana struct { + URL string + Token string + ProxyURL string + CertPool *x509.CertPool +} + +// GraphiteAnnotation represents a Grafana API annotation in Graphite format +type GraphitePayload struct { + //What string `json:"what"` //optional + When int64 `json:"when"` //optional unix timestamp (ms) + Text string `json:"text"` + Tags []string `json:"tags,omitempty"` +} + +// NewGrafana validates the Grafana URL and returns a Grafana object +func NewGrafana(URL string, proxyURL string, token string, certPool *x509.CertPool) (*Grafana, error) { + _, err := url.ParseRequestURI(URL) + if err != nil { + return nil, fmt.Errorf("invalid Grafana URL %s", URL) + } + + return &Grafana{ + URL: URL, + ProxyURL: proxyURL, + Token: token, + CertPool: certPool, + }, nil +} + +// Post annotation +func (s *Grafana) Post(event events.Event) error { + // Skip any update events + if isCommitStatus(event.Metadata, "update") { + return nil + } + + sfields := make([]string, 0, len(event.Metadata)) + sfields = append(sfields, "flux") + sfields = append(sfields, event.ReportingController) + for k, v := range event.Metadata { + sfields = append(sfields, fmt.Sprintf("%s: %s", k, v)) + } + // add tag to filter on grafana + + payload := GraphitePayload{ + When: event.Timestamp.Unix(), + Text: fmt.Sprintf("%s/%s.%s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace), + Tags: sfields, + } + + err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) { + if s.Token != "" { + request.Header.Add("Authorization", "Bearer "+s.Token) + } + }) + if err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + return nil +} diff --git a/internal/notifier/grafana_test.go b/internal/notifier/grafana_test.go new file mode 100644 index 000000000..8705c38dc --- /dev/null +++ b/internal/notifier/grafana_test.go @@ -0,0 +1,49 @@ +/* +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 TestGrafana_Post(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var payload = GraphitePayload{} + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + + require.Equal(t, "gitrepository/webapp.gitops-system", payload.Text) + require.Equal(t, "flux", payload.Tags[0]) + require.Equal(t, "source-controller", payload.Tags[1]) + require.Equal(t, "test: metadata", payload.Tags[2]) + })) + defer ts.Close() + + grafana, err := NewGrafana(ts.URL, "", "", nil) + require.NoError(t, err) + + err = grafana.Post(testEvent()) + require.NoError(t, err) +}