diff --git a/.drone.yml b/.drone.yml index 7f493bc..6a14429 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,4 +8,4 @@ steps: image: golang commands: - go mod download - - go test + - go test -cover -race -vet all -mod readonly ./... diff --git a/admin.go b/admin.go index 1caf0c3..72c823b 100644 --- a/admin.go +++ b/admin.go @@ -11,6 +11,10 @@ import ( func (c *Client) CreateUser(user User) (int64, error) { id := int64(0) data, err := json.Marshal(user) + if err != nil { + return id, err + } + req, err := c.newRequest("POST", "/api/admin/users", nil, bytes.NewBuffer(data)) if err != nil { return id, err diff --git a/alertnotification.go b/alertnotification.go index ce9ccb3..ad98eda 100644 --- a/alertnotification.go +++ b/alertnotification.go @@ -10,6 +10,7 @@ import ( type AlertNotification struct { Id int64 `json:"id,omitempty"` + Uid string `json:"uid"` Name string `json:"name"` Type string `json:"type"` IsDefault bool `json:"isDefault"` diff --git a/client.go b/client.go index f152fe9..10b7ffb 100644 --- a/client.go +++ b/client.go @@ -29,9 +29,9 @@ func New(auth, baseURL string) (*Client, error) { } key := "" if strings.Contains(auth, ":") { - split := strings.Split(auth, ":") + split := strings.SplitN(auth, ":", 2) u.User = url.UserPassword(split[0], split[1]) - } else { + } else if auth != "" { key = fmt.Sprintf("Bearer %s", auth) } return &Client{ diff --git a/dashboard.go b/dashboard.go old mode 100755 new mode 100644 index b5250a7..47dac9d --- a/dashboard.go +++ b/dashboard.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "log" + "net/url" "os" ) @@ -24,11 +25,27 @@ type DashboardSaveResponse struct { Version int64 `json:"version"` } +type DashboardSearchResponse struct { + Id uint `json:"id"` + Uid string `json:"uid"` + Title string `json:"title"` + Uri string `json:"uri"` + Url string `json:"url"` + Slug string `json:"slug"` + Type string `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` + FolderId uint `json:"folderId"` + FolderUid string `json:"folderUid"` + FolderTitle string `json:"folderTitle"` + FolderUrl string `json:"folderUrl"` +} + type Dashboard struct { Meta DashboardMeta `json:"meta"` Model map[string]interface{} `json:"dashboard"` Folder int64 `json:"folderId"` - Overwrite bool `json:overwrite` + Overwrite bool `json:"overwrite"` } // Deprecated: use NewDashboard instead @@ -93,6 +110,33 @@ func (c *Client) NewDashboard(dashboard Dashboard) (*DashboardSaveResponse, erro return result, err } +func (c *Client) Dashboards() ([]DashboardSearchResponse, error) { + dashboards := make([]DashboardSearchResponse, 0) + query := url.Values{} + // search only dashboards + query.Add("type", "dash-db") + req, err := c.newRequest("GET", "/api/search", query, nil) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return dashboards, err + } + if resp.StatusCode != 200 { + return dashboards, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return dashboards, err + } + + err = json.Unmarshal(data, &dashboards) + return dashboards, err +} + // Deprecated: Starting from Grafana v5.0. Please update to use DashboardByUID instead. func (c *Client) Dashboard(slug string) (*Dashboard, error) { return c.dashboard(fmt.Sprintf("/api/dashboards/db/%s", slug)) @@ -154,4 +198,4 @@ func (c *Client) deleteDashboard(path string) error { } return nil -} \ No newline at end of file +} diff --git a/dashboard_test.go b/dashboard_test.go new file mode 100644 index 0000000..8c9a4ae --- /dev/null +++ b/dashboard_test.go @@ -0,0 +1,162 @@ +package gapi + +import ( + "testing" + + "github.com/gobs/pretty" +) + +const ( + createdAndUpdateDashboardResponse = `{ + "slug": "test", + "id": 1, + "uid": "nErXDvCkzz", + "status": "success", + "version": 1 + }` + + getDashboardResponse = `{ + "dashboard": { + "id": 1, + "uid": "cIBgcSjkk", + "title": "Production Overview", + "version": 0 + }, + "meta": { + "isStarred": false, + "url": "/d/cIBgcSjkk/production-overview", + "slug": "production-overview" + } + }` + + getDashboardsJSON = `[ + { + "id": 1, + "uid": "RGAPB1cZz", + "title": "Grafana Stats", + "uri": "db/grafana-stats", + "url": "/dashboards/d/RGAPB1cZz/grafana-stat", + "slug": "", + "type": "dash-db", + "tags": [], + "isStarred": false + } + ]` +) + +func TestDashboardCreateAndUpdate(t *testing.T) { + server, client := gapiTestTools(200, createdAndUpdateDashboardResponse) + defer server.Close() + + dashboard := Dashboard{ + Model: map[string]interface{}{ + "title": "test", + }, + Folder: 0, + Overwrite: false, + } + + resp, err := client.NewDashboard(dashboard) + if err != nil { + t.Fatal(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Uid != "nErXDvCkzz" { + t.Errorf("Invalid uid - %s, Expected %s", resp.Uid, "nErXDvCkzz") + } + + for _, code := range []int{400, 401, 403, 412} { + server.code = code + _, err = client.NewDashboard(dashboard) + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestDashboardGet(t *testing.T) { + server, client := gapiTestTools(200, getDashboardResponse) + defer server.Close() + + resp, err := client.Dashboard("test") + if err != nil { + t.Error(err) + } + uid, ok := resp.Model["uid"] + if !ok || uid != "cIBgcSjkk" { + t.Errorf("Invalid uid - %s, Expected %s", uid, "cIBgcSjkk") + } + + resp, err = client.DashboardByUID("cIBgcSjkk") + if err != nil { + t.Error(err) + } + uid, ok = resp.Model["uid"] + if !ok || uid != "cIBgcSjkk" { + t.Errorf("Invalid uid - %s, Expected %s", uid, "cIBgcSjkk") + } + + for _, code := range []int{401, 403, 404} { + server.code = code + _, err = client.Dashboard("test") + if err == nil { + t.Errorf("%d not detected", code) + } + + _, err = client.DashboardByUID("cIBgcSjkk") + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestDashboardDelete(t *testing.T) { + server, client := gapiTestTools(200, "") + defer server.Close() + + err := client.DeleteDashboard("test") + if err != nil { + t.Error(err) + } + + err = client.DeleteDashboardByUID("cIBgcSjkk") + if err != nil { + t.Error(err) + } + + for _, code := range []int{401, 403, 404, 412} { + server.code = code + + err = client.DeleteDashboard("test") + if err == nil { + t.Errorf("%d not detected", code) + } + + err = client.DeleteDashboardByUID("cIBgcSjkk") + if err == nil { + t.Errorf("%d not detected", code) + } + } +} + +func TestDashboards(t *testing.T) { + server, client := gapiTestTools(200, getDashboardsJSON) + defer server.Close() + + dashboards, err := client.Dashboards() + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(dashboards)) + + if len(dashboards) != 1 { + t.Error("Length of returned dashboards should be 1") + } + + if dashboards[0].Id != 1 || dashboards[0].Title != "Grafana Stats" { + t.Error("Not correctly parsing returned dashboards.") + } +} \ No newline at end of file diff --git a/datasource.go b/datasource.go index f600db0..8e56506 100644 --- a/datasource.go +++ b/datasource.go @@ -78,6 +78,12 @@ type JSONData struct { // Used by Prometheus HttpMethod string `json:"httpMethod,omitempty"` QueryTimeout string `json:"queryTimeout,omitempty"` + + // Used by Stackdriver + AuthenticationType string `json:"authenticationType,omitempty"` + ClientEmail string `json:"clientEmail,omitempty"` + DefaultProject string `json:"defaultProject,omitempty"` + TokenUri string `json:"tokenUri,omitempty"` } // SecureJSONData is a representation of the datasource `secureJsonData` property @@ -92,6 +98,9 @@ type SecureJSONData struct { // Used by Cloudwatch AccessKey string `json:"accessKey,omitempty"` SecretKey string `json:"secretKey,omitempty"` + + // Used by Stackdriver + PrivateKey string `json:"privateKey,omitempty"` } func (c *Client) NewDataSource(s *DataSource) (int64, error) { diff --git a/datasource_test.go b/datasource_test.go index 4988719..7c3ff7f 100644 --- a/datasource_test.go +++ b/datasource_test.go @@ -1,10 +1,6 @@ package gapi import ( - "fmt" - "net/http" - "net/http/httptest" - "net/url" "testing" "github.com/gobs/pretty" @@ -14,31 +10,6 @@ const ( createdDataSourceJSON = `{"id":1,"message":"Datasource added", "name": "test_datasource"}` ) -func gapiTestTools(code int, body string) (*httptest.Server, *Client) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(code) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, body) - })) - - tr := &http.Transport{ - Proxy: func(req *http.Request) (*url.URL, error) { - return url.Parse(server.URL) - }, - } - - httpClient := &http.Client{Transport: tr} - - url := url.URL{ - Scheme: "http", - Host: "my-grafana.com", - } - - client := &Client{"my-key", url, httpClient} - - return server, client -} - func TestNewDataSource(t *testing.T) { server, client := gapiTestTools(200, createdDataSourceJSON) defer server.Close() diff --git a/folder.go b/folder.go index 55613c6..ffa68ab 100644 --- a/folder.go +++ b/folder.go @@ -63,6 +63,9 @@ func (c *Client) NewFolder(title string) (Folder, error) { "title": title, } data, err := json.Marshal(dataMap) + if err != nil { + return folder, err + } req, err := c.newRequest("POST", "/api/folders", nil, bytes.NewBuffer(data)) if err != nil { return folder, err @@ -91,6 +94,9 @@ func (c *Client) UpdateFolder(id string, name string) error { "name": name, } data, err := json.Marshal(dataMap) + if err != nil { + return err + } req, err := c.newRequest("PUT", fmt.Sprintf("/api/folders/%s", id), nil, bytes.NewBuffer(data)) if err != nil { return err diff --git a/mock.go b/mock.go new file mode 100644 index 0000000..607e27d --- /dev/null +++ b/mock.go @@ -0,0 +1,45 @@ +package gapi + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" +) + +type mockServer struct { + code int + server *httptest.Server +} + +func (m *mockServer) Close() { + m.server.Close() +} + +func gapiTestTools(code int, body string) (*mockServer, *Client) { + mock := &mockServer{ + code: code, + } + + mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(mock.code) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, body) + })) + + tr := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(mock.server.URL) + }, + } + + httpClient := &http.Client{Transport: tr} + + url := url.URL{ + Scheme: "http", + Host: "my-grafana.com", + } + + client := &Client{"my-key", url, httpClient} + return mock, client +} diff --git a/org_users.go b/org_users.go index 69d8e0d..a20eddc 100644 --- a/org_users.go +++ b/org_users.go @@ -46,6 +46,9 @@ func (c *Client) AddOrgUser(orgId int64, user, role string) error { "role": role, } data, err := json.Marshal(dataMap) + if err != nil { + return err + } req, err := c.newRequest("POST", fmt.Sprintf("/api/orgs/%d/users", orgId), nil, bytes.NewBuffer(data)) if err != nil { return err @@ -65,6 +68,9 @@ func (c *Client) UpdateOrgUser(orgId, userId int64, role string) error { "role": role, } data, err := json.Marshal(dataMap) + if err != nil { + return err + } req, err := c.newRequest("PATCH", fmt.Sprintf("/api/orgs/%d/users/%d", orgId, userId), nil, bytes.NewBuffer(data)) if err != nil { return err diff --git a/orgs.go b/orgs.go index 2ef4e2d..4df27ab 100644 --- a/orgs.go +++ b/orgs.go @@ -78,11 +78,15 @@ func (c *Client) Org(id int64) (Org, error) { } func (c *Client) NewOrg(name string) (int64, error) { + id := int64(0) + dataMap := map[string]string{ "name": name, } data, err := json.Marshal(dataMap) - id := int64(0) + if err != nil { + return id, err + } req, err := c.newRequest("POST", "/api/orgs", nil, bytes.NewBuffer(data)) if err != nil { return id, err @@ -114,6 +118,9 @@ func (c *Client) UpdateOrg(id int64, name string) error { "name": name, } data, err := json.Marshal(dataMap) + if err != nil { + return err + } req, err := c.newRequest("PUT", fmt.Sprintf("/api/orgs/%d", id), nil, bytes.NewBuffer(data)) if err != nil { return err diff --git a/playlist.go b/playlist.go new file mode 100644 index 0000000..e8a3b44 --- /dev/null +++ b/playlist.go @@ -0,0 +1,134 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type PlaylistItem struct { + Type string `json:"type"` + Value string `json:"value"` + Order int `json:"order"` + Title string `json:"title"` +} + +type Playlist struct { + Id int `json:"id"` + Name string `json:"name"` + Interval string `json:"interval"` + Items []PlaylistItem `json:"items"` +} + +func (c *Client) Playlist(id int) (*Playlist, error) { + path := fmt.Sprintf("/api/playlists/%d", id) + req, err := c.newRequest("GET", path, nil, nil) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + playlist := &Playlist{} + + err = json.Unmarshal(data, &playlist) + if err != nil { + return nil, err + } + + return playlist, nil +} + +func (c *Client) NewPlaylist(playlist Playlist) (int, error) { + data, err := json.Marshal(playlist) + if err != nil { + return 0, err + } + + req, err := c.newRequest("POST", "/api/playlists", nil, bytes.NewBuffer(data)) + if err != nil { + return 0, err + } + + resp, err := c.Do(req) + if err != nil { + return 0, err + } + + if resp.StatusCode != 200 { + return 0, errors.New(resp.Status) + } + + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + result := struct { + Id int + }{} + + err = json.Unmarshal(data, &result) + if err != nil { + return 0, err + } + + return result.Id, nil +} + +func (c *Client) UpdatePlaylist(playlist Playlist) error { + path := fmt.Sprintf("/api/playlists/%d", playlist.Id) + data, err := json.Marshal(playlist) + if err != nil { + return err + } + + req, err := c.newRequest("PUT", path, nil, bytes.NewBuffer(data)) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} + +func (c *Client) DeletePlaylist(id int) error { + path := fmt.Sprintf("/api/playlists/%d", id) + req, err := c.newRequest("DELETE", path, nil, nil) + if err != nil { + return err + } + + resp, err := c.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New(resp.Status) + } + + return nil +} diff --git a/playlist_test.go b/playlist_test.go new file mode 100644 index 0000000..89f1d61 --- /dev/null +++ b/playlist_test.go @@ -0,0 +1,102 @@ +package gapi + +import ( + "testing" +) + +const ( + createAndUpdatePlaylistResponse = ` { + "id": 1, + "name": "my playlist", + "interval": "5m" + }` + + getPlaylistResponse = `{ + "id" : 2, + "name": "my playlist", + "interval": "5m", + "orgId": "my org", + "items": [ + { + "id": 1, + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my dasboard" + }, + { + "id": 1, + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my dasboard" + } + ] + }` +) + +func TestPlaylistCreateAndUpdate(t *testing.T) { + server, client := gapiTestTools(200, createAndUpdatePlaylistResponse) + defer server.Close() + + playlist := Playlist{ + Name: "my playlist", + Interval: "5m", + Items: []PlaylistItem{ + PlaylistItem{}, + }, + } + + // create + id, err := client.NewPlaylist(playlist) + if err != nil { + t.Fatal(err) + } + + if id != 1 { + t.Errorf("Invalid id - %d, Expected %d", id, 1) + } + + // update + playlist.Items = append(playlist.Items, PlaylistItem{ + Type: "dashboard_by_id", + Value: "1", + Order: 1, + Title: "my dashboard", + }) + + err = client.UpdatePlaylist(playlist) + if err != nil { + t.Fatal(err) + } +} + +func TestGetPlaylist(t *testing.T) { + server, client := gapiTestTools(200, getPlaylistResponse) + defer server.Close() + + playlist, err := client.Playlist(1) + if err != nil { + t.Error(err) + } + + if playlist.Id != 2 { + t.Errorf("Invalid id - %d, Expected %d", playlist.Id, 1) + } + + if len(playlist.Items) != 2 { + t.Errorf("Invalid len(items) - %d, Expected %d", len(playlist.Items), 2) + } +} + +func TestDeletePlaylist(t *testing.T) { + server, client := gapiTestTools(200, "") + defer server.Close() + + err := client.DeletePlaylist(1) + if err != nil { + t.Error(err) + } +}