-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create datadog notification provider
Signed-off-by: Michael Parker <michael@parker.gg>
- Loading branch information
Showing
10 changed files
with
373 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.