Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/datadoghq/v1alpha1/datadoggenericresource_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type SupportedResourcesType string

// When adding a new type, make sure to update the kubebuilder validation enum marker
const (
Dashboard SupportedResourcesType = "dashboard"
Downtime SupportedResourcesType = "downtime"
Monitor SupportedResourcesType = "monitor"
Notebook SupportedResourcesType = "notebook"
Expand All @@ -24,7 +25,7 @@ const (
// +k8s:openapi-gen=true
type DatadogGenericResourceSpec struct {
// Type is the type of the API object
// +kubebuilder:validation:Enum=downtime;monitor;notebook;synthetics_api_test;synthetics_browser_test
// +kubebuilder:validation:Enum=dashboard;downtime;monitor;notebook;synthetics_api_test;synthetics_browser_test
Type SupportedResourcesType `json:"type"`
// JsonSpec is the specification of the API object
JsonSpec string `json:"jsonSpec"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

var allowedCustomResourcesEnumMap = map[SupportedResourcesType]string{
Dashboard: "",
Downtime: "",
Monitor: "",
Notebook: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ spec:
type:
description: Type is the type of the API object
enum:
- dashboard
Comment thread
wdhif marked this conversation as resolved.
- downtime
- monitor
- notebook
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"type": {
"description": "Type is the type of the API object",
"enum": [
"dashboard",
"downtime",
"monitor",
"notebook",
Expand Down
1 change: 1 addition & 0 deletions docs/datadog_generic_resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ A `DatadogGenericResource` object has two fields:
| `synthetics_browser_test` | v1.12.0 | https://docs.datadoghq.com/api/latest/synthetics/#create-a-browser-test | [Browser test manifest](../examples/datadoggenericresource/browser-test-sample.yaml) |
| `monitor` | v1.13.0 | https://docs.datadoghq.com/api/latest/monitors/#create-a-monitor | [Monitor manifest](../examples/datadoggenericresource/monitor-sample.yaml) |
| `downtime` | v1.22.0 | https://docs.datadoghq.com/api/latest/downtimes/#schedule-a-downtime | [Downtime manifest](../examples/datadoggenericresource/downtime-sample.yaml) |
| `dashboard` | v1.27.0 | https://docs.datadoghq.com/api/latest/dashboards/#create-a-dashboard | [Dashboard manifest](../examples/datadoggenericresource/dashboard-sample.yaml) |

## Prerequisites

Expand Down
54 changes: 54 additions & 0 deletions examples/datadoggenericresource/dashboard-sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
apiVersion: datadoghq.com/v1alpha1
kind: DatadogGenericResource
metadata:
name: ddgr-dashboard-sample
spec:
type: dashboard
jsonSpec: |-
{
"title": "Example Dashboard (DatadogGenericResource)",
"layout_type": "ordered",
"tags": [
"team:example"
],
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "System CPU Usage",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"requests": [
{
"formulas": [
{
"formula": "query1"
}
],
"queries": [
{
"name": "query1",
"data_source": "metrics",
"query": "avg:system.cpu.user{*} by {host}"
}
],
"response_format": "timeseries",
"style": {
"palette": "dog_classic",
"line_type": "solid",
"line_width": "normal"
},
"display_type": "line"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 3
}
}
]
}
3 changes: 3 additions & 0 deletions internal/controller/datadoggenericresource/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (

type Reconciler struct {
client client.Client
datadogDashboardsClient *datadogV1.DashboardsApi
datadogSyntheticsClient *datadogV1.SyntheticsApi
datadogNotebooksClient *datadogV1.NotebooksApi
datadogMonitorsClient *datadogV1.MonitorsApi
Expand All @@ -57,6 +58,7 @@ func NewReconciler(client client.Client, creds config.Creds, scheme *runtime.Sch

return &Reconciler{
client: client,
datadogDashboardsClient: ddClient.DashboardsClient,
datadogSyntheticsClient: ddClient.SyntheticsClient,
datadogNotebooksClient: ddClient.NotebooksClient,
datadogMonitorsClient: ddClient.MonitorsClient,
Expand All @@ -74,6 +76,7 @@ func (r *Reconciler) UpdateDatadogClient(newCreds config.Creds) error {
if err != nil {
return fmt.Errorf("unable to create Datadog API Client in DatadogGenericResource: %w", err)
}
r.datadogDashboardsClient = ddClient.DashboardsClient
r.datadogSyntheticsClient = ddClient.SyntheticsClient
r.datadogNotebooksClient = ddClient.NotebooksClient
r.datadogMonitorsClient = ddClient.MonitorsClient
Expand Down
95 changes: 95 additions & 0 deletions internal/controller/datadoggenericresource/dashboards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package datadoggenericresource

import (
"context"
"encoding/json"

"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
"github.com/go-logr/logr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
)

type DashboardHandler struct{}

func (h *DashboardHandler) createResourcefunc(r *Reconciler, logger logr.Logger, instance *v1alpha1.DatadogGenericResource, status *v1alpha1.DatadogGenericResourceStatus, now metav1.Time, hash string) error {
createdDashboard, err := createDashboard(r.datadogAuth, r.datadogDashboardsClient, instance)
if err != nil {
logger.Error(err, "error creating dashboard")
updateErrStatus(status, now, v1alpha1.DatadogSyncStatusCreateError, "CreatingCustomResource", err)
return err
}
logger.Info("created a new dashboard", "dashboard Id", createdDashboard.GetId())
updateStatusFromDashboard(createdDashboard, status, hash)
return nil
}

// updateStatusFromDashboard populates the status fields from a Datadog Dashboard API response.
func updateStatusFromDashboard(dashboard datadogV1.Dashboard, status *v1alpha1.DatadogGenericResourceStatus, hash string) {
status.Id = dashboard.GetId()
createdTime := metav1.NewTime(dashboard.GetCreatedAt())
status.Created = &createdTime
status.LastForceSyncTime = &createdTime
status.Creator = dashboard.GetAuthorHandle()
status.SyncStatus = v1alpha1.DatadogSyncStatusOK
status.CurrentHash = hash
}

func (h *DashboardHandler) getResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
_, err := getDashboard(r.datadogAuth, r.datadogDashboardsClient, instance.Status.Id)
return err
}

func (h *DashboardHandler) updateResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
_, err := updateDashboard(r.datadogAuth, r.datadogDashboardsClient, instance)
return err
}

func (h *DashboardHandler) deleteResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
return deleteDashboard(r.datadogAuth, r.datadogDashboardsClient, instance.Status.Id)
}

func getDashboard(auth context.Context, client *datadogV1.DashboardsApi, dashboardID string) (datadogV1.Dashboard, error) {
dashboard, _, err := client.GetDashboard(auth, dashboardID)
if err != nil {
return datadogV1.Dashboard{}, translateClientError(err, "error getting dashboard")
}
return dashboard, nil
}

func createDashboard(auth context.Context, client *datadogV1.DashboardsApi, instance *v1alpha1.DatadogGenericResource) (datadogV1.Dashboard, error) {
dashboardCreateData := &datadogV1.Dashboard{}
if err := json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardCreateData); err != nil {
return datadogV1.Dashboard{}, translateClientError(err, "error unmarshalling dashboard spec")
}
dashboard, _, err := client.CreateDashboard(auth, *dashboardCreateData)
if err != nil {
Comment thread
wdhif marked this conversation as resolved.
return datadogV1.Dashboard{}, translateClientError(err, "error creating dashboard")
}
return dashboard, nil
}

func updateDashboard(auth context.Context, client *datadogV1.DashboardsApi, instance *v1alpha1.DatadogGenericResource) (datadogV1.Dashboard, error) {
dashboardUpdateData := &datadogV1.Dashboard{}
if err := json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardUpdateData); err != nil {
return datadogV1.Dashboard{}, translateClientError(err, "error unmarshalling dashboard spec")
}
dashboardUpdated, _, err := client.UpdateDashboard(auth, instance.Status.Id, *dashboardUpdateData)
if err != nil {
return datadogV1.Dashboard{}, translateClientError(err, "error updating dashboard")
Comment thread
wdhif marked this conversation as resolved.
}
return dashboardUpdated, nil
}

func deleteDashboard(auth context.Context, client *datadogV1.DashboardsApi, dashboardID string) error {
if _, _, err := client.DeleteDashboard(auth, dashboardID); err != nil {
return translateClientError(err, "error deleting dashboard")
}
return nil
}
78 changes: 78 additions & 0 deletions internal/controller/datadoggenericresource/dashboards_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package datadoggenericresource

import (
"testing"
"time"

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

"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
)

func Test_updateStatusFromDashboard(t *testing.T) {
hash := "test-hash"
createdAt := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)

tests := []struct {
name string
dashboard datadogV1.Dashboard
expectedStatus v1alpha1.DatadogGenericResourceStatus
}{
{
name: "all fields populated",
dashboard: func() datadogV1.Dashboard {
d := datadogV1.Dashboard{}
d.SetId("abc-123")
d.SetAuthorHandle("user@example.com")
d.SetCreatedAt(createdAt)
return d
}(),
expectedStatus: v1alpha1.DatadogGenericResourceStatus{
Id: "abc-123",
Creator: "user@example.com",
SyncStatus: v1alpha1.DatadogSyncStatusOK,
CurrentHash: hash,
},
},
{
name: "missing author handle",
dashboard: func() datadogV1.Dashboard {
d := datadogV1.Dashboard{}
d.SetId("abc-456")
d.SetCreatedAt(createdAt)
return d
}(),
expectedStatus: v1alpha1.DatadogGenericResourceStatus{
Id: "abc-456",
Creator: "",
SyncStatus: v1alpha1.DatadogSyncStatusOK,
CurrentHash: hash,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status := &v1alpha1.DatadogGenericResourceStatus{}
updateStatusFromDashboard(tt.dashboard, status, hash)

assert.Equal(t, tt.expectedStatus.Id, status.Id)
assert.Equal(t, tt.expectedStatus.Creator, status.Creator)
assert.Equal(t, tt.expectedStatus.SyncStatus, status.SyncStatus)
assert.Equal(t, tt.expectedStatus.CurrentHash, status.CurrentHash)
assert.Equal(t, createdAt, status.Created.Time)
assert.Equal(t, createdAt, status.LastForceSyncTime.Time)
})
}
}

func Test_DashboardHandler_getHandler(t *testing.T) {
handler := getHandler(v1alpha1.Dashboard)
assert.IsType(t, &DashboardHandler{}, handler)
}
2 changes: 2 additions & 0 deletions internal/controller/datadoggenericresource/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func apiCreateAndUpdateStatus(r *Reconciler, logger logr.Logger, instance *v1alp

func getHandler(resourceType v1alpha1.SupportedResourcesType) ResourceHandler {
switch resourceType {
case v1alpha1.Dashboard:
return &DashboardHandler{}
case v1alpha1.Downtime:
return &DowntimeHandler{}
case v1alpha1.Monitor:
Expand Down
3 changes: 3 additions & 0 deletions pkg/datadogclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func InitDatadogDashboardClient(logger logr.Logger, creds config.Creds) (Datadog
}

type DatadogGenericClient struct {
DashboardsClient *datadogV1.DashboardsApi
SyntheticsClient *datadogV1.SyntheticsApi
NotebooksClient *datadogV1.NotebooksApi
MonitorsClient *datadogV1.MonitorsApi
Expand All @@ -111,6 +112,7 @@ func InitDatadogGenericClient(logger logr.Logger, creds config.Creds) (DatadogGe

configV1 := datadogapi.NewConfiguration()
apiClient := datadogapi.NewAPIClient(configV1)
dashboardsClient := datadogV1.NewDashboardsApi(apiClient)
syntheticsClient := datadogV1.NewSyntheticsApi(apiClient)
notebooksClient := datadogV1.NewNotebooksApi(apiClient)
monitorsClient := datadogV1.NewMonitorsApi(apiClient)
Expand All @@ -122,6 +124,7 @@ func InitDatadogGenericClient(logger logr.Logger, creds config.Creds) (DatadogGe
}

return DatadogGenericClient{
DashboardsClient: dashboardsClient,
SyntheticsClient: syntheticsClient,
NotebooksClient: notebooksClient,
MonitorsClient: monitorsClient,
Expand Down
Loading