├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── admin.go ├── admin_test.go ├── alert.go ├── alert_test.go ├── alerting_alert_rule.go ├── alerting_alert_rule_test.go ├── alerting_contact_point.go ├── alerting_contact_point_test.go ├── alerting_message_template.go ├── alerting_message_template_test.go ├── alerting_mute_timing.go ├── alerting_mute_timing_test.go ├── alerting_notification_policy.go ├── alerting_notification_policy_test.go ├── alertnotification.go ├── alertnotification_test.go ├── annotation.go ├── annotation_test.go ├── api_key.go ├── api_key_test.go ├── builtin_role_assignments.go ├── builtin_role_assignments_test.go ├── client.go ├── client_test.go ├── cloud_access_policy.go ├── cloud_access_policy_token.go ├── cloud_api_key.go ├── cloud_grafana_api_key.go ├── cloud_grafana_service_account.go ├── cloud_org.go ├── cloud_plugin.go ├── cloud_plugin_test.go ├── cloud_regions.go ├── cloud_regions_test.go ├── cloud_stack.go ├── cloud_stack_test.go ├── dashboard.go ├── dashboard_permissions.go ├── dashboard_permissions_test.go ├── dashboard_public.go ├── dashboard_public_test.go ├── dashboard_test.go ├── datasource.go ├── datasource_cache.go ├── datasource_cache_test.go ├── datasource_permissions.go ├── datasource_permissions_test.go ├── datasource_test.go ├── errors.go ├── folder.go ├── folder_dashboard_search.go ├── folder_dashboard_search_test.go ├── folder_permissions.go ├── folder_permissions_test.go ├── folder_test.go ├── go.mod ├── go.sum ├── health.go ├── library_panel.go ├── library_panel_test.go ├── mock_test.go ├── org_preferences.go ├── org_preferences_test.go ├── org_users.go ├── org_users_test.go ├── orgs.go ├── orgs_test.go ├── playlist.go ├── playlist_test.go ├── preferences.go ├── report.go ├── report_test.go ├── resource.go ├── resource_permissions.go ├── resource_permissions_test.go ├── resource_test.go ├── role.go ├── role_assignments.go ├── role_test.go ├── service_account.go ├── service_account_permissions.go ├── service_account_test.go ├── slo.go ├── slo_test.go ├── snapshot.go ├── snapshot_test.go ├── team.go ├── team_external_group.go ├── team_external_group_test.go ├── team_permissions.go ├── teams_test.go ├── user.go └── user_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # vim swap files 27 | .*.sw? 28 | .idea -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Docs on CODEOWNERS: 2 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | # 4 | # Later codeowner matches take precedence over earlier ones. 5 | 6 | # Default owner 7 | * @grafana/platform-cat 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana HTTP API Client for Go 2 | 3 | :warning: This repository is deprecated. All further development should go towards https://github.com/grafana/grafana-openapi-client-go (and the underlying Grafana OpenAPI specification) 4 | -------------------------------------------------------------------------------- /admin.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // PauseAllAlertsResponse represents the response body for a PauseAllAlerts request. 9 | type PauseAllAlertsResponse struct { 10 | AlertsAffected int64 `json:"alertsAffected,omitempty"` 11 | State string `json:"state,omitempty"` 12 | Message string `json:"message,omitempty"` 13 | } 14 | 15 | // CreateUser creates a Grafana user. 16 | func (c *Client) CreateUser(user User) (int64, error) { 17 | id := int64(0) 18 | data, err := json.Marshal(user) 19 | if err != nil { 20 | return id, err 21 | } 22 | 23 | created := struct { 24 | ID int64 `json:"id"` 25 | }{} 26 | 27 | err = c.request("POST", "/api/admin/users", nil, data, &created) 28 | if err != nil { 29 | return id, err 30 | } 31 | 32 | return created.ID, err 33 | } 34 | 35 | // DeleteUser deletes a Grafana user. 36 | func (c *Client) DeleteUser(id int64) error { 37 | return c.request("DELETE", fmt.Sprintf("/api/admin/users/%d", id), nil, nil, nil) 38 | } 39 | 40 | // UpdateUserPassword updates a user password. 41 | func (c *Client) UpdateUserPassword(id int64, password string) error { 42 | body := map[string]string{"password": password} 43 | data, err := json.Marshal(body) 44 | if err != nil { 45 | return err 46 | } 47 | return c.request("PUT", fmt.Sprintf("/api/admin/users/%d/password", id), nil, data, nil) 48 | } 49 | 50 | // UpdateUserPermissions sets a user's admin status. 51 | func (c *Client) UpdateUserPermissions(id int64, isAdmin bool) error { 52 | body := map[string]bool{"isGrafanaAdmin": isAdmin} 53 | data, err := json.Marshal(body) 54 | if err != nil { 55 | return err 56 | } 57 | return c.request("PUT", fmt.Sprintf("/api/admin/users/%d/permissions", id), nil, data, nil) 58 | } 59 | 60 | // PauseAllAlerts pauses all Grafana alerts. 61 | func (c *Client) PauseAllAlerts() (PauseAllAlertsResponse, error) { 62 | result := PauseAllAlertsResponse{} 63 | data, err := json.Marshal(PauseAlertRequest{ 64 | Paused: true, 65 | }) 66 | if err != nil { 67 | return result, err 68 | } 69 | 70 | err = c.request("POST", "/api/admin/pause-all-alerts", nil, data, &result) 71 | 72 | return result, err 73 | } 74 | -------------------------------------------------------------------------------- /admin_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | const ( 11 | createUserJSON = `{"id":1,"message":"User created"}` 12 | deleteUserJSON = `{"message":"User deleted"}` 13 | updateUserPasswordJSON = `{"message":"User password updated"}` 14 | updateUserPermissionsJSON = `{"message":"User permissions updated"}` 15 | 16 | pauseAllAlertsJSON = `{ 17 | "alertsAffected": 1, 18 | "state": "Paused", 19 | "message": "alert paused" 20 | }` 21 | ) 22 | 23 | func TestCreateUser(t *testing.T) { 24 | client := gapiTestTools(t, 200, createUserJSON) 25 | user := User{ 26 | Email: "admin@localhost", 27 | Login: "admin", 28 | Name: "Administrator", 29 | Password: "password", 30 | } 31 | resp, err := client.CreateUser(user) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | if resp != 1 { 37 | t.Error("Not correctly parsing returned user message.") 38 | } 39 | } 40 | 41 | func TestDeleteUser(t *testing.T) { 42 | client := gapiTestTools(t, 200, deleteUserJSON) 43 | 44 | err := client.DeleteUser(int64(1)) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | } 49 | 50 | func TestUpdateUserPassword(t *testing.T) { 51 | client := gapiTestTools(t, 200, updateUserPasswordJSON) 52 | 53 | err := client.UpdateUserPassword(int64(1), "new-password") 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | 59 | func TestUpdateUserPermissions(t *testing.T) { 60 | client := gapiTestTools(t, 200, updateUserPermissionsJSON) 61 | 62 | err := client.UpdateUserPermissions(int64(1), false) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | } 67 | 68 | func TestPauseAllAlerts(t *testing.T) { 69 | client := gapiTestTools(t, 200, pauseAllAlertsJSON) 70 | 71 | res, err := client.PauseAllAlerts() 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | 76 | t.Log(pretty.PrettyFormat(res)) 77 | 78 | if res.State != "Paused" { 79 | t.Error("pause all alerts response should contain the correct response message") 80 | } 81 | } 82 | 83 | func TestPauseAllAlerts_500(t *testing.T) { 84 | client := gapiTestTools(t, 500, pauseAllAlertsJSON) 85 | 86 | _, err := client.PauseAllAlerts() 87 | if !strings.Contains(err.Error(), "status: 500") { 88 | t.Errorf("expected error to contain 'status: 500'; got: %s", err.Error()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /alert.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // Alert represents a Grafana API Alert 10 | type Alert struct { 11 | ID int64 `json:"id,omitempty"` 12 | DashboardID int64 `json:"dashboardId,omitempty"` 13 | DashboardUID string `json:"dashboardUid,omitempty"` 14 | DashboardSlug string `json:"dashboardSlug,omitempty"` 15 | PanelID int64 `json:"panelId,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | State string `json:"state,omitempty"` 18 | NewStateDate string `json:"newStateDate,omitempty"` 19 | EvalDate string `json:"evalDate,omitempty"` 20 | ExecutionError string `json:"executionError,omitempty"` 21 | URL string `json:"url,omitempty"` 22 | } 23 | 24 | // PauseAlertRequest represents the request payload for a PauseAlert request. 25 | type PauseAlertRequest struct { 26 | Paused bool `json:"paused"` 27 | } 28 | 29 | // PauseAlertResponse represents the response body for a PauseAlert request. 30 | type PauseAlertResponse struct { 31 | AlertID int64 `json:"alertId,omitempty"` 32 | State string `json:"state,omitempty"` 33 | Message string `json:"message,omitempty"` 34 | } 35 | 36 | // Alerts fetches the annotations queried with the params it's passed. 37 | func (c *Client) Alerts(params url.Values) ([]Alert, error) { 38 | result := []Alert{} 39 | err := c.request("GET", "/api/alerts", params, nil, &result) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return result, err 45 | } 46 | 47 | // Alert fetches and returns an individual Grafana alert. 48 | func (c *Client) Alert(id int64) (Alert, error) { 49 | path := fmt.Sprintf("/api/alerts/%d", id) 50 | result := Alert{} 51 | err := c.request("GET", path, nil, nil, &result) 52 | if err != nil { 53 | return result, err 54 | } 55 | 56 | return result, err 57 | } 58 | 59 | // PauseAlert pauses the Grafana alert whose ID it's passed. 60 | func (c *Client) PauseAlert(id int64) (PauseAlertResponse, error) { 61 | path := fmt.Sprintf("/api/alerts/%d", id) 62 | result := PauseAlertResponse{} 63 | data, err := json.Marshal(PauseAlertRequest{ 64 | Paused: true, 65 | }) 66 | if err != nil { 67 | return result, err 68 | } 69 | 70 | err = c.request("POST", path, nil, data, &result) 71 | if err != nil { 72 | return result, err 73 | } 74 | 75 | return result, err 76 | } 77 | -------------------------------------------------------------------------------- /alert_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gobs/pretty" 9 | ) 10 | 11 | const ( 12 | alertsJSON = `[{ 13 | "id": 1, 14 | "dashboardId": 1, 15 | "dashboardUId": "ABcdEFghij", 16 | "dashboardSlug": "sensors", 17 | "panelId": 1, 18 | "name": "fire place sensor", 19 | "state": "alerting", 20 | "newStateDate": "2018-05-14T05:55:20+02:00", 21 | "evalDate": "0001-01-01T00:00:00Z", 22 | "evalData": null, 23 | "executionError": "", 24 | "url": "http://grafana.com/dashboard/db/sensors" 25 | }]` 26 | 27 | alertJSON = `{ 28 | "id": 1, 29 | "dashboardId": 1, 30 | "dashboardUId": "ABcdEFghij", 31 | "dashboardSlug": "sensors", 32 | "panelId": 1, 33 | "name": "fire place sensor", 34 | "state": "alerting", 35 | "message": "Someone is trying to break in through the fire place", 36 | "newStateDate": "2018-05-14T05:55:20+02:00", 37 | "evalDate": "0001-01-01T00:00:00Z", 38 | "executionError": "", 39 | "url": "http://grafana.com/dashboard/db/sensors" 40 | }` 41 | 42 | pauseAlertJSON = `{ 43 | "alertId": 1, 44 | "state": "Paused", 45 | "message": "alert paused" 46 | }` 47 | ) 48 | 49 | func TestAlerts(t *testing.T) { 50 | client := gapiTestTools(t, 200, alertsJSON) 51 | 52 | params := url.Values{} 53 | params.Add("dashboardId", "123") 54 | 55 | as, err := client.Alerts(params) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | t.Log(pretty.PrettyFormat(as)) 61 | 62 | if as[0].ID != 1 { 63 | t.Error("alerts response should contain alerts with an ID") 64 | } 65 | } 66 | 67 | func TestAlerts_500(t *testing.T) { 68 | client := gapiTestTools(t, 500, alertsJSON) 69 | 70 | params := url.Values{} 71 | params.Add("dashboardId", "123") 72 | 73 | _, err := client.Alerts(params) 74 | if !strings.Contains(err.Error(), "status: 500") { 75 | t.Errorf("expected error to contain 'status: 500'; got: %s", err.Error()) 76 | } 77 | } 78 | 79 | func TestAlert(t *testing.T) { 80 | client := gapiTestTools(t, 200, alertJSON) 81 | 82 | res, err := client.Alert(1) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | 87 | t.Log(pretty.PrettyFormat(res)) 88 | 89 | if res.ID != 1 { 90 | t.Error("alert response should contain the ID of the queried alert") 91 | } 92 | } 93 | 94 | func TestAlert_500(t *testing.T) { 95 | client := gapiTestTools(t, 500, alertJSON) 96 | 97 | _, err := client.Alert(1) 98 | if !strings.Contains(err.Error(), "status: 500") { 99 | t.Errorf("expected error to contain 'status: 500'; got: %s", err.Error()) 100 | } 101 | } 102 | 103 | func TestPauseAlert(t *testing.T) { 104 | client := gapiTestTools(t, 200, pauseAlertJSON) 105 | 106 | res, err := client.PauseAlert(1) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | 111 | t.Log(pretty.PrettyFormat(res)) 112 | 113 | if res.State != "Paused" { 114 | t.Error("pause alert response should contain the correct response message") 115 | } 116 | } 117 | 118 | func TestPauseAlert_500(t *testing.T) { 119 | client := gapiTestTools(t, 500, pauseAlertJSON) 120 | 121 | _, err := client.PauseAlert(1) 122 | if !strings.Contains(err.Error(), "status: 500") { 123 | t.Errorf("expected error to contain 'status: 500'; got: %s", err.Error()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /alerting_contact_point.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // ContactPoint represents a Grafana Alerting contact point. 10 | type ContactPoint struct { 11 | UID string `json:"uid"` 12 | Name string `json:"name"` 13 | Type string `json:"type"` 14 | Settings map[string]interface{} `json:"settings"` 15 | DisableResolveMessage bool `json:"disableResolveMessage"` 16 | Provenance string `json:"provenance"` 17 | } 18 | 19 | // ContactPoints fetches all contact points. 20 | func (c *Client) ContactPoints() ([]ContactPoint, error) { 21 | ps := make([]ContactPoint, 0) 22 | err := c.request("GET", "/api/v1/provisioning/contact-points", nil, nil, &ps) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return ps, nil 27 | } 28 | 29 | // ContactPointsByName fetches contact points with the given name. 30 | func (c *Client) ContactPointsByName(name string) ([]ContactPoint, error) { 31 | ps := make([]ContactPoint, 0) 32 | params := url.Values{} 33 | params.Add("name", name) 34 | err := c.request("GET", "/api/v1/provisioning/contact-points", params, nil, &ps) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return ps, nil 39 | } 40 | 41 | // ContactPoint fetches a single contact point, identified by its UID. 42 | func (c *Client) ContactPoint(uid string) (ContactPoint, error) { 43 | ps, err := c.ContactPoints() 44 | if err != nil { 45 | return ContactPoint{}, err 46 | } 47 | 48 | for _, p := range ps { 49 | if p.UID == uid { 50 | return p, nil 51 | } 52 | } 53 | return ContactPoint{}, fmt.Errorf("contact point with uid %s not found", uid) 54 | } 55 | 56 | // NewContactPoint creates a new contact point. 57 | func (c *Client) NewContactPoint(p *ContactPoint) (string, error) { 58 | req, err := json.Marshal(p) 59 | if err != nil { 60 | return "", err 61 | } 62 | result := ContactPoint{} 63 | 64 | err = c.request("POST", "/api/v1/provisioning/contact-points", nil, req, &result) 65 | if err != nil { 66 | return "", err 67 | } 68 | return result.UID, nil 69 | } 70 | 71 | // UpdateContactPoint replaces a contact point, identified by contact point's UID. 72 | func (c *Client) UpdateContactPoint(p *ContactPoint) error { 73 | uri := fmt.Sprintf("/api/v1/provisioning/contact-points/%s", p.UID) 74 | req, err := json.Marshal(p) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return c.request("PUT", uri, nil, req, nil) 80 | } 81 | 82 | // DeleteContactPoint deletes a contact point. 83 | func (c *Client) DeleteContactPoint(uid string) error { 84 | uri := fmt.Sprintf("/api/v1/provisioning/contact-points/%s", uid) 85 | return c.request("DELETE", uri, nil, nil, nil) 86 | } 87 | -------------------------------------------------------------------------------- /alerting_contact_point_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | func TestContactPoints(t *testing.T) { 10 | t.Run("get contact points succeeds", func(t *testing.T) { 11 | client := gapiTestTools(t, 200, getContactPointsJSON) 12 | 13 | ps, err := client.ContactPoints() 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | t.Log(pretty.PrettyFormat(ps)) 19 | if len(ps) != 2 { 20 | t.Errorf("wrong number of contact points returned, got %d", len(ps)) 21 | } 22 | if ps[0].UID != "" { 23 | t.Errorf("incorrect UID - expected %s on element %d, got %s", "", 0, ps[0].UID) 24 | } 25 | if ps[1].UID != "rc5r0bjnz" { 26 | t.Errorf("incorrect UID - expected %s on element %d, got %s", "rc5r0bjnz", 1, ps[1].UID) 27 | } 28 | }) 29 | 30 | t.Run("get contact points by name succeeds", func(t *testing.T) { 31 | client := gapiTestTools(t, 200, getContactPointsQueryJSON) 32 | 33 | ps, err := client.ContactPointsByName("slack-receiver-1") 34 | 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | t.Log(pretty.PrettyFormat(ps)) 39 | if len(ps) != 1 { 40 | t.Errorf("wrong number of contact points returned, got %d", len(ps)) 41 | } 42 | if ps[0].UID != "rc5r0bjnz" { 43 | t.Errorf("incorrect UID - expected %s on element %d, got %s", "rc5r0bjnz", 0, ps[0].UID) 44 | } 45 | }) 46 | 47 | t.Run("get contact point succeeds", func(t *testing.T) { 48 | client := gapiTestTools(t, 200, getContactPointsJSON) 49 | 50 | p, err := client.ContactPoint("rc5r0bjnz") 51 | 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | t.Log(pretty.PrettyFormat(p)) 56 | if p.UID != "rc5r0bjnz" { 57 | t.Errorf("incorrect UID - expected %s got %s", "rc5r0bjnz", p.UID) 58 | } 59 | }) 60 | 61 | t.Run("get non-existent contact point fails", func(t *testing.T) { 62 | client := gapiTestTools(t, 200, getContactPointsJSON) 63 | 64 | p, err := client.ContactPoint("does not exist") 65 | 66 | if err == nil { 67 | t.Errorf("expected error but got nil") 68 | t.Log(pretty.PrettyFormat(p)) 69 | } 70 | }) 71 | 72 | t.Run("create contact point succeeds", func(t *testing.T) { 73 | client := gapiTestTools(t, 201, writeContactPointJSON) 74 | p := createContactPoint() 75 | 76 | uid, err := client.NewContactPoint(&p) 77 | 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | if uid != "rc5r0bjnz" { 82 | t.Errorf("unexpected UID returned, got %s", uid) 83 | } 84 | }) 85 | 86 | t.Run("update contact point succeeds", func(t *testing.T) { 87 | client := gapiTestTools(t, 200, writeContactPointJSON) 88 | p := createContactPoint() 89 | p.UID = "on7otbj7k" 90 | 91 | err := client.UpdateContactPoint(&p) 92 | 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | }) 97 | 98 | t.Run("delete contact point succeeds", func(t *testing.T) { 99 | client := gapiTestTools(t, 204, "") 100 | 101 | err := client.DeleteContactPoint("rc5r0bjnz") 102 | 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | }) 107 | } 108 | 109 | func createContactPoint() ContactPoint { 110 | return ContactPoint{ 111 | Name: "slack-receiver-123", 112 | Type: "slack", 113 | DisableResolveMessage: false, 114 | Settings: map[string]interface{}{ 115 | "recipient": "@zxcv", 116 | "token": "test-token", 117 | "url": "https://test-url", 118 | }, 119 | } 120 | } 121 | 122 | const getContactPointsJSON = ` 123 | [ 124 | { 125 | "uid": "", 126 | "name": "default-email-receiver", 127 | "type": "email", 128 | "disableResolveMessage": false, 129 | "settings": { 130 | "addresses": "" 131 | } 132 | }, 133 | { 134 | "uid": "rc5r0bjnz", 135 | "name": "slack-receiver-1", 136 | "type": "slack", 137 | "disableResolveMessage": false, 138 | "settings": { 139 | "recipient": "@foo", 140 | "token": "[REDACTED]", 141 | "url": "[REDACTED]" 142 | } 143 | } 144 | ]` 145 | 146 | const getContactPointsQueryJSON = ` 147 | [ 148 | { 149 | "uid": "rc5r0bjnz", 150 | "name": "slack-receiver-1", 151 | "type": "slack", 152 | "disableResolveMessage": false, 153 | "settings": { 154 | "recipient": "@foo", 155 | "token": "[REDACTED]", 156 | "url": "[REDACTED]" 157 | } 158 | } 159 | ]` 160 | 161 | const writeContactPointJSON = ` 162 | { 163 | "uid": "rc5r0bjnz", 164 | "name": "slack-receiver-1", 165 | "type": "slack", 166 | "disableResolveMessage": false, 167 | "settings": { 168 | "recipient": "@foo", 169 | "token": "[REDACTED]", 170 | "url": "[REDACTED]" 171 | } 172 | } 173 | ` 174 | -------------------------------------------------------------------------------- /alerting_message_template.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // AlertingMessageTemplate is a re-usable template for Grafana Alerting messages. 9 | type AlertingMessageTemplate struct { 10 | Name string `json:"name"` 11 | Template string `json:"template"` 12 | } 13 | 14 | // MessageTemplates fetches all message templates. 15 | func (c *Client) MessageTemplates() ([]AlertingMessageTemplate, error) { 16 | ts := make([]AlertingMessageTemplate, 0) 17 | err := c.request("GET", "/api/v1/provisioning/templates", nil, nil, &ts) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return ts, nil 22 | } 23 | 24 | // MessageTemplate fetches a single message template, identified by its name. 25 | func (c *Client) MessageTemplate(name string) (*AlertingMessageTemplate, error) { 26 | t := AlertingMessageTemplate{} 27 | uri := fmt.Sprintf("/api/v1/provisioning/templates/%s", name) 28 | err := c.request("GET", uri, nil, nil, &t) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &t, err 33 | } 34 | 35 | // SetMessageTemplate creates or updates a message template. 36 | func (c *Client) SetMessageTemplate(name, content string) error { 37 | req := struct { 38 | Template string `json:"template"` 39 | }{Template: content} 40 | body, err := json.Marshal(req) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | uri := fmt.Sprintf("/api/v1/provisioning/templates/%s", name) 46 | return c.request("PUT", uri, nil, body, nil) 47 | } 48 | 49 | // DeleteMessageTemplate deletes a message template. 50 | func (c *Client) DeleteMessageTemplate(name string) error { 51 | uri := fmt.Sprintf("/api/v1/provisioning/templates/%s", name) 52 | return c.request("DELETE", uri, nil, nil, nil) 53 | } 54 | -------------------------------------------------------------------------------- /alerting_message_template_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | func TestMessageTemplates(t *testing.T) { 10 | t.Run("get message templates succeeds", func(t *testing.T) { 11 | client := gapiTestTools(t, 200, getMessageTemplatesJSON) 12 | 13 | ts, err := client.MessageTemplates() 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | t.Log(pretty.PrettyFormat(ts)) 19 | if len(ts) != 2 { 20 | t.Errorf("wrong number of templates returned, got %#v", ts) 21 | } 22 | if ts[0].Name != "template-one" { 23 | t.Errorf("incorrect name - expected %s on element %d, got %#v", "template-one", 0, ts) 24 | } 25 | if ts[1].Name != "template-two" { 26 | t.Errorf("incorrect name - expected %s on element %d, got %#v", "template-two", 0, ts) 27 | } 28 | }) 29 | 30 | t.Run("get message template succeeds", func(t *testing.T) { 31 | client := gapiTestTools(t, 200, messageTemplateJSON) 32 | 33 | tmpl, err := client.MessageTemplate("template-one") 34 | 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | t.Log(pretty.PrettyFormat(tmpl)) 39 | if tmpl.Name != "template-one" { 40 | t.Errorf("incorrect name - expected %s, got %#v", "template-one", tmpl) 41 | } 42 | }) 43 | 44 | t.Run("get non-existent message template fails", func(t *testing.T) { 45 | client := gapiTestTools(t, 404, ``) 46 | 47 | tmpl, err := client.MessageTemplate("does not exist") 48 | 49 | if err == nil { 50 | t.Errorf("expected error but got nil") 51 | t.Log(pretty.PrettyFormat(tmpl)) 52 | } 53 | }) 54 | 55 | t.Run("put message template succeeds", func(t *testing.T) { 56 | client := gapiTestTools(t, 202, messageTemplateJSON) 57 | 58 | err := client.SetMessageTemplate("template-three", "{{define \"template-one\" }}\n content three\n{{ end }}") 59 | 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | }) 64 | 65 | t.Run("delete message template succeeds", func(t *testing.T) { 66 | client := gapiTestTools(t, 204, ``) 67 | 68 | err := client.DeleteMessageTemplate("template-three") 69 | 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | }) 74 | } 75 | 76 | const getMessageTemplatesJSON = ` 77 | [ 78 | { 79 | "name": "template-one", 80 | "template": "{{define \"template-one\" }}\n content one\n{{ end }}" 81 | }, 82 | { 83 | "name": "template-two", 84 | "template": "{{define \"template-one\" }}\n content two\n{{ end }}" 85 | } 86 | ] 87 | ` 88 | 89 | const messageTemplateJSON = ` 90 | { 91 | "name": "template-one", 92 | "template": "{{define \"template-one\" }}\n content one\n{{ end }}" 93 | } 94 | ` 95 | -------------------------------------------------------------------------------- /alerting_mute_timing.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // MuteTiming represents a Grafana Alerting mute timing. 9 | type MuteTiming struct { 10 | Name string `json:"name"` 11 | TimeIntervals []TimeInterval `json:"time_intervals"` 12 | Provenance string `json:"provenance,omitempty"` 13 | } 14 | 15 | // TimeInterval describes intervals of time using a Prometheus-defined standard. 16 | type TimeInterval struct { 17 | Times []TimeRange `json:"times,omitempty"` 18 | Weekdays []WeekdayRange `json:"weekdays,omitempty"` 19 | DaysOfMonth []DayOfMonthRange `json:"days_of_month,omitempty"` 20 | Months []MonthRange `json:"months,omitempty"` 21 | Years []YearRange `json:"years,omitempty"` 22 | Location Location `json:"location,omitempty"` 23 | } 24 | 25 | // TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. 26 | type TimeRange struct { 27 | StartMinute string `json:"start_time"` 28 | EndMinute string `json:"end_time"` 29 | } 30 | 31 | // A WeekdayRange is an inclusive range of weekdays, e.g. "monday" or "tuesday:thursday". 32 | type WeekdayRange string 33 | 34 | // A DayOfMonthRange is an inclusive range of days, 1-31, within a month, e.g. "1" or "14:16". Negative values can be used to represent days counting from the end of a month, e.g. "-1". 35 | type DayOfMonthRange string 36 | 37 | // A MonthRange is an inclusive range of months, either numerical or full calendar month, e.g "1:3", "december", or "may:august". 38 | type MonthRange string 39 | 40 | // A YearRange is a positive inclusive range of years, e.g. "2030" or "2021:2022". 41 | type YearRange string 42 | 43 | // A Location time zone for the time interval in IANA time zone database, e.g. "America/New_York" 44 | type Location string 45 | 46 | // MuteTimings fetches all mute timings. 47 | func (c *Client) MuteTimings() ([]MuteTiming, error) { 48 | mts := make([]MuteTiming, 0) 49 | err := c.request("GET", "/api/v1/provisioning/mute-timings", nil, nil, &mts) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return mts, nil 54 | } 55 | 56 | // MuteTiming fetches a single mute timing, identified by its name. 57 | func (c *Client) MuteTiming(name string) (MuteTiming, error) { 58 | mt := MuteTiming{} 59 | uri := fmt.Sprintf("/api/v1/provisioning/mute-timings/%s", name) 60 | err := c.request("GET", uri, nil, nil, &mt) 61 | return mt, err 62 | } 63 | 64 | // NewMuteTiming creates a new mute timing. 65 | func (c *Client) NewMuteTiming(mt *MuteTiming) error { 66 | req, err := json.Marshal(mt) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return c.request("POST", "/api/v1/provisioning/mute-timings", nil, req, nil) 72 | } 73 | 74 | // UpdateMuteTiming updates a mute timing. 75 | func (c *Client) UpdateMuteTiming(mt *MuteTiming) error { 76 | uri := fmt.Sprintf("/api/v1/provisioning/mute-timings/%s", mt.Name) 77 | req, err := json.Marshal(mt) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return c.request("PUT", uri, nil, req, nil) 83 | } 84 | 85 | // DeleteMutetiming deletes a mute timing. 86 | func (c *Client) DeleteMuteTiming(name string) error { 87 | uri := fmt.Sprintf("/api/v1/provisioning/mute-timings/%s", name) 88 | return c.request("DELETE", uri, nil, nil, nil) 89 | } 90 | -------------------------------------------------------------------------------- /alerting_mute_timing_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | func TestMuteTimings(t *testing.T) { 10 | t.Run("get mute timings succeeds", func(t *testing.T) { 11 | client := gapiTestTools(t, 200, getMuteTimingsJSON) 12 | 13 | mts, err := client.MuteTimings() 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | t.Log(pretty.PrettyFormat(mts)) 19 | if len(mts) != 2 { 20 | t.Errorf("wrong number of mute timings returned, got %#v", mts) 21 | } 22 | if mts[0].Name != "timing one" { 23 | t.Errorf("incorrect name - expected %s on element %d, got %#v", "timing one", 0, mts) 24 | } 25 | if mts[1].Name != "another timing" { 26 | t.Errorf("incorrect name - expected %s on element %d, got %#v", "another timing", 1, mts) 27 | } 28 | }) 29 | 30 | t.Run("get mute timing succeeds", func(t *testing.T) { 31 | client := gapiTestTools(t, 200, muteTimingJSON) 32 | 33 | mt, err := client.MuteTiming("timing one") 34 | 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | t.Log(pretty.PrettyFormat(mt)) 39 | if mt.Name != "timing one" { 40 | t.Errorf("incorrect name - expected %s, got %#v", "timing one", mt) 41 | } 42 | }) 43 | 44 | t.Run("get non-existent mute timing fails", func(t *testing.T) { 45 | client := gapiTestTools(t, 404, muteTimingJSON) 46 | 47 | mt, err := client.MuteTiming("does not exist") 48 | 49 | if err == nil { 50 | t.Errorf("expected error but got nil") 51 | t.Log(pretty.PrettyFormat(mt)) 52 | } 53 | }) 54 | 55 | t.Run("create mute timing succeeds", func(t *testing.T) { 56 | client := gapiTestTools(t, 201, muteTimingJSON) 57 | mt := createMuteTiming() 58 | 59 | err := client.NewMuteTiming(&mt) 60 | 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | }) 65 | 66 | t.Run("update mute timing succeeds", func(t *testing.T) { 67 | client := gapiTestTools(t, 200, muteTimingJSON) 68 | mt := createMuteTiming() 69 | mt.TimeIntervals[0].Weekdays = []WeekdayRange{"tuesday", "thursday"} 70 | 71 | err := client.UpdateMuteTiming(&mt) 72 | 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | }) 77 | 78 | t.Run("delete mute timing succeeds", func(t *testing.T) { 79 | client := gapiTestTools(t, 204, muteTimingJSON) 80 | 81 | err := client.DeleteMuteTiming("timing two") 82 | 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | }) 87 | } 88 | 89 | func createMuteTiming() MuteTiming { 90 | return MuteTiming{ 91 | Name: "timing two", 92 | TimeIntervals: []TimeInterval{ 93 | { 94 | Times: []TimeRange{ 95 | { 96 | StartMinute: "13:13", 97 | EndMinute: "15:15", 98 | }, 99 | }, 100 | Weekdays: []WeekdayRange{"monday", "wednesday"}, 101 | Months: []MonthRange{"1:3", "4"}, 102 | Years: []YearRange{"2022", "2023"}, 103 | Location: "America/New_York", 104 | }, 105 | }, 106 | } 107 | } 108 | 109 | const getMuteTimingsJSON = ` 110 | [ 111 | { 112 | "name": "timing one", 113 | "time_intervals": [ 114 | { 115 | "times": [ 116 | { 117 | "start_time": "13:13", 118 | "end_time": "15:15" 119 | } 120 | ], 121 | "weekdays": [ 122 | "monday:wednesday" 123 | ], 124 | "months": [ 125 | "1" 126 | ], 127 | "location": "America/New_York" 128 | } 129 | ] 130 | }, 131 | { 132 | "name": "another timing", 133 | "time_intervals": [ 134 | { 135 | "days_of_month": [ 136 | "1" 137 | ], 138 | "years": [ 139 | "2030" 140 | ] 141 | } 142 | ] 143 | } 144 | ]` 145 | 146 | const muteTimingJSON = ` 147 | { 148 | "name": "timing one", 149 | "time_intervals": [ 150 | { 151 | "times": [ 152 | { 153 | "start_time": "13:13", 154 | "end_time": "15:15" 155 | } 156 | ], 157 | "weekdays": [ 158 | "monday:wednesday" 159 | ], 160 | "months": [ 161 | "1" 162 | ] 163 | } 164 | ] 165 | }` 166 | -------------------------------------------------------------------------------- /alerting_notification_policy.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Represents a notification routing tree in Grafana Alerting. 9 | type NotificationPolicyTree struct { 10 | Receiver string `json:"receiver,omitempty"` 11 | GroupBy []string `json:"group_by,omitempty"` 12 | Routes []SpecificPolicy `json:"routes,omitempty"` 13 | GroupWait string `json:"group_wait,omitempty"` 14 | GroupInterval string `json:"group_interval,omitempty"` 15 | RepeatInterval string `json:"repeat_interval,omitempty"` 16 | Provenance string `json:"provenance,omitempty"` 17 | } 18 | 19 | // Represents a non-root node in a notification routing tree. 20 | type SpecificPolicy struct { 21 | Receiver string `json:"receiver,omitempty"` 22 | GroupBy []string `json:"group_by,omitempty"` 23 | ObjectMatchers Matchers `json:"object_matchers,omitempty"` 24 | MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"` 25 | Continue bool `json:"continue"` 26 | Routes []SpecificPolicy `json:"routes,omitempty"` 27 | GroupWait string `json:"group_wait,omitempty"` 28 | GroupInterval string `json:"group_interval,omitempty"` 29 | RepeatInterval string `json:"repeat_interval,omitempty"` 30 | } 31 | 32 | type Matchers []Matcher 33 | 34 | type Matcher struct { 35 | Type MatchType 36 | Name string 37 | Value string 38 | } 39 | 40 | type MatchType int 41 | 42 | const ( 43 | MatchEqual MatchType = iota 44 | MatchNotEqual 45 | MatchRegexp 46 | MatchNotRegexp 47 | ) 48 | 49 | func (m MatchType) String() string { 50 | typeToStr := map[MatchType]string{ 51 | MatchEqual: "=", 52 | MatchNotEqual: "!=", 53 | MatchRegexp: "=~", 54 | MatchNotRegexp: "!~", 55 | } 56 | if str, ok := typeToStr[m]; ok { 57 | return str 58 | } 59 | panic("unknown match type") 60 | } 61 | 62 | // UnmarshalJSON implements the json.Unmarshaler interface for Matchers. 63 | func (m *Matchers) UnmarshalJSON(data []byte) error { 64 | var rawMatchers [][3]string 65 | if err := json.Unmarshal(data, &rawMatchers); err != nil { 66 | return err 67 | } 68 | for _, rawMatcher := range rawMatchers { 69 | var matchType MatchType 70 | switch rawMatcher[1] { 71 | case "=": 72 | matchType = MatchEqual 73 | case "!=": 74 | matchType = MatchNotEqual 75 | case "=~": 76 | matchType = MatchRegexp 77 | case "!~": 78 | matchType = MatchNotRegexp 79 | default: 80 | return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) 81 | } 82 | 83 | matcher := Matcher{ 84 | Type: matchType, 85 | Name: rawMatcher[0], 86 | Value: rawMatcher[2], 87 | } 88 | *m = append(*m, matcher) 89 | } 90 | return nil 91 | } 92 | 93 | // MarshalJSON implements the json.Marshaler interface for Matchers. 94 | func (m Matchers) MarshalJSON() ([]byte, error) { 95 | if len(m) == 0 { 96 | return nil, nil 97 | } 98 | result := make([][3]string, len(m)) 99 | for i, matcher := range m { 100 | result[i] = [3]string{matcher.Name, matcher.Type.String(), matcher.Value} 101 | } 102 | return json.Marshal(result) 103 | } 104 | 105 | // NotificationPolicy fetches the notification policy tree. 106 | func (c *Client) NotificationPolicyTree() (NotificationPolicyTree, error) { 107 | np := NotificationPolicyTree{} 108 | err := c.request("GET", "/api/v1/provisioning/policies", nil, nil, &np) 109 | return np, err 110 | } 111 | 112 | // SetNotificationPolicy sets the notification policy tree. 113 | func (c *Client) SetNotificationPolicyTree(np *NotificationPolicyTree) error { 114 | req, err := json.Marshal(np) 115 | if err != nil { 116 | return err 117 | } 118 | return c.request("PUT", "/api/v1/provisioning/policies", nil, req, nil) 119 | } 120 | 121 | func (c *Client) ResetNotificationPolicyTree() error { 122 | return c.request("DELETE", "/api/v1/provisioning/policies", nil, nil, nil) 123 | } 124 | -------------------------------------------------------------------------------- /alerting_notification_policy_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | func TestNotificationPolicies(t *testing.T) { 10 | t.Run("get policy tree succeeds", func(t *testing.T) { 11 | client := gapiTestTools(t, 200, notificationPolicyJSON) 12 | 13 | np, err := client.NotificationPolicyTree() 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | t.Log(pretty.PrettyFormat(np)) 19 | if np.Receiver != "grafana-default-email" { 20 | t.Errorf("wrong receiver, got %#v", np.Receiver) 21 | } 22 | if len(np.Routes) != 1 { 23 | t.Errorf("wrong number of routes returned, got %#v", np) 24 | } 25 | }) 26 | 27 | t.Run("set policy tree succeeds", func(t *testing.T) { 28 | client := gapiTestTools(t, 202, `{"message":"created"}`) 29 | np := createNotificationPolicy() 30 | 31 | err := client.SetNotificationPolicyTree(&np) 32 | 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | }) 37 | 38 | t.Run("reset policy tree succeeds", func(t *testing.T) { 39 | client := gapiTestTools(t, 200, notificationPolicyJSON) 40 | 41 | err := client.ResetNotificationPolicyTree() 42 | 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | }) 47 | } 48 | 49 | func createNotificationPolicy() NotificationPolicyTree { 50 | return NotificationPolicyTree{ 51 | Receiver: "grafana-default-email", 52 | GroupBy: []string{"asdfasdf", "alertname"}, 53 | Routes: []SpecificPolicy{ 54 | { 55 | Receiver: "grafana-default-email", 56 | ObjectMatchers: Matchers{ 57 | { 58 | Type: MatchNotEqual, 59 | Name: "abc", 60 | Value: "def", 61 | }, 62 | }, 63 | Continue: true, 64 | }, 65 | { 66 | Receiver: "grafana-default-email", 67 | ObjectMatchers: Matchers{ 68 | { 69 | Type: MatchRegexp, 70 | Name: "jkl", 71 | Value: "something.*", 72 | }, 73 | }, 74 | Continue: false, 75 | }, 76 | }, 77 | GroupWait: "10s", 78 | GroupInterval: "5m", 79 | RepeatInterval: "4h", 80 | } 81 | } 82 | 83 | const notificationPolicyJSON = ` 84 | { 85 | "receiver": "grafana-default-email", 86 | "group_by": [ 87 | "..." 88 | ], 89 | "routes": [ 90 | { 91 | "receiver": "grafana-default-email", 92 | "object_matchers": [ 93 | [ 94 | "a", 95 | "=", 96 | "b" 97 | ], 98 | [ 99 | "asdf", 100 | "!=", 101 | "jk" 102 | ] 103 | ] 104 | } 105 | ], 106 | "group_wait": "5s", 107 | "group_interval": "1m", 108 | "repeat_interval": "1h" 109 | }` 110 | -------------------------------------------------------------------------------- /alertnotification.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // AlertNotification represents a Grafana alert notification. 9 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use ContactPoint instead. 10 | type AlertNotification struct { 11 | ID int64 `json:"id,omitempty"` 12 | UID string `json:"uid"` 13 | Name string `json:"name"` 14 | Type string `json:"type"` 15 | IsDefault bool `json:"isDefault"` 16 | DisableResolveMessage bool `json:"disableResolveMessage"` 17 | SendReminder bool `json:"sendReminder"` 18 | Frequency string `json:"frequency"` 19 | Settings interface{} `json:"settings"` 20 | SecureFields interface{} `json:"secureFields,omitempty"` 21 | SecureSettings interface{} `json:"secureSettings,omitempty"` 22 | } 23 | 24 | // AlertNotifications fetches and returns Grafana alert notifications. 25 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use ContactPoints instead. 26 | func (c *Client) AlertNotifications() ([]AlertNotification, error) { 27 | alertnotifications := make([]AlertNotification, 0) 28 | 29 | err := c.request("GET", "/api/alert-notifications/", nil, nil, &alertnotifications) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return alertnotifications, err 35 | } 36 | 37 | // AlertNotification fetches and returns a Grafana alert notification. 38 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use ContactPoint instead. 39 | func (c *Client) AlertNotification(id int64) (*AlertNotification, error) { 40 | path := fmt.Sprintf("/api/alert-notifications/%d", id) 41 | result := &AlertNotification{} 42 | err := c.request("GET", path, nil, nil, result) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return result, err 48 | } 49 | 50 | // NewAlertNotification creates a new Grafana alert notification. 51 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use NewContactPoint instead. 52 | func (c *Client) NewAlertNotification(a *AlertNotification) (int64, error) { 53 | data, err := json.Marshal(a) 54 | if err != nil { 55 | return 0, err 56 | } 57 | result := struct { 58 | ID int64 `json:"id"` 59 | }{} 60 | 61 | err = c.request("POST", "/api/alert-notifications", nil, data, &result) 62 | if err != nil { 63 | return 0, err 64 | } 65 | 66 | return result.ID, err 67 | } 68 | 69 | // UpdateAlertNotification updates a Grafana alert notification. 70 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use UpdateContactPoint instead. 71 | func (c *Client) UpdateAlertNotification(a *AlertNotification) error { 72 | path := fmt.Sprintf("/api/alert-notifications/%d", a.ID) 73 | data, err := json.Marshal(a) 74 | if err != nil { 75 | return err 76 | } 77 | err = c.request("PUT", path, nil, data, nil) 78 | 79 | return err 80 | } 81 | 82 | // DeleteAlertNotification deletes a Grafana alert notification. 83 | // Deprecated: Grafana Legacy Alerting is deprecated as of 9.0 and will be removed in the future. Use DeleteContactPoint instead. 84 | func (c *Client) DeleteAlertNotification(id int64) error { 85 | path := fmt.Sprintf("/api/alert-notifications/%d", id) 86 | 87 | return c.request("DELETE", path, nil, nil, nil) 88 | } 89 | -------------------------------------------------------------------------------- /alertnotification_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getAlertNotificationsJSON = ` 11 | [ 12 | { 13 | "id": 1, 14 | "uid": "team-a-email-notifier", 15 | "name": "Team A", 16 | "type": "email", 17 | "isDefault": false, 18 | "sendReminder": false, 19 | "disableResolveMessage": false, 20 | "settings": { 21 | "addresses": "dev@grafana.com" 22 | }, 23 | "created": "2018-04-23T14:44:09+02:00", 24 | "updated": "2018-08-20T15:47:49+02:00" 25 | } 26 | ] 27 | ` 28 | getAlertNotificationJSON = ` 29 | { 30 | "id": 1, 31 | "uid": "team-a-email-notifier", 32 | "name": "Team A", 33 | "type": "email", 34 | "isDefault": false, 35 | "sendReminder": false, 36 | "disableResolveMessage": false, 37 | "settings": { 38 | "addresses": "dev@grafana.com" 39 | }, 40 | "created": "2018-04-23T14:44:09+02:00", 41 | "updated": "2018-08-20T15:47:49+02:00" 42 | } 43 | ` 44 | createdAlertNotificationJSON = ` 45 | { 46 | "id": 1, 47 | "uid": "new-alert-notification", 48 | "name": "Team A", 49 | "type": "email", 50 | "isDefault": false, 51 | "sendReminder": true, 52 | "frequency": "15m", 53 | "settings": { 54 | "addresses": "dev@grafana.com" 55 | } 56 | } 57 | ` 58 | updatedAlertNotificationJSON = ` 59 | { 60 | "uid": "new-alert-notification", 61 | "name": "Team A", 62 | "type": "email", 63 | "isDefault": false, 64 | "sendReminder": true, 65 | "frequency": "15m", 66 | "settings": { 67 | "addresses": "dev@grafana.com" 68 | } 69 | } 70 | ` 71 | deletedAlertNotificationJSON = ` 72 | { 73 | "message":"Notification deleted" 74 | } 75 | ` 76 | ) 77 | 78 | func TestAlertNotifications(t *testing.T) { 79 | client := gapiTestTools(t, 200, getAlertNotificationsJSON) 80 | 81 | alertnotifications, err := client.AlertNotifications() 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | 86 | t.Log(pretty.PrettyFormat(alertnotifications)) 87 | 88 | if len(alertnotifications) != 1 { 89 | t.Error("Length of returned alert notifications should be 1") 90 | } 91 | if alertnotifications[0].ID != 1 || alertnotifications[0].Name != "Team A" { 92 | t.Error("Not correctly parsing returned alert notifications.") 93 | } 94 | } 95 | 96 | func TestAlertNotification(t *testing.T) { 97 | client := gapiTestTools(t, 200, getAlertNotificationJSON) 98 | 99 | alertnotification := int64(1) 100 | resp, err := client.AlertNotification(alertnotification) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | t.Log(pretty.PrettyFormat(resp)) 106 | 107 | if resp.ID != alertnotification || resp.Name != "Team A" { 108 | t.Error("Not correctly parsing returned alert notification.") 109 | } 110 | } 111 | 112 | func TestNewAlertNotification(t *testing.T) { 113 | client := gapiTestTools(t, 200, createdAlertNotificationJSON) 114 | 115 | an := &AlertNotification{ 116 | Name: "Team A", 117 | Type: "email", 118 | IsDefault: false, 119 | DisableResolveMessage: true, 120 | SendReminder: true, 121 | Frequency: "15m", 122 | Settings: map[string]string{ 123 | "addresses": "dev@grafana.com", 124 | }, 125 | } 126 | resp, err := client.NewAlertNotification(an) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | t.Log(pretty.PrettyFormat(resp)) 132 | 133 | if resp != 1 { 134 | t.Error("Not correctly parsing returned creation message.") 135 | } 136 | } 137 | 138 | func TestUpdateAlertNotification(t *testing.T) { 139 | client := gapiTestTools(t, 200, updatedAlertNotificationJSON) 140 | 141 | an := &AlertNotification{ 142 | ID: 1, 143 | Name: "Team A", 144 | Type: "email", 145 | IsDefault: false, 146 | DisableResolveMessage: true, 147 | SendReminder: true, 148 | Frequency: "15m", 149 | Settings: map[string]string{ 150 | "addresses": "dev@grafana.com", 151 | }, 152 | } 153 | 154 | err := client.UpdateAlertNotification(an) 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | } 159 | 160 | func TestDeleteAlertNotification(t *testing.T) { 161 | client := gapiTestTools(t, 200, deletedAlertNotificationJSON) 162 | 163 | err := client.DeleteAlertNotification(1) 164 | if err != nil { 165 | t.Error(err) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /annotation.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // Annotation represents a Grafana API Annotation 10 | type Annotation struct { 11 | ID int64 `json:"id,omitempty"` 12 | AlertID int64 `json:"alertId,omitempty"` 13 | DashboardID int64 `json:"dashboardId,omitempty"` 14 | DashboardUID string `json:"dashboardUID,omitempty"` 15 | PanelID int64 `json:"panelId"` 16 | UserID int64 `json:"userId,omitempty"` 17 | UserName string `json:"userName,omitempty"` 18 | NewState string `json:"newState,omitempty"` 19 | PrevState string `json:"prevState,omitempty"` 20 | Time int64 `json:"time"` 21 | TimeEnd int64 `json:"timeEnd,omitempty"` 22 | Text string `json:"text"` 23 | Metric string `json:"metric,omitempty"` 24 | RegionID int64 `json:"regionId,omitempty"` 25 | Type string `json:"type,omitempty"` 26 | Tags []string `json:"tags,omitempty"` 27 | IsRegion bool `json:"isRegion,omitempty"` 28 | } 29 | 30 | // GraphiteAnnotation represents a Grafana API annotation in Graphite format 31 | type GraphiteAnnotation struct { 32 | What string `json:"what"` 33 | When int64 `json:"when"` 34 | Data string `json:"data"` 35 | Tags []string `json:"tags,omitempty"` 36 | } 37 | 38 | // Annotations fetches the annotations queried with the params it's passed 39 | func (c *Client) Annotations(params url.Values) ([]Annotation, error) { 40 | result := []Annotation{} 41 | err := c.request("GET", "/api/annotations", params, nil, &result) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return result, err 47 | } 48 | 49 | // NewAnnotation creates a new annotation with the Annotation it is passed 50 | func (c *Client) NewAnnotation(a *Annotation) (int64, error) { 51 | data, err := json.Marshal(a) 52 | if err != nil { 53 | return 0, err 54 | } 55 | 56 | result := struct { 57 | ID int64 `json:"id"` 58 | }{} 59 | 60 | err = c.request("POST", "/api/annotations", nil, data, &result) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | return result.ID, err 66 | } 67 | 68 | // NewGraphiteAnnotation creates a new annotation with the GraphiteAnnotation it is passed 69 | func (c *Client) NewGraphiteAnnotation(gfa *GraphiteAnnotation) (int64, error) { 70 | data, err := json.Marshal(gfa) 71 | if err != nil { 72 | return 0, err 73 | } 74 | 75 | result := struct { 76 | ID int64 `json:"id"` 77 | }{} 78 | 79 | err = c.request("POST", "/api/annotations/graphite", nil, data, &result) 80 | if err != nil { 81 | return 0, err 82 | } 83 | 84 | return result.ID, err 85 | } 86 | 87 | // UpdateAnnotation updates all properties an existing annotation with the Annotation it is passed. 88 | func (c *Client) UpdateAnnotation(id int64, a *Annotation) (string, error) { 89 | path := fmt.Sprintf("/api/annotations/%d", id) 90 | data, err := json.Marshal(a) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | result := struct { 96 | Message string `json:"message"` 97 | }{} 98 | 99 | err = c.request("PUT", path, nil, data, &result) 100 | if err != nil { 101 | return "", err 102 | } 103 | 104 | return result.Message, err 105 | } 106 | 107 | // PatchAnnotation updates one or more properties of an existing annotation that matches the specified ID. 108 | func (c *Client) PatchAnnotation(id int64, a *Annotation) (string, error) { 109 | path := fmt.Sprintf("/api/annotations/%d", id) 110 | data, err := json.Marshal(a) 111 | if err != nil { 112 | return "", err 113 | } 114 | 115 | result := struct { 116 | Message string `json:"message"` 117 | }{} 118 | 119 | err = c.request("PATCH", path, nil, data, &result) 120 | if err != nil { 121 | return "", err 122 | } 123 | 124 | return result.Message, err 125 | } 126 | 127 | // DeleteAnnotation deletes the annotation of the ID it is passed 128 | func (c *Client) DeleteAnnotation(id int64) (string, error) { 129 | path := fmt.Sprintf("/api/annotations/%d", id) 130 | result := struct { 131 | Message string `json:"message"` 132 | }{} 133 | 134 | err := c.request("DELETE", path, nil, nil, &result) 135 | if err != nil { 136 | return "", err 137 | } 138 | 139 | return result.Message, err 140 | } 141 | 142 | // DeleteAnnotationByRegionID deletes the annotation corresponding to the region ID it is passed 143 | func (c *Client) DeleteAnnotationByRegionID(id int64) (string, error) { 144 | path := fmt.Sprintf("/api/annotations/region/%d", id) 145 | result := struct { 146 | Message string `json:"message"` 147 | }{} 148 | 149 | err := c.request("DELETE", path, nil, nil, &result) 150 | if err != nil { 151 | return "", err 152 | } 153 | 154 | return result.Message, err 155 | } 156 | -------------------------------------------------------------------------------- /annotation_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | const ( 11 | annotationsJSON = `[{ 12 | "id": 1124, 13 | "alertId": 0, 14 | "dashboardId": 468, 15 | "panelId": 2, 16 | "userId": 1, 17 | "userName": "", 18 | "newState": "", 19 | "prevState": "", 20 | "time": 1507266395000, 21 | "text": "test", 22 | "metric": "", 23 | "regionId": 1123, 24 | "type": "event", 25 | "tags": [ 26 | "tag1", 27 | "tag2" 28 | ], 29 | "data": {} 30 | }]` 31 | 32 | newAnnotationJSON = `{ 33 | "message":"Annotation added", 34 | "id": 1, 35 | "endId": 2 36 | }` 37 | 38 | newGraphiteAnnotationJSON = `{ 39 | "message":"Annotation added", 40 | "id": 1 41 | }` 42 | 43 | updateAnnotationJSON = `{"message":"Annotation updated"}` 44 | 45 | patchAnnotationJSON = `{"message":"Annotation patched"}` 46 | 47 | deleteAnnotationJSON = `{"message":"Annotation deleted"}` 48 | ) 49 | 50 | func TestAnnotations(t *testing.T) { 51 | client := gapiTestTools(t, 200, annotationsJSON) 52 | 53 | params := url.Values{} 54 | params.Add("from", "1506676478816") 55 | params.Add("to", "1507281278816") 56 | params.Add("limit", "100") 57 | 58 | as, err := client.Annotations(params) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | t.Log(pretty.PrettyFormat(as)) 64 | 65 | if as[0].ID != 1124 { 66 | t.Error("annotations response should contain annotations with an ID") 67 | } 68 | } 69 | 70 | func TestNewAnnotation(t *testing.T) { 71 | client := gapiTestTools(t, 200, newAnnotationJSON) 72 | 73 | a := Annotation{ 74 | DashboardID: 123, 75 | PanelID: 456, 76 | Time: 1507037197339, 77 | IsRegion: true, 78 | TimeEnd: 1507180805056, 79 | Tags: []string{"tag1", "tag2"}, 80 | Text: "text description", 81 | } 82 | res, err := client.NewAnnotation(&a) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | t.Log(pretty.PrettyFormat(res)) 88 | 89 | if res != 1 { 90 | t.Error("new annotation response should contain the ID of the new annotation") 91 | } 92 | } 93 | 94 | func TestUpdateAnnotation(t *testing.T) { 95 | client := gapiTestTools(t, 200, updateAnnotationJSON) 96 | 97 | a := Annotation{ 98 | Text: "new text description", 99 | } 100 | res, err := client.UpdateAnnotation(1, &a) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | t.Log(pretty.PrettyFormat(res)) 106 | 107 | if res != "Annotation updated" { 108 | t.Error("update annotation response should contain the correct response message") 109 | } 110 | } 111 | 112 | func TestPatchAnnotation(t *testing.T) { 113 | client := gapiTestTools(t, 200, patchAnnotationJSON) 114 | 115 | a := Annotation{ 116 | Text: "new text description", 117 | } 118 | res, err := client.PatchAnnotation(1, &a) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | 123 | t.Log(pretty.PrettyFormat(res)) 124 | 125 | if res != "Annotation patched" { 126 | t.Error("patch annotation response should contain the correct response message") 127 | } 128 | } 129 | 130 | func TestNewGraphiteAnnotation(t *testing.T) { 131 | client := gapiTestTools(t, 200, newGraphiteAnnotationJSON) 132 | 133 | a := GraphiteAnnotation{ 134 | What: "what", 135 | When: 1507180805056, 136 | Tags: []string{"tag1", "tag2"}, 137 | Data: "data", 138 | } 139 | res, err := client.NewGraphiteAnnotation(&a) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | t.Log(pretty.PrettyFormat(res)) 145 | 146 | if res != 1 { 147 | t.Error("new annotation response should contain the ID of the new annotation") 148 | } 149 | } 150 | 151 | func TestDeleteAnnotation(t *testing.T) { 152 | client := gapiTestTools(t, 200, deleteAnnotationJSON) 153 | 154 | res, err := client.DeleteAnnotation(1) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | 159 | t.Log(pretty.PrettyFormat(res)) 160 | 161 | if res != "Annotation deleted" { 162 | t.Error("delete annotation response should contain the correct response message") 163 | } 164 | } 165 | 166 | func TestDeleteAnnotationByRegionID(t *testing.T) { 167 | client := gapiTestTools(t, 200, deleteAnnotationJSON) 168 | 169 | res, err := client.DeleteAnnotationByRegionID(1) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | t.Log(pretty.PrettyFormat(res)) 175 | 176 | if res != "Annotation deleted" { 177 | t.Error("delete annotation by region ID response should contain the correct response message") 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /api_key.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | type CreateAPIKeyRequest struct { 12 | Name string `json:"name"` 13 | Role string `json:"role"` 14 | SecondsToLive int64 `json:"secondsToLive,omitempty"` 15 | } 16 | 17 | type CreateAPIKeyResponse struct { 18 | // ID field only returned after Grafana v7. 19 | ID int64 `json:"id,omitempty"` 20 | Name string `json:"name"` 21 | Key string `json:"key"` 22 | } 23 | 24 | type GetAPIKeysResponse struct { 25 | ID int64 `json:"id"` 26 | Name string `json:"name"` 27 | Role string `json:"role"` 28 | Expiration time.Time `json:"expiration,omitempty"` 29 | } 30 | 31 | type DeleteAPIKeyResponse struct { 32 | Message string `json:"message"` 33 | } 34 | 35 | // CreateAPIKey creates a new Grafana API key. 36 | func (c *Client) CreateAPIKey(request CreateAPIKeyRequest) (CreateAPIKeyResponse, error) { 37 | response := CreateAPIKeyResponse{} 38 | 39 | data, err := json.Marshal(request) 40 | if err != nil { 41 | return response, err 42 | } 43 | 44 | err = c.request("POST", "/api/auth/keys", nil, data, &response) 45 | return response, err 46 | } 47 | 48 | // GetAPIKeys retrieves a list of all API keys. 49 | func (c *Client) GetAPIKeys(includeExpired bool) ([]*GetAPIKeysResponse, error) { 50 | response := make([]*GetAPIKeysResponse, 0) 51 | 52 | query := url.Values{} 53 | query.Add("includeExpired", strconv.FormatBool(includeExpired)) 54 | 55 | err := c.request("GET", "/api/auth/keys", query, nil, &response) 56 | return response, err 57 | } 58 | 59 | // DeleteAPIKey deletes the Grafana API key with the specified ID. 60 | func (c *Client) DeleteAPIKey(id int64) (DeleteAPIKeyResponse, error) { 61 | response := DeleteAPIKeyResponse{} 62 | 63 | path := fmt.Sprintf("/api/auth/keys/%d", id) 64 | err := c.request("DELETE", path, nil, nil, &response) 65 | return response, err 66 | } 67 | -------------------------------------------------------------------------------- /api_key_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | createAPIKeyJSON = `{"name":"key-name", "key":"mock-api-key"}` //#nosec 11 | deleteAPIKeyJSON = `{"message":"API key deleted"}` //#nosec 12 | 13 | getAPIKeysJSON = `[ 14 | { 15 | "id": 1, 16 | "name": "key-name-2", 17 | "role": "Viewer" 18 | }, 19 | { 20 | "id": 2, 21 | "name": "key-name-2", 22 | "role": "Admin", 23 | "expiration": "2021-10-30T10:52:03+03:00" 24 | } 25 | ]` //#nosec 26 | ) 27 | 28 | func TestCreateAPIKey(t *testing.T) { 29 | client := gapiTestTools(t, 200, createAPIKeyJSON) 30 | 31 | req := CreateAPIKeyRequest{ 32 | Name: "key-name", 33 | Role: "Viewer", 34 | SecondsToLive: 0, 35 | } 36 | 37 | res, err := client.CreateAPIKey(req) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | t.Log(pretty.PrettyFormat(res)) 43 | } 44 | 45 | func TestDeleteAPIKey(t *testing.T) { 46 | client := gapiTestTools(t, 200, deleteAPIKeyJSON) 47 | 48 | res, err := client.DeleteAPIKey(int64(1)) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | 53 | t.Log(pretty.PrettyFormat(res)) 54 | } 55 | 56 | func TestGetAPIKeys(t *testing.T) { 57 | client := gapiTestTools(t, 200, getAPIKeysJSON) 58 | 59 | res, err := client.GetAPIKeys(true) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | 64 | t.Log(pretty.PrettyFormat(res)) 65 | } 66 | -------------------------------------------------------------------------------- /builtin_role_assignments.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const baseURL = "/api/access-control/builtin-roles" 9 | 10 | type BuiltInRoleAssignment struct { 11 | BuiltinRole string `json:"builtInRole"` 12 | RoleUID string `json:"roleUid"` 13 | Global bool `json:"global"` 14 | } 15 | 16 | // GetBuiltInRoleAssignments gets all built-in role assignments. Available only in Grafana Enterprise 8.+. 17 | func (c *Client) GetBuiltInRoleAssignments() (map[string][]*Role, error) { 18 | br := make(map[string][]*Role) 19 | err := c.request("GET", baseURL, nil, nil, &br) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return br, nil 24 | } 25 | 26 | // NewBuiltInRoleAssignment creates a new built-in role assignment. Available only in Grafana Enterprise 8.+. 27 | func (c *Client) NewBuiltInRoleAssignment(builtInRoleAssignment BuiltInRoleAssignment) (*BuiltInRoleAssignment, error) { 28 | body, err := json.Marshal(builtInRoleAssignment) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | br := &BuiltInRoleAssignment{} 34 | 35 | err = c.request("POST", baseURL, nil, body, &br) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return br, err 41 | } 42 | 43 | // DeleteBuiltInRoleAssignment remove the built-in role assignments. Available only in Grafana Enterprise 8.+. 44 | func (c *Client) DeleteBuiltInRoleAssignment(builtInRole BuiltInRoleAssignment) error { 45 | data, err := json.Marshal(builtInRole) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | qp := map[string][]string{ 51 | "global": {fmt.Sprint(builtInRole.Global)}, 52 | } 53 | url := fmt.Sprintf("%s/%s/roles/%s", baseURL, builtInRole.BuiltinRole, builtInRole.RoleUID) 54 | err = c.request("DELETE", url, qp, data, nil) 55 | 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /builtin_role_assignments_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | newBuiltInRoleAssignmentResponse = ` 9 | { 10 | "message": "Built-in role grant added" 11 | } 12 | ` 13 | getBuiltInRoleAssignmentsResponse = ` 14 | { 15 | "Grafana Admin": [ 16 | { 17 | "version": 1, 18 | "uid": "tJTyTNqMk", 19 | "name": "grafana:roles:users:admin:read", 20 | "description": "", 21 | "global": true 22 | } 23 | ], 24 | "Viewer": [ 25 | { 26 | "version": 2, 27 | "uid": "tJTyTNqMk1", 28 | "name": "custom:reports:editor", 29 | "description": "Role to allow users to create/read reports", 30 | "global": false 31 | } 32 | ] 33 | } 34 | ` 35 | 36 | removeBuiltInRoleAssignmentResponse = ` 37 | { 38 | "message": "Built-in role grant removed" 39 | } 40 | ` 41 | ) 42 | 43 | func TestNewBuiltInRoleAssignment(t *testing.T) { 44 | client := gapiTestTools(t, 200, newBuiltInRoleAssignmentResponse) 45 | 46 | br := BuiltInRoleAssignment{ 47 | Global: false, 48 | RoleUID: "test:policy", 49 | BuiltinRole: "Viewer", 50 | } 51 | 52 | _, err := client.NewBuiltInRoleAssignment(br) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | } 57 | 58 | func TestGetBuiltInRoleAssignments(t *testing.T) { 59 | client := gapiTestTools(t, 200, getBuiltInRoleAssignmentsResponse) 60 | 61 | resp, err := client.GetBuiltInRoleAssignments() 62 | 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | expected := map[string][]*Role{ 68 | "Grafana Admin": { 69 | { 70 | Version: 1, 71 | Global: true, 72 | Name: "grafana:roles:users:admin:read", 73 | UID: "tJTyTNqMk", 74 | Description: "", 75 | }, 76 | }, 77 | "Viewer": { 78 | { 79 | Version: 2, 80 | Global: false, 81 | Name: "custom:reports:editor", 82 | UID: "tJTyTNqMk1", 83 | Description: "Role to allow users to create/read reports", 84 | }, 85 | }, 86 | } 87 | 88 | if len(expected["Viewer"]) != len(resp["Viewer"]) || len(expected["Grafana Admin"]) != len(resp["Grafana Admin"]) { 89 | t.Error("Unexpected built-in role assignments.") 90 | } 91 | } 92 | 93 | func TestDeleteBuiltInRoleAssignment(t *testing.T) { 94 | client := gapiTestTools(t, 200, removeBuiltInRoleAssignmentResponse) 95 | 96 | br := BuiltInRoleAssignment{ 97 | Global: false, 98 | RoleUID: "test:policy", 99 | BuiltinRole: "Viewer", 100 | } 101 | err := client.DeleteBuiltInRoleAssignment(br) 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cloud_access_policy.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type CloudAccessPolicyLabelPolicy struct { 11 | Selector string `json:"selector"` 12 | } 13 | 14 | type CloudAccessPolicyRealm struct { 15 | Type string `json:"type"` 16 | Identifier string `json:"identifier"` 17 | LabelPolicies []CloudAccessPolicyLabelPolicy `json:"labelPolicies"` 18 | } 19 | 20 | type CreateCloudAccessPolicyInput struct { 21 | Name string `json:"name"` 22 | DisplayName string `json:"displayName"` 23 | Scopes []string `json:"scopes"` 24 | Realms []CloudAccessPolicyRealm `json:"realms"` 25 | } 26 | 27 | type UpdateCloudAccessPolicyInput struct { 28 | DisplayName string `json:"displayName"` 29 | Scopes []string `json:"scopes"` 30 | Realms []CloudAccessPolicyRealm `json:"realms"` 31 | } 32 | 33 | type CloudAccessPolicy struct { 34 | Name string `json:"name"` 35 | DisplayName string `json:"displayName"` 36 | Scopes []string `json:"scopes"` 37 | Realms []CloudAccessPolicyRealm `json:"realms"` 38 | 39 | // The following fields are not part of the input, but are returned by the API. 40 | ID string `json:"id"` 41 | OrgID string `json:"orgId"` 42 | CreatedAt time.Time `json:"createdAt"` 43 | UpdatedAt time.Time `json:"updatedAt"` 44 | } 45 | 46 | type CloudAccessPolicyItems struct { 47 | Items []*CloudAccessPolicy `json:"items"` 48 | } 49 | 50 | func (c *Client) CloudAccessPolicies(region string) (CloudAccessPolicyItems, error) { 51 | policies := CloudAccessPolicyItems{} 52 | err := c.request("GET", "/api/v1/accesspolicies", url.Values{ 53 | "region": []string{region}, 54 | }, nil, &policies) 55 | 56 | return policies, err 57 | } 58 | 59 | func (c *Client) CloudAccessPolicyByID(region, id string) (CloudAccessPolicy, error) { 60 | policy := CloudAccessPolicy{} 61 | err := c.request("GET", fmt.Sprintf("/api/v1/accesspolicies/%s", id), url.Values{ 62 | "region": []string{region}, 63 | }, nil, &policy) 64 | 65 | return policy, err 66 | } 67 | 68 | func (c *Client) CreateCloudAccessPolicy(region string, input CreateCloudAccessPolicyInput) (CloudAccessPolicy, error) { 69 | result := CloudAccessPolicy{} 70 | 71 | data, err := json.Marshal(input) 72 | if err != nil { 73 | return result, err 74 | } 75 | 76 | err = c.request("POST", "/api/v1/accesspolicies", url.Values{ 77 | "region": []string{region}, 78 | }, data, &result) 79 | 80 | return result, err 81 | } 82 | 83 | func (c *Client) UpdateCloudAccessPolicy(region, id string, input UpdateCloudAccessPolicyInput) (CloudAccessPolicy, error) { 84 | result := CloudAccessPolicy{} 85 | 86 | data, err := json.Marshal(input) 87 | if err != nil { 88 | return result, err 89 | } 90 | 91 | err = c.request("POST", fmt.Sprintf("/api/v1/accesspolicies/%s", id), url.Values{ 92 | "region": []string{region}, 93 | }, data, &result) 94 | 95 | return result, err 96 | } 97 | 98 | func (c *Client) DeleteCloudAccessPolicy(region, id string) error { 99 | return c.request("DELETE", fmt.Sprintf("/api/v1/accesspolicies/%s", id), url.Values{ 100 | "region": []string{region}, 101 | }, nil, nil) 102 | } 103 | -------------------------------------------------------------------------------- /cloud_access_policy_token.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type CreateCloudAccessPolicyTokenInput struct { 11 | AccessPolicyID string `json:"accessPolicyId"` 12 | Name string `json:"name"` 13 | DisplayName string `json:"displayName,omitempty"` 14 | ExpiresAt *time.Time `json:"expiresAt,omitempty"` 15 | } 16 | 17 | type UpdateCloudAccessPolicyTokenInput struct { 18 | DisplayName string `json:"displayName"` 19 | } 20 | 21 | type CloudAccessPolicyToken struct { 22 | ID string `json:"id"` 23 | AccessPolicyID string `json:"accessPolicyId"` 24 | Name string `json:"name"` 25 | DisplayName string `json:"displayName"` 26 | ExpiresAt *time.Time `json:"expiresAt"` 27 | FirstUsedAt time.Time `json:"firstUsedAt"` 28 | CreatedAt time.Time `json:"createdAt"` 29 | UpdatedAt *time.Time `json:"updatedAt"` 30 | 31 | Token string `json:"token,omitempty"` // Only returned when creating a token. 32 | } 33 | 34 | type CloudAccessPolicyTokenItems struct { 35 | Items []*CloudAccessPolicyToken `json:"items"` 36 | } 37 | 38 | func (c *Client) CloudAccessPolicyTokens(region, accessPolicyID string) (CloudAccessPolicyTokenItems, error) { 39 | tokens := CloudAccessPolicyTokenItems{} 40 | err := c.request("GET", "/api/v1/tokens", url.Values{ 41 | "region": []string{region}, 42 | "accessPolicyId": []string{accessPolicyID}, 43 | }, nil, &tokens) 44 | 45 | return tokens, err 46 | } 47 | 48 | func (c *Client) CloudAccessPolicyTokenByID(region, id string) (CloudAccessPolicyToken, error) { 49 | token := CloudAccessPolicyToken{} 50 | err := c.request("GET", fmt.Sprintf("/api/v1/tokens/%s", id), url.Values{ 51 | "region": []string{region}, 52 | }, nil, &token) 53 | 54 | return token, err 55 | } 56 | 57 | func (c *Client) CreateCloudAccessPolicyToken(region string, input CreateCloudAccessPolicyTokenInput) (CloudAccessPolicyToken, error) { 58 | token := CloudAccessPolicyToken{} 59 | 60 | data, err := json.Marshal(input) 61 | if err != nil { 62 | return token, err 63 | } 64 | 65 | err = c.request("POST", "/api/v1/tokens", url.Values{ 66 | "region": []string{region}, 67 | }, data, &token) 68 | 69 | return token, err 70 | } 71 | 72 | func (c *Client) UpdateCloudAccessPolicyToken(region, id string, input UpdateCloudAccessPolicyTokenInput) (CloudAccessPolicyToken, error) { 73 | token := CloudAccessPolicyToken{} 74 | 75 | data, err := json.Marshal(input) 76 | if err != nil { 77 | return token, err 78 | } 79 | 80 | err = c.request("POST", fmt.Sprintf("/api/v1/tokens/%s", id), url.Values{ 81 | "region": []string{region}, 82 | }, data, &token) 83 | 84 | return token, err 85 | } 86 | 87 | func (c *Client) DeleteCloudAccessPolicyToken(region, id string) error { 88 | return c.request("DELETE", fmt.Sprintf("/api/v1/tokens/%s", id), url.Values{ 89 | "region": []string{region}, 90 | }, nil, nil) 91 | } 92 | -------------------------------------------------------------------------------- /cloud_api_key.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type CreateCloudAPIKeyInput struct { 9 | Name string `json:"name"` 10 | Role string `json:"role"` 11 | } 12 | 13 | type ListCloudAPIKeysOutput struct { 14 | Items []*CloudAPIKey 15 | } 16 | 17 | type CloudAPIKey struct { 18 | ID int 19 | Name string 20 | Role string 21 | Token string 22 | Expiration string 23 | } 24 | 25 | func (c *Client) CreateCloudAPIKey(org string, input *CreateCloudAPIKeyInput) (*CloudAPIKey, error) { 26 | resp := CloudAPIKey{} 27 | data, err := json.Marshal(input) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | err = c.request("POST", fmt.Sprintf("/api/orgs/%s/api-keys", org), nil, data, &resp) 33 | return &resp, err 34 | } 35 | 36 | func (c *Client) ListCloudAPIKeys(org string) (*ListCloudAPIKeysOutput, error) { 37 | resp := &ListCloudAPIKeysOutput{} 38 | err := c.request("GET", fmt.Sprintf("/api/orgs/%s/api-keys", org), nil, nil, &resp) 39 | return resp, err 40 | } 41 | 42 | func (c *Client) DeleteCloudAPIKey(org string, keyName string) error { 43 | return c.request("DELETE", fmt.Sprintf("/api/orgs/%s/api-keys/%s", org, keyName), nil, nil, nil) 44 | } 45 | -------------------------------------------------------------------------------- /cloud_grafana_api_key.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // This function creates a API key inside the Grafana instance running in stack `stack`. It's used in order 9 | // to provision API keys inside Grafana while just having access to a Grafana Cloud API key. 10 | // 11 | // See https://grafana.com/docs/grafana-cloud/api/#create-grafana-api-keys for more information. 12 | func (c *Client) CreateGrafanaAPIKeyFromCloud(stack string, input *CreateAPIKeyRequest) (*CreateAPIKeyResponse, error) { 13 | data, err := json.Marshal(input) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | resp := &CreateAPIKeyResponse{} 19 | err = c.request("POST", fmt.Sprintf("/api/instances/%s/api/auth/keys", stack), nil, data, resp) 20 | return resp, err 21 | } 22 | -------------------------------------------------------------------------------- /cloud_grafana_service_account.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // This function creates a service account inside the Grafana instance running in stack `stack`. It's used in order 11 | // to provision service accounts inside Grafana while just having access to a Grafana Cloud API key. 12 | func (c *Client) CreateGrafanaServiceAccountFromCloud(stack string, input *CreateServiceAccountRequest) (*ServiceAccountDTO, error) { 13 | data, err := json.Marshal(input) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | resp := &ServiceAccountDTO{} 19 | err = c.request(http.MethodPost, fmt.Sprintf("/api/instances/%s/api/serviceaccounts", stack), nil, data, resp) 20 | return resp, err 21 | } 22 | 23 | // This function creates a service account token inside the Grafana instance running in stack `stack`. It's used in order 24 | // to provision service accounts inside Grafana while just having access to a Grafana Cloud API key. 25 | func (c *Client) CreateGrafanaServiceAccountTokenFromCloud(stack string, input *CreateServiceAccountTokenRequest) (*CreateServiceAccountTokenResponse, error) { 26 | data, err := json.Marshal(input) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | resp := &CreateServiceAccountTokenResponse{} 32 | err = c.request(http.MethodPost, fmt.Sprintf("/api/instances/%s/api/serviceaccounts/%d/tokens", stack, input.ServiceAccountID), nil, data, resp) 33 | return resp, err 34 | } 35 | 36 | // The Grafana Cloud API is disconnected from the Grafana API on the stacks unfortunately. That's why we can't use 37 | // the Grafana Cloud API key to fully manage service accounts on the Grafana API. The only thing we can do is to create 38 | // a temporary Admin service account, and create a Grafana API client with that. 39 | func (c *Client) CreateTemporaryStackGrafanaClient(stackSlug, tempSaPrefix string, tempKeyDuration time.Duration) (tempClient *Client, cleanup func() error, err error) { 40 | stack, err := c.StackBySlug(stackSlug) 41 | if err != nil { 42 | return nil, nil, err 43 | } 44 | 45 | name := fmt.Sprintf("%s%d", tempSaPrefix, time.Now().UnixNano()) 46 | 47 | req := &CreateServiceAccountRequest{ 48 | Name: name, 49 | Role: "Admin", 50 | } 51 | 52 | sa, err := c.CreateGrafanaServiceAccountFromCloud(stackSlug, req) 53 | if err != nil { 54 | return nil, nil, err 55 | } 56 | 57 | tokenRequest := &CreateServiceAccountTokenRequest{ 58 | Name: name, 59 | ServiceAccountID: sa.ID, 60 | SecondsToLive: int64(tempKeyDuration.Seconds()), 61 | } 62 | 63 | token, err := c.CreateGrafanaServiceAccountTokenFromCloud(stackSlug, tokenRequest) 64 | if err != nil { 65 | return nil, nil, err 66 | } 67 | 68 | client, err := New(stack.URL, Config{APIKey: token.Key}) 69 | if err != nil { 70 | return nil, nil, err 71 | } 72 | 73 | cleanup = func() error { 74 | _, err = client.DeleteServiceAccount(sa.ID) 75 | return err 76 | } 77 | 78 | return client, cleanup, nil 79 | } 80 | -------------------------------------------------------------------------------- /cloud_org.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type CloudOrg struct { 9 | ID int64 `json:"id"` 10 | Slug string `json:"slug"` 11 | Name string `json:"name"` 12 | URL string `json:"url"` 13 | CreatedAt time.Time `json:"createdAt"` 14 | UpdatedAt time.Time `json:"updatedAt"` 15 | } 16 | 17 | func (c *Client) GetCloudOrg(org string) (CloudOrg, error) { 18 | resp := CloudOrg{} 19 | err := c.request("GET", fmt.Sprintf("/api/orgs/%s", org), nil, nil, &resp) 20 | return resp, err 21 | } 22 | -------------------------------------------------------------------------------- /cloud_plugin.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type Plugin struct { 11 | ID int `json:"id"` 12 | Name string `json:"name"` 13 | Slug string `json:"slug"` 14 | Version string `json:"version"` 15 | Description string `json:"description"` 16 | } 17 | 18 | type CloudPluginInstallation struct { 19 | ID int `json:"id"` 20 | InstanceID int `json:"instanceId"` 21 | InstanceURL string `json:"instanceUrl"` 22 | InstanceSlug string `json:"instanceSlug"` 23 | PluginID int `json:"pluginId"` 24 | PluginSlug string `json:"pluginSlug"` 25 | PluginName string `json:"pluginName"` 26 | Version string `json:"version"` 27 | } 28 | 29 | // InstallCloudPlugin installs the specified plugin to the given stack. 30 | func (c *Client) InstallCloudPlugin(stackSlug string, pluginSlug string, pluginVersion string) (*CloudPluginInstallation, error) { 31 | installPluginRequest := struct { 32 | Plugin string `json:"plugin"` 33 | Version string `json:"version"` 34 | }{ 35 | Plugin: pluginSlug, 36 | Version: pluginVersion, 37 | } 38 | 39 | data, err := json.Marshal(installPluginRequest) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | var installation CloudPluginInstallation 45 | 46 | err = c.request("POST", fmt.Sprintf("/api/instances/%s/plugins", stackSlug), nil, data, &installation) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return &installation, nil 52 | } 53 | 54 | // UninstallCloudPlugin uninstalls the specified plugin to the given stack. 55 | func (c *Client) UninstallCloudPlugin(stackSlug string, pluginSlug string) error { 56 | return c.request("DELETE", fmt.Sprintf("/api/instances/%s/plugins/%s", stackSlug, pluginSlug), nil, nil, nil) 57 | } 58 | 59 | // IsCloudPluginInstalled returns a boolean if the specified plugin is installed on the stack. 60 | func (c *Client) IsCloudPluginInstalled(stackSlug string, pluginSlug string) (bool, error) { 61 | req, err := c.newRequest("GET", fmt.Sprintf("/api/instances/%s/plugins/%s", stackSlug, pluginSlug), nil, nil) 62 | if err != nil { 63 | return false, err 64 | } 65 | 66 | resp, err := c.client.Do(req) 67 | if err != nil { 68 | return false, err 69 | } 70 | 71 | defer resp.Body.Close() 72 | 73 | if resp.StatusCode != http.StatusOK { 74 | if resp.StatusCode == http.StatusNotFound { 75 | return false, nil 76 | } 77 | bodyContents, err := io.ReadAll(resp.Body) 78 | if err != nil { 79 | return false, err 80 | } 81 | 82 | return false, fmt.Errorf("status: %d, body: %v", resp.StatusCode, string(bodyContents)) 83 | } 84 | 85 | return true, nil 86 | } 87 | 88 | // GetCloudPluginInstallation returns the cloud plugin installation details for the specified plugin. 89 | func (c *Client) GetCloudPluginInstallation(stackSlug string, pluginSlug string) (*CloudPluginInstallation, error) { 90 | var installation CloudPluginInstallation 91 | 92 | err := c.request("GET", fmt.Sprintf("/api/instances/%s/plugins/%s", stackSlug, pluginSlug), nil, nil, &installation) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return &installation, nil 98 | } 99 | 100 | // PluginBySlug returns the plugin with the given slug. 101 | // An error will be returned given an unknown slug. 102 | func (c *Client) PluginBySlug(slug string) (*Plugin, error) { 103 | p := Plugin{} 104 | 105 | err := c.request("GET", fmt.Sprintf("/api/plugins/%s", slug), nil, nil, &p) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return &p, nil 111 | } 112 | 113 | // PluginByID returns the plugin with the given id. 114 | // An error will be returned given an unknown ID. 115 | func (c *Client) PluginByID(pluginID int64) (*Plugin, error) { 116 | p := Plugin{} 117 | 118 | err := c.request("GET", fmt.Sprintf("/api/plugins/%d", pluginID), nil, nil, p) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return &p, nil 124 | } 125 | -------------------------------------------------------------------------------- /cloud_plugin_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | installPluginJSON = ` 9 | { 10 | "id": 123, 11 | "instanceId": 2, 12 | "instanceUrl": "mystack.grafana.net", 13 | "instanceSlug": "mystack", 14 | "pluginId": 3, 15 | "pluginSlug": "some-plugin", 16 | "pluginName": "Some Plugin", 17 | "version": "1.2.3", 18 | "latestVersion": "1.2.3", 19 | "createdAt": "2021-12-22T14:02:46.000Z", 20 | "updatedAt": null 21 | }` 22 | uninstallPluginJSON = ` 23 | { 24 | "id": 123, 25 | "instanceId": 2, 26 | "instanceUrl": "mystack.grafana.net", 27 | "instanceSlug": "mystack", 28 | "pluginId": 3, 29 | "pluginSlug": "some-plugin", 30 | "pluginName": "Some Plugin", 31 | "version": "1.2.3", 32 | "latestVersion": "1.2.3", 33 | "createdAt": "2021-12-22T14:02:46.000Z", 34 | "updatedAt": null 35 | }` 36 | getPluginJSON = ` 37 | { 38 | "id": 34, 39 | "name": "Some Plugin", 40 | "slug": "some-plugin", 41 | "version": "1.2.3", 42 | "description": "Some Plugin for adding functionality" 43 | }` 44 | ) 45 | 46 | func TestInstallCloudPlugin(t *testing.T) { 47 | client := gapiTestTools(t, 200, installPluginJSON) 48 | 49 | installation, err := client.InstallCloudPlugin("some-stack", "some-plugin", "1.2.3") 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | 54 | expectedInstallation := CloudPluginInstallation{} 55 | err = UnmarshalJSONToStruct(installPluginJSON, &expectedInstallation) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if *installation != expectedInstallation { 61 | t.Errorf("Unexpected installation - Actual: %v, Expected: %v", installation, expectedInstallation) 62 | } 63 | 64 | for _, code := range []int{401, 403, 404, 412} { 65 | client = gapiTestTools(t, code, "error") 66 | 67 | installation, err = client.InstallCloudPlugin("some-stack", "some-plugin", "1.2.3") 68 | if err == nil { 69 | t.Errorf("%d not detected", code) 70 | } 71 | if installation != nil { 72 | t.Errorf("Expected empty installation, got %v", installation) 73 | } 74 | } 75 | } 76 | 77 | func TestUninstallCloudPlugin(t *testing.T) { 78 | client := gapiTestTools(t, 200, uninstallPluginJSON) 79 | 80 | err := client.UninstallCloudPlugin("some-stack", "some-plugin") 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | 85 | for _, code := range []int{401, 403, 404, 412} { 86 | client = gapiTestTools(t, code, "error") 87 | 88 | err = client.UninstallCloudPlugin("some-stack", "some-plugin") 89 | if err == nil { 90 | t.Errorf("%d not detected", code) 91 | } 92 | } 93 | } 94 | 95 | func TestIsCloudPluginInstalled(t *testing.T) { 96 | client := gapiTestTools(t, 200, getPluginJSON) 97 | 98 | ok, err := client.IsCloudPluginInstalled("some-stack", "some-plugin") 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | 103 | if !ok { 104 | t.Errorf("Expected plugin installation - Expected true, got false") 105 | } 106 | 107 | client = gapiTestTools(t, 404, "error") 108 | ok, err = client.IsCloudPluginInstalled("some-stack", "some-plugin") 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | 113 | if ok { 114 | t.Errorf("Unexpected plugin installation - Expected false, got true") 115 | } 116 | 117 | for _, code := range []int{401, 403, 412} { 118 | client = gapiTestTools(t, code, "error") 119 | 120 | _, err := client.IsCloudPluginInstalled("some-stack", "some-plugin") 121 | if err == nil { 122 | t.Errorf("%d not detected", code) 123 | } 124 | } 125 | } 126 | 127 | func TestGetCloudPluginInstallation(t *testing.T) { 128 | client := gapiTestTools(t, 200, installPluginJSON) 129 | 130 | installation, err := client.GetCloudPluginInstallation("some-stack", "some-plugin") 131 | if err != nil { 132 | t.Error(err) 133 | } 134 | 135 | expectedInstallation := CloudPluginInstallation{} 136 | err = UnmarshalJSONToStruct(installPluginJSON, &expectedInstallation) 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | if *installation != expectedInstallation { 142 | t.Errorf("Unexpected installation - Actual: %v, Expected: %v", installation, expectedInstallation) 143 | } 144 | 145 | for _, code := range []int{401, 403, 404, 412} { 146 | client = gapiTestTools(t, code, "error") 147 | 148 | installation, err = client.GetCloudPluginInstallation("some-stack", "some-plugin") 149 | if err == nil { 150 | t.Errorf("%d not detected", code) 151 | } 152 | if installation != nil { 153 | t.Errorf("Expected empty installation, got %v", installation) 154 | } 155 | } 156 | } 157 | 158 | func TestPlugin(t *testing.T) { 159 | client := gapiTestTools(t, 200, getPluginJSON) 160 | 161 | plugin, err := client.PluginBySlug("some-plugin") 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | 166 | expectedPlugin := Plugin{} 167 | err = UnmarshalJSONToStruct(getPluginJSON, &expectedPlugin) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | if *plugin != expectedPlugin { 173 | t.Errorf("Unexpected plugin - Actual: %v, Expected: %v", plugin, expectedPlugin) 174 | } 175 | 176 | for _, code := range []int{404} { 177 | client = gapiTestTools(t, code, "error") 178 | 179 | _, err = client.PluginBySlug("some-plugin") 180 | if err == nil { 181 | t.Errorf("%d not detected", code) 182 | } 183 | } 184 | } 185 | 186 | func TestPluginByID(t *testing.T) { 187 | client := gapiTestTools(t, 200, getPluginJSON) 188 | 189 | plugin, err := client.PluginBySlug("some-plugin") 190 | if err != nil { 191 | t.Error(err) 192 | } 193 | 194 | expectedPlugin := Plugin{} 195 | err = UnmarshalJSONToStruct(getPluginJSON, &expectedPlugin) 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | if *plugin != expectedPlugin { 201 | t.Errorf("Unexpected plugin - Actual: %v, Expected: %v", plugin, expectedPlugin) 202 | } 203 | 204 | for _, code := range []int{404} { 205 | client = gapiTestTools(t, code, "error") 206 | 207 | _, err = client.PluginByID(123) 208 | if err == nil { 209 | t.Errorf("%d not detected", code) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /cloud_regions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import "fmt" 4 | 5 | // CloudRegion represents a Grafana Cloud region. 6 | // https://grafana.com/docs/grafana-cloud/reference/cloud-api/#list-regions 7 | type CloudRegion struct { 8 | ID int `json:"id"` 9 | Status string `json:"status"` 10 | Slug string `json:"slug"` 11 | Name string `json:"name"` 12 | Description string `json:"description"` 13 | CreatedAt string `json:"createdAt"` 14 | UpdatedAt string `json:"updatedAt"` 15 | Visibility string `json:"visibility"` 16 | 17 | // Service URLs for the region 18 | StackStateServiceURL string `json:"stackStateServiceUrl"` 19 | SyntheticMonitoringAPIURL string `json:"syntheticMonitoringApiUrl"` 20 | IntegrationsAPIURL string `json:"integrationsApiUrl"` 21 | HostedExportersAPIURL string `json:"hostedExportersApiUrl"` 22 | MachineLearningAPIURL string `json:"machineLearningApiUrl"` 23 | IncidentsAPIURL string `json:"incidentsApiUrl"` 24 | 25 | // Hosted Grafana 26 | HGClusterID int `json:"hgClusterId"` 27 | HGClusterSlug string `json:"hgClusterSlug"` 28 | HGClusterName string `json:"hgClusterName"` 29 | HGClusterURL string `json:"hgClusterUrl"` 30 | 31 | // Hosted Metrics: Prometheus 32 | HMPromClusterID int `json:"hmPromClusterId"` 33 | HMPromClusterSlug string `json:"hmPromClusterSlug"` 34 | HMPromClusterName string `json:"hmPromClusterName"` 35 | HMPromClusterURL string `json:"hmPromClusterUrl"` 36 | 37 | // Hosted Metrics: Graphite 38 | HMGraphiteClusterID int `json:"hmGraphiteClusterId"` 39 | HMGraphiteClusterSlug string `json:"hmGraphiteClusterSlug"` 40 | HMGraphiteClusterName string `json:"hmGraphiteClusterName"` 41 | HMGraphiteClusterURL string `json:"hmGraphiteClusterUrl"` 42 | 43 | // Hosted Logs 44 | HLClusterID int `json:"hlClusterId"` 45 | HLClusterSlug string `json:"hlClusterSlug"` 46 | HLClusterName string `json:"hlClusterName"` 47 | HLClusterURL string `json:"hlClusterUrl"` 48 | 49 | // Alertmanager 50 | AMClusterID int `json:"amClusterId"` 51 | AMClusterSlug string `json:"amClusterSlug"` 52 | AMClusterName string `json:"amClusterName"` 53 | AMClusterURL string `json:"amClusterUrl"` 54 | 55 | // Hosted Traces 56 | HTClusterID int `json:"htClusterId"` 57 | HTClusterSlug string `json:"htClusterSlug"` 58 | HTClusterName string `json:"htClusterName"` 59 | HTClusterURL string `json:"htClusterUrl"` 60 | } 61 | 62 | // CloudRegionsResponse represents the response from the Grafana Cloud regions API. 63 | type CloudRegionsResponse struct { 64 | Items []CloudRegion `json:"items"` 65 | } 66 | 67 | // GetCloudRegions fetches and returns all Grafana Cloud regions. 68 | func (c *Client) GetCloudRegions() (CloudRegionsResponse, error) { 69 | var regions CloudRegionsResponse 70 | err := c.request("GET", "/api/stack-regions", nil, nil, ®ions) 71 | return regions, err 72 | } 73 | 74 | // GetCloudRegionBySlug fetches and returns the cloud region which matches the given slug. 75 | // You can also provide a numeric region ID. 76 | func (c *Client) GetCloudRegionBySlug(slug string) (CloudRegion, error) { 77 | var region CloudRegion 78 | err := c.request("GET", fmt.Sprintf("/api/stack-regions/%s", slug), nil, nil, ®ion) 79 | return region, err 80 | } 81 | -------------------------------------------------------------------------------- /cloud_regions_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import "testing" 4 | 5 | var ( 6 | cloudRegionResponse = `{ 7 | "id": 1, 8 | "status": "active", 9 | "slug": "us", 10 | "name": "United States", 11 | "description": "United States", 12 | "createdAt": "2021-08-20T20:00:27.000Z", 13 | "updatedAt": "2022-01-18T20:00:51.000Z", 14 | "visibility": "public", 15 | "stackStateServiceUrl": "http://apiserver.stackstate.svc.cluster.local", 16 | "syntheticMonitoringApiUrl": "https://synthetic-monitoring-api.grafana.net", 17 | "integrationsApiUrl": "https://integrations-api-us-central.grafana.net", 18 | "hostedExportersApiUrl": "https://hosted-exporters-api-us-central.grafana.net", 19 | "machineLearningApiUrl": "https://machine-learning-prod-us-central-0.grafana.net/machine-learning", 20 | "incidentApiUrl": null, 21 | "hgClusterId": 69, 22 | "hgClusterSlug": "prod-us-central-0", 23 | "hgClusterName": "prod-us-central-0", 24 | "hgClusterUrl": "https://hg-api-prod-us-central-0.grafana.net", 25 | "hmPromClusterId": 105, 26 | "hmPromClusterSlug": "prod-10-prod-us-central-0", 27 | "hmPromClusterName": "cortex-prod-10", 28 | "hmPromClusterUrl": "https://prometheus-prod-10-prod-us-central-0.grafana.net", 29 | "hmGraphiteClusterId": 105, 30 | "hmGraphiteClusterSlug": "prod-10-prod-us-central-0", 31 | "hmGraphiteClusterName": "cortex-prod-10", 32 | "hmGraphiteClusterUrl": "https://prometheus-prod-10-prod-us-central-0.grafana.net", 33 | "hlClusterId": 84, 34 | "hlClusterSlug": "loki-prod-us-central-0", 35 | "hlClusterName": "Hosted Logs Cluster (prod-us-central-0)", 36 | "hlClusterUrl": "https://logs-prod3.grafana.net", 37 | "amClusterId": 68, 38 | "amClusterSlug": "alertmanager-us-central1", 39 | "amClusterName": "alertmanager-us-central1", 40 | "amClusterUrl": "https://alertmanager-us-central1.grafana.net", 41 | "htClusterId": 78, 42 | "htClusterSlug": "tempo-prod-us-central1", 43 | "htClusterName": "tempo-prod-us-central1", 44 | "htClusterUrl": "https://tempo-us-central1.grafana.net" 45 | } 46 | ` 47 | cloudRegionsResponse = `{ 48 | "items": [ 49 | ` + cloudRegionResponse + ` 50 | ] 51 | } 52 | ` 53 | expectedRegion = CloudRegion{ID: 1, 54 | Status: "active", 55 | Slug: "us", 56 | Name: "United States", 57 | Description: "United States", 58 | CreatedAt: "2021-08-20T20:00:27.000Z", 59 | UpdatedAt: "2022-01-18T20:00:51.000Z", 60 | Visibility: "public", 61 | StackStateServiceURL: "http://apiserver.stackstate.svc.cluster.local", 62 | SyntheticMonitoringAPIURL: "https://synthetic-monitoring-api.grafana.net", 63 | IntegrationsAPIURL: "https://integrations-api-us-central.grafana.net", 64 | HostedExportersAPIURL: "https://hosted-exporters-api-us-central.grafana.net", 65 | MachineLearningAPIURL: "https://machine-learning-prod-us-central-0.grafana.net/machine-learning", 66 | IncidentsAPIURL: "", 67 | HGClusterID: 69, 68 | HGClusterSlug: "prod-us-central-0", 69 | HGClusterName: "prod-us-central-0", 70 | HGClusterURL: "https://hg-api-prod-us-central-0.grafana.net", 71 | HMPromClusterID: 105, 72 | HMPromClusterSlug: "prod-10-prod-us-central-0", 73 | HMPromClusterName: "cortex-prod-10", 74 | HMPromClusterURL: "https://prometheus-prod-10-prod-us-central-0.grafana.net", 75 | HMGraphiteClusterID: 105, 76 | HMGraphiteClusterSlug: "prod-10-prod-us-central-0", 77 | HMGraphiteClusterName: "cortex-prod-10", 78 | HMGraphiteClusterURL: "https://prometheus-prod-10-prod-us-central-0.grafana.net", 79 | HLClusterID: 84, 80 | HLClusterSlug: "loki-prod-us-central-0", 81 | HLClusterName: "Hosted Logs Cluster (prod-us-central-0)", 82 | HLClusterURL: "https://logs-prod3.grafana.net", 83 | AMClusterID: 68, 84 | AMClusterSlug: "alertmanager-us-central1", 85 | AMClusterName: "alertmanager-us-central1", 86 | AMClusterURL: "https://alertmanager-us-central1.grafana.net", 87 | HTClusterID: 78, 88 | HTClusterSlug: "tempo-prod-us-central1", 89 | HTClusterName: "tempo-prod-us-central1", 90 | HTClusterURL: "https://tempo-us-central1.grafana.net"} 91 | ) 92 | 93 | func TestCloudRegions(t *testing.T) { 94 | client := gapiTestTools(t, 200, cloudRegionsResponse) 95 | 96 | regions, err := client.GetCloudRegions() 97 | 98 | if err != nil { 99 | t.Fatalf("expected error to be nil; got: %s", err.Error()) 100 | } 101 | 102 | // check that the number of items is the same 103 | if got := len(regions.Items); got != 1 { 104 | t.Errorf("Length of returned regions - Actual regions count: %d, Expected regions count: %d", got, 1) 105 | } 106 | 107 | if got := regions.Items[0]; got != expectedRegion { 108 | t.Errorf("Unexpected Region - Got:\n%#v\n, Expected:\n%#v\n", got, expectedRegion) 109 | } 110 | } 111 | 112 | func TestCloudRegionBySlug(t *testing.T) { 113 | client := gapiTestTools(t, 200, cloudRegionResponse) 114 | 115 | resp, err := client.GetCloudRegionBySlug("us") 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | if resp != expectedRegion { 121 | t.Errorf("Unexpected Region - Got:\n%#v\n, Expected:\n%#v\n", resp, expectedRegion) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /dashboard.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // DashboardMeta represents Grafana dashboard meta. 10 | type DashboardMeta struct { 11 | IsStarred bool `json:"isStarred"` 12 | Slug string `json:"slug"` 13 | Folder int64 `json:"folderId"` 14 | FolderUID string `json:"folderUid"` 15 | URL string `json:"url"` 16 | } 17 | 18 | // DashboardSaveResponse represents the Grafana API response to creating or saving a dashboard. 19 | type DashboardSaveResponse struct { 20 | Slug string `json:"slug"` 21 | ID int64 `json:"id"` 22 | UID string `json:"uid"` 23 | Status string `json:"status"` 24 | Version int64 `json:"version"` 25 | } 26 | 27 | // Dashboard represents a Grafana dashboard. 28 | type Dashboard struct { 29 | Model map[string]interface{} `json:"dashboard"` 30 | FolderID int64 `json:"folderId"` 31 | 32 | // This field is read-only. It is not used when creating a new dashboard. 33 | Meta DashboardMeta `json:"meta"` 34 | 35 | // These fields are only used when creating a new dashboard, they will always be empty when getting a dashboard. 36 | Overwrite bool `json:"overwrite,omitempty"` 37 | Message string `json:"message,omitempty"` 38 | FolderUID string `json:"folderUid,omitempty"` 39 | } 40 | 41 | // SaveDashboard is a deprecated method for saving a Grafana dashboard. Use NewDashboard. 42 | // Deprecated: Use NewDashboard instead. 43 | func (c *Client) SaveDashboard(model map[string]interface{}, overwrite bool) (*DashboardSaveResponse, error) { 44 | wrapper := map[string]interface{}{ 45 | "dashboard": model, 46 | "overwrite": overwrite, 47 | } 48 | data, err := json.Marshal(wrapper) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | result := &DashboardSaveResponse{} 54 | err = c.request("POST", "/api/dashboards/db", nil, data, &result) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return result, err 60 | } 61 | 62 | // NewDashboard creates a new Grafana dashboard. 63 | func (c *Client) NewDashboard(dashboard Dashboard) (*DashboardSaveResponse, error) { 64 | data, err := json.Marshal(dashboard) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | result := &DashboardSaveResponse{} 70 | err = c.request("POST", "/api/dashboards/db", nil, data, &result) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return result, err 76 | } 77 | 78 | // Dashboards fetches and returns all dashboards. 79 | func (c *Client) Dashboards() ([]FolderDashboardSearchResponse, error) { 80 | const limit = 1000 81 | 82 | var ( 83 | page = 0 84 | newDashboards []FolderDashboardSearchResponse 85 | dashboards []FolderDashboardSearchResponse 86 | query = make(url.Values) 87 | ) 88 | 89 | query.Set("type", "dash-db") 90 | query.Set("limit", fmt.Sprint(limit)) 91 | 92 | for { 93 | page++ 94 | query.Set("page", fmt.Sprint(page)) 95 | 96 | if err := c.request("GET", "/api/search", query, nil, &newDashboards); err != nil { 97 | return nil, err 98 | } 99 | 100 | dashboards = append(dashboards, newDashboards...) 101 | 102 | if len(newDashboards) < limit { 103 | return dashboards, nil 104 | } 105 | } 106 | } 107 | 108 | // Dashboard will be removed. 109 | // Deprecated: Starting from Grafana v5.0. Use DashboardByUID instead. 110 | func (c *Client) Dashboard(slug string) (*Dashboard, error) { 111 | return c.dashboard(fmt.Sprintf("/api/dashboards/db/%s", slug)) 112 | } 113 | 114 | // DashboardByUID gets a dashboard by UID. 115 | func (c *Client) DashboardByUID(uid string) (*Dashboard, error) { 116 | return c.dashboard(fmt.Sprintf("/api/dashboards/uid/%s", uid)) 117 | } 118 | 119 | // DashboardsByIDs uses the folder and dashboard search endpoint to find 120 | // dashboards by list of dashboard IDs. 121 | func (c *Client) DashboardsByIDs(ids []int64) ([]FolderDashboardSearchResponse, error) { 122 | dashboardIdsJSON, err := json.Marshal(ids) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | params := url.Values{ 128 | "type": {"dash-db"}, 129 | "dashboardIds": {string(dashboardIdsJSON)}, 130 | } 131 | return c.FolderDashboardSearch(params) 132 | } 133 | 134 | func (c *Client) dashboard(path string) (*Dashboard, error) { 135 | result := &Dashboard{} 136 | err := c.request("GET", path, nil, nil, &result) 137 | if err != nil { 138 | return nil, err 139 | } 140 | result.FolderID = result.Meta.Folder 141 | 142 | return result, err 143 | } 144 | 145 | // DeleteDashboard will be removed. 146 | // Deprecated: Starting from Grafana v5.0. Use DeleteDashboardByUID instead. 147 | func (c *Client) DeleteDashboard(slug string) error { 148 | return c.deleteDashboard(fmt.Sprintf("/api/dashboards/db/%s", slug)) 149 | } 150 | 151 | // DeleteDashboardByUID deletes a dashboard by UID. 152 | func (c *Client) DeleteDashboardByUID(uid string) error { 153 | return c.deleteDashboard(fmt.Sprintf("/api/dashboards/uid/%s", uid)) 154 | } 155 | 156 | func (c *Client) deleteDashboard(path string) error { 157 | return c.request("DELETE", path, nil, nil, nil) 158 | } 159 | -------------------------------------------------------------------------------- /dashboard_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // DashboardPermission has information such as a dashboard, user, team, role and permission. 9 | type DashboardPermission struct { 10 | DashboardID int64 `json:"dashboardId"` 11 | DashboardUID string `json:"uid"` 12 | UserID int64 `json:"userId"` 13 | TeamID int64 `json:"teamId"` 14 | Role string `json:"role"` 15 | IsFolder bool `json:"isFolder"` 16 | Inherited bool `json:"inherited"` 17 | 18 | // Permission levels are 19 | // 1 = View 20 | // 2 = Edit 21 | // 4 = Admin 22 | Permission int64 `json:"permission"` 23 | PermissionName string `json:"permissionName"` 24 | } 25 | 26 | // DashboardPermissions fetches and returns the permissions for the dashboard whose ID it's passed. 27 | func (c *Client) DashboardPermissions(id int64) ([]*DashboardPermission, error) { 28 | permissions := make([]*DashboardPermission, 0) 29 | err := c.request("GET", fmt.Sprintf("/api/dashboards/id/%d/permissions", id), nil, nil, &permissions) 30 | return permissions, err 31 | } 32 | 33 | // UpdateDashboardPermissions remove existing permissions if items are not included in the request. 34 | func (c *Client) UpdateDashboardPermissions(id int64, items *PermissionItems) error { 35 | path := fmt.Sprintf("/api/dashboards/id/%d/permissions", id) 36 | data, err := json.Marshal(items) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return c.request("POST", path, nil, data, nil) 42 | } 43 | 44 | // DashboardPermissionsByUID fetches and returns the permissions for the dashboard whose UID it's passed. 45 | func (c *Client) DashboardPermissionsByUID(uid string) ([]*DashboardPermission, error) { 46 | permissions := make([]*DashboardPermission, 0) 47 | err := c.request("GET", fmt.Sprintf("/api/dashboards/uid/%s/permissions", uid), nil, nil, &permissions) 48 | return permissions, err 49 | } 50 | 51 | // UpdateDashboardPermissionsByUID remove existing permissions if items are not included in the request. 52 | func (c *Client) UpdateDashboardPermissionsByUID(uid string, items *PermissionItems) error { 53 | path := fmt.Sprintf("/api/dashboards/uid/%s/permissions", uid) 54 | data, err := json.Marshal(items) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return c.request("POST", path, nil, data, nil) 60 | } 61 | 62 | func (c *Client) ListDashboardResourcePermissions(uid string) ([]*ResourcePermission, error) { 63 | return c.listResourcePermissions(DashboardsResource, ResourceUID(uid)) 64 | } 65 | 66 | func (c *Client) SetDashboardResourcePermissions(uid string, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 67 | return c.setResourcePermissions(DashboardsResource, ResourceUID(uid), body) 68 | } 69 | 70 | func (c *Client) SetUserDashboardResourcePermissions(dashboardUID string, userID int64, permission string) (*SetResourcePermissionsResponse, error) { 71 | return c.setResourcePermissionByAssignment( 72 | DashboardsResource, 73 | ResourceUID(dashboardUID), 74 | UsersResource, 75 | ResourceID(userID), 76 | SetResourcePermissionBody{ 77 | Permission: SetResourcePermissionItem{ 78 | UserID: userID, 79 | Permission: permission, 80 | }, 81 | }, 82 | ) 83 | } 84 | 85 | func (c *Client) SetTeamDashboardResourcePermissions(dashboardUID string, teamID int64, permission string) (*SetResourcePermissionsResponse, error) { 86 | return c.setResourcePermissionByAssignment( 87 | DashboardsResource, 88 | ResourceUID(dashboardUID), 89 | TeamsResource, 90 | ResourceID(teamID), 91 | SetResourcePermissionBody{ 92 | Permission: SetResourcePermissionItem{ 93 | TeamID: teamID, 94 | Permission: permission, 95 | }, 96 | }, 97 | ) 98 | } 99 | 100 | func (c *Client) SetBuiltInRoleDashboardResourcePermissions(dashboardUID string, builtInRole string, permission string) (*SetResourcePermissionsResponse, error) { 101 | return c.setResourcePermissionByAssignment( 102 | DashboardsResource, 103 | ResourceUID(dashboardUID), 104 | BuiltInRolesResource, 105 | ResourceUID(builtInRole), 106 | SetResourcePermissionBody{ 107 | Permission: SetResourcePermissionItem{ 108 | BuiltinRole: builtInRole, 109 | Permission: permission, 110 | }, 111 | }, 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /dashboard_permissions_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getDashboardPermissionsJSON = ` 11 | [ 12 | { 13 | "dashboardId": 1, 14 | "created": "2017-06-20T02:00:00+02:00", 15 | "updated": "2017-06-20T02:00:00+02:00", 16 | "userId": 0, 17 | "userLogin": "", 18 | "userEmail": "", 19 | "teamId": 0, 20 | "team": "", 21 | "role": "Viewer", 22 | "permission": 1, 23 | "permissionName": "View", 24 | "uid": "nErXDvCkzz", 25 | "title": "", 26 | "slug": "", 27 | "isFolder": false, 28 | "url": "", 29 | "inherited": false 30 | }, 31 | { 32 | "dashboardId": 2, 33 | "created": "2017-06-20T02:00:00+02:00", 34 | "updated": "2017-06-20T02:00:00+02:00", 35 | "userId": 0, 36 | "userLogin": "", 37 | "userEmail": "", 38 | "teamId": 0, 39 | "team": "", 40 | "role": "Editor", 41 | "permission": 2, 42 | "permissionName": "Edit", 43 | "uid": "nErXDvCkzz", 44 | "title": "", 45 | "slug": "", 46 | "isFolder": false, 47 | "url": "", 48 | "inherited": true 49 | } 50 | ] 51 | ` 52 | updateDashboardPermissionsJSON = ` 53 | { 54 | "message": "Dashboard permissions updated" 55 | } 56 | ` 57 | ) 58 | 59 | func TestDashboardPermissions(t *testing.T) { 60 | client := gapiTestTools(t, 200, getDashboardPermissionsJSON) 61 | 62 | resp, err := client.DashboardPermissions(1) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | t.Log(pretty.PrettyFormat(resp)) 68 | 69 | expects := []*DashboardPermission{ 70 | { 71 | DashboardID: 1, 72 | DashboardUID: "nErXDvCkzz", 73 | Role: "Viewer", 74 | UserID: 0, 75 | TeamID: 0, 76 | IsFolder: false, 77 | Inherited: false, 78 | Permission: 1, 79 | PermissionName: "View", 80 | }, 81 | { 82 | DashboardID: 2, 83 | DashboardUID: "nErXDvCkzz", 84 | Role: "Editor", 85 | UserID: 0, 86 | TeamID: 0, 87 | IsFolder: false, 88 | Inherited: true, 89 | Permission: 2, 90 | PermissionName: "Edit", 91 | }, 92 | } 93 | 94 | for i, expect := range expects { 95 | t.Run("check data", func(t *testing.T) { 96 | if resp[i].DashboardID != expect.DashboardID || 97 | resp[i].DashboardUID != expect.DashboardUID || 98 | resp[i].Role != expect.Role || 99 | resp[i].UserID != expect.UserID || 100 | resp[i].TeamID != expect.TeamID || 101 | resp[i].IsFolder != expect.IsFolder || 102 | resp[i].Inherited != expect.Inherited || 103 | resp[i].Permission != expect.Permission || 104 | resp[i].PermissionName != expect.PermissionName { 105 | t.Error("Not correctly parsing returned dashboard permission") 106 | } 107 | }) 108 | } 109 | } 110 | 111 | func TestUpdateDashboardPermissions(t *testing.T) { 112 | client := gapiTestTools(t, 200, updateDashboardPermissionsJSON) 113 | 114 | items := &PermissionItems{ 115 | Items: []*PermissionItem{ 116 | { 117 | Role: "viewer", 118 | Permission: 1, 119 | }, 120 | { 121 | Role: "Editor", 122 | Permission: 2, 123 | }, 124 | { 125 | TeamID: 1, 126 | Permission: 1, 127 | }, 128 | { 129 | UserID: 11, 130 | Permission: 4, 131 | }, 132 | }, 133 | } 134 | err := client.UpdateDashboardPermissions(1, items) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | } 139 | 140 | func TestDashboardPermissionsByUID(t *testing.T) { 141 | client := gapiTestTools(t, 200, getDashboardPermissionsJSON) 142 | 143 | resp, err := client.DashboardPermissionsByUID("nErXDvCkzz") 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | t.Log(pretty.PrettyFormat(resp)) 149 | 150 | expects := []*DashboardPermission{ 151 | { 152 | DashboardID: 1, 153 | DashboardUID: "nErXDvCkzz", 154 | Role: "Viewer", 155 | UserID: 0, 156 | TeamID: 0, 157 | IsFolder: false, 158 | Inherited: false, 159 | Permission: 1, 160 | PermissionName: "View", 161 | }, 162 | { 163 | DashboardID: 2, 164 | DashboardUID: "nErXDvCkzz", 165 | Role: "Editor", 166 | UserID: 0, 167 | TeamID: 0, 168 | IsFolder: false, 169 | Inherited: true, 170 | Permission: 2, 171 | PermissionName: "Edit", 172 | }, 173 | } 174 | 175 | for i, expect := range expects { 176 | t.Run("check data", func(t *testing.T) { 177 | if resp[i].DashboardID != expect.DashboardID || 178 | resp[i].DashboardUID != expect.DashboardUID || 179 | resp[i].Role != expect.Role || 180 | resp[i].UserID != expect.UserID || 181 | resp[i].TeamID != expect.TeamID || 182 | resp[i].IsFolder != expect.IsFolder || 183 | resp[i].Inherited != expect.Inherited || 184 | resp[i].Permission != expect.Permission || 185 | resp[i].PermissionName != expect.PermissionName { 186 | t.Error("Not correctly parsing returned dashboard permission") 187 | } 188 | }) 189 | } 190 | } 191 | 192 | func TestUpdateDashboardPermissionsByUID(t *testing.T) { 193 | client := gapiTestTools(t, 200, updateDashboardPermissionsJSON) 194 | 195 | items := &PermissionItems{ 196 | Items: []*PermissionItem{ 197 | { 198 | Role: "viewer", 199 | Permission: 1, 200 | }, 201 | { 202 | Role: "Editor", 203 | Permission: 2, 204 | }, 205 | { 206 | TeamID: 1, 207 | Permission: 1, 208 | }, 209 | { 210 | UserID: 11, 211 | Permission: 4, 212 | }, 213 | }, 214 | } 215 | err := client.UpdateDashboardPermissionsByUID("nErXDvCkzz", items) 216 | if err != nil { 217 | t.Error(err) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /dashboard_public.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // PublicDashboardPayload represents a public dashboard payload. 10 | type PublicDashboardPayload struct { 11 | UID string `json:"uid"` 12 | AccessToken string `json:"accessToken"` 13 | TimeSelectionEnabled bool `json:"timeSelectionEnabled"` 14 | IsEnabled bool `json:"isEnabled"` 15 | AnnotationsEnabled bool `json:"annotationsEnabled"` 16 | Share string `json:"share"` 17 | } 18 | 19 | // PublicDashboard represents a public dashboard. 20 | type PublicDashboard struct { 21 | UID string `json:"uid"` 22 | DashboardUID string `json:"dashboardUid"` 23 | AccessToken string `json:"accessToken"` 24 | TimeSelectionEnabled bool `json:"timeSelectionEnabled"` 25 | IsEnabled bool `json:"isEnabled"` 26 | AnnotationsEnabled bool `json:"annotationsEnabled"` 27 | Share string `json:"share"` 28 | CreatedBy int64 `json:"createdBy"` 29 | UpdatedBy int64 `json:"updatedBy"` 30 | CreatedAt time.Time `json:"createdAt"` 31 | UpdatedAt time.Time `json:"updatedAt"` 32 | } 33 | 34 | type PublicDashboardListResponseWithPagination struct { 35 | PublicDashboards []*PublicDashboardListResponse `json:"publicDashboards"` 36 | TotalCount int64 `json:"totalCount"` 37 | Page int `json:"page"` 38 | PerPage int `json:"perPage"` 39 | } 40 | 41 | type PublicDashboardListResponse struct { 42 | UID string `json:"uid"` 43 | AccessToken string `json:"accessToken"` 44 | Title string `json:"title"` 45 | DashboardUID string `json:"dashboardUid"` 46 | IsEnabled bool `json:"isEnabled"` 47 | } 48 | 49 | // NewPublicDashboard creates a new Grafana public dashboard. 50 | func (c *Client) NewPublicDashboard(dashboardUID string, publicDashboard PublicDashboardPayload) (*PublicDashboard, error) { 51 | data, err := json.Marshal(publicDashboard) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | result := &PublicDashboard{} 57 | err = c.request("POST", fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID), nil, data, &result) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return result, err 63 | } 64 | 65 | // DeletePublicDashboard deletes a Grafana public dashboard. 66 | func (c *Client) DeletePublicDashboard(dashboardUID string, publicDashboardUID string) error { 67 | return c.request("DELETE", fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID), nil, nil, nil) 68 | } 69 | 70 | // PublicDashboards fetches and returns the Grafana public dashboards. 71 | func (c *Client) PublicDashboards() (*PublicDashboardListResponseWithPagination, error) { 72 | publicdashboards := &PublicDashboardListResponseWithPagination{} 73 | err := c.request("GET", "/api/dashboards/public-dashboards", nil, nil, &publicdashboards) 74 | if err != nil { 75 | return publicdashboards, err 76 | } 77 | 78 | return publicdashboards, err 79 | } 80 | 81 | // PublicDashboardbyUID fetches and returns a Grafana public dashboard by uid. 82 | func (c *Client) PublicDashboardbyUID(dashboardUID string) (*PublicDashboard, error) { 83 | publicDashboard := &PublicDashboard{} 84 | err := c.request("GET", fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID), nil, nil, &publicDashboard) 85 | if err != nil { 86 | return publicDashboard, err 87 | } 88 | 89 | return publicDashboard, err 90 | } 91 | 92 | // UpdatePublicDashboard updates a Grafana public dashboard. 93 | func (c *Client) UpdatePublicDashboard(dashboardUID string, publicDashboardUID string, publicDashboard PublicDashboardPayload) (*PublicDashboard, error) { 94 | data, err := json.Marshal(publicDashboard) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | result := &PublicDashboard{} 100 | err = c.request("PATCH", fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID), nil, data, &result) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return result, err 106 | } 107 | -------------------------------------------------------------------------------- /dashboard_public_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | createPublicDashboard = `{ 11 | "uid": "fdc8b8fd-72cb-45d2-927a-75900e4f6e70", 12 | "dashboardUid": "nErXDvCkzz", 13 | "isEnabled": true, 14 | "share": "public" 15 | }` 16 | updatePublicDashboard = `{ 17 | "timeSelectionEnabled": true, 18 | "isEnabled": true, 19 | "annotationsEnabled": true 20 | }` 21 | publicDashboardByUID = `{ 22 | "uid": "cd56d9fd-f3d4-486d-afba-a21760e2acbe", 23 | "dashboardUid": "xCpsVuc4z", 24 | "accessToken": "5c948bf96e6a4b13bd91975f9a2028b7", 25 | "createdBy": 1, 26 | "updatedBy": 1, 27 | "createdAt": "2023-09-05T11:41:14-03:00", 28 | "updatedAt": "2023-09-05T11:41:14-03:00", 29 | "timeSelectionEnabled": false, 30 | "isEnabled": true, 31 | "annotationsEnabled": false, 32 | "share": "public" 33 | }` 34 | publicDashboardList = `{ 35 | "publicDashboards": [ 36 | { 37 | "uid": "e9f29a3c-fcc3-4fc5-a690-ae39c97d24ba", 38 | "accessToken": "6c13ec1997ba48c5af8c9c5079049692", 39 | "title": "A Datasource not found query", 40 | "dashboardUid": "d2f21d0a-76c7-47ec-b5f3-9dda16e5a996", 41 | "isEnabled": true 42 | }, 43 | { 44 | "uid": "a174f604-6fe7-47de-97b4-48b7e401b540", 45 | "accessToken": "d1fcff345c0f45e8a78c096c9696034a", 46 | "title": "A Issue heatmap bargauge panel", 47 | "dashboardUid": "51DiOw0Vz", 48 | "isEnabled": true 49 | } 50 | ], 51 | "totalCount": 2, 52 | "page": 1, 53 | "perPage": 1000 54 | }` 55 | ) 56 | 57 | func TestNewPublicDashboard(t *testing.T) { 58 | const dashboardUID = "nErXDvCkzz" 59 | 60 | client := gapiTestTools(t, 200, createPublicDashboard) 61 | 62 | publicDashboard := PublicDashboardPayload{ 63 | UID: "fdc8b8fd-72cb-45d2-927a-75900e4f6e70", 64 | AccessToken: "b1d5f3f534d84375a897f3969b6157f3", 65 | IsEnabled: true, 66 | Share: "public", 67 | } 68 | 69 | resp, err := client.NewPublicDashboard(dashboardUID, publicDashboard) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | t.Log(pretty.PrettyFormat(resp)) 75 | 76 | if resp.UID != "fdc8b8fd-72cb-45d2-927a-75900e4f6e70" { 77 | t.Errorf("Invalid uid - %s, Expected %s", resp.UID, "fdc8b8fd-72cb-45d2-927a-75900e4f6e70") 78 | } 79 | 80 | if resp.DashboardUID != dashboardUID { 81 | t.Errorf("Invalid dashboard uid - %s, Expected %s", resp.DashboardUID, dashboardUID) 82 | } 83 | } 84 | 85 | func TestDeletePublicDashboard(t *testing.T) { 86 | client := gapiTestTools(t, 200, "") 87 | 88 | err := client.DeletePublicDashboard("nErXDvCkza", "fdc8b8fd-72cb-45d2-927a-75900e4f6e70") 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | 94 | func TestPublicDashboards(t *testing.T) { 95 | client := gapiTestTools(t, 200, publicDashboardList) 96 | 97 | resp, err := client.PublicDashboards() 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | t.Log(pretty.PrettyFormat(resp)) 103 | 104 | if len(resp.PublicDashboards) != 2 || resp.TotalCount != 2 { 105 | t.Error("Length of returned public dashboards should be 2") 106 | } 107 | if resp.PublicDashboards[0].UID != "e9f29a3c-fcc3-4fc5-a690-ae39c97d24ba" || resp.PublicDashboards[0].AccessToken != "6c13ec1997ba48c5af8c9c5079049692" { 108 | t.Error("Not correctly parsing returned public dashboards.") 109 | } 110 | } 111 | 112 | func TestPublicDashboardByUID(t *testing.T) { 113 | client := gapiTestTools(t, 200, publicDashboardByUID) 114 | 115 | resp, err := client.PublicDashboardbyUID("xCpsVuc4z") 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | t.Log(pretty.PrettyFormat(resp)) 121 | 122 | if resp.UID != "cd56d9fd-f3d4-486d-afba-a21760e2acbe" { 123 | t.Errorf("Invalid uid - %s, Expected %s", resp.UID, "cd56d9fd-f3d4-486d-afba-a21760e2acbe") 124 | } 125 | 126 | if resp.DashboardUID != "xCpsVuc4z" { 127 | t.Errorf("Invalid dashboard uid - %s, Expected %s", resp.DashboardUID, "xCpsVuc4z") 128 | } 129 | } 130 | 131 | func TestUpdatePublicDashboard(t *testing.T) { 132 | client := gapiTestTools(t, 200, updatePublicDashboard) 133 | 134 | publicDashboard := PublicDashboardPayload{ 135 | IsEnabled: true, 136 | TimeSelectionEnabled: true, 137 | AnnotationsEnabled: true, 138 | } 139 | 140 | resp, err := client.UpdatePublicDashboard("xCpsVuc4z", "cd56d9fd-f3d4-486d-afba-a21760e2acbe", publicDashboard) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | 145 | t.Log(pretty.PrettyFormat(resp)) 146 | 147 | if !resp.IsEnabled { 148 | t.Errorf("Invalid IsEnabled - %t, Expected %t", resp.IsEnabled, true) 149 | } 150 | 151 | if !resp.TimeSelectionEnabled { 152 | t.Errorf("Invalid TimeSelectionEnabled - %t, Expected %t", resp.TimeSelectionEnabled, true) 153 | } 154 | 155 | if !resp.AnnotationsEnabled { 156 | t.Errorf("Invalid AnnotationsEnabled - %t, Expected %t", resp.AnnotationsEnabled, true) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /dashboard_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gobs/pretty" 9 | ) 10 | 11 | const ( 12 | createdAndUpdateDashboardResponse = `{ 13 | "slug": "test", 14 | "id": 1, 15 | "uid": "nErXDvCkzz", 16 | "status": "success", 17 | "version": 1 18 | }` 19 | 20 | getDashboardResponse = `{ 21 | "dashboard": { 22 | "id": 1, 23 | "uid": "cIBgcSjkk", 24 | "title": "Production Overview", 25 | "version": 0 26 | }, 27 | "meta": { 28 | "isStarred": false, 29 | "url": "/d/cIBgcSjkk/production-overview", 30 | "slug": "production-overview", 31 | "folderID": 3, 32 | "folderUid": "test" 33 | } 34 | }` 35 | 36 | getDashboardsJSON = `{ 37 | "id": 1, 38 | "uid": "RGAPB1cZz", 39 | "title": "Grafana Stats", 40 | "uri": "db/grafana-stats", 41 | "url": "/dashboards/d/RGAPB1cZz/grafana-stat", 42 | "slug": "", 43 | "type": "dash-db", 44 | "tags": [], 45 | "isStarred": false 46 | }` 47 | ) 48 | 49 | func TestDashboardCreateAndUpdate(t *testing.T) { 50 | client := gapiTestTools(t, 200, createdAndUpdateDashboardResponse) 51 | 52 | dashboard := Dashboard{ 53 | Model: map[string]interface{}{ 54 | "title": "test", 55 | }, 56 | FolderID: 0, 57 | FolderUID: "l3KqBxCMz", 58 | Overwrite: false, 59 | } 60 | 61 | resp, err := client.NewDashboard(dashboard) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | t.Log(pretty.PrettyFormat(resp)) 67 | 68 | if resp.UID != "nErXDvCkzz" { 69 | t.Errorf("Invalid uid - %s, Expected %s", resp.UID, "nErXDvCkzz") 70 | } 71 | 72 | for _, code := range []int{400, 401, 403, 412} { 73 | client = gapiTestTools(t, code, "error") 74 | _, err = client.NewDashboard(dashboard) 75 | if err == nil { 76 | t.Errorf("%d not detected", code) 77 | } 78 | } 79 | } 80 | 81 | func TestDashboardGet(t *testing.T) { 82 | t.Run("By slug", func(t *testing.T) { 83 | client := gapiTestTools(t, 200, getDashboardResponse) 84 | 85 | resp, err := client.Dashboard("test") 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | uid, ok := resp.Model["uid"] 90 | if !ok || uid != "cIBgcSjkk" { 91 | t.Errorf("Invalid uid - %s, Expected %s", uid, "cIBgcSjkk") 92 | } 93 | }) 94 | 95 | t.Run("By UID", func(t *testing.T) { 96 | client := gapiTestTools(t, 200, getDashboardResponse) 97 | 98 | resp, err := client.DashboardByUID("cIBgcSjkk") 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | uid, ok := resp.Model["uid"] 103 | if !ok || uid != "cIBgcSjkk" { 104 | t.Fatalf("Invalid UID - %s, Expected %s", uid, "cIBgcSjkk") 105 | } 106 | }) 107 | 108 | for _, code := range []int{401, 403, 404} { 109 | t.Run(fmt.Sprintf("Dashboard error: %d", code), func(t *testing.T) { 110 | client := gapiTestToolsFromCalls(t, []mockServerCall{{code, "error"}, {code, "error"}}) 111 | _, err := client.Dashboard("test") 112 | if err == nil { 113 | t.Errorf("%d not detected", code) 114 | } 115 | 116 | _, err = client.DashboardByUID("cIBgcSjkk") 117 | if err == nil { 118 | t.Errorf("%d not detected", code) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestDashboardDelete(t *testing.T) { 125 | t.Run("By slug", func(t *testing.T) { 126 | client := gapiTestTools(t, 200, "") 127 | err := client.DeleteDashboard("test") 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | }) 132 | 133 | t.Run("By UID", func(t *testing.T) { 134 | client := gapiTestTools(t, 200, "") 135 | err := client.DeleteDashboardByUID("cIBgcSjkk") 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | }) 140 | 141 | for _, code := range []int{401, 403, 404, 412} { 142 | t.Run(fmt.Sprintf("Dashboard error: %d", code), func(t *testing.T) { 143 | client := gapiTestToolsFromCalls(t, []mockServerCall{{code, "error"}, {code, "error"}}) 144 | 145 | err := client.DeleteDashboard("test") 146 | if err == nil { 147 | t.Errorf("%d not detected", code) 148 | } 149 | 150 | err = client.DeleteDashboardByUID("cIBgcSjkk") 151 | if err == nil { 152 | t.Errorf("%d not detected", code) 153 | } 154 | }) 155 | } 156 | } 157 | 158 | func TestDashboards(t *testing.T) { 159 | mockData := strings.Repeat(getDashboardsJSON+",", 1000) // make 1000 dashboards. 160 | mockData = "[" + mockData[:len(mockData)-1] + "]" // remove trailing comma; make a json list. 161 | 162 | // This creates 1000 + 1000 + 1 (2001, 3 calls) worth of dashboards. 163 | client := gapiTestToolsFromCalls(t, []mockServerCall{ 164 | {200, mockData}, 165 | {200, mockData}, 166 | {200, "[" + getDashboardsJSON + "]"}, 167 | }) 168 | 169 | const dashCount = 2001 170 | 171 | dashboards, err := client.Dashboards() 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | 176 | if len(dashboards) != dashCount { 177 | t.Errorf("Length of returned dashboards should be %d", dashCount) 178 | } 179 | 180 | if dashboards[0].ID != 1 || dashboards[0].Title != "Grafana Stats" { 181 | t.Error("Not correctly parsing returned dashboards.") 182 | } 183 | 184 | if dashboards[dashCount-1].ID != 1 || dashboards[dashCount-1].Title != "Grafana Stats" { 185 | t.Error("Not correctly parsing returned dashboards.") 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /datasource_cache.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type DatasourceCache struct { 9 | Message string `json:"message"` 10 | DatasourceID int64 `json:"dataSourceID"` 11 | DatasourceUID string `json:"dataSourceUID"` 12 | Enabled bool `json:"enabled"` 13 | TTLQueriesMs int64 `json:"ttlQueriesMs"` 14 | TTLResourcesMs int64 `json:"ttlResourcesMs"` 15 | UseDefaultTLS bool `json:"useDefaultTTL"` 16 | DefaultTTLMs int64 `json:"defaultTTLMs"` 17 | Created string `json:"created"` 18 | Updated string `json:"updated"` 19 | } 20 | 21 | type DatasourceCachePayload struct { 22 | DatasourceID int64 `json:"dataSourceID"` 23 | DatasourceUID string `json:"dataSourceUID"` 24 | Enabled bool `json:"enabled"` 25 | UseDefaultTLS bool `json:"useDefaultTTL"` 26 | TTLQueriesMs int64 `json:"ttlQueriesMs"` 27 | TTLResourcesMs int64 `json:"ttlResourcesMs"` 28 | } 29 | 30 | // EnableDatasourceCache enables the datasource cache (this is a datasource setting) 31 | func (c *Client) EnableDatasourceCache(id int64) error { 32 | path := fmt.Sprintf("/api/datasources/%d/cache/enable", id) 33 | if err := c.request("POST", path, nil, nil, nil); err != nil { 34 | return fmt.Errorf("error enabling cache at %s: %w", path, err) 35 | } 36 | return nil 37 | } 38 | 39 | // DisableDatasourceCache disables the datasource cache (this is a datasource setting) 40 | func (c *Client) DisableDatasourceCache(id int64) error { 41 | path := fmt.Sprintf("/api/datasources/%d/cache/disable", id) 42 | if err := c.request("POST", path, nil, nil, nil); err != nil { 43 | return fmt.Errorf("error disabling cache at %s: %w", path, err) 44 | } 45 | return nil 46 | } 47 | 48 | // UpdateDatasourceCache updates the cache configurations 49 | func (c *Client) UpdateDatasourceCache(id int64, payload *DatasourceCachePayload) error { 50 | path := fmt.Sprintf("/api/datasources/%d/cache", id) 51 | data, err := json.Marshal(payload) 52 | if err != nil { 53 | return fmt.Errorf("marshal err: %w", err) 54 | } 55 | 56 | if err = c.request("POST", path, nil, data, nil); err != nil { 57 | return fmt.Errorf("error updating cache at %s: %w", path, err) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // DatasourceCache fetches datasource cache configuration 64 | func (c *Client) DatasourceCache(id int64) (*DatasourceCache, error) { 65 | path := fmt.Sprintf("/api/datasources/%d/cache", id) 66 | cache := &DatasourceCache{} 67 | err := c.request("GET", path, nil, nil, cache) 68 | if err != nil { 69 | return cache, fmt.Errorf("error getting cache at %s: %w", path, err) 70 | } 71 | return cache, nil 72 | } 73 | -------------------------------------------------------------------------------- /datasource_cache_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getDatasourceCacheJSON = ` 11 | { 12 | "message": "Data source cache settings loaded", 13 | "dataSourceID": 1, 14 | "dataSourceUID": "jZrmlLCGka", 15 | "enabled": true, 16 | "useDefaultTTL": false, 17 | "ttlQueriesMs": 60000, 18 | "ttlResourcesMs": 300000, 19 | "defaultTTLMs": 300000, 20 | "created": "2023-04-21T11:49:22-04:00", 21 | "updated": "2023-04-24T17:03:40-04:00" 22 | }` 23 | updateDatasourceCacheJSON = ` 24 | { 25 | "message": "Data source cache settings updated", 26 | "dataSourceID": 1, 27 | "dataSourceUID": "jZrmlLCGka", 28 | "enabled": true, 29 | "useDefaultTTL": false, 30 | "ttlQueriesMs": 60000, 31 | "ttlResourcesMs": 300000, 32 | "defaultTTLMs": 300000, 33 | "created": "2023-04-21T11:49:22-04:00", 34 | "updated": "2023-04-24T17:03:40-04:00" 35 | }` 36 | ) 37 | 38 | func TestDatasourceCache(t *testing.T) { 39 | client := gapiTestTools(t, 200, getDatasourceCacheJSON) 40 | resp, err := client.DatasourceCache(1) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | t.Log(pretty.PrettyFormat(resp)) 46 | 47 | expects := DatasourceCache{ 48 | Message: "Data source cache settings loaded", 49 | DatasourceID: 1, 50 | DatasourceUID: "jZrmlLCGka", 51 | Enabled: true, 52 | UseDefaultTLS: false, 53 | TTLQueriesMs: 60000, 54 | TTLResourcesMs: 300000, 55 | DefaultTTLMs: 300000, 56 | Created: "2023-04-21T11:49:22-04:00", 57 | Updated: "2023-04-24T17:03:40-04:00", 58 | } 59 | 60 | if resp.Enabled != expects.Enabled || 61 | resp.DatasourceUID != expects.DatasourceUID || 62 | resp.UseDefaultTLS != expects.UseDefaultTLS || 63 | resp.TTLQueriesMs != expects.TTLQueriesMs || 64 | resp.TTLResourcesMs != expects.TTLResourcesMs || 65 | resp.DefaultTTLMs != expects.DefaultTTLMs { 66 | t.Error("Not correctly parsing returned datasource cache") 67 | } 68 | } 69 | 70 | func TestUpdateDatasourceCache(t *testing.T) { 71 | client := gapiTestTools(t, 200, updateDatasourceCacheJSON) 72 | payload := &DatasourceCachePayload{ 73 | DatasourceID: 1, 74 | DatasourceUID: "jZrmlLCGka", 75 | Enabled: true, 76 | UseDefaultTLS: true, 77 | TTLQueriesMs: 6000, 78 | TTLResourcesMs: 30000, 79 | } 80 | err := client.UpdateDatasourceCache(1, payload) 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /datasource_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type DatasourcePermissionType int 9 | 10 | const ( 11 | _ DatasourcePermissionType = iota // 0 is not a valid permission 12 | DatasourcePermissionQuery 13 | DatasourcePermissionEdit 14 | ) 15 | 16 | // DatasourcePermission has information such as a datasource, user, team, role and permission. 17 | type DatasourcePermission struct { 18 | ID int64 `json:"id"` 19 | DatasourceID int64 `json:"datasourceId"` 20 | UserID int64 `json:"userId"` 21 | UserEmail string `json:"userEmail"` 22 | TeamID int64 `json:"teamId"` 23 | BuiltInRole string `json:"builtInRole"` 24 | 25 | // Permission levels are 26 | // 1 = Query 27 | // 2 = Edit 28 | Permission DatasourcePermissionType `json:"permission"` 29 | PermissionName string `json:"permissionName"` 30 | } 31 | 32 | type DatasourcePermissionsResponse struct { 33 | DatasourceID int64 `json:"datasourceId"` 34 | Enabled bool `json:"enabled"` 35 | Permissions []*DatasourcePermission `json:"permissions"` 36 | } 37 | 38 | type DatasourcePermissionAddPayload struct { 39 | UserID int64 `json:"userId"` 40 | TeamID int64 `json:"teamId"` 41 | BuiltInRole string `json:"builtinRole"` 42 | Permission DatasourcePermissionType `json:"permission"` 43 | } 44 | 45 | // EnableDatasourcePermissions enables the datasource permissions (this is a datasource setting) 46 | func (c *Client) EnableDatasourcePermissions(id int64) error { 47 | path := fmt.Sprintf("/api/datasources/%d/enable-permissions", id) 48 | if err := c.request("POST", path, nil, nil, nil); err != nil { 49 | return fmt.Errorf("error enabling permissions at %s: %w", path, err) 50 | } 51 | return nil 52 | } 53 | 54 | // DisableDatasourcePermissions disables the datasource permissions (this is a datasource setting) 55 | func (c *Client) DisableDatasourcePermissions(id int64) error { 56 | path := fmt.Sprintf("/api/datasources/%d/disable-permissions", id) 57 | if err := c.request("POST", path, nil, nil, nil); err != nil { 58 | return fmt.Errorf("error disabling permissions at %s: %w", path, err) 59 | } 60 | return nil 61 | } 62 | 63 | // DatasourcePermissions fetches and returns the permissions for the datasource whose ID it's passed. 64 | func (c *Client) DatasourcePermissions(id int64) (*DatasourcePermissionsResponse, error) { 65 | path := fmt.Sprintf("/api/datasources/%d/permissions", id) 66 | var out *DatasourcePermissionsResponse 67 | err := c.request("GET", path, nil, nil, &out) 68 | if err != nil { 69 | return out, fmt.Errorf("error getting permissions at %s: %w", path, err) 70 | } 71 | 72 | return out, nil 73 | } 74 | 75 | // AddDatasourcePermission adds the given permission item 76 | func (c *Client) AddDatasourcePermission(id int64, item *DatasourcePermissionAddPayload) error { 77 | path := fmt.Sprintf("/api/datasources/%d/permissions", id) 78 | data, err := json.Marshal(item) 79 | if err != nil { 80 | return fmt.Errorf("marshal err: %w", err) 81 | } 82 | 83 | if err = c.request("POST", path, nil, data, nil); err != nil { 84 | return fmt.Errorf("error adding permissions at %s: %w", path, err) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // RemoveDatasourcePermission removes the permission with the given id 91 | func (c *Client) RemoveDatasourcePermission(id, permissionID int64) error { 92 | path := fmt.Sprintf("/api/datasources/%d/permissions/%d", id, permissionID) 93 | if err := c.request("DELETE", path, nil, nil, nil); err != nil { 94 | return fmt.Errorf("error deleting permissions at %s: %w", path, err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (c *Client) ListDatasourceResourcePermissions(uid string) ([]*ResourcePermission, error) { 101 | return c.listResourcePermissions(DatasourcesResource, ResourceUID(uid)) 102 | } 103 | 104 | func (c *Client) SetDatasourceResourcePermissions(uid string, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 105 | return c.setResourcePermissions(DatasourcesResource, ResourceUID(uid), body) 106 | } 107 | 108 | func (c *Client) SetUserDatasourceResourcePermissions(datasourceUID string, userID int64, permission string) (*SetResourcePermissionsResponse, error) { 109 | return c.setResourcePermissionByAssignment( 110 | DatasourcesResource, 111 | ResourceUID(datasourceUID), 112 | UsersResource, 113 | ResourceID(userID), 114 | SetResourcePermissionBody{ 115 | Permission: SetResourcePermissionItem{ 116 | UserID: userID, 117 | Permission: permission, 118 | }, 119 | }, 120 | ) 121 | } 122 | 123 | func (c *Client) SetTeamDatasourceResourcePermissions(datasourceUID string, teamID int64, permission string) (*SetResourcePermissionsResponse, error) { 124 | return c.setResourcePermissionByAssignment( 125 | DatasourcesResource, 126 | ResourceUID(datasourceUID), 127 | TeamsResource, 128 | ResourceID(teamID), 129 | SetResourcePermissionBody{ 130 | Permission: SetResourcePermissionItem{ 131 | TeamID: teamID, 132 | Permission: permission, 133 | }, 134 | }, 135 | ) 136 | } 137 | 138 | func (c *Client) SetBuiltInRoleDatasourceResourcePermissions(datasourceUID string, builtInRole string, permission string) (*SetResourcePermissionsResponse, error) { 139 | return c.setResourcePermissionByAssignment( 140 | DatasourcesResource, 141 | ResourceUID(datasourceUID), 142 | BuiltInRolesResource, 143 | ResourceUID(builtInRole), 144 | SetResourcePermissionBody{ 145 | Permission: SetResourcePermissionItem{ 146 | BuiltinRole: builtInRole, 147 | Permission: permission, 148 | }, 149 | }, 150 | ) 151 | } 152 | -------------------------------------------------------------------------------- /datasource_permissions_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getDatasourcePermissionsJSON = `{ 11 | "datasourceId": 1, 12 | "enabled": true, 13 | "permissions": [ 14 | { 15 | "datasourceId": 1, 16 | "userId": 1, 17 | "userLogin": "user", 18 | "userEmail": "user@test.com", 19 | "userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", 20 | "permission": 1, 21 | "permissionName": "Query", 22 | "created": "2017-06-20T02:00:00+02:00", 23 | "updated": "2017-06-20T02:00:00+02:00" 24 | }, 25 | { 26 | "datasourceId": 2, 27 | "teamId": 1, 28 | "team": "A Team", 29 | "teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", 30 | "permission": 1, 31 | "permissionName": "Query", 32 | "created": "2017-06-20T02:00:00+02:00", 33 | "updated": "2017-06-20T02:00:00+02:00" 34 | }, 35 | { 36 | "datasourceId": 1, 37 | "permission": 2, 38 | "permissionName": "Edit", 39 | "builtInRole": "Viewer", 40 | "created": "2017-06-20T02:00:00+02:00", 41 | "updated": "2017-06-20T02:00:00+02:00" 42 | } 43 | ] 44 | }` 45 | addDatasourcePermissionsJSON = `{ 46 | "message": "Datasource permission added" 47 | }` 48 | ) 49 | 50 | func TestDatasourcePermissions(t *testing.T) { 51 | client := gapiTestTools(t, 200, getDatasourcePermissionsJSON) 52 | 53 | resp, err := client.DatasourcePermissions(1) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | t.Log(pretty.PrettyFormat(resp)) 59 | 60 | expects := []*DatasourcePermission{ 61 | { 62 | DatasourceID: 1, 63 | UserID: 1, 64 | TeamID: 0, 65 | Permission: 1, 66 | PermissionName: "Query", 67 | }, 68 | { 69 | DatasourceID: 2, 70 | UserID: 0, 71 | TeamID: 1, 72 | Permission: 1, 73 | PermissionName: "Query", 74 | }, 75 | } 76 | 77 | for i, expect := range expects { 78 | t.Run("check data", func(t *testing.T) { 79 | if resp.Permissions[i].DatasourceID != expect.DatasourceID || 80 | resp.Permissions[i].UserID != expect.UserID || 81 | resp.Permissions[i].TeamID != expect.TeamID || 82 | resp.Permissions[i].Permission != expect.Permission || 83 | resp.Permissions[i].PermissionName != expect.PermissionName { 84 | t.Error("Not correctly parsing returned datasource permission") 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func TestAddDatasourcePermissions(t *testing.T) { 91 | for _, item := range []*DatasourcePermissionAddPayload{ 92 | { 93 | TeamID: 1, 94 | Permission: 1, 95 | }, 96 | { 97 | UserID: 11, 98 | Permission: 1, 99 | }, 100 | { 101 | BuiltInRole: "Viewer", 102 | Permission: 2, 103 | }, 104 | } { 105 | client := gapiTestTools(t, 200, addDatasourcePermissionsJSON) 106 | err := client.AddDatasourcePermission(1, item) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import "fmt" 4 | 5 | type ErrNotFound struct { 6 | BodyContents []byte 7 | } 8 | 9 | func (e ErrNotFound) Error() string { 10 | return fmt.Sprintf("status: 404, body: %s", e.BodyContents) 11 | } 12 | -------------------------------------------------------------------------------- /folder.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // Folder represents a Grafana folder. 10 | type Folder struct { 11 | ID int64 `json:"id"` 12 | UID string `json:"uid"` 13 | Title string `json:"title"` 14 | URL string `json:"url"` 15 | } 16 | 17 | type FolderPayload struct { 18 | Title string `json:"title"` 19 | UID string `json:"uid,omitempty"` 20 | Overwrite bool `json:"overwrite,omitempty"` 21 | } 22 | 23 | // Folders fetches and returns Grafana folders. 24 | func (c *Client) Folders() ([]Folder, error) { 25 | const limit = 1000 26 | var ( 27 | page = 0 28 | newFolders []Folder 29 | folders []Folder 30 | query = make(url.Values) 31 | ) 32 | query.Set("limit", fmt.Sprint(limit)) 33 | for { 34 | page++ 35 | query.Set("page", fmt.Sprint(page)) 36 | 37 | if err := c.request("GET", "/api/folders/", query, nil, &newFolders); err != nil { 38 | return nil, err 39 | } 40 | 41 | folders = append(folders, newFolders...) 42 | 43 | if len(newFolders) < limit { 44 | return folders, nil 45 | } 46 | } 47 | } 48 | 49 | // Folder fetches and returns the Grafana folder whose ID it's passed. 50 | func (c *Client) Folder(id int64) (*Folder, error) { 51 | folder := &Folder{} 52 | err := c.request("GET", fmt.Sprintf("/api/folders/id/%d", id), nil, nil, folder) 53 | if err != nil { 54 | return folder, err 55 | } 56 | 57 | return folder, err 58 | } 59 | 60 | // Folder fetches and returns the Grafana folder whose UID it's passed. 61 | func (c *Client) FolderByUID(uid string) (*Folder, error) { 62 | folder := &Folder{} 63 | err := c.request("GET", fmt.Sprintf("/api/folders/%s", uid), nil, nil, folder) 64 | if err != nil { 65 | return folder, err 66 | } 67 | 68 | return folder, err 69 | } 70 | 71 | // NewFolder creates a new Grafana folder. 72 | func (c *Client) NewFolder(title string, uid ...string) (Folder, error) { 73 | if len(uid) > 1 { 74 | return Folder{}, fmt.Errorf("too many arguments. Expected 1 or 2") 75 | } 76 | 77 | folder := Folder{} 78 | payload := FolderPayload{ 79 | Title: title, 80 | } 81 | if len(uid) == 1 { 82 | payload.UID = uid[0] 83 | } 84 | data, err := json.Marshal(payload) 85 | if err != nil { 86 | return folder, err 87 | } 88 | 89 | err = c.request("POST", "/api/folders", nil, data, &folder) 90 | if err != nil { 91 | return folder, err 92 | } 93 | 94 | return folder, err 95 | } 96 | 97 | // UpdateFolder updates the folder whose UID it's passed. 98 | func (c *Client) UpdateFolder(uid string, title string, newUID ...string) error { 99 | payload := FolderPayload{ 100 | Title: title, 101 | Overwrite: true, 102 | } 103 | if len(newUID) == 1 { 104 | payload.UID = newUID[0] 105 | } 106 | data, err := json.Marshal(payload) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | return c.request("PUT", fmt.Sprintf("/api/folders/%s", uid), nil, data, nil) 112 | } 113 | 114 | func ForceDeleteFolderRules() url.Values { 115 | query := make(url.Values) 116 | query.Set("forceDeleteRules", "true") 117 | return query 118 | } 119 | 120 | // DeleteFolder deletes the folder whose ID it's passed. 121 | func (c *Client) DeleteFolder(id string, optionalQueryParams ...url.Values) error { 122 | query := url.Values{} 123 | for _, param := range optionalQueryParams { 124 | for paramKey := range param { 125 | query.Set(paramKey, param.Get(paramKey)) 126 | } 127 | } 128 | 129 | return c.request("DELETE", fmt.Sprintf("/api/folders/%s", id), query, nil, nil) 130 | } 131 | -------------------------------------------------------------------------------- /folder_dashboard_search.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // FolderDashboardSearchResponse represents the Grafana API dashboard search response. 8 | type FolderDashboardSearchResponse struct { 9 | ID uint `json:"id"` 10 | UID string `json:"uid"` 11 | Title string `json:"title"` 12 | URI string `json:"uri"` 13 | URL string `json:"url"` 14 | Slug string `json:"slug"` 15 | Type string `json:"type"` 16 | Tags []string `json:"tags"` 17 | IsStarred bool `json:"isStarred"` 18 | FolderID uint `json:"folderId"` 19 | FolderUID string `json:"folderUid"` 20 | FolderTitle string `json:"folderTitle"` 21 | FolderURL string `json:"folderUrl"` 22 | } 23 | 24 | // FolderDashboardSearch uses the folder and dashboard search endpoint to find 25 | // dashboards based on the params passed in. 26 | func (c *Client) FolderDashboardSearch(params url.Values) (resp []FolderDashboardSearchResponse, err error) { 27 | err = c.request("GET", "/api/search", params, nil, &resp) 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /folder_dashboard_search_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | 10 | // This response is copied from the examples in the API docs: 11 | // https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/ 12 | getFolderDashboardSearchResponse = `[ 13 | { 14 | "id": 163, 15 | "uid": "000000163", 16 | "title": "Folder", 17 | "url": "/dashboards/f/000000163/folder", 18 | "type": "dash-folder", 19 | "tags": [], 20 | "isStarred": false, 21 | "uri":"db/folder" 22 | }, 23 | { 24 | "id":1, 25 | "uid": "cIBgcSjkk", 26 | "title":"Production Overview", 27 | "url": "/d/cIBgcSjkk/production-overview", 28 | "type":"dash-db", 29 | "tags":["prod"], 30 | "isStarred":true, 31 | "uri":"db/production-overview" 32 | }, 33 | { 34 | "id":1, 35 | "uid": "cIBgcSjkk", 36 | "title":"Production Overview", 37 | "url": "/d/cIBgcSjkk/production-overview", 38 | "type":"dash-db", 39 | "tags":["prod"], 40 | "isStarred":true, 41 | "folderId": 2, 42 | "folderUid": "000000163", 43 | "folderTitle": "Folder", 44 | "folderUrl": "/dashboards/f/000000163/folder", 45 | "uri":"db/production-overview" 46 | } 47 | ]` 48 | ) 49 | 50 | func TestFolderDashboardSearch(t *testing.T) { 51 | client := gapiTestTools(t, 200, getFolderDashboardSearchResponse) 52 | resp, err := client.FolderDashboardSearch(url.Values{}) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if len(resp) != 3 { 57 | t.Errorf("Expected 3 objects in response, got %d", len(resp)) 58 | } 59 | if resp[0].ID != 163 || resp[0].Title != "Folder" { 60 | t.Error("Not correctly parsing response.") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /folder_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // FolderPermission has information such as a folder, user, team, role and permission. 9 | type FolderPermission struct { 10 | ID int64 `json:"id"` 11 | FolderUID string `json:"uid"` 12 | UserID int64 `json:"userId"` 13 | TeamID int64 `json:"teamId"` 14 | Role string `json:"role"` 15 | IsFolder bool `json:"isFolder"` 16 | 17 | // Permission levels are 18 | // 1 = View 19 | // 2 = Edit 20 | // 4 = Admin 21 | Permission int64 `json:"permission"` 22 | PermissionName string `json:"permissionName"` 23 | 24 | // optional fields 25 | FolderID int64 `json:"folderId,omitempty"` 26 | DashboardID int64 `json:"dashboardId,omitempty"` 27 | } 28 | 29 | // PermissionItems represents Grafana folder permission items. 30 | type PermissionItems struct { 31 | Items []*PermissionItem `json:"items"` 32 | } 33 | 34 | // PermissionItem represents a Grafana folder permission item. 35 | type PermissionItem struct { 36 | // As you can see the docs, each item has a pair of [Role|TeamID|UserID] and Permission. 37 | // unnecessary fields are omitted. 38 | Role string `json:"role,omitempty"` 39 | TeamID int64 `json:"teamId,omitempty"` 40 | UserID int64 `json:"userId,omitempty"` 41 | Permission int64 `json:"permission"` 42 | } 43 | 44 | // FolderPermissions fetches and returns the permissions for the folder whose ID it's passed. 45 | func (c *Client) FolderPermissions(fid string) ([]*FolderPermission, error) { 46 | permissions := make([]*FolderPermission, 0) 47 | err := c.request("GET", fmt.Sprintf("/api/folders/%s/permissions", fid), nil, nil, &permissions) 48 | if err != nil { 49 | return permissions, err 50 | } 51 | 52 | return permissions, nil 53 | } 54 | 55 | // UpdateFolderPermissions remove existing permissions if items are not included in the request. 56 | func (c *Client) UpdateFolderPermissions(fid string, items *PermissionItems) error { 57 | path := fmt.Sprintf("/api/folders/%s/permissions", fid) 58 | data, err := json.Marshal(items) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return c.request("POST", path, nil, data, nil) 64 | } 65 | 66 | func (c *Client) ListFolderResourcePermissions(uid string) ([]*ResourcePermission, error) { 67 | return c.listResourcePermissions(FoldersResource, ResourceUID(uid)) 68 | } 69 | 70 | func (c *Client) SetFolderResourcePermissions(uid string, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 71 | return c.setResourcePermissions(FoldersResource, ResourceUID(uid), body) 72 | } 73 | 74 | func (c *Client) SetUserFolderResourcePermissions(folderUID string, userID int64, permission string) (*SetResourcePermissionsResponse, error) { 75 | return c.setResourcePermissionByAssignment( 76 | FoldersResource, 77 | ResourceUID(folderUID), 78 | UsersResource, 79 | ResourceID(userID), 80 | SetResourcePermissionBody{ 81 | Permission: SetResourcePermissionItem{ 82 | UserID: userID, 83 | Permission: permission, 84 | }, 85 | }, 86 | ) 87 | } 88 | 89 | func (c *Client) SetTeamFolderResourcePermissions(folderUID string, teamID int64, permission string) (*SetResourcePermissionsResponse, error) { 90 | return c.setResourcePermissionByAssignment( 91 | FoldersResource, 92 | ResourceUID(folderUID), 93 | TeamsResource, 94 | ResourceID(teamID), 95 | SetResourcePermissionBody{ 96 | Permission: SetResourcePermissionItem{ 97 | TeamID: teamID, 98 | Permission: permission, 99 | }, 100 | }, 101 | ) 102 | } 103 | 104 | func (c *Client) SetBuiltInRoleFolderResourcePermissions(folderUID string, builtInRole string, permission string) (*SetResourcePermissionsResponse, error) { 105 | return c.setResourcePermissionByAssignment( 106 | FoldersResource, 107 | ResourceUID(folderUID), 108 | BuiltInRolesResource, 109 | ResourceUID(builtInRole), 110 | SetResourcePermissionBody{ 111 | Permission: SetResourcePermissionItem{ 112 | BuiltinRole: builtInRole, 113 | Permission: permission, 114 | }, 115 | }, 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /folder_permissions_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getFolderPermissionsJSON = ` 11 | [ 12 | { 13 | "id": 1, 14 | "folderId": -1, 15 | "created": "2017-06-20T02:00:00+02:00", 16 | "updated": "2017-06-20T02:00:00+02:00", 17 | "userId": 0, 18 | "userLogin": "", 19 | "userEmail": "", 20 | "teamId": 0, 21 | "team": "", 22 | "role": "Viewer", 23 | "permission": 1, 24 | "permissionName": "View", 25 | "uid": "nErXDvCkzz", 26 | "title": "", 27 | "slug": "", 28 | "isFolder": false, 29 | "url": "" 30 | }, 31 | { 32 | "id": 2, 33 | "dashboardId": -1, 34 | "created": "2017-06-20T02:00:00+02:00", 35 | "updated": "2017-06-20T02:00:00+02:00", 36 | "userId": 0, 37 | "userLogin": "", 38 | "userEmail": "", 39 | "teamId": 0, 40 | "team": "", 41 | "role": "Editor", 42 | "permission": 2, 43 | "permissionName": "Edit", 44 | "uid": "", 45 | "title": "", 46 | "slug": "", 47 | "isFolder": false, 48 | "url": "" 49 | } 50 | ] 51 | ` 52 | updateFolderPermissionsJSON = ` 53 | { 54 | "message": "Folder permissions updated" 55 | } 56 | ` 57 | ) 58 | 59 | func TestFolderPermissions(t *testing.T) { 60 | client := gapiTestTools(t, 200, getFolderPermissionsJSON) 61 | 62 | fid := "nErXDvCkzz" 63 | resp, err := client.FolderPermissions(fid) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | t.Log(pretty.PrettyFormat(resp)) 69 | 70 | expects := []*FolderPermission{ 71 | { 72 | ID: 1, 73 | FolderUID: "nErXDvCkzz", 74 | UserID: 0, 75 | TeamID: 0, 76 | Role: "Viewer", 77 | IsFolder: false, 78 | Permission: 1, 79 | PermissionName: "View", 80 | FolderID: -1, 81 | DashboardID: 0, 82 | }, 83 | { 84 | ID: 2, 85 | FolderUID: "", 86 | UserID: 0, 87 | TeamID: 0, 88 | Role: "Editor", 89 | IsFolder: false, 90 | Permission: 2, 91 | PermissionName: "Edit", 92 | FolderID: 0, 93 | DashboardID: -1, 94 | }, 95 | } 96 | 97 | for i, expect := range expects { 98 | t.Run("check data", func(t *testing.T) { 99 | if resp[i].ID != expect.ID || resp[i].Role != expect.Role { 100 | t.Error("Not correctly parsing returned folder permission") 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func TestUpdateFolderPermissions(t *testing.T) { 107 | client := gapiTestTools(t, 200, updateFolderPermissionsJSON) 108 | 109 | items := &PermissionItems{ 110 | Items: []*PermissionItem{ 111 | { 112 | Role: "viewer", 113 | Permission: 1, 114 | }, 115 | { 116 | Role: "Editor", 117 | Permission: 2, 118 | }, 119 | { 120 | TeamID: 1, 121 | Permission: 1, 122 | }, 123 | { 124 | UserID: 11, 125 | Permission: 4, 126 | }, 127 | }, 128 | } 129 | err := client.UpdateFolderPermissions("nErXDvCkzz", items) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /folder_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | const ( 11 | getFoldersJSON = `{ 12 | "id":1, 13 | "uid": "nErXDvCkzz", 14 | "title": "Departmenet ABC", 15 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 16 | "hasAcl": false, 17 | "canSave": true, 18 | "canEdit": true, 19 | "canAdmin": true, 20 | "createdBy": "admin", 21 | "created": "2018-01-31T17:43:12+01:00", 22 | "updatedBy": "admin", 23 | "updated": "2018-01-31T17:43:12+01:00", 24 | "version": 1 25 | }` 26 | getFolderJSON = ` 27 | { 28 | "id":1, 29 | "uid": "nErXDvCkzz", 30 | "title": "Departmenet ABC", 31 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 32 | "hasAcl": false, 33 | "canSave": true, 34 | "canEdit": true, 35 | "canAdmin": true, 36 | "createdBy": "admin", 37 | "created": "2018-01-31T17:43:12+01:00", 38 | "updatedBy": "admin", 39 | "updated": "2018-01-31T17:43:12+01:00", 40 | "version": 1 41 | } 42 | ` 43 | createdFolderJSON = ` 44 | { 45 | "id":1, 46 | "uid": "nErXDvCkzz", 47 | "title": "Departmenet ABC", 48 | "url": "/dashboards/f/nErXDvCkzz/department-abc", 49 | "hasAcl": false, 50 | "canSave": true, 51 | "canEdit": true, 52 | "canAdmin": true, 53 | "createdBy": "admin", 54 | "created": "2018-01-31T17:43:12+01:00", 55 | "updatedBy": "admin", 56 | "updated": "2018-01-31T17:43:12+01:00", 57 | "version": 1 58 | } 59 | ` 60 | updatedFolderJSON = ` 61 | { 62 | "id":1, 63 | "uid": "nErXDvCkzz", 64 | "title": "Departmenet DEF", 65 | "url": "/dashboards/f/nErXDvCkzz/department-def", 66 | "hasAcl": false, 67 | "canSave": true, 68 | "canEdit": true, 69 | "canAdmin": true, 70 | "createdBy": "admin", 71 | "created": "2018-01-31T17:43:12+01:00", 72 | "updatedBy": "admin", 73 | "updated": "2018-01-31T17:43:12+01:00", 74 | "version": 1 75 | } 76 | ` 77 | deletedFolderJSON = ` 78 | { 79 | "message":"Folder deleted" 80 | } 81 | ` 82 | ) 83 | 84 | func TestFolders(t *testing.T) { 85 | mockData := strings.Repeat(getFoldersJSON+",", 1000) // make 1000 folders. 86 | mockData = "[" + mockData[:len(mockData)-1] + "]" // remove trailing comma; make a json list. 87 | 88 | // This creates 1000 + 1000 + 1 (2001, 3 calls) worth of folders. 89 | client := gapiTestToolsFromCalls(t, []mockServerCall{ 90 | {200, mockData}, 91 | {200, mockData}, 92 | {200, "[" + getFolderJSON + "]"}, 93 | }) 94 | 95 | const dashCount = 2001 96 | 97 | folders, err := client.Folders() 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | t.Log(pretty.PrettyFormat(folders)) 103 | 104 | if len(folders) != dashCount { 105 | t.Errorf("Length of returned folders should be %d", dashCount) 106 | } 107 | if folders[0].ID != 1 || folders[0].Title != "Departmenet ABC" { 108 | t.Error("Not correctly parsing returned folders.") 109 | } 110 | if folders[dashCount-1].ID != 1 || folders[dashCount-1].Title != "Departmenet ABC" { 111 | t.Error("Not correctly parsing returned folders.") 112 | } 113 | } 114 | 115 | func TestFolder(t *testing.T) { 116 | client := gapiTestTools(t, 200, getFolderJSON) 117 | 118 | folder := int64(1) 119 | resp, err := client.Folder(folder) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | t.Log(pretty.PrettyFormat(resp)) 125 | 126 | if resp.ID != folder || resp.Title != "Departmenet ABC" { 127 | t.Error("Not correctly parsing returned folder.") 128 | } 129 | } 130 | 131 | func TestFolderByUid(t *testing.T) { 132 | client := gapiTestTools(t, 200, getFolderJSON) 133 | 134 | folder := "nErXDvCkzz" 135 | resp, err := client.FolderByUID(folder) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | 140 | t.Log(pretty.PrettyFormat(resp)) 141 | 142 | if resp.UID != folder || resp.Title != "Departmenet ABC" { 143 | t.Error("Not correctly parsing returned folder.") 144 | } 145 | } 146 | 147 | func TestNewFolder(t *testing.T) { 148 | client := gapiTestTools(t, 200, createdFolderJSON) 149 | 150 | resp, err := client.NewFolder("test-folder") 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | t.Log(pretty.PrettyFormat(resp)) 156 | 157 | if resp.UID != "nErXDvCkzz" { 158 | t.Error("Not correctly parsing returned creation message.") 159 | } 160 | } 161 | 162 | func TestUpdateFolder(t *testing.T) { 163 | client := gapiTestTools(t, 200, updatedFolderJSON) 164 | 165 | err := client.UpdateFolder("nErXDvCkzz", "test-folder") 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | } 170 | 171 | func TestDeleteFolder(t *testing.T) { 172 | client := gapiTestTools(t, 200, deletedFolderJSON) 173 | 174 | err := client.DeleteFolder("nErXDvCkzz") 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/grafana-api-golang-client 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b 7 | github.com/hashicorp/go-cleanhttp v0.5.2 8 | github.com/stretchr/testify v1.8.4 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= 4 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= 5 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 6 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 10 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /health.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | type HealthResponse struct { 4 | Commit string `json:"commit,omitempty"` 5 | Database string `json:"database,omitempty"` 6 | Version string `json:"version,omitempty"` 7 | } 8 | 9 | func (c *Client) Health() (HealthResponse, error) { 10 | health := HealthResponse{} 11 | err := c.request("GET", "/api/health", nil, nil, &health) 12 | if err != nil { 13 | return health, err 14 | } 15 | return health, nil 16 | } 17 | -------------------------------------------------------------------------------- /mock_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "testing" 9 | ) 10 | 11 | type mockServerCall struct { 12 | code int 13 | body string 14 | } 15 | 16 | type mockServer struct { 17 | upcomingCalls []mockServerCall 18 | executedCalls []mockServerCall 19 | server *httptest.Server 20 | } 21 | 22 | func gapiTestTools(t *testing.T, code int, body string) *Client { 23 | t.Helper() 24 | return gapiTestToolsFromCalls(t, []mockServerCall{{code, body}}) 25 | } 26 | 27 | func gapiTestToolsFromCalls(t *testing.T, calls []mockServerCall) *Client { 28 | t.Helper() 29 | 30 | mock := &mockServer{ 31 | upcomingCalls: calls, 32 | } 33 | 34 | mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | if len(mock.upcomingCalls) == 0 { 36 | t.Fatalf("unexpected call to %s %s", r.Method, r.URL) 37 | } 38 | call := mock.upcomingCalls[0] 39 | if len(calls) > 1 { 40 | mock.upcomingCalls = mock.upcomingCalls[1:] 41 | } else { 42 | mock.upcomingCalls = nil 43 | } 44 | w.WriteHeader(call.code) 45 | w.Header().Set("Content-Type", "application/json") 46 | fmt.Fprint(w, call.body) 47 | mock.executedCalls = append(mock.executedCalls, call) 48 | })) 49 | 50 | tr := &http.Transport{ 51 | Proxy: func(req *http.Request) (*url.URL, error) { 52 | return url.Parse(mock.server.URL) 53 | }, 54 | } 55 | 56 | httpClient := &http.Client{Transport: tr} 57 | 58 | client, err := New("http://my-grafana.com", Config{APIKey: "my-key", Client: httpClient}) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | t.Cleanup(func() { 64 | mock.server.Close() 65 | }) 66 | 67 | return client 68 | } 69 | -------------------------------------------------------------------------------- /org_preferences.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // UpdateOrgPreferencesResponse represents the response to a request 8 | // updating Grafana org preferences. 9 | type UpdateOrgPreferencesResponse struct { 10 | Message string `json:"message"` 11 | } 12 | 13 | // OrgPreferences fetches org preferences. 14 | func (c *Client) OrgPreferences() (Preferences, error) { 15 | var prefs Preferences 16 | err := c.request("GET", "/api/org/preferences", nil, nil, &prefs) 17 | return prefs, err 18 | } 19 | 20 | // UpdateOrgPreferences updates only those org preferences specified in the passed Preferences, without impacting others. 21 | func (c *Client) UpdateOrgPreferences(p Preferences) (UpdateOrgPreferencesResponse, error) { 22 | var resp UpdateOrgPreferencesResponse 23 | data, err := json.Marshal(p) 24 | if err != nil { 25 | return resp, err 26 | } 27 | 28 | err = c.request("PATCH", "/api/org/preferences", nil, data, &resp) 29 | if err != nil { 30 | return resp, err 31 | } 32 | 33 | return resp, err 34 | } 35 | 36 | // UpdateAllOrgPreferences overrwrites all org preferences with the passed Preferences. 37 | func (c *Client) UpdateAllOrgPreferences(p Preferences) (UpdateOrgPreferencesResponse, error) { 38 | var resp UpdateOrgPreferencesResponse 39 | data, err := json.Marshal(p) 40 | if err != nil { 41 | return resp, err 42 | } 43 | 44 | err = c.request("PUT", "/api/org/preferences", nil, data, &resp) 45 | if err != nil { 46 | return resp, err 47 | } 48 | 49 | return resp, err 50 | } 51 | -------------------------------------------------------------------------------- /org_preferences_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | getOrgPreferencesJSON = `{"theme": "foo","homeDashboardId": 0,"timezone": "","weekStart": "","navbar": {"savedItems": null},"queryHistory": {"homeTab": ""}}` 9 | updateOrgPreferencesJSON = `{"message":"Preferences updated"}` 10 | ) 11 | 12 | func TestOrgPreferences(t *testing.T) { 13 | client := gapiTestTools(t, 200, getOrgPreferencesJSON) 14 | 15 | resp, err := client.OrgPreferences() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | expected := "foo" 21 | if resp.Theme != expected { 22 | t.Errorf("Expected org preferences theme '%s'; got '%s'", expected, resp.Theme) 23 | } 24 | } 25 | 26 | func TestUpdateOrgPreferences(t *testing.T) { 27 | client := gapiTestTools(t, 200, updateOrgPreferencesJSON) 28 | 29 | resp, err := client.UpdateOrgPreferences(Preferences{ 30 | Theme: "foo", 31 | }) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | expected := "Preferences updated" 37 | if resp.Message != expected { 38 | t.Errorf("Expected org preferences message '%s'; got '%s'", expected, resp.Message) 39 | } 40 | } 41 | 42 | func TestUpdateAllOrgPreference(t *testing.T) { 43 | client := gapiTestTools(t, 200, updateOrgPreferencesJSON) 44 | 45 | resp, err := client.UpdateAllOrgPreferences(Preferences{ 46 | Theme: "foo", 47 | }) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | expected := "Preferences updated" 53 | if resp.Message != expected { 54 | t.Errorf("Expected org preferences message '%s'; got '%s'", expected, resp.Message) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /org_users.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // OrgUser represents a Grafana org user. 9 | type OrgUser struct { 10 | OrgID int64 `json:"orgId"` 11 | UserID int64 `json:"userId"` 12 | Email string `json:"email"` 13 | Login string `json:"login"` 14 | Role string `json:"role"` 15 | } 16 | 17 | // OrgUsersCurrent returns all org users within the current organization. 18 | // This endpoint is accessible to users with org admin role. 19 | func (c *Client) OrgUsersCurrent() ([]OrgUser, error) { 20 | users := make([]OrgUser, 0) 21 | err := c.request("GET", "/api/org/users", nil, nil, &users) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return users, err 26 | } 27 | 28 | // OrgUsers fetches and returns the users for the org whose ID it's passed. 29 | func (c *Client) OrgUsers(orgID int64) ([]OrgUser, error) { 30 | users := make([]OrgUser, 0) 31 | err := c.request("GET", fmt.Sprintf("/api/orgs/%d/users", orgID), nil, nil, &users) 32 | if err != nil { 33 | return users, err 34 | } 35 | 36 | return users, err 37 | } 38 | 39 | // AddOrgUser adds a user to an org with the specified role. 40 | func (c *Client) AddOrgUser(orgID int64, user, role string) error { 41 | dataMap := map[string]string{ 42 | "loginOrEmail": user, 43 | "role": role, 44 | } 45 | data, err := json.Marshal(dataMap) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return c.request("POST", fmt.Sprintf("/api/orgs/%d/users", orgID), nil, data, nil) 51 | } 52 | 53 | // UpdateOrgUser updates and org user. 54 | func (c *Client) UpdateOrgUser(orgID, userID int64, role string) error { 55 | dataMap := map[string]string{ 56 | "role": role, 57 | } 58 | data, err := json.Marshal(dataMap) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return c.request("PATCH", fmt.Sprintf("/api/orgs/%d/users/%d", orgID, userID), nil, data, nil) 64 | } 65 | 66 | // RemoveOrgUser removes a user from an org. 67 | func (c *Client) RemoveOrgUser(orgID, userID int64) error { 68 | return c.request("DELETE", fmt.Sprintf("/api/orgs/%d/users/%d", orgID, userID), nil, nil, nil) 69 | } 70 | -------------------------------------------------------------------------------- /org_users_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getOrgUsersJSON = `[{"orgID":1,"userID":1,"email":"admin@localhost","avatarUrl":"/avatar/46d229b033af06a191ff2267bca9ae56","login":"admin","role":"Admin","lastSeenAt":"2018-06-28T14:16:11Z","lastSeenAtAge":"\u003c 1m"}]` 11 | addOrgUserJSON = `{"message":"User added to organization"}` 12 | updateOrgUserJSON = `{"message":"Organization user updated"}` 13 | removeOrgUserJSON = `{"message":"User removed from organization"}` 14 | ) 15 | 16 | func TestOrgUsersCurrent(t *testing.T) { 17 | client := gapiTestTools(t, 200, getOrgUsersJSON) 18 | 19 | resp, err := client.OrgUsersCurrent() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | user := OrgUser{ 25 | OrgID: 1, 26 | UserID: 1, 27 | Email: "admin@localhost", 28 | Login: "admin", 29 | Role: "Admin", 30 | } 31 | 32 | if resp[0] != user { 33 | t.Error("Not correctly parsing returned organization users.") 34 | } 35 | } 36 | 37 | func TestOrgUsers(t *testing.T) { 38 | client := gapiTestTools(t, 200, getOrgUsersJSON) 39 | 40 | org := int64(1) 41 | resp, err := client.OrgUsers(org) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | t.Log(pretty.PrettyFormat(resp)) 47 | 48 | user := OrgUser{ 49 | OrgID: 1, 50 | UserID: 1, 51 | Email: "admin@localhost", 52 | Login: "admin", 53 | Role: "Admin", 54 | } 55 | 56 | if resp[0] != user { 57 | t.Error("Not correctly parsing returned organization users.") 58 | } 59 | } 60 | 61 | func TestAddOrgUser(t *testing.T) { 62 | client := gapiTestTools(t, 200, addOrgUserJSON) 63 | 64 | orgID, user, role := int64(1), "admin@localhost", "Admin" 65 | 66 | err := client.AddOrgUser(orgID, user, role) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | 72 | func TestUpdateOrgUser(t *testing.T) { 73 | client := gapiTestTools(t, 200, updateOrgUserJSON) 74 | 75 | orgID, userID, role := int64(1), int64(1), "Editor" 76 | 77 | err := client.UpdateOrgUser(orgID, userID, role) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | } 82 | 83 | func TestRemoveOrgUser(t *testing.T) { 84 | client := gapiTestTools(t, 200, removeOrgUserJSON) 85 | 86 | orgID, userID := int64(1), int64(1) 87 | 88 | err := client.RemoveOrgUser(orgID, userID) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /orgs.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Org represents a Grafana org. 9 | type Org struct { 10 | ID int64 `json:"id"` 11 | Name string `json:"name"` 12 | } 13 | 14 | // Orgs fetches and returns the Grafana orgs. 15 | func (c *Client) Orgs() ([]Org, error) { 16 | orgs := make([]Org, 0) 17 | err := c.request("GET", "/api/orgs/", nil, nil, &orgs) 18 | if err != nil { 19 | return orgs, err 20 | } 21 | 22 | return orgs, err 23 | } 24 | 25 | // OrgByName fetches and returns the org whose name it's passed. 26 | func (c *Client) OrgByName(name string) (Org, error) { 27 | org := Org{} 28 | err := c.request("GET", fmt.Sprintf("/api/orgs/name/%s", name), nil, nil, &org) 29 | if err != nil { 30 | return org, err 31 | } 32 | 33 | return org, err 34 | } 35 | 36 | // Org fetches and returns the org whose ID it's passed. 37 | func (c *Client) Org(id int64) (Org, error) { 38 | org := Org{} 39 | err := c.request("GET", fmt.Sprintf("/api/orgs/%d", id), nil, nil, &org) 40 | if err != nil { 41 | return org, err 42 | } 43 | 44 | return org, err 45 | } 46 | 47 | // NewOrg creates a new Grafana org. 48 | func (c *Client) NewOrg(name string) (int64, error) { 49 | id := int64(0) 50 | 51 | dataMap := map[string]string{ 52 | "name": name, 53 | } 54 | data, err := json.Marshal(dataMap) 55 | if err != nil { 56 | return id, err 57 | } 58 | tmp := struct { 59 | ID int64 `json:"orgId"` 60 | }{} 61 | 62 | err = c.request("POST", "/api/orgs", nil, data, &tmp) 63 | if err != nil { 64 | return id, err 65 | } 66 | 67 | return tmp.ID, err 68 | } 69 | 70 | // UpdateOrg updates a Grafana org. 71 | func (c *Client) UpdateOrg(id int64, name string) error { 72 | dataMap := map[string]string{ 73 | "name": name, 74 | } 75 | data, err := json.Marshal(dataMap) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return c.request("PUT", fmt.Sprintf("/api/orgs/%d", id), nil, data, nil) 81 | } 82 | 83 | // DeleteOrg deletes the Grafana org whose ID it's passed. 84 | func (c *Client) DeleteOrg(id int64) error { 85 | return c.request("DELETE", fmt.Sprintf("/api/orgs/%d", id), nil, nil, nil) 86 | } 87 | -------------------------------------------------------------------------------- /orgs_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getOrgsJSON = `[{"id":1,"name":"Main Org."},{"id":2,"name":"Test Org."}]` 11 | getOrgJSON = `{"id":1,"name":"Main Org.","address":{"address1":"","address2":"","city":"","zipCode":"","state":"","country":""}}` 12 | createdOrgJSON = `{"message":"Organization created","orgId":1}` 13 | updatedOrgJSON = `{"message":"Organization updated"}` 14 | deletedOrgJSON = `{"message":"Organization deleted"}` 15 | ) 16 | 17 | func TestOrgs(t *testing.T) { 18 | client := gapiTestTools(t, 200, getOrgsJSON) 19 | 20 | orgs, err := client.Orgs() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | t.Log(pretty.PrettyFormat(orgs)) 26 | 27 | if len(orgs) != 2 { 28 | t.Error("Length of returned orgs should be 2") 29 | } 30 | if orgs[0].ID != 1 || orgs[0].Name != "Main Org." { 31 | t.Error("Not correctly parsing returned organizations.") 32 | } 33 | } 34 | 35 | func TestOrgByName(t *testing.T) { 36 | client := gapiTestTools(t, 200, getOrgJSON) 37 | 38 | org := "Main Org." 39 | resp, err := client.OrgByName(org) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | t.Log(pretty.PrettyFormat(resp)) 45 | 46 | if resp.ID != 1 || resp.Name != org { 47 | t.Error("Not correctly parsing returned organization.") 48 | } 49 | } 50 | 51 | func TestOrg(t *testing.T) { 52 | client := gapiTestTools(t, 200, getOrgJSON) 53 | 54 | org := int64(1) 55 | resp, err := client.Org(org) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | t.Log(pretty.PrettyFormat(resp)) 61 | 62 | if resp.ID != org || resp.Name != "Main Org." { 63 | t.Error("Not correctly parsing returned organization.") 64 | } 65 | } 66 | 67 | func TestNewOrg(t *testing.T) { 68 | client := gapiTestTools(t, 200, createdOrgJSON) 69 | 70 | resp, err := client.NewOrg("test-org") 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | t.Log(pretty.PrettyFormat(resp)) 76 | 77 | if resp != 1 { 78 | t.Error("Not correctly parsing returned creation message.") 79 | } 80 | } 81 | 82 | func TestUpdateOrg(t *testing.T) { 83 | client := gapiTestTools(t, 200, updatedOrgJSON) 84 | 85 | err := client.UpdateOrg(int64(1), "test-org") 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | } 90 | 91 | func TestDeleteOrg(t *testing.T) { 92 | client := gapiTestTools(t, 200, deletedOrgJSON) 93 | 94 | err := client.DeleteOrg(int64(1)) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /playlist.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // PlaylistItem represents a Grafana playlist item. 9 | type PlaylistItem struct { 10 | Type string `json:"type"` 11 | Value string `json:"value"` 12 | Order int `json:"order"` 13 | Title string `json:"title"` 14 | } 15 | 16 | // Playlist represents a Grafana playlist. 17 | type Playlist struct { 18 | ID int `json:"id,omitempty"` // Grafana < 9.0 19 | UID string `json:"uid,omitempty"` // Grafana >= 9.0 20 | Name string `json:"name"` 21 | Interval string `json:"interval"` 22 | Items []PlaylistItem `json:"items"` 23 | } 24 | 25 | // Grafana 9.0+ returns the ID and the UID but uses the UID in the API calls. 26 | // Grafana <9 only returns the ID. 27 | func (p *Playlist) QueryID() string { 28 | if p.UID != "" { 29 | return p.UID 30 | } 31 | return fmt.Sprintf("%d", p.ID) 32 | } 33 | 34 | // Playlist fetches and returns a Grafana playlist. 35 | func (c *Client) Playlist(idOrUID string) (*Playlist, error) { 36 | path := fmt.Sprintf("/api/playlists/%s", idOrUID) 37 | playlist := &Playlist{} 38 | err := c.request("GET", path, nil, nil, playlist) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return playlist, nil 44 | } 45 | 46 | // NewPlaylist creates a new Grafana playlist. 47 | func (c *Client) NewPlaylist(playlist Playlist) (string, error) { 48 | data, err := json.Marshal(playlist) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | var result Playlist 54 | 55 | err = c.request("POST", "/api/playlists", nil, data, &result) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | return result.QueryID(), nil 61 | } 62 | 63 | // UpdatePlaylist updates a Grafana playlist. 64 | func (c *Client) UpdatePlaylist(playlist Playlist) error { 65 | path := fmt.Sprintf("/api/playlists/%s", playlist.QueryID()) 66 | data, err := json.Marshal(playlist) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return c.request("PUT", path, nil, data, nil) 72 | } 73 | 74 | // DeletePlaylist deletes the Grafana playlist whose ID it's passed. 75 | func (c *Client) DeletePlaylist(idOrUID string) error { 76 | path := fmt.Sprintf("/api/playlists/%s", idOrUID) 77 | 78 | return c.request("DELETE", path, nil, nil, nil) 79 | } 80 | -------------------------------------------------------------------------------- /playlist_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | createAndUpdatePlaylistResponse = ` { 9 | "uid": "1", 10 | "name": "my playlist", 11 | "interval": "5m" 12 | }` 13 | 14 | getPlaylistResponse = `{ 15 | "uid": "2", 16 | "name": "my playlist", 17 | "interval": "5m", 18 | "orgId": "my org", 19 | "items": [ 20 | { 21 | "id": 1, 22 | "playlistId": 1, 23 | "type": "dashboard_by_id", 24 | "value": "3", 25 | "order": 1, 26 | "title":"my dasboard" 27 | }, 28 | { 29 | "id": 1, 30 | "playlistId": 1, 31 | "type": "dashboard_by_id", 32 | "value": "3", 33 | "order": 1, 34 | "title":"my dasboard" 35 | } 36 | ] 37 | }` 38 | ) 39 | 40 | func TestPlaylistCreateAndUpdate(t *testing.T) { 41 | client := gapiTestToolsFromCalls(t, []mockServerCall{ 42 | {200, createAndUpdatePlaylistResponse}, 43 | {200, createAndUpdatePlaylistResponse}, 44 | }) 45 | 46 | playlist := Playlist{ 47 | Name: "my playlist", 48 | Interval: "5m", 49 | Items: []PlaylistItem{ 50 | {}, 51 | }, 52 | } 53 | 54 | // create 55 | id, err := client.NewPlaylist(playlist) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if id != "1" { 61 | t.Errorf("Invalid id - %s, Expected %s", id, "1") 62 | } 63 | 64 | // update 65 | playlist.Items = append(playlist.Items, PlaylistItem{ 66 | Type: "dashboard_by_id", 67 | Value: "1", 68 | Order: 1, 69 | Title: "my dashboard", 70 | }) 71 | 72 | err = client.UpdatePlaylist(playlist) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | } 77 | 78 | func TestGetPlaylist(t *testing.T) { 79 | client := gapiTestTools(t, 200, getPlaylistResponse) 80 | 81 | playlist, err := client.Playlist("2") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | if playlist.UID != "2" { 87 | t.Errorf("Invalid id - %s, Expected %s", playlist.UID, "2") 88 | } 89 | 90 | if len(playlist.Items) != 2 { 91 | t.Errorf("Invalid len(items) - %d, Expected %d", len(playlist.Items), 2) 92 | } 93 | } 94 | 95 | func TestDeletePlaylist(t *testing.T) { 96 | client := gapiTestTools(t, 200, "") 97 | 98 | err := client.DeletePlaylist("1") 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /preferences.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | // NavLink represents a Grafana nav link. 4 | type NavLink struct { 5 | ID string `json:"id,omitempty"` 6 | Text string `json:"text,omitempty"` 7 | URL string `json:"url,omitempty"` 8 | Target string `json:"target,omitempty"` 9 | } 10 | 11 | // NavbarPreference represents a Grafana navbar preference. 12 | type NavbarPreference struct { 13 | SavedItems []NavLink `json:"savedItems"` 14 | } 15 | 16 | // QueryHistoryPreference represents a Grafana query history preference. 17 | type QueryHistoryPreference struct { 18 | HomeTab string `json:"homeTab"` 19 | } 20 | 21 | // Preferences represents Grafana preferences. 22 | type Preferences struct { 23 | Theme string `json:"theme,omitempty"` 24 | HomeDashboardID int64 `json:"homeDashboardId,omitempty"` 25 | HomeDashboardUID string `json:"homeDashboardUID,omitempty"` 26 | Timezone string `json:"timezone,omitempty"` 27 | WeekStart string `json:"weekStart,omitempty"` 28 | Locale string `json:"locale,omitempty"` 29 | Navbar NavbarPreference `json:"navbar,omitempty"` 30 | QueryHistory QueryHistoryPreference `json:"queryHistory,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // ReportSchedule represents the schedule from a Grafana report. 10 | type ReportSchedule struct { 11 | StartDate *time.Time `json:"startDate,omitempty"` 12 | EndDate *time.Time `json:"endDate,omitempty"` 13 | Frequency string `json:"frequency"` 14 | IntervalFrequency string `json:"intervalFrequency"` 15 | IntervalAmount int64 `json:"intervalAmount"` 16 | WorkdaysOnly bool `json:"workdaysOnly"` 17 | TimeZone string `json:"timeZone"` 18 | DayOfMonth string `json:"dayOfMonth,omitempty"` 19 | } 20 | 21 | // ReportOptions represents the options for a Grafana report. 22 | type ReportOptions struct { 23 | Orientation string `json:"orientation"` 24 | Layout string `json:"layout"` 25 | } 26 | 27 | // ReportDashboardTimeRange represents the time range from a dashboard on a Grafana report. 28 | type ReportDashboardTimeRange struct { 29 | From string `json:"from"` 30 | To string `json:"to"` 31 | } 32 | 33 | // ReportDashboardIdentifier represents the identifier for a dashboard on a Grafana report. 34 | type ReportDashboardIdentifier struct { 35 | ID int64 `json:"id,omitempty"` 36 | UID string `json:"uid,omitempty"` 37 | Name string `json:"name,omitempty"` 38 | } 39 | 40 | // ReportDashboard represents a dashboard on a Grafana report. 41 | type ReportDashboard struct { 42 | Dashboard ReportDashboardIdentifier `json:"dashboard"` 43 | TimeRange ReportDashboardTimeRange `json:"timeRange"` 44 | Variables map[string]string `json:"reportVariables"` 45 | } 46 | 47 | // Report represents a Grafana report. 48 | type Report struct { 49 | // ReadOnly 50 | ID int64 `json:"id,omitempty"` 51 | UserID int64 `json:"userId,omitempty"` 52 | OrgID int64 `json:"orgId,omitempty"` 53 | State string `json:"state,omitempty"` 54 | 55 | Dashboards []ReportDashboard `json:"dashboards"` 56 | 57 | Name string `json:"name"` 58 | Recipients string `json:"recipients"` 59 | ReplyTo string `json:"replyTo"` 60 | Message string `json:"message"` 61 | Schedule ReportSchedule `json:"schedule"` 62 | Options ReportOptions `json:"options"` 63 | EnableDashboardURL bool `json:"enableDashboardUrl"` 64 | EnableCSV bool `json:"enableCsv"` 65 | Formats []string `json:"formats"` 66 | ScaleFactor int64 `json:"scaleFactor"` 67 | } 68 | 69 | // Report fetches and returns a Grafana report. 70 | func (c *Client) Report(id int64) (*Report, error) { 71 | path := fmt.Sprintf("/api/reports/%d", id) 72 | report := &Report{} 73 | err := c.request("GET", path, nil, nil, report) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return report, nil 79 | } 80 | 81 | // NewReport creates a new Grafana report. 82 | func (c *Client) NewReport(report Report) (int64, error) { 83 | data, err := json.Marshal(report) 84 | if err != nil { 85 | return 0, err 86 | } 87 | 88 | result := struct { 89 | ID int64 90 | }{} 91 | 92 | err = c.request("POST", "/api/reports", nil, data, &result) 93 | if err != nil { 94 | return 0, err 95 | } 96 | 97 | return result.ID, nil 98 | } 99 | 100 | // UpdateReport updates a Grafana report. 101 | func (c *Client) UpdateReport(report Report) error { 102 | path := fmt.Sprintf("/api/reports/%d", report.ID) 103 | data, err := json.Marshal(report) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | return c.request("PUT", path, nil, data, nil) 109 | } 110 | 111 | // DeleteReport deletes the Grafana report whose ID it's passed. 112 | func (c *Client) DeleteReport(id int64) error { 113 | path := fmt.Sprintf("/api/reports/%d", id) 114 | 115 | return c.request("DELETE", path, nil, nil, nil) 116 | } 117 | -------------------------------------------------------------------------------- /report_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | var ( 11 | getReportJSON = ` 12 | { 13 | "id": 4, 14 | "userId": 0, 15 | "orgId": 1, 16 | "dashboards": [ 17 | { 18 | "dashboard": { 19 | "id": 33, 20 | "uid": "nErXDvCkzz", 21 | "name": "Terraform Acceptance Test" 22 | }, 23 | "timeRange": { 24 | "from": "now-1h", 25 | "to": "now" 26 | } 27 | } 28 | ], 29 | "name": "My Report", 30 | "recipients": "test@test.com", 31 | "replyTo": "", 32 | "message": "", 33 | "schedule": { 34 | "startDate": "2020-01-01T00:00:00Z", 35 | "endDate": null, 36 | "frequency": "custom", 37 | "intervalFrequency": "weeks", 38 | "intervalAmount": 2, 39 | "workdaysOnly": true, 40 | "dayOfMonth": "1", 41 | "day": "wednesday", 42 | "hour": 0, 43 | "minute": 0, 44 | "timeZone": "GMT" 45 | }, 46 | "options": { 47 | "orientation": "landscape", 48 | "layout": "grid" 49 | }, 50 | "templateVars": {}, 51 | "enableDashboardUrl": true, 52 | "enableCsv": true, 53 | "state": "", 54 | "created": "2022-01-11T15:09:13Z", 55 | "updated": "2022-01-11T16:18:34Z" 56 | } 57 | ` 58 | createReportJSON = ` 59 | { 60 | "id": 4 61 | } 62 | ` 63 | now = time.Now() 64 | testReport = Report{ 65 | Name: "My Report", 66 | Recipients: "test@test.com", 67 | Schedule: ReportSchedule{ 68 | StartDate: &now, 69 | EndDate: nil, 70 | Frequency: "custom", 71 | IntervalFrequency: "weeks", 72 | IntervalAmount: 2, 73 | WorkdaysOnly: true, 74 | TimeZone: "GMT", 75 | }, 76 | Dashboards: []ReportDashboard{ 77 | { 78 | Dashboard: ReportDashboardIdentifier{ 79 | ID: 33, 80 | UID: "nErXDvCkzz", 81 | }, 82 | TimeRange: ReportDashboardTimeRange{ 83 | From: "now-1h", 84 | To: "now", 85 | }, 86 | }, 87 | }, 88 | Options: ReportOptions{ 89 | Orientation: "landscape", 90 | Layout: "grid", 91 | }, 92 | EnableDashboardURL: true, 93 | EnableCSV: true, 94 | } 95 | ) 96 | 97 | func TestReport(t *testing.T) { 98 | client := gapiTestTools(t, 200, getReportJSON) 99 | 100 | report := int64(4) 101 | resp, err := client.Report(report) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | t.Log(pretty.PrettyFormat(resp)) 107 | 108 | if resp.ID != report || resp.Name != "My Report" { 109 | t.Error("Not correctly parsing returned report.") 110 | } 111 | } 112 | 113 | func TestNewReport(t *testing.T) { 114 | client := gapiTestTools(t, 200, createReportJSON) 115 | 116 | resp, err := client.NewReport(testReport) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | t.Log(pretty.PrettyFormat(resp)) 122 | 123 | if resp != 4 { 124 | t.Error("Not correctly parsing returned creation message.") 125 | } 126 | } 127 | 128 | func TestUpdateReport(t *testing.T) { 129 | client := gapiTestTools(t, 200, "") 130 | 131 | err := client.UpdateReport(testReport) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | } 136 | 137 | func TestDeleteReport(t *testing.T) { 138 | client := gapiTestTools(t, 200, "") 139 | 140 | err := client.DeleteReport(4) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | const ( 9 | DashboardsResource = "dashboards" 10 | DatasourcesResource = "datasources" 11 | FoldersResource = "folders" 12 | ServiceAccountsResource = "serviceaccounts" 13 | TeamsResource = "teams" 14 | UsersResource = "users" 15 | BuiltInRolesResource = "builtInRoles" 16 | ) 17 | 18 | // ResourceIdent represents anything that can be considered a resource identifier. 19 | type ResourceIdent interface { 20 | fmt.Stringer 21 | } 22 | 23 | // ResourceID wraps `int64` to be a valid `ResourceIdent` 24 | type ResourceID int64 25 | 26 | func (id ResourceID) String() string { 27 | return strconv.FormatInt(int64(id), 10) 28 | } 29 | 30 | // ResourceUID wraps `string` to be a valid `ResourceIdent` 31 | type ResourceUID string 32 | 33 | func (id ResourceUID) String() string { 34 | return string(id) 35 | } 36 | -------------------------------------------------------------------------------- /resource_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type ResourcePermission struct { 9 | ID int64 `json:"id"` 10 | RoleName string `json:"roleName"` 11 | IsManaged bool `json:"isManaged"` 12 | IsInherited bool `json:"isInherited"` 13 | IsServiceAccount bool `json:"isServiceAccount"` 14 | UserID int64 `json:"userId,omitempty"` 15 | UserLogin string `json:"userLogin,omitempty"` 16 | Team string `json:"team,omitempty"` 17 | TeamID int64 `json:"teamId,omitempty"` 18 | BuiltInRole string `json:"builtInRole,omitempty"` 19 | Actions []string `json:"actions"` 20 | Permission string `json:"permission"` 21 | } 22 | 23 | type SetResourcePermissionsBody struct { 24 | Permissions []SetResourcePermissionItem `json:"permissions"` 25 | } 26 | 27 | type SetResourcePermissionBody struct { 28 | Permission SetResourcePermissionItem `json:"permission"` 29 | } 30 | 31 | type SetResourcePermissionItem struct { 32 | UserID int64 `json:"userId,omitempty"` 33 | TeamID int64 `json:"teamId,omitempty"` 34 | BuiltinRole string `json:"builtInRole,omitempty"` 35 | Permission string `json:"permission"` 36 | } 37 | 38 | type SetResourcePermissionsResponse struct { 39 | Message string `json:"message"` 40 | } 41 | 42 | func (c *Client) listResourcePermissions(resource string, ident ResourceIdent) ([]*ResourcePermission, error) { 43 | path := fmt.Sprintf("/api/access-control/%s/%s", resource, ident.String()) 44 | result := make([]*ResourcePermission, 0) 45 | if err := c.request("GET", path, nil, nil, &result); err != nil { 46 | return nil, fmt.Errorf("error getting %s resource permissions at %s: %w", resource, path, err) 47 | } 48 | return result, nil 49 | } 50 | 51 | func (c *Client) setResourcePermissions(resource string, ident ResourceIdent, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 52 | path := fmt.Sprintf("/api/access-control/%s/%s", resource, ident.String()) 53 | data, err := json.Marshal(body) 54 | if err != nil { 55 | return nil, fmt.Errorf("marshal err: %w", err) 56 | } 57 | 58 | result := SetResourcePermissionsResponse{} 59 | if err := c.request("POST", path, nil, data, &result); err != nil { 60 | return nil, fmt.Errorf("error setting %s resource permissions at %s: %w", resource, path, err) 61 | } 62 | return &result, nil 63 | } 64 | 65 | func (c *Client) setResourcePermissionByAssignment( 66 | resource string, 67 | ident ResourceIdent, 68 | assignmentKind string, 69 | assignmentIdent ResourceIdent, 70 | body SetResourcePermissionBody, 71 | ) (*SetResourcePermissionsResponse, error) { 72 | path := fmt.Sprintf("/api/access-control/%s/%s/%s/%s", resource, ident.String(), assignmentKind, assignmentIdent.String()) 73 | data, err := json.Marshal(body) 74 | if err != nil { 75 | return nil, fmt.Errorf("marshal err: %w", err) 76 | } 77 | 78 | result := SetResourcePermissionsResponse{} 79 | if err := c.request("POST", path, nil, data, &result); err != nil { 80 | return nil, fmt.Errorf("error setting %s resource permissions for %s at %s: %w", resource, assignmentKind, path, err) 81 | } 82 | return &result, nil 83 | } 84 | -------------------------------------------------------------------------------- /resource_permissions_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | const ( 11 | resourcePermissionsListJSON = `[ 12 | { 13 | "id": 1, 14 | "roleName": "basic:admin", 15 | "isManaged": false, 16 | "isInherited": false, 17 | "isServiceAccount": false, 18 | "builtInRole": "Admin", 19 | "actions": [ 20 | "datasources:delete", 21 | "datasources:query", 22 | "datasources:read", 23 | "datasources:write", 24 | "datasources.caching:read", 25 | "datasources.caching:write", 26 | "datasources.permissions:read", 27 | "datasources.permissions:write" 28 | ], 29 | "permission": "Admin" 30 | } 31 | ]` 32 | resourcePermissionsResponseJSON = `{"message":"Permissions updated"}` 33 | ) 34 | 35 | func TestListResourcePermissions(t *testing.T) { 36 | client := gapiTestTools(t, http.StatusOK, resourcePermissionsListJSON) 37 | res, err := client.listResourcePermissions("datasources", ResourceID(1)) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | t.Log(pretty.PrettyFormat(res)) 42 | } 43 | 44 | func TestSetResourcePermissions(t *testing.T) { 45 | client := gapiTestTools(t, http.StatusOK, resourcePermissionsResponseJSON) 46 | res, err := client.setResourcePermissions("datasources", ResourceID(1), SetResourcePermissionsBody{ 47 | Permissions: []SetResourcePermissionItem{ 48 | { 49 | UserID: 1, 50 | Permission: "View", 51 | }, 52 | }, 53 | }) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | t.Log(pretty.PrettyFormat(res)) 58 | } 59 | 60 | func TestSetResourcePermissionsByAssignment(t *testing.T) { 61 | client := gapiTestTools(t, http.StatusOK, resourcePermissionsResponseJSON) 62 | res, err := client.setResourcePermissionByAssignment("datasources", ResourceID(1), "users", ResourceID(1), SetResourcePermissionBody{ 63 | Permission: SetResourcePermissionItem{ 64 | Permission: "View", 65 | }, 66 | }) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | t.Log(pretty.PrettyFormat(res)) 71 | } 72 | -------------------------------------------------------------------------------- /resource_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestResourceIdent(t *testing.T) { 10 | require.Equal(t, "1", ResourceID(1).String()) 11 | require.Equal(t, ResourceUID("testing").String(), "testing") 12 | } 13 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type Role struct { 10 | Version int64 `json:"version"` 11 | UID string `json:"uid,omitempty"` 12 | Name string `json:"name"` 13 | Description string `json:"description"` 14 | Global bool `json:"global"` 15 | Group string `json:"group"` 16 | DisplayName string `json:"displayName"` 17 | Hidden bool `json:"hidden"` 18 | Permissions []Permission `json:"permissions,omitempty"` 19 | } 20 | 21 | type Permission struct { 22 | Action string `json:"action"` 23 | Scope string `json:"scope"` 24 | } 25 | 26 | // GetRole fetches and returns Grafana roles. Available only in Grafana Enterprise 8.+. 27 | func (c *Client) GetRoles() ([]Role, error) { 28 | const limit = 1000 29 | var ( 30 | page = 0 31 | newRoles []Role 32 | roles []Role 33 | query = make(url.Values) 34 | ) 35 | query.Set("limit", fmt.Sprint(limit)) 36 | for { 37 | page++ 38 | query.Set("page", fmt.Sprint(page)) 39 | 40 | if err := c.request("GET", "/api/access-control/roles", query, nil, &newRoles); err != nil { 41 | return nil, err 42 | } 43 | 44 | roles = append(roles, newRoles...) 45 | 46 | if len(newRoles) < limit { 47 | return roles, nil 48 | } 49 | } 50 | } 51 | 52 | // GetRole gets a role with permissions for the given UID. Available only in Grafana Enterprise 8.+. 53 | func (c *Client) GetRole(uid string) (*Role, error) { 54 | r := &Role{} 55 | err := c.request("GET", buildURL(uid), nil, nil, r) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return r, nil 60 | } 61 | 62 | // NewRole creates a new role with permissions. Available only in Grafana Enterprise 8.+. 63 | func (c *Client) NewRole(role Role) (*Role, error) { 64 | data, err := json.Marshal(role) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | r := &Role{} 70 | 71 | err = c.request("POST", "/api/access-control/roles", nil, data, &r) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return r, err 77 | } 78 | 79 | // UpdateRole updates the role and permissions. Available only in Grafana Enterprise 8.+. 80 | func (c *Client) UpdateRole(role Role) error { 81 | data, err := json.Marshal(role) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | err = c.request("PUT", buildURL(role.UID), nil, data, nil) 87 | 88 | return err 89 | } 90 | 91 | // DeleteRole deletes the role with it's permissions. Available only in Grafana Enterprise 8.+. 92 | func (c *Client) DeleteRole(uid string, global bool) error { 93 | qp := map[string][]string{ 94 | "global": {fmt.Sprint(global)}, 95 | } 96 | return c.request("DELETE", buildURL(uid), qp, nil, nil) 97 | } 98 | 99 | func buildURL(uid string) string { 100 | const rootURL = "/api/access-control/roles" 101 | return fmt.Sprintf("%s/%s", rootURL, uid) 102 | } 103 | -------------------------------------------------------------------------------- /role_assignments.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type RoleAssignments struct { 10 | RoleUID string `json:"role_uid"` 11 | Users []int `json:"users,omitempty"` 12 | Teams []int `json:"teams,omitempty"` 13 | ServiceAccounts []int `json:"service_accounts,omitempty"` 14 | } 15 | 16 | func (c *Client) GetRoleAssignments(uid string) (*RoleAssignments, error) { 17 | assignments := &RoleAssignments{} 18 | url := fmt.Sprintf("/api/access-control/roles/%s/assignments", uid) 19 | if err := c.request(http.MethodGet, url, nil, nil, assignments); err != nil { 20 | return nil, err 21 | } 22 | 23 | return assignments, nil 24 | } 25 | 26 | func (c *Client) UpdateRoleAssignments(ra *RoleAssignments) (*RoleAssignments, error) { 27 | response := &RoleAssignments{} 28 | 29 | data, err := json.Marshal(ra) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | url := fmt.Sprintf("/api/access-control/roles/%s/assignments", ra.RoleUID) 35 | err = c.request(http.MethodPut, url, nil, data, &response) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return response, nil 41 | } 42 | -------------------------------------------------------------------------------- /service_account_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | func (c *Client) ListServiceAccountResourcePermissions(id int64) ([]*ResourcePermission, error) { 4 | return c.listResourcePermissions(ServiceAccountsResource, ResourceID(id)) 5 | } 6 | 7 | func (c *Client) SetServiceAccountResourcePermissions(id int64, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 8 | return c.setResourcePermissions(ServiceAccountsResource, ResourceID(id), body) 9 | } 10 | 11 | func (c *Client) SetUserServiceAccountResourcePermissions(id int64, userID int64, permission string) (*SetResourcePermissionsResponse, error) { 12 | return c.setResourcePermissionByAssignment( 13 | ServiceAccountsResource, 14 | ResourceID(id), 15 | UsersResource, 16 | ResourceID(userID), 17 | SetResourcePermissionBody{ 18 | Permission: SetResourcePermissionItem{ 19 | UserID: userID, 20 | Permission: permission, 21 | }, 22 | }, 23 | ) 24 | } 25 | 26 | func (c *Client) SetTeamServiceAccountResourcePermissions(id int64, teamID int64, permission string) (*SetResourcePermissionsResponse, error) { 27 | return c.setResourcePermissionByAssignment( 28 | ServiceAccountsResource, 29 | ResourceID(id), 30 | TeamsResource, 31 | ResourceID(teamID), 32 | SetResourcePermissionBody{ 33 | Permission: SetResourcePermissionItem{ 34 | TeamID: teamID, 35 | Permission: permission, 36 | }, 37 | }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /service_account_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gobs/pretty" 8 | ) 9 | 10 | const ( 11 | serviceAccountJSON = `{ 12 | "id": 8, 13 | "name": "newSA", 14 | "login": "sa-newsa", 15 | "orgId": 1, 16 | "isDisabled": false, 17 | "role": "", 18 | "tokens": 0, 19 | "avatarUrl": "" 20 | }` 21 | searchServiceAccountsJSON = `{ 22 | "totalCount": 2, 23 | "serviceAccounts": [ 24 | { 25 | "id": 8, 26 | "name": "newSA", 27 | "login": "sa-newsa", 28 | "orgId": 1, 29 | "isDisabled": false, 30 | "role": "", 31 | "tokens": 1, 32 | "avatarUrl": "/avatar/0e94f33c929884a5163d953582f27fec" 33 | }, 34 | { 35 | "id": 9, 36 | "name": "newnewSA", 37 | "login": "sa-newnewsa", 38 | "orgId": 1, 39 | "isDisabled": true, 40 | "role": "Admin", 41 | "tokens": 2, 42 | "avatarUrl": "/avatar/0e29f33c929824a5163d953582e83abe" 43 | } 44 | ], 45 | "page": 1, 46 | "perPage": 1000 47 | }` 48 | createServiceAccountTokenJSON = `{"name":"key-name", "key":"mock-api-key"}` //#nosec 49 | deleteServiceAccountTokenJSON = `{"message":"Service account token deleted"}` //#nosec 50 | deleteServiceAccountJSON = `{"message":"service account deleted"}` 51 | 52 | getServiceAccountTokensJSON = `[ 53 | { 54 | "id": 4, 55 | "name": "testToken", 56 | "created": "2022-06-15T15:19:00+02:00", 57 | "expiration": "2022-06-15T16:17:20+02:00", 58 | "secondsUntilExpiration": 3412.443626017, 59 | "hasExpired": false 60 | }, 61 | { 62 | "id": 1, 63 | "name": "testToken2", 64 | "created": "2022-01-15T15:19:00+02:00", 65 | "expiration": "2022-02-15T16:17:20+02:00", 66 | "secondsUntilExpiration": 0, 67 | "hasExpired": true 68 | }, 69 | { 70 | "id": 6, 71 | "name": "testTokenzx", 72 | "created": "2022-06-15T15:39:54+02:00", 73 | "expiration": null, 74 | "secondsUntilExpiration": 0, 75 | "hasExpired": false 76 | } 77 | ]` //#nosec 78 | ) 79 | 80 | func TestCreateServiceAccountToken(t *testing.T) { 81 | client := gapiTestTools(t, http.StatusOK, createServiceAccountTokenJSON) 82 | 83 | req := CreateServiceAccountTokenRequest{ 84 | Name: "key-name", 85 | SecondsToLive: 0, 86 | } 87 | 88 | res, err := client.CreateServiceAccountToken(req) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | 93 | t.Log(pretty.PrettyFormat(res)) 94 | } 95 | 96 | func TestCreateServiceAccount(t *testing.T) { 97 | client := gapiTestTools(t, http.StatusOK, serviceAccountJSON) 98 | 99 | isDisabled := true 100 | req := CreateServiceAccountRequest{ 101 | Name: "newSA", 102 | Role: "Admin", 103 | IsDisabled: &isDisabled, 104 | } 105 | 106 | res, err := client.CreateServiceAccount(req) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | 111 | t.Log(pretty.PrettyFormat(res)) 112 | } 113 | 114 | func TestUpdateServiceAccount(t *testing.T) { 115 | client := gapiTestTools(t, http.StatusOK, serviceAccountJSON) 116 | 117 | isDisabled := false 118 | req := UpdateServiceAccountRequest{ 119 | Name: "", 120 | Role: "Admin", 121 | IsDisabled: &isDisabled, 122 | } 123 | 124 | res, err := client.UpdateServiceAccount(5, req) 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | 129 | t.Log(pretty.PrettyFormat(res)) 130 | } 131 | 132 | func TestDeleteServiceAccount(t *testing.T) { 133 | client := gapiTestTools(t, http.StatusOK, deleteServiceAccountJSON) 134 | 135 | res, err := client.DeleteServiceAccount(int64(1)) 136 | if err != nil { 137 | t.Error(err) 138 | } 139 | 140 | t.Log(pretty.PrettyFormat(res)) 141 | } 142 | 143 | func TestDeleteServiceAccountToken(t *testing.T) { 144 | client := gapiTestTools(t, http.StatusOK, deleteServiceAccountTokenJSON) 145 | 146 | res, err := client.DeleteServiceAccountToken(int64(1), int64(1)) 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | 151 | t.Log(pretty.PrettyFormat(res)) 152 | } 153 | 154 | func TestGetServiceAccounts(t *testing.T) { 155 | client := gapiTestTools(t, http.StatusOK, searchServiceAccountsJSON) 156 | 157 | res, err := client.GetServiceAccounts() 158 | if err != nil { 159 | t.Error(err) 160 | } 161 | 162 | t.Log(pretty.PrettyFormat(res)) 163 | } 164 | 165 | func TestGetServiceAccountTokens(t *testing.T) { 166 | client := gapiTestTools(t, http.StatusOK, getServiceAccountTokensJSON) 167 | 168 | res, err := client.GetServiceAccountTokens(5) 169 | if err != nil { 170 | t.Error(err) 171 | } 172 | 173 | t.Log(pretty.PrettyFormat(res)) 174 | } 175 | -------------------------------------------------------------------------------- /slo_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | func TestSLOs(t *testing.T) { 10 | t.Run("list all SLOs succeeds", func(t *testing.T) { 11 | client := gapiTestTools(t, 200, getSlosJSON) 12 | 13 | resp, err := client.ListSlos() 14 | 15 | slos := resp.Slos 16 | 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | t.Log(pretty.PrettyFormat(slos)) 21 | if len(slos) != 1 { 22 | t.Errorf("wrong number of contact points returned, got %d", len(slos)) 23 | } 24 | if slos[0].Name != "list-slos" { 25 | t.Errorf("incorrect name - expected Name-Test, got %s", slos[0].Name) 26 | } 27 | }) 28 | 29 | t.Run("get individual SLO succeeds", func(t *testing.T) { 30 | client := gapiTestTools(t, 200, getSloJSON) 31 | 32 | slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf") 33 | 34 | t.Log(pretty.PrettyFormat(slo)) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | if slo.UUID != "qkkrknp12w6tmsdcrfkdf" { 39 | t.Errorf("incorrect UID - expected qkkrknp12w6tmsdcrfkdf, got %s", slo.UUID) 40 | } 41 | }) 42 | 43 | t.Run("get non-existent SLOs fails", func(t *testing.T) { 44 | client := gapiTestTools(t, 404, getSlosJSON) 45 | 46 | slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf") 47 | 48 | if err == nil { 49 | t.Log(pretty.PrettyFormat(slo)) 50 | t.Error("expected error but got nil") 51 | } 52 | }) 53 | 54 | t.Run("create SLO succeeds", func(t *testing.T) { 55 | client := gapiTestTools(t, 201, createSloJSON) 56 | slo := generateSlo() 57 | 58 | resp, err := client.CreateSlo(slo) 59 | 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | if resp.UUID != "sjnp8wobcbs3eit28n8yb" { 64 | t.Errorf("unexpected UID returned, got %s", resp.UUID) 65 | } 66 | }) 67 | 68 | t.Run("update SLO succeeds", func(t *testing.T) { 69 | client := gapiTestTools(t, 200, createSloJSON) 70 | slo := generateSlo() 71 | slo.Description = "Updated Description" 72 | 73 | err := client.UpdateSlo(slo.UUID, slo) 74 | 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | }) 79 | 80 | t.Run("delete SLO succeeds", func(t *testing.T) { 81 | client := gapiTestTools(t, 204, "") 82 | 83 | err := client.DeleteSlo("qkkrknp12w6tmsdcrfkdf") 84 | 85 | if err != nil { 86 | t.Log(err) 87 | t.Error(err) 88 | } 89 | }) 90 | } 91 | 92 | const getSlosJSON = ` 93 | { 94 | "slos": [ 95 | { 96 | "uuid": "qkkrknp12w6tmsdcrfkdf", 97 | "name": "list-slos", 98 | "description": "list-slos-description", 99 | "query": { 100 | "freeform": { 101 | "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" 102 | }, 103 | "type": "freeform" 104 | }, 105 | "objectives": [ 106 | { 107 | "value": 0.995, 108 | "window": "28d" 109 | } 110 | ], 111 | "drillDownDashboardRef": { 112 | "uid": "5IkqX6P4k" 113 | } 114 | } 115 | ] 116 | }` 117 | 118 | const getSloJSON = ` 119 | { 120 | "uuid": "qkkrknp12w6tmsdcrfkdf", 121 | "name": "Name-Test", 122 | "description": "Description-Test", 123 | "query": { 124 | "freeform": { 125 | "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" 126 | }, 127 | "type": "freeform" 128 | }, 129 | "objectives": [ 130 | { 131 | "value": 0.995, 132 | "window": "28d" 133 | } 134 | ], 135 | "drillDownDashboardRef": { 136 | "uid": "5IkqX6P4k" 137 | } 138 | }` 139 | 140 | const createSloJSON = ` 141 | { 142 | "uuid": "sjnp8wobcbs3eit28n8yb", 143 | "name": "test-name", 144 | "description": "test-description", 145 | "query": { 146 | "freeform": { 147 | "query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" 148 | }, 149 | "type": "freeform" 150 | }, 151 | "objectives": [ 152 | { 153 | "value": 0.995, 154 | "window": "30d" 155 | } 156 | ], 157 | "drillDownDashboardRef": { 158 | "uid": "zz5giRyVk" 159 | } 160 | } 161 | ` 162 | 163 | func generateSlo() Slo { 164 | objective := []Objective{{Value: 0.995, Window: "30d"}} 165 | query := Query{ 166 | Freeform: &FreeformQuery{ 167 | Query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", 168 | }, 169 | Type: QueryTypeFreeform, 170 | } 171 | 172 | slo := Slo{ 173 | Name: "test-name", 174 | Description: "test-description", 175 | Objectives: objective, 176 | Query: query, 177 | } 178 | 179 | return slo 180 | } 181 | -------------------------------------------------------------------------------- /snapshot.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Snapshot represents a Grafana snapshot. 8 | type Snapshot struct { 9 | Model map[string]interface{} `json:"dashboard"` 10 | Expires int64 `json:"expires"` 11 | } 12 | 13 | // SnapshotResponse represents the Grafana API response to creating a dashboard. 14 | type SnapshotCreateResponse struct { 15 | DeleteKey string `json:"deleteKey"` 16 | DeleteURL string `json:"deleteUrl"` 17 | Key string `json:"key"` 18 | URL string `json:"url"` 19 | ID int64 `json:"id"` 20 | } 21 | 22 | // NewSnapshot creates a new Grafana snapshot. 23 | func (c *Client) NewSnapshot(snapshot Snapshot) (*SnapshotCreateResponse, error) { 24 | data, err := json.Marshal(snapshot) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | result := &SnapshotCreateResponse{} 30 | err = c.request("POST", "/api/snapshots", nil, data, &result) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return result, err 36 | } 37 | -------------------------------------------------------------------------------- /snapshot_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | createdSnapshotResponse = `{ 11 | "deleteKey":"XXXXXXX", 12 | "deleteUrl":"myurl/api/snapshots-delete/XXXXXXX", 13 | "key":"YYYYYYY", 14 | "url":"myurl/dashboard/snapshot/YYYYYYY", 15 | "id": 1 16 | }` 17 | ) 18 | 19 | func TestSnapshotCreate(t *testing.T) { 20 | client := gapiTestTools(t, 200, createdSnapshotResponse) 21 | 22 | snapshot := Snapshot{ 23 | Model: map[string]interface{}{ 24 | "title": "test", 25 | }, 26 | Expires: 3600, 27 | } 28 | 29 | resp, err := client.NewSnapshot(snapshot) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | t.Log(pretty.PrettyFormat(resp)) 35 | 36 | if resp.DeleteKey != "XXXXXXX" { 37 | t.Errorf("Invalid key - %s, Expected %s", resp.DeleteKey, "XXXXXXX") 38 | } 39 | 40 | for _, code := range []int{400, 401, 403, 412} { 41 | client = gapiTestTools(t, code, "error") 42 | _, err = client.NewSnapshot(snapshot) 43 | if err == nil { 44 | t.Errorf("%d not detected", code) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /team.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // SearchTeam represents a search for a Grafana team. 10 | type SearchTeam struct { 11 | TotalCount int64 `json:"totalCount,omitempty"` 12 | Teams []*Team `json:"teams,omitempty"` 13 | Page int64 `json:"page,omitempty"` 14 | PerPage int64 `json:"perPage,omitempty"` 15 | } 16 | 17 | // Team consists of a get response 18 | // It's used in Add and Update API 19 | type Team struct { 20 | ID int64 `json:"id,omitempty"` 21 | OrgID int64 `json:"orgId,omitempty"` 22 | Name string `json:"name"` 23 | Email string `json:"email,omitempty"` 24 | AvatarURL string `json:"avatarUrl,omitempty"` 25 | MemberCount int64 `json:"memberCount,omitempty"` 26 | Permission int64 `json:"permission,omitempty"` 27 | } 28 | 29 | // TeamMember represents a Grafana team member. 30 | type TeamMember struct { 31 | OrgID int64 `json:"orgId,omitempty"` 32 | TeamID int64 `json:"teamId,omitempty"` 33 | UserID int64 `json:"userID,omitempty"` 34 | Email string `json:"email,omitempty"` 35 | Login string `json:"login,omitempty"` 36 | AvatarURL string `json:"avatarUrl,omitempty"` 37 | Permission int64 `json:"permission,omitempty"` 38 | Labels []string `json:"labels,omitempty"` 39 | } 40 | 41 | // SearchTeam searches Grafana teams and returns the results. 42 | func (c *Client) SearchTeam(query string) (*SearchTeam, error) { 43 | var result SearchTeam 44 | 45 | page := "1" 46 | perPage := "1000" 47 | path := "/api/teams/search" 48 | queryValues := url.Values{} 49 | queryValues.Set("page", page) 50 | queryValues.Set("perPage", perPage) 51 | queryValues.Set("query", query) 52 | 53 | err := c.request("GET", path, queryValues, nil, &result) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &result, nil 59 | } 60 | 61 | // Team fetches and returns the Grafana team whose ID it's passed. 62 | func (c *Client) Team(id int64) (*Team, error) { 63 | team := &Team{} 64 | err := c.request("GET", fmt.Sprintf("/api/teams/%d", id), nil, nil, team) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return team, nil 70 | } 71 | 72 | // AddTeam makes a new team 73 | // email arg is an optional value. 74 | // If you don't want to set email, please set "" (empty string). 75 | // When team creation is successful, returns the team ID. 76 | func (c *Client) AddTeam(name string, email string) (int64, error) { 77 | id := int64(0) 78 | path := "/api/teams" 79 | team := Team{ 80 | Name: name, 81 | Email: email, 82 | } 83 | data, err := json.Marshal(team) 84 | if err != nil { 85 | return id, err 86 | } 87 | 88 | tmp := struct { 89 | ID int64 `json:"teamId"` 90 | }{} 91 | 92 | err = c.request("POST", path, nil, data, &tmp) 93 | if err != nil { 94 | return id, err 95 | } 96 | 97 | return tmp.ID, err 98 | } 99 | 100 | // UpdateTeam updates a Grafana team. 101 | func (c *Client) UpdateTeam(id int64, name string, email string) error { 102 | path := fmt.Sprintf("/api/teams/%d", id) 103 | team := Team{ 104 | Name: name, 105 | } 106 | // add param if email exists 107 | if email != "" { 108 | team.Email = email 109 | } 110 | data, err := json.Marshal(team) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return c.request("PUT", path, nil, data, nil) 116 | } 117 | 118 | // DeleteTeam deletes the Grafana team whose ID it's passed. 119 | func (c *Client) DeleteTeam(id int64) error { 120 | return c.request("DELETE", fmt.Sprintf("/api/teams/%d", id), nil, nil, nil) 121 | } 122 | 123 | // TeamMembers fetches and returns the team members for the Grafana team whose ID it's passed. 124 | func (c *Client) TeamMembers(id int64) ([]*TeamMember, error) { 125 | members := make([]*TeamMember, 0) 126 | err := c.request("GET", fmt.Sprintf("/api/teams/%d/members", id), nil, nil, &members) 127 | if err != nil { 128 | return members, err 129 | } 130 | 131 | return members, nil 132 | } 133 | 134 | // AddTeamMember adds a user to the Grafana team whose ID it's passed. 135 | func (c *Client) AddTeamMember(id int64, userID int64) error { 136 | path := fmt.Sprintf("/api/teams/%d/members", id) 137 | member := TeamMember{UserID: userID} 138 | data, err := json.Marshal(member) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | return c.request("POST", path, nil, data, nil) 144 | } 145 | 146 | // RemoveMemberFromTeam removes a user from the Grafana team whose ID it's passed. 147 | func (c *Client) RemoveMemberFromTeam(id int64, userID int64) error { 148 | path := fmt.Sprintf("/api/teams/%d/members/%d", id, userID) 149 | 150 | return c.request("DELETE", path, nil, nil, nil) 151 | } 152 | 153 | // TeamPreferences fetches and returns preferences for the Grafana team whose ID it's passed. 154 | func (c *Client) TeamPreferences(id int64) (*Preferences, error) { 155 | preferences := &Preferences{} 156 | err := c.request("GET", fmt.Sprintf("/api/teams/%d/preferences", id), nil, nil, preferences) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return preferences, nil 162 | } 163 | 164 | // UpdateTeamPreferences updates team preferences for the Grafana team whose ID it's passed. 165 | func (c *Client) UpdateTeamPreferences(id int64, preferences Preferences) error { 166 | path := fmt.Sprintf("/api/teams/%d/preferences", id) 167 | data, err := json.Marshal(preferences) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | return c.request("PUT", path, nil, data, nil) 173 | } 174 | -------------------------------------------------------------------------------- /team_external_group.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // TeamGroup represents a Grafana TeamGroup. 9 | type TeamGroup struct { 10 | OrgID int64 `json:"orgId,omitempty"` 11 | TeamID int64 `json:"teamId,omitempty"` 12 | GroupID string `json:"groupID,omitempty"` 13 | } 14 | 15 | // TeamGroups fetches and returns the list of Grafana team group whose Team ID it's passed. 16 | func (c *Client) TeamGroups(id int64) ([]TeamGroup, error) { 17 | teamGroups := make([]TeamGroup, 0) 18 | err := c.request("GET", fmt.Sprintf("/api/teams/%d/groups", id), nil, nil, &teamGroups) 19 | if err != nil { 20 | return teamGroups, err 21 | } 22 | 23 | return teamGroups, nil 24 | } 25 | 26 | // NewTeamGroup creates a new Grafana Team Group . 27 | func (c *Client) NewTeamGroup(id int64, groupID string) error { 28 | dataMap := map[string]string{ 29 | "groupId": groupID, 30 | } 31 | data, err := json.Marshal(dataMap) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return c.request("POST", fmt.Sprintf("/api/teams/%d/groups", id), nil, data, nil) 37 | } 38 | 39 | // DeleteTeam deletes the Grafana team whose ID it's passed. 40 | func (c *Client) DeleteTeamGroup(id int64, groupID string) error { 41 | return c.request("DELETE", fmt.Sprintf("/api/teams/%d/groups/%s", id, groupID), nil, nil, nil) 42 | } 43 | -------------------------------------------------------------------------------- /team_external_group_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getTeamGroupsJSON = ` 11 | [ 12 | { 13 | "orgId": 1, 14 | "teamId": 1, 15 | "groupId": "test" 16 | } 17 | ] 18 | ` 19 | createdTeamGroupJSON = ` 20 | { 21 | "message":"Group added to Team" 22 | } 23 | ` 24 | 25 | deletedTeamGroupJSON = ` 26 | { 27 | "message":"Team Group removed" 28 | } 29 | ` 30 | ) 31 | 32 | func TestTeamGroups(t *testing.T) { 33 | client := gapiTestTools(t, 200, getTeamGroupsJSON) 34 | 35 | teamID := int64(1) 36 | teamGroups, err := client.TeamGroups(teamID) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | t.Log(pretty.PrettyFormat(teamGroups)) 42 | 43 | if len(teamGroups) != 1 { 44 | t.Error("Length of returned teamGroups should be 1") 45 | } 46 | if teamGroups[0].TeamID != 1 || teamGroups[0].OrgID != 1 || teamGroups[0].GroupID != "test" { 47 | t.Error("Not correctly parsing returned teamGroups.") 48 | } 49 | } 50 | 51 | func TestNewTeamGroup(t *testing.T) { 52 | client := gapiTestTools(t, 200, createdTeamGroupJSON) 53 | 54 | err := client.NewTeamGroup(int64(1), "test") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | } 59 | 60 | func TestDeleteTeamGroup(t *testing.T) { 61 | client := gapiTestTools(t, 200, deletedTeamGroupJSON) 62 | 63 | err := client.DeleteTeamGroup(int64(1), "test") 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /team_permissions.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | func (c *Client) ListTeamResourcePermissions(uid string) ([]*ResourcePermission, error) { 4 | return c.listResourcePermissions(TeamsResource, ResourceUID(uid)) 5 | } 6 | 7 | func (c *Client) SetTeamResourcePermissions(uid string, body SetResourcePermissionsBody) (*SetResourcePermissionsResponse, error) { 8 | return c.setResourcePermissions(TeamsResource, ResourceUID(uid), body) 9 | } 10 | 11 | func (c *Client) SetUserTeamResourcePermissions(teamUID string, userID int64, permission string) (*SetResourcePermissionsResponse, error) { 12 | return c.setResourcePermissionByAssignment( 13 | TeamsResource, 14 | ResourceUID(teamUID), 15 | UsersResource, 16 | ResourceID(userID), 17 | SetResourcePermissionBody{ 18 | Permission: SetResourcePermissionItem{ 19 | UserID: userID, 20 | Permission: permission, 21 | }, 22 | }, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | // User represents a Grafana user. It is structured after the UserProfileDTO 11 | // struct in the Grafana codebase. 12 | type User struct { 13 | ID int64 `json:"id,omitempty"` 14 | Email string `json:"email,omitempty"` 15 | Name string `json:"name,omitempty"` 16 | Login string `json:"login,omitempty"` 17 | Theme string `json:"theme,omitempty"` 18 | OrgID int64 `json:"orgId,omitempty"` 19 | IsAdmin bool `json:"isGrafanaAdmin,omitempty"` 20 | IsDisabled bool `json:"isDisabled,omitempty"` 21 | IsExternal bool `json:"isExternal,omitempty"` 22 | UpdatedAt time.Time `json:"updatedAt,omitempty"` 23 | CreatedAt time.Time `json:"createdAt,omitempty"` 24 | AuthLabels []string `json:"authLabels,omitempty"` 25 | AvatarURL string `json:"avatarUrl,omitempty"` 26 | Password string `json:"password,omitempty"` 27 | } 28 | 29 | // UserSearch represents a Grafana user as returned by API endpoints that 30 | // return a collection of Grafana users. This representation of user has 31 | // reduced and differing fields. It is structured after the UserSearchHitDTO 32 | // struct in the Grafana codebase. 33 | type UserSearch struct { 34 | ID int64 `json:"id,omitempty"` 35 | Email string `json:"email,omitempty"` 36 | Name string `json:"name,omitempty"` 37 | Login string `json:"login,omitempty"` 38 | IsAdmin bool `json:"isAdmin,omitempty"` 39 | IsDisabled bool `json:"isDisabled,omitempty"` 40 | LastSeenAt time.Time `json:"lastSeenAt,omitempty"` 41 | LastSeenAtAge string `json:"lastSeenAtAge,omitempty"` 42 | AuthLabels []string `json:"authLabels,omitempty"` 43 | AvatarURL string `json:"avatarUrl,omitempty"` 44 | } 45 | 46 | // Users fetches and returns Grafana users. 47 | func (c *Client) Users() (users []UserSearch, err error) { 48 | var ( 49 | page = 1 50 | newUsers []UserSearch 51 | ) 52 | for len(newUsers) > 0 || page == 1 { 53 | query := url.Values{} 54 | query.Add("page", fmt.Sprintf("%d", page)) 55 | if err = c.request("GET", "/api/users", query, nil, &newUsers); err != nil { 56 | return 57 | } 58 | users = append(users, newUsers...) 59 | page++ 60 | } 61 | 62 | return 63 | } 64 | 65 | // User fetches a user by ID. 66 | func (c *Client) User(id int64) (user User, err error) { 67 | err = c.request("GET", fmt.Sprintf("/api/users/%d", id), nil, nil, &user) 68 | return 69 | } 70 | 71 | // UserByEmail fetches a user by email address. 72 | func (c *Client) UserByEmail(email string) (user User, err error) { 73 | query := url.Values{} 74 | query.Add("loginOrEmail", email) 75 | err = c.request("GET", "/api/users/lookup", query, nil, &user) 76 | return 77 | } 78 | 79 | // UserUpdate updates a user by ID. 80 | func (c *Client) UserUpdate(u User) error { 81 | data, err := json.Marshal(u) 82 | if err != nil { 83 | return err 84 | } 85 | return c.request("PUT", fmt.Sprintf("/api/users/%d", u.ID), nil, data, nil) 86 | } 87 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package gapi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobs/pretty" 7 | ) 8 | 9 | const ( 10 | getUsersJSON = `[{"id":1,"email":"users@localhost","isAdmin":true}]` 11 | getUserJSON = `{"id":2,"email":"user@localhost","isGrafanaAdmin":false}` 12 | getUserByEmailJSON = `{"id":3,"email":"userByEmail@localhost","isGrafanaAdmin":true}` 13 | getUserUpdateJSON = `{"id":4,"email":"userUpdate@localhost","isGrafanaAdmin":false}` 14 | ) 15 | 16 | func TestUsers(t *testing.T) { 17 | client := gapiTestToolsFromCalls(t, []mockServerCall{ 18 | {200, getUsersJSON}, 19 | {200, "null"}, 20 | }) 21 | 22 | resp, err := client.Users() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | t.Log(pretty.PrettyFormat(resp)) 28 | 29 | if len(resp) != 1 { 30 | t.Fatal("No users were returned.") 31 | } 32 | 33 | user := resp[0] 34 | 35 | if user.Email != "users@localhost" || 36 | user.ID != 1 || 37 | user.IsAdmin != true { 38 | t.Error("Not correctly parsing returned users.") 39 | } 40 | } 41 | 42 | func TestUser(t *testing.T) { 43 | client := gapiTestTools(t, 200, getUserJSON) 44 | 45 | user, err := client.User(1) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | t.Log(pretty.PrettyFormat(user)) 51 | 52 | if user.Email != "user@localhost" || 53 | user.ID != 2 || 54 | user.IsAdmin != false { 55 | t.Error("Not correctly parsing returned user.") 56 | } 57 | } 58 | 59 | func TestUserByEmail(t *testing.T) { 60 | client := gapiTestTools(t, 200, getUserByEmailJSON) 61 | 62 | user, err := client.UserByEmail("admin@localhost") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | t.Log(pretty.PrettyFormat(user)) 68 | 69 | if user.Email != "userByEmail@localhost" || 70 | user.ID != 3 || 71 | user.IsAdmin != true { 72 | t.Error("Not correctly parsing returned user.") 73 | } 74 | } 75 | 76 | func TestUserUpdate(t *testing.T) { 77 | client := gapiTestToolsFromCalls(t, []mockServerCall{ 78 | {200, getUserJSON}, 79 | {200, getUserUpdateJSON}, 80 | }) 81 | 82 | user, err := client.User(4) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | user.IsAdmin = true 87 | err = client.UserUpdate(user) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | } 92 | --------------------------------------------------------------------------------