diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0c8ea5c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..c6d8ecf1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem?** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index daf913b1..b98422d4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ _testmain.go *.exe *.test *.prof + +# vim swap files +.*.sw? diff --git a/README.md b/README.md index 859bd902..581af01d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # grafana-api-golang-client + Grafana HTTP API Client for Go + +## Tests + +To run the tests: + +``` +go test +``` diff --git a/admin.go b/admin.go index 3da1ae92..1caf0c3c 100644 --- a/admin.go +++ b/admin.go @@ -6,32 +6,38 @@ import ( "errors" "fmt" "io/ioutil" - - "github.com/grafana/grafana/pkg/api/dtos" ) -func (c *Client) CreateUserForm(settings dtos.AdminCreateUserForm) error { - data, err := json.Marshal(settings) - req, err := c.newRequest("POST", "/api/admin/users", bytes.NewBuffer(data)) +func (c *Client) CreateUser(user User) (int64, error) { + id := int64(0) + data, err := json.Marshal(user) + req, err := c.newRequest("POST", "/api/admin/users", nil, bytes.NewBuffer(data)) if err != nil { - return err + return id, err } resp, err := c.Do(req) if err != nil { - return err + return id, err + } + if resp.StatusCode != 200 { + return id, errors.New(resp.Status) } data, err = ioutil.ReadAll(resp.Body) if err != nil { - return err + return id, err } - if resp.StatusCode != 200 { - return errors.New(resp.Status) + created := struct { + Id int64 `json:"id"` + }{} + err = json.Unmarshal(data, &created) + if err != nil { + return id, err } - return err + return created.Id, err } func (c *Client) DeleteUser(id int64) error { - req, err := c.newRequest("DELETE", fmt.Sprintf("/api/admin/users/%d", id), nil) + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/admin/users/%d", id), nil, nil) if err != nil { return err } diff --git a/admin_test.go b/admin_test.go new file mode 100644 index 00000000..5a836b81 --- /dev/null +++ b/admin_test.go @@ -0,0 +1,39 @@ +package gapi + +import ( + "testing" +) + +const ( + createUserJSON = `{"id":1,"message":"User created"}` + deleteUserJSON = `{"message":"User deleted"}` +) + +func TestCreateUser(t *testing.T) { + server, client := gapiTestTools(200, createUserJSON) + defer server.Close() + user := User{ + Email: "admin@localhost", + Login: "admin", + Name: "Administrator", + Password: "password", + } + resp, err := client.CreateUser(user) + if err != nil { + t.Error(err) + } + + if resp != 1 { + t.Error("Not correctly parsing returned user message.") + } +} + +func TestDeleteUser(t *testing.T) { + server, client := gapiTestTools(200, deleteUserJSON) + defer server.Close() + + err := client.DeleteUser(int64(1)) + if err != nil { + t.Error(err) + } +} diff --git a/alertnotification.go b/alertnotification.go new file mode 100644 index 00000000..ce9ccb36 --- /dev/null +++ b/alertnotification.go @@ -0,0 +1,140 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type AlertNotification struct { + Id int64 `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + DisableResolveMessage bool `json:"disableResolveMessage"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Settings interface{} `json:"settings"` +} + +func (c *Client) AlertNotifications() ([]AlertNotification, error) { + alertnotifications := make([]AlertNotification, 0) + + req, err := c.newRequest("GET", "/api/alert-notifications/", 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 + } + + err = json.Unmarshal(data, &alertnotifications) + return alertnotifications, err +} + +func (c *Client) AlertNotification(id int64) (*AlertNotification, error) { + path := fmt.Sprintf("/api/alert-notifications/%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 + } + + result := &AlertNotification{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) NewAlertNotification(a *AlertNotification) (int64, error) { + data, err := json.Marshal(a) + if err != nil { + return 0, err + } + req, err := c.newRequest("POST", "/api/alert-notifications", 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 int64 `json:"id"` + }{} + err = json.Unmarshal(data, &result) + return result.Id, err +} + +func (c *Client) UpdateAlertNotification(a *AlertNotification) error { + path := fmt.Sprintf("/api/alert-notifications/%d", a.Id) + data, err := json.Marshal(a) + 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) DeleteAlertNotification(id int64) error { + path := fmt.Sprintf("/api/alert-notifications/%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/alertnotification_test.go b/alertnotification_test.go new file mode 100644 index 00000000..05763546 --- /dev/null +++ b/alertnotification_test.go @@ -0,0 +1,171 @@ +package gapi + +import ( + "github.com/gobs/pretty" + "testing" +) + +const ( + getAlertNotificationsJSON = ` +[ + { + "id": 1, + "uid": "team-a-email-notifier", + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": false, + "disableResolveMessage": false, + "settings": { + "addresses": "dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" + } +] + ` + getAlertNotificationJSON = ` +{ + "id": 1, + "uid": "team-a-email-notifier", + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": false, + "disableResolveMessage": false, + "settings": { + "addresses": "dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" +} +` + createdAlertNotificationJSON = ` +{ + "id": 1, + "uid": "new-alert-notification", + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": true, + "frequency": "15m", + "settings": { + "addresses": "dev@grafana.com" + } +} +` + updatedAlertNotificationJSON = ` +{ + "uid": "new-alert-notification", + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": true, + "frequency": "15m", + "settings": { + "addresses": "dev@grafana.com" + } +} +` + deletedAlertNotificationJSON = ` +{ + "message":"Notification deleted" +} +` +) + +func TestAlertNotifications(t *testing.T) { + server, client := gapiTestTools(200, getAlertNotificationsJSON) + defer server.Close() + + alertnotifications, err := client.AlertNotifications() + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(alertnotifications)) + + if len(alertnotifications) != 1 { + t.Error("Length of returned alert notifications should be 1") + } + if alertnotifications[0].Id != 1 || alertnotifications[0].Name != "Team A" { + t.Error("Not correctly parsing returned alert notifications.") + } +} + +func TestAlertNotification(t *testing.T) { + server, client := gapiTestTools(200, getAlertNotificationJSON) + defer server.Close() + + alertnotification := int64(1) + resp, err := client.AlertNotification(alertnotification) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Id != alertnotification || resp.Name != "Team A" { + t.Error("Not correctly parsing returned alert notification.") + } +} + +func TestNewAlertNotification(t *testing.T) { + server, client := gapiTestTools(200, createdAlertNotificationJSON) + defer server.Close() + + an := &AlertNotification{ + Name: "Team A", + Type: "email", + IsDefault: false, + DisableResolveMessage: true, + SendReminder: true, + Frequency: "15m", + Settings: map[string]string{ + "addresses": "dev@grafana.com", + }, + } + resp, err := client.NewAlertNotification(an) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp != 1 { + t.Error("Not correctly parsing returned creation message.") + } +} + +func TestUpdateAlertNotification(t *testing.T) { + server, client := gapiTestTools(200, updatedAlertNotificationJSON) + defer server.Close() + + an := &AlertNotification{ + Id: 1, + Name: "Team A", + Type: "email", + IsDefault: false, + DisableResolveMessage: true, + SendReminder: true, + Frequency: "15m", + Settings: map[string]string{ + "addresses": "dev@grafana.com", + }, + } + + err := client.UpdateAlertNotification(an) + if err != nil { + t.Error(err) + } +} + +func TestDeleteAlertNotification(t *testing.T) { + server, client := gapiTestTools(200, deletedAlertNotificationJSON) + defer server.Close() + + err := client.DeleteAlertNotification(1) + if err != nil { + t.Error(err) + } +} diff --git a/client.go b/client.go index 1e4846cc..d2d12669 100644 --- a/client.go +++ b/client.go @@ -4,8 +4,11 @@ import ( "bytes" "fmt" "io" + "log" "net/http" "net/url" + "os" + "path" "strings" ) @@ -36,9 +39,10 @@ func New(auth, baseURL string) (*Client, error) { }, nil } -func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) { +func (c *Client) newRequest(method, requestPath string, query url.Values, body io.Reader) (*http.Request, error) { url := c.baseURL - url.Path = path + url.Path = path.Join(url.Path, requestPath) + url.RawQuery = query.Encode() req, err := http.NewRequest(method, url.String(), body) if err != nil { return req, err @@ -46,11 +50,15 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, if c.key != "" { req.Header.Add("Authorization", c.key) } - if body == nil { - fmt.Println("request to ", url.String(), "with no body data") - } else { - fmt.Println("request to ", url.String(), "with body data", body.(*bytes.Buffer).String()) + + if os.Getenv("GF_LOG") != "" { + if body == nil { + log.Printf("request (%s) to %s with no body data", method, url.String()) + } else { + log.Printf("request (%s) to %s with body data: %s", method, url.String(), body.(*bytes.Buffer).String()) + } } + req.Header.Add("Content-Type", "application/json") return req, err } diff --git a/dashboard.go b/dashboard.go new file mode 100644 index 00000000..2d6418f1 --- /dev/null +++ b/dashboard.go @@ -0,0 +1,141 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "os" +) + +type DashboardMeta struct { + IsStarred bool `json:"isStarred"` + Slug string `json:"slug"` + Folder int64 `json:"folderId"` +} + +type DashboardSaveResponse struct { + Slug string `json:"slug"` + Id int64 `json:"id"` + Uid string `json:"uid"` + Status string `json:"status"` + Version int64 `json:"version"` +} + +type Dashboard struct { + Meta DashboardMeta `json:"meta"` + Model map[string]interface{} `json:"dashboard"` + Folder int64 `json:"folderId"` + Overwrite bool `json:overwrite` +} + +// Deprecated: use NewDashboard instead +func (c *Client) SaveDashboard(model map[string]interface{}, overwrite bool) (*DashboardSaveResponse, error) { + wrapper := map[string]interface{}{ + "dashboard": model, + "overwrite": overwrite, + } + data, err := json.Marshal(wrapper) + if err != nil { + return nil, err + } + req, err := c.newRequest("POST", "/api/dashboards/db", nil, bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + data, _ = ioutil.ReadAll(resp.Body) + return nil, fmt.Errorf("status: %d, body: %s", resp.StatusCode, data) + } + + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &DashboardSaveResponse{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) NewDashboard(dashboard Dashboard) (*DashboardSaveResponse, error) { + data, err := json.Marshal(dashboard) + if err != nil { + return nil, err + } + req, err := c.newRequest("POST", "/api/dashboards/db", nil, bytes.NewBuffer(data)) + 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 + } + + result := &DashboardSaveResponse{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) Dashboard(slug string) (*Dashboard, error) { + path := fmt.Sprintf("/api/dashboards/db/%s", slug) + 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 + } + + result := &Dashboard{} + err = json.Unmarshal(data, &result) + result.Folder = result.Meta.Folder + if os.Getenv("GF_LOG") != "" { + log.Printf("got back dashboard response %s", data) + } + return result, err +} + +func (c *Client) DeleteDashboard(slug string) error { + path := fmt.Sprintf("/api/dashboards/db/%s", slug) + 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/datasource.go b/datasource.go new file mode 100644 index 00000000..a5b40dea --- /dev/null +++ b/datasource.go @@ -0,0 +1,141 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type DataSource struct { + Id int64 `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + URL string `json:"url"` + Access string `json:"access"` + + Database string `json:"database,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + + OrgId int64 `json:"orgId,omitempty"` + IsDefault bool `json:"isDefault"` + + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser,omitempty"` + BasicAuthPassword string `json:"basicAuthPassword,omitempty"` + + JSONData JSONData `json:"jsonData,omitempty"` + SecureJSONData SecureJSONData `json:"secureJsonData,omitempty"` +} + +// JSONData is a representation of the datasource `jsonData` property +type JSONData struct { + AssumeRoleArn string `json:"assumeRoleArn,omitempty"` + AuthType string `json:"authType,omitempty"` + CustomMetricsNamespaces string `json:"customMetricsNamespaces,omitempty"` + DefaultRegion string `json:"defaultRegion,omitempty"` + TlsSkipVerify bool `json:"tlsSkipVerify,omitempty"` +} + +// SecureJSONData is a representation of the datasource `secureJsonData` property +type SecureJSONData struct { + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` +} + +func (c *Client) NewDataSource(s *DataSource) (int64, error) { + data, err := json.Marshal(s) + if err != nil { + return 0, err + } + req, err := c.newRequest("POST", "/api/datasources", 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 int64 `json:"id"` + }{} + err = json.Unmarshal(data, &result) + return result.Id, err +} + +func (c *Client) UpdateDataSource(s *DataSource) error { + path := fmt.Sprintf("/api/datasources/%d", s.Id) + data, err := json.Marshal(s) + 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) DataSource(id int64) (*DataSource, error) { + path := fmt.Sprintf("/api/datasources/%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 + } + + result := &DataSource{} + err = json.Unmarshal(data, &result) + return result, err +} + +func (c *Client) DeleteDataSource(id int64) error { + path := fmt.Sprintf("/api/datasources/%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/datasource_test.go b/datasource_test.go new file mode 100644 index 00000000..c2ee1b79 --- /dev/null +++ b/datasource_test.go @@ -0,0 +1,75 @@ +package gapi + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gobs/pretty" +) + +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() + + ds := &DataSource{ + Name: "foo", + Type: "cloudwatch", + URL: "http://some-url.com", + Access: "access", + IsDefault: true, + JSONData: JSONData{ + AssumeRoleArn: "arn:aws:iam::123:role/some-role", + AuthType: "keys", + CustomMetricsNamespaces: "SomeNamespace", + DefaultRegion: "us-east-1", + TlsSkipVerify: true, + }, + SecureJSONData: SecureJSONData{ + AccessKey: "123", + SecretKey: "456", + }, + } + + created, err := client.NewDataSource(ds) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(created)) + + if created != 1 { + t.Error("datasource creation response should return the created datasource ID") + } +} diff --git a/folder.go b/folder.go new file mode 100644 index 00000000..55613c60 --- /dev/null +++ b/folder.go @@ -0,0 +1,121 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type Folder struct { + Id int64 `json:"id"` + Uid string `json:"uid"` + Title string `json:"title"` +} + +func (c *Client) Folders() ([]Folder, error) { + folders := make([]Folder, 0) + + req, err := c.newRequest("GET", "/api/folders/", nil, nil) + if err != nil { + return folders, err + } + resp, err := c.Do(req) + if err != nil { + return folders, err + } + if resp.StatusCode != 200 { + return folders, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return folders, err + } + err = json.Unmarshal(data, &folders) + return folders, err +} + +func (c *Client) Folder(id int64) (*Folder, error) { + folder := &Folder{} + req, err := c.newRequest("GET", fmt.Sprintf("/api/folders/id/%d", id), nil, nil) + if err != nil { + return folder, err + } + resp, err := c.Do(req) + if err != nil { + return folder, err + } + if resp.StatusCode != 200 { + return folder, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return folder, err + } + err = json.Unmarshal(data, &folder) + return folder, err +} + +func (c *Client) NewFolder(title string) (Folder, error) { + folder := Folder{} + dataMap := map[string]string{ + "title": title, + } + data, err := json.Marshal(dataMap) + req, err := c.newRequest("POST", "/api/folders", nil, bytes.NewBuffer(data)) + if err != nil { + return folder, err + } + resp, err := c.Do(req) + if err != nil { + return folder, err + } + if resp.StatusCode != 200 { + data, _ = ioutil.ReadAll(resp.Body) + return folder, fmt.Errorf("status: %s body: %s", resp.Status, data) + } + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return folder, err + } + err = json.Unmarshal(data, &folder) + if err != nil { + return folder, err + } + return folder, err +} + +func (c *Client) UpdateFolder(id string, name string) error { + dataMap := map[string]string{ + "name": name, + } + data, err := json.Marshal(dataMap) + req, err := c.newRequest("PUT", fmt.Sprintf("/api/folders/%s", id), 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 err +} + +func (c *Client) DeleteFolder(id string) error { + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/folders/%s", id), 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 err +} diff --git a/folder_test.go b/folder_test.go new file mode 100644 index 00000000..f02f313d --- /dev/null +++ b/folder_test.go @@ -0,0 +1,156 @@ +package gapi + +import ( + "github.com/gobs/pretty" + "testing" +) + +const ( + getFoldersJSON = ` +[ + { + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 + } +] + ` + getFolderJSON = ` +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +` + createdFolderJSON = ` +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet ABC", + "url": "/dashboards/f/nErXDvCkzz/department-abc", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +` + updatedFolderJSON = ` +{ + "id":1, + "uid": "nErXDvCkzz", + "title": "Departmenet DEF", + "url": "/dashboards/f/nErXDvCkzz/department-def", + "hasAcl": false, + "canSave": true, + "canEdit": true, + "canAdmin": true, + "createdBy": "admin", + "created": "2018-01-31T17:43:12+01:00", + "updatedBy": "admin", + "updated": "2018-01-31T17:43:12+01:00", + "version": 1 +} +` + deletedFolderJSON = ` +{ + "message":"Folder deleted" +} +` +) + +func TestFolders(t *testing.T) { + server, client := gapiTestTools(200, getFoldersJSON) + defer server.Close() + + folders, err := client.Folders() + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(folders)) + + if len(folders) != 1 { + t.Error("Length of returned folders should be 1") + } + if folders[0].Id != 1 || folders[0].Title != "Departmenet ABC" { + t.Error("Not correctly parsing returned folders.") + } +} + +func TestFolder(t *testing.T) { + server, client := gapiTestTools(200, getFolderJSON) + defer server.Close() + + folder := int64(1) + resp, err := client.Folder(folder) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Id != folder || resp.Title != "Departmenet ABC" { + t.Error("Not correctly parsing returned folder.") + } +} + +func TestNewFolder(t *testing.T) { + server, client := gapiTestTools(200, createdFolderJSON) + defer server.Close() + + resp, err := client.NewFolder("test-folder") + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Uid != "nErXDvCkzz" { + t.Error("Not correctly parsing returned creation message.") + } +} + +func TestUpdateFolder(t *testing.T) { + server, client := gapiTestTools(200, updatedFolderJSON) + defer server.Close() + + err := client.UpdateFolder("nErXDvCkzz", "test-folder") + if err != nil { + t.Error(err) + } +} + +func TestDeleteFolder(t *testing.T) { + server, client := gapiTestTools(200, deletedFolderJSON) + defer server.Close() + + err := client.DeleteFolder("nErXDvCkzz") + if err != nil { + t.Error(err) + } +} diff --git a/org_users.go b/org_users.go new file mode 100644 index 00000000..69d8e0d4 --- /dev/null +++ b/org_users.go @@ -0,0 +1,95 @@ +package gapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" +) + +type OrgUser struct { + OrgId int64 `json:"orgId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Login string `json:"login"` + Role string `json:"role"` +} + +func (c *Client) OrgUsers(orgId int64) ([]OrgUser, error) { + users := make([]OrgUser, 0) + req, err := c.newRequest("GET", fmt.Sprintf("/api/orgs/%d/users", orgId), nil, nil) + if err != nil { + return users, err + } + resp, err := c.Do(req) + if err != nil { + return users, err + } + if resp.StatusCode != 200 { + return users, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return users, err + } + err = json.Unmarshal(data, &users) + if err != nil { + return users, err + } + return users, err +} + +func (c *Client) AddOrgUser(orgId int64, user, role string) error { + dataMap := map[string]string{ + "loginOrEmail": user, + "role": role, + } + data, err := json.Marshal(dataMap) + req, err := c.newRequest("POST", fmt.Sprintf("/api/orgs/%d/users", orgId), 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 err +} + +func (c *Client) UpdateOrgUser(orgId, userId int64, role string) error { + dataMap := map[string]string{ + "role": role, + } + data, err := json.Marshal(dataMap) + req, err := c.newRequest("PATCH", fmt.Sprintf("/api/orgs/%d/users/%d", orgId, userId), 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 err +} + +func (c *Client) RemoveOrgUser(orgId, userId int64) error { + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/orgs/%d/users/%d", orgId, userId), 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 err +} diff --git a/org_users_test.go b/org_users_test.go new file mode 100644 index 00000000..b03a9c2a --- /dev/null +++ b/org_users_test.go @@ -0,0 +1,74 @@ +package gapi + +import ( + "github.com/gobs/pretty" + "testing" +) + +const ( + getOrgUsersJSON = `[{"orgId":1,"userId":1,"email":"admin@localhost","avatarUrl":"/avatar/46d229b033af06a191ff2267bca9ae56","login":"admin","role":"Admin","lastSeenAt":"2018-06-28T14:16:11Z","lastSeenAtAge":"\u003c 1m"}]` + addOrgUserJSON = `{"message":"User added to organization"}` + updateOrgUserJSON = `{"message":"Organization user updated"}` + removeOrgUserJSON = `{"message":"User removed from organization"}` +) + +func TestOrgUsers(t *testing.T) { + server, client := gapiTestTools(200, getOrgUsersJSON) + defer server.Close() + + org := int64(1) + resp, err := client.OrgUsers(org) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + user := OrgUser{ + OrgId: 1, + UserId: 1, + Email: "admin@localhost", + Login: "admin", + Role: "Admin", + } + + if resp[0] != user { + t.Error("Not correctly parsing returned organization users.") + } +} + +func TestAddOrgUser(t *testing.T) { + server, client := gapiTestTools(200, addOrgUserJSON) + defer server.Close() + + orgId, user, role := int64(1), "admin@localhost", "Admin" + + err := client.AddOrgUser(orgId, user, role) + if err != nil { + t.Error(err) + } +} + +func TestUpdateOrgUser(t *testing.T) { + server, client := gapiTestTools(200, updateOrgUserJSON) + defer server.Close() + + orgId, userId, role := int64(1), int64(1), "Editor" + + err := client.UpdateOrgUser(orgId, userId, role) + if err != nil { + t.Error(err) + } +} + +func TestRemoveOrgUser(t *testing.T) { + server, client := gapiTestTools(200, removeOrgUserJSON) + defer server.Close() + + orgId, userId := int64(1), int64(1) + + err := client.RemoveOrgUser(orgId, userId) + if err != nil { + t.Error(err) + } +} diff --git a/orgs.go b/orgs.go index 5a38732c..2ef4e2d5 100644 --- a/orgs.go +++ b/orgs.go @@ -9,14 +9,14 @@ import ( ) type Org struct { - Id int64 - Name string + Id int64 `json:"id"` + Name string `json:"name"` } func (c *Client) Orgs() ([]Org, error) { orgs := make([]Org, 0) - req, err := c.newRequest("GET", "/api/orgs/", nil) + req, err := c.newRequest("GET", "/api/orgs/", nil, nil) if err != nil { return orgs, err } @@ -35,12 +35,86 @@ func (c *Client) Orgs() ([]Org, error) { return orgs, err } -func (c *Client) NewOrg(name string) error { - settings := map[string]string{ +func (c *Client) OrgByName(name string) (Org, error) { + org := Org{} + req, err := c.newRequest("GET", fmt.Sprintf("/api/orgs/name/%s", name), nil, nil) + if err != nil { + return org, err + } + resp, err := c.Do(req) + if err != nil { + return org, err + } + if resp.StatusCode != 200 { + return org, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return org, err + } + err = json.Unmarshal(data, &org) + return org, err +} + +func (c *Client) Org(id int64) (Org, error) { + org := Org{} + req, err := c.newRequest("GET", fmt.Sprintf("/api/orgs/%d", id), nil, nil) + if err != nil { + return org, err + } + resp, err := c.Do(req) + if err != nil { + return org, err + } + if resp.StatusCode != 200 { + return org, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return org, err + } + err = json.Unmarshal(data, &org) + return org, err +} + +func (c *Client) NewOrg(name string) (int64, error) { + dataMap := map[string]string{ + "name": name, + } + data, err := json.Marshal(dataMap) + id := int64(0) + req, err := c.newRequest("POST", "/api/orgs", nil, bytes.NewBuffer(data)) + if err != nil { + return id, err + } + resp, err := c.Do(req) + if err != nil { + return id, err + } + if resp.StatusCode != 200 { + return id, errors.New(resp.Status) + } + data, err = ioutil.ReadAll(resp.Body) + if err != nil { + return id, err + } + tmp := struct { + Id int64 `json:"orgId"` + }{} + err = json.Unmarshal(data, &tmp) + if err != nil { + return id, err + } + id = tmp.Id + return id, err +} + +func (c *Client) UpdateOrg(id int64, name string) error { + dataMap := map[string]string{ "name": name, } - data, err := json.Marshal(settings) - req, err := c.newRequest("POST", "/api/orgs", bytes.NewBuffer(data)) + data, err := json.Marshal(dataMap) + req, err := c.newRequest("PUT", fmt.Sprintf("/api/orgs/%d", id), nil, bytes.NewBuffer(data)) if err != nil { return err } @@ -55,7 +129,7 @@ func (c *Client) NewOrg(name string) error { } func (c *Client) DeleteOrg(id int64) error { - req, err := c.newRequest("DELETE", fmt.Sprintf("/api/orgs/%d", id), nil) + req, err := c.newRequest("DELETE", fmt.Sprintf("/api/orgs/%d", id), nil, nil) if err != nil { return err } diff --git a/orgs_test.go b/orgs_test.go new file mode 100644 index 00000000..ca81ab43 --- /dev/null +++ b/orgs_test.go @@ -0,0 +1,103 @@ +package gapi + +import ( + "github.com/gobs/pretty" + "testing" +) + +const ( + getOrgsJSON = `[{"id":1,"name":"Main Org."},{"id":2,"name":"Test Org."}]` + getOrgJSON = `{"id":1,"name":"Main Org.","address":{"address1":"","address2":"","city":"","zipCode":"","state":"","country":""}}` + createdOrgJSON = `{"message":"Organization created","orgId":1}` + updatedOrgJSON = `{"message":"Organization updated"}` + deletedOrgJSON = `{"message":"Organization deleted"}` +) + +func TestOrgs(t *testing.T) { + server, client := gapiTestTools(200, getOrgsJSON) + defer server.Close() + + orgs, err := client.Orgs() + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(orgs)) + + if len(orgs) != 2 { + t.Error("Length of returned orgs should be 2") + } + if orgs[0].Id != 1 || orgs[0].Name != "Main Org." { + t.Error("Not correctly parsing returned organizations.") + } +} + +func TestOrgByName(t *testing.T) { + server, client := gapiTestTools(200, getOrgJSON) + defer server.Close() + + org := "Main Org." + resp, err := client.OrgByName(org) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Id != 1 || resp.Name != org { + t.Error("Not correctly parsing returned organization.") + } +} + +func TestOrg(t *testing.T) { + server, client := gapiTestTools(200, getOrgJSON) + defer server.Close() + + org := int64(1) + resp, err := client.Org(org) + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp.Id != org || resp.Name != "Main Org." { + t.Error("Not correctly parsing returned organization.") + } +} + +func TestNewOrg(t *testing.T) { + server, client := gapiTestTools(200, createdOrgJSON) + defer server.Close() + + resp, err := client.NewOrg("test-org") + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + if resp != 1 { + t.Error("Not correctly parsing returned creation message.") + } +} + +func TestUpdateOrg(t *testing.T) { + server, client := gapiTestTools(200, updatedOrgJSON) + defer server.Close() + + err := client.UpdateOrg(int64(1), "test-org") + if err != nil { + t.Error(err) + } +} + +func TestDeleteOrg(t *testing.T) { + server, client := gapiTestTools(200, deletedOrgJSON) + defer server.Close() + + err := client.DeleteOrg(int64(1)) + if err != nil { + t.Error(err) + } +} diff --git a/user.go b/user.go index 13a6e6ff..66f56349 100644 --- a/user.go +++ b/user.go @@ -4,19 +4,21 @@ import ( "encoding/json" "errors" "io/ioutil" + "net/url" ) type User struct { - Id int64 - Email string - Name string - Login string - IsAdmin bool + Id int64 `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login,omitempty"` + Password string `json:"password,omitempty"` + IsAdmin bool `json:"isAdmin,omitempty"` } func (c *Client) Users() ([]User, error) { users := make([]User, 0) - req, err := c.newRequest("GET", "/api/users", nil) + req, err := c.newRequest("GET", "/api/users", nil, nil) if err != nil { return users, err } @@ -37,3 +39,38 @@ func (c *Client) Users() ([]User, error) { } return users, err } + +func (c *Client) UserByEmail(email string) (User, error) { + user := User{} + query := url.Values{} + query.Add("loginOrEmail", email) + req, err := c.newRequest("GET", "/api/users/lookup", query, nil) + if err != nil { + return user, err + } + resp, err := c.Do(req) + if err != nil { + return user, err + } + if resp.StatusCode != 200 { + return user, errors.New(resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return user, err + } + tmp := struct { + Id int64 `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login,omitempty"` + Password string `json:"password,omitempty"` + IsAdmin bool `json:"isGrafanaAdmin,omitempty"` + }{} + err = json.Unmarshal(data, &tmp) + if err != nil { + return user, err + } + user = User(tmp) + return user, err +} diff --git a/user_test.go b/user_test.go new file mode 100644 index 00000000..dee17397 --- /dev/null +++ b/user_test.go @@ -0,0 +1,58 @@ +package gapi + +import ( + "github.com/gobs/pretty" + "testing" +) + +const ( + getUsersJSON = `[{"id":1,"name":"","login":"admin","email":"admin@localhost","avatarUrl":"/avatar/46d229b033af06a191ff2267bca9ae56","isAdmin":true,"lastSeenAt":"2018-06-28T14:42:24Z","lastSeenAtAge":"\u003c 1m"}]` + getUserByEmailJSON = `{"id":1,"email":"admin@localhost","name":"","login":"admin","theme":"","orgId":1,"isGrafanaAdmin":true}` +) + +func TestUsers(t *testing.T) { + server, client := gapiTestTools(200, getUsersJSON) + defer server.Close() + + resp, err := client.Users() + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + user := User{ + Id: 1, + Email: "admin@localhost", + Name: "", + Login: "admin", + IsAdmin: true, + } + + if len(resp) != 1 || resp[0] != user { + t.Error("Not correctly parsing returned users.") + } +} + +func TestUserByEmail(t *testing.T) { + server, client := gapiTestTools(200, getUserByEmailJSON) + defer server.Close() + + resp, err := client.UserByEmail("admin@localhost") + if err != nil { + t.Error(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + user := User{ + Id: 1, + Email: "admin@localhost", + Name: "", + Login: "admin", + IsAdmin: true, + } + if resp != user { + t.Error("Not correctly parsing returned user.") + } +}