167 lines
5.3 KiB
Go
167 lines
5.3 KiB
Go
/*
|
|
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
|
|
}
|