Skip to content

Commit 7c966f8

Browse files
wdhifCopilot
andauthored
feat(ddgr): add dashboard support (#2906)
* feat(ddgr): add dashboard support Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com> * Update internal/controller/datadoggenericresource/dashboards_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(ddgr): catch json errors Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com> * fix: add missing post-development tasks Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com> --------- Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a646370 commit 7c966f8

File tree

11 files changed

+241
-1
lines changed

11 files changed

+241
-1
lines changed

api/datadoghq/v1alpha1/datadoggenericresource_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type SupportedResourcesType string
1313

1414
// When adding a new type, make sure to update the kubebuilder validation enum marker
1515
const (
16+
Dashboard SupportedResourcesType = "dashboard"
1617
Downtime SupportedResourcesType = "downtime"
1718
Monitor SupportedResourcesType = "monitor"
1819
Notebook SupportedResourcesType = "notebook"
@@ -24,7 +25,7 @@ const (
2425
// +k8s:openapi-gen=true
2526
type DatadogGenericResourceSpec struct {
2627
// Type is the type of the API object
27-
// +kubebuilder:validation:Enum=downtime;monitor;notebook;synthetics_api_test;synthetics_browser_test
28+
// +kubebuilder:validation:Enum=dashboard;downtime;monitor;notebook;synthetics_api_test;synthetics_browser_test
2829
Type SupportedResourcesType `json:"type"`
2930
// JsonSpec is the specification of the API object
3031
JsonSpec string `json:"jsonSpec"`

api/datadoghq/v1alpha1/datadoggenericresource_validation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
)
1313

1414
var allowedCustomResourcesEnumMap = map[SupportedResourcesType]string{
15+
Dashboard: "",
1516
Downtime: "",
1617
Monitor: "",
1718
Notebook: "",

config/crd/bases/v1/datadoghq.com_datadoggenericresources.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ spec:
5757
type:
5858
description: Type is the type of the API object
5959
enum:
60+
- dashboard
6061
- downtime
6162
- monitor
6263
- notebook

config/crd/bases/v1/datadoghq.com_datadoggenericresources_v1alpha1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"type": {
2525
"description": "Type is the type of the API object",
2626
"enum": [
27+
"dashboard",
2728
"downtime",
2829
"monitor",
2930
"notebook",

docs/datadog_generic_resource.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ A `DatadogGenericResource` object has two fields:
5050
| `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) |
5151
| `monitor` | v1.13.0 | https://docs.datadoghq.com/api/latest/monitors/#create-a-monitor | [Monitor manifest](../examples/datadoggenericresource/monitor-sample.yaml) |
5252
| `downtime` | v1.22.0 | https://docs.datadoghq.com/api/latest/downtimes/#schedule-a-downtime | [Downtime manifest](../examples/datadoggenericresource/downtime-sample.yaml) |
53+
| `dashboard` | v1.27.0 | https://docs.datadoghq.com/api/latest/dashboards/#create-a-dashboard | [Dashboard manifest](../examples/datadoggenericresource/dashboard-sample.yaml) |
5354

5455
## Prerequisites
5556

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
apiVersion: datadoghq.com/v1alpha1
2+
kind: DatadogGenericResource
3+
metadata:
4+
name: ddgr-dashboard-sample
5+
spec:
6+
type: dashboard
7+
jsonSpec: |-
8+
{
9+
"title": "Example Dashboard (DatadogGenericResource)",
10+
"layout_type": "ordered",
11+
"tags": [
12+
"team:example"
13+
],
14+
"widgets": [
15+
{
16+
"definition": {
17+
"type": "timeseries",
18+
"title": "System CPU Usage",
19+
"title_size": "16",
20+
"title_align": "left",
21+
"show_legend": true,
22+
"requests": [
23+
{
24+
"formulas": [
25+
{
26+
"formula": "query1"
27+
}
28+
],
29+
"queries": [
30+
{
31+
"name": "query1",
32+
"data_source": "metrics",
33+
"query": "avg:system.cpu.user{*} by {host}"
34+
}
35+
],
36+
"response_format": "timeseries",
37+
"style": {
38+
"palette": "dog_classic",
39+
"line_type": "solid",
40+
"line_width": "normal"
41+
},
42+
"display_type": "line"
43+
}
44+
]
45+
},
46+
"layout": {
47+
"x": 0,
48+
"y": 0,
49+
"width": 6,
50+
"height": 3
51+
}
52+
}
53+
]
54+
}

internal/controller/datadoggenericresource/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939

4040
type Reconciler struct {
4141
client client.Client
42+
datadogDashboardsClient *datadogV1.DashboardsApi
4243
datadogSyntheticsClient *datadogV1.SyntheticsApi
4344
datadogNotebooksClient *datadogV1.NotebooksApi
4445
datadogMonitorsClient *datadogV1.MonitorsApi
@@ -57,6 +58,7 @@ func NewReconciler(client client.Client, creds config.Creds, scheme *runtime.Sch
5758

5859
return &Reconciler{
5960
client: client,
61+
datadogDashboardsClient: ddClient.DashboardsClient,
6062
datadogSyntheticsClient: ddClient.SyntheticsClient,
6163
datadogNotebooksClient: ddClient.NotebooksClient,
6264
datadogMonitorsClient: ddClient.MonitorsClient,
@@ -74,6 +76,7 @@ func (r *Reconciler) UpdateDatadogClient(newCreds config.Creds) error {
7476
if err != nil {
7577
return fmt.Errorf("unable to create Datadog API Client in DatadogGenericResource: %w", err)
7678
}
79+
r.datadogDashboardsClient = ddClient.DashboardsClient
7780
r.datadogSyntheticsClient = ddClient.SyntheticsClient
7881
r.datadogNotebooksClient = ddClient.NotebooksClient
7982
r.datadogMonitorsClient = ddClient.MonitorsClient
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package datadoggenericresource
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
12+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
13+
"github.com/go-logr/logr"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
16+
"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
17+
)
18+
19+
type DashboardHandler struct{}
20+
21+
func (h *DashboardHandler) createResourcefunc(r *Reconciler, logger logr.Logger, instance *v1alpha1.DatadogGenericResource, status *v1alpha1.DatadogGenericResourceStatus, now metav1.Time, hash string) error {
22+
createdDashboard, err := createDashboard(r.datadogAuth, r.datadogDashboardsClient, instance)
23+
if err != nil {
24+
logger.Error(err, "error creating dashboard")
25+
updateErrStatus(status, now, v1alpha1.DatadogSyncStatusCreateError, "CreatingCustomResource", err)
26+
return err
27+
}
28+
logger.Info("created a new dashboard", "dashboard Id", createdDashboard.GetId())
29+
updateStatusFromDashboard(createdDashboard, status, hash)
30+
return nil
31+
}
32+
33+
// updateStatusFromDashboard populates the status fields from a Datadog Dashboard API response.
34+
func updateStatusFromDashboard(dashboard datadogV1.Dashboard, status *v1alpha1.DatadogGenericResourceStatus, hash string) {
35+
status.Id = dashboard.GetId()
36+
createdTime := metav1.NewTime(dashboard.GetCreatedAt())
37+
status.Created = &createdTime
38+
status.LastForceSyncTime = &createdTime
39+
status.Creator = dashboard.GetAuthorHandle()
40+
status.SyncStatus = v1alpha1.DatadogSyncStatusOK
41+
status.CurrentHash = hash
42+
}
43+
44+
func (h *DashboardHandler) getResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
45+
_, err := getDashboard(r.datadogAuth, r.datadogDashboardsClient, instance.Status.Id)
46+
return err
47+
}
48+
49+
func (h *DashboardHandler) updateResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
50+
_, err := updateDashboard(r.datadogAuth, r.datadogDashboardsClient, instance)
51+
return err
52+
}
53+
54+
func (h *DashboardHandler) deleteResourcefunc(r *Reconciler, instance *v1alpha1.DatadogGenericResource) error {
55+
return deleteDashboard(r.datadogAuth, r.datadogDashboardsClient, instance.Status.Id)
56+
}
57+
58+
func getDashboard(auth context.Context, client *datadogV1.DashboardsApi, dashboardID string) (datadogV1.Dashboard, error) {
59+
dashboard, _, err := client.GetDashboard(auth, dashboardID)
60+
if err != nil {
61+
return datadogV1.Dashboard{}, translateClientError(err, "error getting dashboard")
62+
}
63+
return dashboard, nil
64+
}
65+
66+
func createDashboard(auth context.Context, client *datadogV1.DashboardsApi, instance *v1alpha1.DatadogGenericResource) (datadogV1.Dashboard, error) {
67+
dashboardCreateData := &datadogV1.Dashboard{}
68+
if err := json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardCreateData); err != nil {
69+
return datadogV1.Dashboard{}, translateClientError(err, "error unmarshalling dashboard spec")
70+
}
71+
dashboard, _, err := client.CreateDashboard(auth, *dashboardCreateData)
72+
if err != nil {
73+
return datadogV1.Dashboard{}, translateClientError(err, "error creating dashboard")
74+
}
75+
return dashboard, nil
76+
}
77+
78+
func updateDashboard(auth context.Context, client *datadogV1.DashboardsApi, instance *v1alpha1.DatadogGenericResource) (datadogV1.Dashboard, error) {
79+
dashboardUpdateData := &datadogV1.Dashboard{}
80+
if err := json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardUpdateData); err != nil {
81+
return datadogV1.Dashboard{}, translateClientError(err, "error unmarshalling dashboard spec")
82+
}
83+
dashboardUpdated, _, err := client.UpdateDashboard(auth, instance.Status.Id, *dashboardUpdateData)
84+
if err != nil {
85+
return datadogV1.Dashboard{}, translateClientError(err, "error updating dashboard")
86+
}
87+
return dashboardUpdated, nil
88+
}
89+
90+
func deleteDashboard(auth context.Context, client *datadogV1.DashboardsApi, dashboardID string) error {
91+
if _, _, err := client.DeleteDashboard(auth, dashboardID); err != nil {
92+
return translateClientError(err, "error deleting dashboard")
93+
}
94+
return nil
95+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package datadoggenericresource
7+
8+
import (
9+
"testing"
10+
"time"
11+
12+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
13+
"github.com/stretchr/testify/assert"
14+
15+
"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
16+
)
17+
18+
func Test_updateStatusFromDashboard(t *testing.T) {
19+
hash := "test-hash"
20+
createdAt := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
21+
22+
tests := []struct {
23+
name string
24+
dashboard datadogV1.Dashboard
25+
expectedStatus v1alpha1.DatadogGenericResourceStatus
26+
}{
27+
{
28+
name: "all fields populated",
29+
dashboard: func() datadogV1.Dashboard {
30+
d := datadogV1.Dashboard{}
31+
d.SetId("abc-123")
32+
d.SetAuthorHandle("user@example.com")
33+
d.SetCreatedAt(createdAt)
34+
return d
35+
}(),
36+
expectedStatus: v1alpha1.DatadogGenericResourceStatus{
37+
Id: "abc-123",
38+
Creator: "user@example.com",
39+
SyncStatus: v1alpha1.DatadogSyncStatusOK,
40+
CurrentHash: hash,
41+
},
42+
},
43+
{
44+
name: "missing author handle",
45+
dashboard: func() datadogV1.Dashboard {
46+
d := datadogV1.Dashboard{}
47+
d.SetId("abc-456")
48+
d.SetCreatedAt(createdAt)
49+
return d
50+
}(),
51+
expectedStatus: v1alpha1.DatadogGenericResourceStatus{
52+
Id: "abc-456",
53+
Creator: "",
54+
SyncStatus: v1alpha1.DatadogSyncStatusOK,
55+
CurrentHash: hash,
56+
},
57+
},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
status := &v1alpha1.DatadogGenericResourceStatus{}
63+
updateStatusFromDashboard(tt.dashboard, status, hash)
64+
65+
assert.Equal(t, tt.expectedStatus.Id, status.Id)
66+
assert.Equal(t, tt.expectedStatus.Creator, status.Creator)
67+
assert.Equal(t, tt.expectedStatus.SyncStatus, status.SyncStatus)
68+
assert.Equal(t, tt.expectedStatus.CurrentHash, status.CurrentHash)
69+
assert.Equal(t, createdAt, status.Created.Time)
70+
assert.Equal(t, createdAt, status.LastForceSyncTime.Time)
71+
})
72+
}
73+
}
74+
75+
func Test_DashboardHandler_getHandler(t *testing.T) {
76+
handler := getHandler(v1alpha1.Dashboard)
77+
assert.IsType(t, &DashboardHandler{}, handler)
78+
}

internal/controller/datadoggenericresource/utils.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ func apiCreateAndUpdateStatus(r *Reconciler, logger logr.Logger, instance *v1alp
5656

5757
func getHandler(resourceType v1alpha1.SupportedResourcesType) ResourceHandler {
5858
switch resourceType {
59+
case v1alpha1.Dashboard:
60+
return &DashboardHandler{}
5961
case v1alpha1.Downtime:
6062
return &DowntimeHandler{}
6163
case v1alpha1.Monitor:

0 commit comments

Comments
 (0)