Skip to content

Commit f748031

Browse files
committed
feat(ddgr): add dashboard support
Signed-off-by: Wassim DHIF <wassim.dhif@datadoghq.com>
1 parent a646370 commit f748031

File tree

9 files changed

+182
-1
lines changed

9 files changed

+182
-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",

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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardCreateData)
69+
dashboard, _, err := client.CreateDashboard(auth, *dashboardCreateData)
70+
if err != nil {
71+
return datadogV1.Dashboard{}, translateClientError(err, "error creating dashboard")
72+
}
73+
return dashboard, nil
74+
}
75+
76+
func updateDashboard(auth context.Context, client *datadogV1.DashboardsApi, instance *v1alpha1.DatadogGenericResource) (datadogV1.Dashboard, error) {
77+
dashboardUpdateData := &datadogV1.Dashboard{}
78+
json.Unmarshal([]byte(instance.Spec.JsonSpec), dashboardUpdateData)
79+
dashboardUpdated, _, err := client.UpdateDashboard(auth, instance.Status.Id, *dashboardUpdateData)
80+
if err != nil {
81+
return datadogV1.Dashboard{}, translateClientError(err, "error updating dashboard")
82+
}
83+
return dashboardUpdated, nil
84+
}
85+
86+
func deleteDashboard(auth context.Context, client *datadogV1.DashboardsApi, dashboardID string) error {
87+
if _, _, err := client.DeleteDashboard(auth, dashboardID); err != nil {
88+
return translateClientError(err, "error deleting dashboard")
89+
}
90+
return nil
91+
}
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("wassim.dhif@datadoghq.com")
33+
d.SetCreatedAt(createdAt)
34+
return d
35+
}(),
36+
expectedStatus: v1alpha1.DatadogGenericResourceStatus{
37+
Id: "abc-123",
38+
Creator: "wassim.dhif@datadoghq.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:

pkg/datadogclient/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func InitDatadogDashboardClient(logger logr.Logger, creds config.Creds) (Datadog
9696
}
9797

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

112113
configV1 := datadogapi.NewConfiguration()
113114
apiClient := datadogapi.NewAPIClient(configV1)
115+
dashboardsClient := datadogV1.NewDashboardsApi(apiClient)
114116
syntheticsClient := datadogV1.NewSyntheticsApi(apiClient)
115117
notebooksClient := datadogV1.NewNotebooksApi(apiClient)
116118
monitorsClient := datadogV1.NewMonitorsApi(apiClient)
@@ -122,6 +124,7 @@ func InitDatadogGenericClient(logger logr.Logger, creds config.Creds) (DatadogGe
122124
}
123125

124126
return DatadogGenericClient{
127+
DashboardsClient: dashboardsClient,
125128
SyntheticsClient: syntheticsClient,
126129
NotebooksClient: notebooksClient,
127130
MonitorsClient: monitorsClient,

0 commit comments

Comments
 (0)