From 62ab67c8a3da3ae802b65547bb55396802c96d5b Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Thu, 2 Jul 2026 15:09:38 +0100 Subject: [PATCH 01/10] Handle nil pointer error --- observability-lib/api/dashboard.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index 732d512ed0..b0bd60ebfd 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -36,6 +36,9 @@ func (c *Client) GetDashboardByNameFolderUID(name string, folderUID string) (Get if len(grafanaResp) > 0 { for _, dashboard := range grafanaResp { + if dashboard.Title == nil || dashboard.FolderUID == nil { + continue + } if strings.EqualFold(*dashboard.Title, name) && strings.EqualFold(*dashboard.FolderUID, folderUID) { return dashboard, resp, nil } From ebfa1e38dded3e78d4d803772558b2891df15a03 Mon Sep 17 00:00:00 2001 From: Yashvardhan Nevatia Date: Thu, 2 Jul 2026 15:24:40 +0100 Subject: [PATCH 02/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- observability-lib/api/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index b0bd60ebfd..633f9bb0b2 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -36,7 +36,7 @@ func (c *Client) GetDashboardByNameFolderUID(name string, folderUID string) (Get if len(grafanaResp) > 0 { for _, dashboard := range grafanaResp { - if dashboard.Title == nil || dashboard.FolderUID == nil { + if dashboard.Title == nil || dashboard.FolderUID == nil || dashboard.UID == nil { continue } if strings.EqualFold(*dashboard.Title, name) && strings.EqualFold(*dashboard.FolderUID, folderUID) { From 8a37b5181f2318966404952e64fc1ee83a751bea Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Thu, 2 Jul 2026 16:32:24 +0100 Subject: [PATCH 03/10] fix --- observability-lib/api/dashboard.go | 3 ++- observability-lib/api/folder.go | 24 ++++++++++++++++++ observability-lib/grafana/dashboard.go | 35 +++++++++++++++++++------- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index b0bd60ebfd..66fa6135c2 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -50,7 +50,8 @@ func (c *Client) GetDashboardByNameFolderUID(name string, folderUID string) (Get type PostDashboardRequest struct { Dashboard interface{} `json:"dashboard"` - FolderID int `json:"folderId"` + FolderID int `json:"folderId,omitempty"` + FolderUID string `json:"folderUid,omitempty"` Overwrite bool `json:"overwrite"` } diff --git a/observability-lib/api/folder.go b/observability-lib/api/folder.go index 1dca5a52e2..abb8d8e11c 100644 --- a/observability-lib/api/folder.go +++ b/observability-lib/api/folder.go @@ -28,6 +28,30 @@ func (c *Client) FindOrCreateFolder(name string) (*Folder, error) { return folder, nil } +// GetFolderByUID returns a folder by UID. +func (c *Client) GetFolderByUID(uid string) (*Folder, error) { + var folder Folder + + resp, err := c.resty.R(). + SetHeader("Accept", "application/json"). + SetResult(&folder). + Get(fmt.Sprintf("/api/folders/%s", uid)) + + if err != nil { + return nil, fmt.Errorf("error making API request: %w", err) + } + + statusCode := resp.StatusCode() + if statusCode == 404 { + return nil, nil + } + if statusCode != 200 { + return nil, fmt.Errorf("error fetching folder %q, received unexpected status code %d: %s", uid, statusCode, resp.String()) + } + + return &folder, nil +} + // GetFolderByTitle Get a folder by title func (c *Client) GetFolderByTitle(title string) (*Folder, error) { folders, _, err := c.GetFolders() diff --git a/observability-lib/grafana/dashboard.go b/observability-lib/grafana/dashboard.go index 09b29cfcf4..0a5edd15ed 100644 --- a/observability-lib/grafana/dashboard.go +++ b/observability-lib/grafana/dashboard.go @@ -3,6 +3,7 @@ package grafana import ( "encoding/json" "fmt" + "os" "reflect" "github.com/grafana/grafana-foundation-sdk/go/alerting" @@ -39,11 +40,29 @@ type DeployOptions struct { GrafanaURL string GrafanaToken string FolderName string + FolderUID string // when set, deploy to this folder instead of resolving FolderName by title EnableAlerts bool RuleGroupFromDashboard bool // if true, set the alert rule group to the dashboard title on all alerts NotificationTemplates string } +func resolveDeployFolder(client *api.Client, options *DeployOptions) (*api.Folder, error) { + if options.FolderUID != "" { + folder, err := client.GetFolderByUID(options.FolderUID) + if err != nil { + return nil, err + } + if folder == nil { + return nil, fmt.Errorf("folder with UID %q not found", options.FolderUID) + } + return folder, nil + } + if options.FolderName != "" { + return client.FindOrCreateFolder(options.FolderName) + } + return nil, nil +} + func alertRuleExist(alerts []alerting.Rule, alert alerting.Rule) bool { for _, a := range alerts { if reflect.DeepEqual(a.Title, alert.Title) { @@ -95,13 +114,9 @@ func (o *Observability) DeployToGrafana(options *DeployOptions) error { ) // Create or update folder - var folder *api.Folder - var errFolder error - if options.FolderName != "" { - folder, errFolder = grafanaClient.FindOrCreateFolder(options.FolderName) - if errFolder != nil { - return errFolder - } + folder, errFolder := resolveDeployFolder(grafanaClient, options) + if errFolder != nil { + return errFolder } // Create or update dashboard @@ -123,12 +138,14 @@ func (o *Observability) DeployToGrafana(options *DeployOptions) error { newDashboard, _, errPostDashboard = grafanaClient.PostDashboard(api.PostDashboardRequest{ Dashboard: o.Dashboard, Overwrite: true, - //nolint:gosec // disable G115 - FolderID: int(folder.ID), + FolderUID: folder.UID, }) if errPostDashboard != nil { return errPostDashboard } + if newDashboard.URL != nil && o.Dashboard.Title != nil { + fmt.Fprintf(os.Stderr, "deployed dashboard %q to folder uid=%s: %s\n", *o.Dashboard.Title, folder.UID, *newDashboard.URL) + } } } From d43065d01595f775ef44c2e6012f1775530eedab Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Thu, 2 Jul 2026 19:09:06 +0200 Subject: [PATCH 04/10] API to fetch panels for a dashboard --- observability-lib/api/dashboard.go | 61 ++++++++++++++++++ observability-lib/api/dashboard_test.go | 83 +++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 observability-lib/api/dashboard_test.go diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index 1a0620fbf6..8e410d9fa3 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -87,6 +87,67 @@ func (c *Client) PostDashboard(dashboard PostDashboardRequest) (PostDashboardRes return grafanaResp, resp, nil } +type DashboardPanel struct { + ID int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` +} + +type dashboardPanelJSON struct { + ID int `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Panels []dashboardPanelJSON `json:"panels"` +} + +type getDashboardByUIDResponse struct { + Dashboard struct { + ID uint `json:"id"` + UID string `json:"uid"` + Title string `json:"title"` + Panels []dashboardPanelJSON `json:"panels"` + } `json:"dashboard"` +} + +func collectDashboardPanels(panels []dashboardPanelJSON) []DashboardPanel { + result := make([]DashboardPanel, 0, len(panels)) + for _, panel := range panels { + result = append(result, DashboardPanel{ + ID: panel.ID, + Title: panel.Title, + Type: panel.Type, + }) + if len(panel.Panels) > 0 { + result = append(result, collectDashboardPanels(panel.Panels)...) + } + } + return result +} + +// GetDashboardPanelsByUID returns all panels (including nested row panels) with their IDs and titles. +func (c *Client) GetDashboardPanelsByUID(uid string) ([]DashboardPanel, *resty.Response, error) { + var grafanaResp getDashboardByUIDResponse + + resp, err := c.resty.R(). + SetHeader("Accept", "application/json"). + SetResult(&grafanaResp). + Get(fmt.Sprintf("/api/dashboards/uid/%s", uid)) + + if err != nil { + return nil, resp, fmt.Errorf("error making API request: %w", err) + } + + statusCode := resp.StatusCode() + if statusCode == 404 { + return nil, resp, nil + } + if statusCode != 200 { + return nil, resp, fmt.Errorf("error fetching dashboard %q, received unexpected status code %d: %s", uid, statusCode, resp.String()) + } + + return collectDashboardPanels(grafanaResp.Dashboard.Panels), resp, nil +} + func (c *Client) DeleteDashboardByUID(uid string) (*resty.Response, error) { resp, err := c.resty.R(). SetHeader("Content-Type", "application/json"). diff --git a/observability-lib/api/dashboard_test.go b/observability-lib/api/dashboard_test.go new file mode 100644 index 0000000000..1d145b0088 --- /dev/null +++ b/observability-lib/api/dashboard_test.go @@ -0,0 +1,83 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetDashboardPanelsByUID(t *testing.T) { + dashboardUID := "dashboard-uid" + expectedResponse := getDashboardByUIDResponse{ + Dashboard: struct { + ID uint `json:"id"` + UID string `json:"uid"` + Title string `json:"title"` + Panels []dashboardPanelJSON `json:"panels"` + }{ + ID: 10, + UID: dashboardUID, + Title: "Test Dashboard", + Panels: []dashboardPanelJSON{ + {ID: 1, Title: "Uptime", Type: "stat"}, + { + ID: 2, + Title: "Resource Usage", + Type: "row", + Panels: []dashboardPanelJSON{ + {ID: 3, Title: "CPU Usage", Type: "timeseries"}, + {ID: 4, Title: "Memory Usage", Type: "stat"}, + }, + }, + }, + }, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/dashboards/uid/%s", dashboardUID), r.URL.Path) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + require.NoError(t, json.NewEncoder(w).Encode(expectedResponse)) + })) + defer ts.Close() + + client := &Client{ + resty: resty.New().SetBaseURL(ts.URL), + } + + panels, resp, err := client.GetDashboardPanelsByUID(dashboardUID) + + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode()) + assert.Equal(t, []DashboardPanel{ + {ID: 1, Title: "Uptime", Type: "stat"}, + {ID: 2, Title: "Resource Usage", Type: "row"}, + {ID: 3, Title: "CPU Usage", Type: "timeseries"}, + {ID: 4, Title: "Memory Usage", Type: "stat"}, + }, panels) +} + +func TestGetDashboardPanelsByUID_NotFound(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + + client := &Client{ + resty: resty.New().SetBaseURL(ts.URL), + } + + panels, resp, err := client.GetDashboardPanelsByUID("missing-dashboard") + + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode()) + assert.Nil(t, panels) +} From 086946a5a7706d8b16a9ddf21fe266b2af4316aa Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Fri, 3 Jul 2026 10:07:49 +0100 Subject: [PATCH 05/10] lint --- observability-lib/api/folder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability-lib/api/folder.go b/observability-lib/api/folder.go index abb8d8e11c..fe5ae170a7 100644 --- a/observability-lib/api/folder.go +++ b/observability-lib/api/folder.go @@ -35,7 +35,7 @@ func (c *Client) GetFolderByUID(uid string) (*Folder, error) { resp, err := c.resty.R(). SetHeader("Accept", "application/json"). SetResult(&folder). - Get(fmt.Sprintf("/api/folders/%s", uid)) + Get("/api/folders/" + uid) if err != nil { return nil, fmt.Errorf("error making API request: %w", err) From 10c201d49f4ebb84202f1e132784b19a939fef34 Mon Sep 17 00:00:00 2001 From: Yashvardhan Nevatia Date: Fri, 3 Jul 2026 11:30:40 +0100 Subject: [PATCH 06/10] Update dashboard.go --- observability-lib/api/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index 8e410d9fa3..143743728a 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -131,7 +131,7 @@ func (c *Client) GetDashboardPanelsByUID(uid string) ([]DashboardPanel, *resty.R resp, err := c.resty.R(). SetHeader("Accept", "application/json"). SetResult(&grafanaResp). - Get(fmt.Sprintf("/api/dashboards/uid/%s", uid)) + Get("/api/dashboards/uid/" + uid)) if err != nil { return nil, resp, fmt.Errorf("error making API request: %w", err) From f4a26df210855c5fdeb9e6fb86e5dd30c2d36168 Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Fri, 3 Jul 2026 11:32:56 +0100 Subject: [PATCH 07/10] fix --- observability-lib/api/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index 143743728a..7f47ff87b9 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -131,7 +131,7 @@ func (c *Client) GetDashboardPanelsByUID(uid string) ([]DashboardPanel, *resty.R resp, err := c.resty.R(). SetHeader("Accept", "application/json"). SetResult(&grafanaResp). - Get("/api/dashboards/uid/" + uid)) + Get("/api/dashboards/uid/" + uid) if err != nil { return nil, resp, fmt.Errorf("error making API request: %w", err) From 0b09fd6627aaa55df9a3b10505011df292cd7f66 Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Fri, 3 Jul 2026 12:34:36 +0200 Subject: [PATCH 08/10] fix merge --- observability-lib/api/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability-lib/api/dashboard.go b/observability-lib/api/dashboard.go index 143743728a..7f47ff87b9 100644 --- a/observability-lib/api/dashboard.go +++ b/observability-lib/api/dashboard.go @@ -131,7 +131,7 @@ func (c *Client) GetDashboardPanelsByUID(uid string) ([]DashboardPanel, *resty.R resp, err := c.resty.R(). SetHeader("Accept", "application/json"). SetResult(&grafanaResp). - Get("/api/dashboards/uid/" + uid)) + Get("/api/dashboards/uid/" + uid) if err != nil { return nil, resp, fmt.Errorf("error making API request: %w", err) From aaecdbe9c854aa616e161876deea6a9dcccce6d0 Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Fri, 3 Jul 2026 12:38:10 +0200 Subject: [PATCH 09/10] linter fixes --- observability-lib/api/dashboard_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/observability-lib/api/dashboard_test.go b/observability-lib/api/dashboard_test.go index 1d145b0088..ce9842a9fb 100644 --- a/observability-lib/api/dashboard_test.go +++ b/observability-lib/api/dashboard_test.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" @@ -40,12 +39,12 @@ func TestGetDashboardPanelsByUID(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, fmt.Sprintf("/api/dashboards/uid/%s", dashboardUID), r.URL.Path) + assert.Equal(t, "/api/dashboards/uid/"+dashboardUID, r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Accept")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(expectedResponse)) + assert.NoError(t, json.NewEncoder(w).Encode(expectedResponse)) })) defer ts.Close() From fc7d2612c8c595fd4b08c6f75ed735b431d6f050 Mon Sep 17 00:00:00 2001 From: yashnevatia Date: Fri, 3 Jul 2026 11:38:15 +0100 Subject: [PATCH 10/10] lint --- observability-lib/api/dashboard_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/observability-lib/api/dashboard_test.go b/observability-lib/api/dashboard_test.go index 1d145b0088..a8472fb26b 100644 --- a/observability-lib/api/dashboard_test.go +++ b/observability-lib/api/dashboard_test.go @@ -2,7 +2,6 @@ package api import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" @@ -40,12 +39,14 @@ func TestGetDashboardPanelsByUID(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, fmt.Sprintf("/api/dashboards/uid/%s", dashboardUID), r.URL.Path) + assert.Equal(t, "/api/dashboards/uid/"+dashboardUID, r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Accept")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(expectedResponse)) + if err := json.NewEncoder(w).Encode(expectedResponse); err != nil { + t.Errorf("encode response: %v", err) + } })) defer ts.Close()