Skip to content

Commit

Permalink
feat: create datadog notification provider
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Parker <michael@parker.gg>
  • Loading branch information
mrparkers committed Aug 14, 2023
1 parent ec10ba8 commit af15854
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 1 deletion.
3 changes: 2 additions & 1 deletion api/v1beta2/provider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ const (
OpsgenieProvider string = "opsgenie"
AlertManagerProvider string = "alertmanager"
PagerDutyProvider string = "pagerduty"
DataDogProvider string = "datadog"
)

// ProviderSpec defines the desired state of the Provider.
type ProviderSpec struct {
// Type specifies which Provider implementation to use.
// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;gitea;bitbucket;azuredevops;googlechat;googlepubsub;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch;pagerduty
// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;gitea;bitbucket;azuredevops;googlechat;googlepubsub;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch;pagerduty;datadog
// +required
Type string `json:"type"`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ spec:
- grafana
- githubdispatch
- pagerduty
- datadog
type: string
username:
description: Username specifies the name under which events are posted.
Expand Down
57 changes: 57 additions & 0 deletions docs/spec/v1beta2/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ The supported alerting providers are:
| [Generic webhook](#generic-webhook) | `generic` |
| [Generic webhook with HMAC](#generic-webhook-with-hmac) | `generic-hmac` |
| [Azure Event Hub](#azure-event-hub) | `azureeventhub` |
| [DataDog](#datadog) | `datadog` |
| [Discord](#discord) | `discord` |
| [GitHub dispatch](#github-dispatch) | `githubdispatch` |
| [Google Chat](#google-chat) | `googlechat` |
Expand Down Expand Up @@ -405,6 +406,62 @@ stringData:
address: "https://xxx.webhook.office.com/..."
```

##### DataDog

When `.spec.type` is set to `datadog`, the controller will send a payload for
an [Event](events.md#event-structure) to the provided DataDog API [Address](#address).

The Event will be formatted into a [DataDog Event](https://docs.datadoghq.com/api/latest/events/#post-an-event) and sent to the
API endpoint of the provided DataDog [Address](#address).

This Provider type supports the configuration of a [proxy URL](#https-proxy)
and/or [TLS certificates](#tls-certificates).

The metadata of the Event is included in the DataDog event as extra tags.

###### DataDog example

To configure a Provider for DataDog, create a Secret with [the `token`](#token-example)
set to a [DataDog API key](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys)
(not an application key!) and a `datadog` Provider with a [Secret reference](#secret-reference).

```yaml
---
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Provider
metadata:
name: datadog
namespace: default
spec:
type: datadog
address: https://api.datadoghq.com # DataDog Site US1
secretRef:
name: datadog-secret
---
apiVersion: v1
kind: Secret
metadata:
name: datadog-secret
namespace: default
stringData:
token: <DataDog API Key>
---
apiVersion: notification.toolkit.fluxcd.io/v1beta1
kind: Alert
metadata:
name: datadog-info
namespace: default
spec:
eventSeverity: info
eventSources:
- kind: HelmRelease
name: "*"
providerRef:
name: datadog
eventMetadata:
env: my-k8s-cluster # example of adding a custom `env` tag to the event
```

##### Discord

When `.spec.type` is set to `discord`, the controller will send a payload for
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1
github.com/Azure/azure-amqp-common-go/v4 v4.2.0
github.com/Azure/azure-event-hubs-go/v3 v3.6.1
github.com/DataDog/datadog-api-client-go/v2 v2.15.0
github.com/PagerDuty/go-pagerduty v1.7.0
github.com/containrrr/shoutrrr v0.7.1
github.com/fluxcd/notification-controller/api v1.0.0
Expand Down Expand Up @@ -62,6 +63,7 @@ require (
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/zstd v1.5.2 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand All @@ -83,6 +85,7 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,11 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-api-client-go/v2 v2.15.0 h1:5UVON1xs6Lul4d6R5TmLDqqSJxOkunkm/UdM/fjm+zc=
github.com/DataDog/datadog-api-client-go/v2 v2.15.0/go.mod h1:ZG8wS+y2rUmkRDJZQq7Og7EAPFPage+7vXcmuah2I9o=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
Expand Down Expand Up @@ -824,6 +828,8 @@ github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down
166 changes: 166 additions & 0 deletions internal/notifier/datadog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright 2023 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 (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"

eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
)

type DataDog struct {
apiClient *datadog.APIClient
eventsApi *datadogV1.EventsApi
apiKey string
}

// NewDataDog creates a new DataDog provider by mapping the notification provider API to sensible values for the DataDog API.
// url: The DataDog API endpoint to use. Examples: https://api.datadoghq.com, https://api.datadoghq.eu, etc.
// token: The DataDog API key (not the application key).
// headers: A map of extra tags to add to the event
func NewDataDog(address string, proxyUrl string, certPool *x509.CertPool, token string) (*DataDog, error) {
conf := datadog.NewConfiguration()

if token == "" {
return nil, fmt.Errorf("token cannot be empty")
}

baseUrl, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("failed to parse address %q: %w", address, err)
}

conf.Host = baseUrl.Host
conf.Scheme = baseUrl.Scheme

if proxyUrl != "" || certPool != nil {
transport := &http.Transport{}

if proxyUrl != "" {
proxy, err := url.Parse(proxyUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL %q: %w", proxyUrl, err)
}

transport.Proxy = http.ProxyURL(proxy)
}

if certPool != nil {
transport.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
}

conf.HTTPClient = &http.Client{
Transport: transport,
}
}

apiClient := datadog.NewAPIClient(conf)
eventsApi := datadogV1.NewEventsApi(apiClient)

return &DataDog{
apiClient: apiClient,
eventsApi: eventsApi,
apiKey: token,
}, nil
}

func (d *DataDog) Post(ctx context.Context, event eventv1.Event) error {
dataDogEvent := d.toDataDogEvent(&event)

_, _, err := d.eventsApi.CreateEvent(d.dataDogCtx(ctx), dataDogEvent)
if err != nil {
return fmt.Errorf("failed to post event to DataDog: %w", err)
}

return nil
}

// dataDogCtx returns a context with the DataDog API key set.
// This is one way to authenticate with the DataDog API.
func (d *DataDog) dataDogCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, datadog.ContextAPIKeys, map[string]datadog.APIKey{
"apiKeyAuth": {
Key: d.apiKey,
},
})
}

// toDataDogEvent converts an eventv1.Event to a datadogV1.EventCreateRequest.
func (d *DataDog) toDataDogEvent(event *eventv1.Event) datadogV1.EventCreateRequest {
return datadogV1.EventCreateRequest{
// Note: Title's printf format matches other events from datadog's kubernetes integration
Title: fmt.Sprintf("Events from the %s %s/%s", event.InvolvedObject.Kind, event.InvolvedObject.Name, event.InvolvedObject.Namespace),
Text: event.Message,
Tags: d.toDataDogTags(event),
// fluxcd matches the name datadog picked for their flux integration: https://docs.datadoghq.com/integrations/fluxcd/
SourceTypeName: strPtr("fluxcd"),
DateHappened: int64Ptr(event.Timestamp.Unix()),
AlertType: toDataDogAlertType(event),
}
}

// toDataDogTags parses an eventv1.Event to return a slice of tags.
// We set kind, name, and namespace to the appropriate values of the involved object.
func (d *DataDog) toDataDogTags(event *eventv1.Event) []string {
// Note: Datadog's built in kubernetes tagging is documented here: https://docs.datadoghq.com/containers/kubernetes/tag/?tab=containerizedagent#out-of-the-box-tags
tags := []string{
fmt.Sprintf("flux_reporting_controller:%s", event.ReportingController),
fmt.Sprintf("flux_reason:%s", event.Reason),
// Note: DataDog standardizes kubernetes tags as "kube_*": https://github.com/DataDog/datadog-agent/blob/82dc933aa86de037c70fe960384aa06a62e457a8/pkg/collector/corechecks/cluster/kubernetesapiserver/events_common.go#L48
fmt.Sprintf("kube_kind:%s", event.InvolvedObject.Kind),
fmt.Sprintf("kube_name:%s", event.InvolvedObject.Name),
fmt.Sprintf("kube_namespace:%s", event.InvolvedObject.Namespace),
}

// add extra tags from event metadata
for k, v := range event.Metadata {
tags = append(tags, fmt.Sprintf("%s:%s", k, v))
}

// Note: https://docs.datadoghq.com/getting_started/tagging/
// "Tags are converted to lowercase"
// To keep the events consistent, we run toLower on all input strings.
for idx := range tags {
tags[idx] = strings.ToLower(tags[idx])
}

return tags
}

// toDataDogAlertType parses an eventv1.Event to return a datadogV1.EventAlertType.
func toDataDogAlertType(event *eventv1.Event) *datadogV1.EventAlertType {
if event.Severity == eventv1.EventSeverityError {
return dataDogEventAlertTypePtr(datadogV1.EVENTALERTTYPE_ERROR)
}

return dataDogEventAlertTypePtr(datadogV1.EVENTALERTTYPE_INFO)
}

func dataDogEventAlertTypePtr(t datadogV1.EventAlertType) *datadogV1.EventAlertType {
return &t
}
53 changes: 53 additions & 0 deletions internal/notifier/datadog_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package notifier

import (
"context"
"crypto/x509"
"io"
"net/http"
"net/http/httptest"
"testing"

fuzz "github.com/AdaLogics/go-fuzz-headers"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/stretchr/testify/require"
)

func Fuzz_DataDog(f *testing.F) {
f.Add("token", "error", "", []byte{}, []byte{})
f.Add("token", "info", "", []byte{}, []byte{})

f.Fuzz(func(t *testing.T,
apiKey, severity, message string, seed, response []byte) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/events", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(response)
require.NoError(t, err)
_, err = io.Copy(io.Discard, r.Body)
require.NoError(t, err)
require.NoError(t, r.Body.Close())
})
ts := httptest.NewServer(mux)
defer ts.Close()

var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)

dd, err := NewDataDog(ts.URL, "", &cert, apiKey)
if err != nil {
return
}

event := eventv1.Event{}
_ = fuzz.NewConsumer(seed).GenerateStruct(&event)

if event.Metadata == nil {
event.Metadata = map[string]string{}
}

event.Message = message
event.Severity = severity

_ = dd.Post(context.TODO(), event)
})
}
Loading

0 comments on commit af15854

Please sign in to comment.