Skip to content

Commit

Permalink
Return HTTP response codes as typed errors
Browse files Browse the repository at this point in the history
This allows to differentiate a datasource not found from other errors, e.g.

```go
_, err = client.GetDatasource(ctx, dsID)

if errors.Is(err, sdk.ErrNotFound) {
    fmt.Fprintf(os.Stderr, "Creating new datasource %s (id=%d)\n", ds.Name, ds.ID)
}
```
  • Loading branch information
suhlig committed Aug 15, 2021
1 parent 9de4d14 commit 4f854eb
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 92 deletions.
17 changes: 3 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ It was made foremost for
later separated from it and moved to this new repository because the
library is useful per se.

The library requires at least Go 1.13.

## Library design principles

1. SDK offers client functionality so it covers Grafana REST API with
Expand Down Expand Up @@ -102,20 +104,7 @@ datasources. State of support for misc API parts noted below.
| Frontend settings | - |
| Admin | partially |

There is no exact roadmap. The integration tests are being run against the
following Grafana versions:

* [6.7.1](./travis.yml)
* [6.6.2](/.travis.yml)
* [6.5.3](/.travis.yml)
* [6.4.5](/.travis.yml)

With the following Go versions:

* 1.14.x
* 1.13.x
* 1.12.x
* 1.11.x
The integration tests are being run for the Grafana and Go versions listed in [`.github/workflows/go.yml`](.github/workflows/go.yml).

I still have interest to this library development but not always have
time for it. So I gladly accept new contributions. Drop an issue or
Expand Down
19 changes: 16 additions & 3 deletions rest-admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
)

// CreateUser creates a new global user.
Expand All @@ -14,13 +15,17 @@ func (r *Client) CreateUser(ctx context.Context, user User) (StatusMessage, erro
raw []byte
resp StatusMessage
err error
code int
)
if raw, err = json.Marshal(user); err != nil {
return StatusMessage{}, err
}
if raw, _, err = r.post(ctx, "api/admin/users", nil, raw); err != nil {
if raw, code, err = r.post(ctx, "api/admin/users", nil, raw); err != nil {
return StatusMessage{}, err
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, err
}
Expand All @@ -35,13 +40,17 @@ func (r *Client) UpdateUserPermissions(ctx context.Context, permissions UserPerm
raw []byte
reply StatusMessage
err error
code int
)
if raw, err = json.Marshal(permissions); err != nil {
return StatusMessage{}, err
}
if raw, _, err = r.put(ctx, fmt.Sprintf("api/admin/users/%d/permissions", uid), nil, raw); err != nil {
if raw, code, err = r.put(ctx, fmt.Sprintf("api/admin/users/%d/permissions", uid), nil, raw); err != nil {
return StatusMessage{}, err
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &reply)
return reply, err
}
Expand All @@ -54,11 +63,15 @@ func (r *Client) SwitchUserContext(ctx context.Context, uid uint, oid uint) (Sta
raw []byte
resp StatusMessage
err error
code int
)

if raw, _, err = r.post(ctx, fmt.Sprintf("/api/users/%d/using/%d", uid, oid), nil, raw); err != nil {
if raw, code, err = r.post(ctx, fmt.Sprintf("/api/users/%d/using/%d", uid, oid), nil, raw); err != nil {
return StatusMessage{}, err
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, err
}
Expand Down
17 changes: 9 additions & 8 deletions rest-alertnotification.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
)

// GetAllAlertNotifications gets all alert notification channels.
Expand All @@ -34,7 +35,7 @@ func (c *Client) GetAllAlertNotifications(ctx context.Context) ([]AlertNotificat
if raw, code, err = c.get(ctx, "api/alert-notifications", nil); err != nil {
return nil, err
}
if code != 200 {
if code != http.StatusOK {
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &an)
Expand All @@ -53,7 +54,7 @@ func (c *Client) GetAlertNotificationUID(ctx context.Context, uid string) (Alert
if raw, code, err = c.get(ctx, fmt.Sprintf("api/alert-notifications/uid/%s", uid), nil); err != nil {
return an, err
}
if code != 200 {
if code != http.StatusOK {
return an, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &an)
Expand All @@ -72,7 +73,7 @@ func (c *Client) GetAlertNotificationID(ctx context.Context, id uint) (AlertNoti
if raw, code, err = c.get(ctx, fmt.Sprintf("api/alert-notifications/%d", id), nil); err != nil {
return an, err
}
if code != 200 {
if code != http.StatusOK {
return an, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &an)
Expand All @@ -93,7 +94,7 @@ func (c *Client) CreateAlertNotification(ctx context.Context, an AlertNotificati
if raw, code, err = c.post(ctx, "api/alert-notifications", nil, raw); err != nil {
return -1, err
}
if code != 200 {
if code != http.StatusOK {
return -1, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
result := struct {
Expand All @@ -117,7 +118,7 @@ func (c *Client) UpdateAlertNotificationUID(ctx context.Context, an AlertNotific
if raw, code, err = c.put(ctx, fmt.Sprintf("api/alert-notifications/uid/%s", uid), nil, raw); err != nil {
return err
}
if code != 200 {
if code != http.StatusOK {
return fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
return nil
Expand All @@ -137,7 +138,7 @@ func (c *Client) UpdateAlertNotificationID(ctx context.Context, an AlertNotifica
if raw, code, err = c.put(ctx, fmt.Sprintf("api/alert-notifications/%d", id), nil, raw); err != nil {
return err
}
if code != 200 {
if code != http.StatusOK {
return fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
return nil
Expand All @@ -154,7 +155,7 @@ func (c *Client) DeleteAlertNotificationUID(ctx context.Context, uid string) err
if raw, code, err = c.delete(ctx, fmt.Sprintf("api/alert-notifications/uid/%s", uid)); err != nil {
return err
}
if code != 200 {
if code != http.StatusOK {
return fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
return nil
Expand All @@ -171,7 +172,7 @@ func (c *Client) DeleteAlertNotificationID(ctx context.Context, id uint) error {
if raw, code, err = c.delete(ctx, fmt.Sprintf("api/alert-notifications/%d", id)); err != nil {
return err
}
if code != 200 {
if code != http.StatusOK {
return fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
return nil
Expand Down
25 changes: 21 additions & 4 deletions rest-annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
Expand All @@ -19,13 +20,17 @@ func (r *Client) CreateAnnotation(ctx context.Context, a CreateAnnotationRequest
raw []byte
resp StatusMessage
err error
code int
)
if raw, err = json.Marshal(a); err != nil {
return StatusMessage{}, errors.Wrap(err, "marshal request")
}
if raw, _, err = r.post(ctx, "api/annotations", nil, raw); err != nil {
if raw, code, err = r.post(ctx, "api/annotations", nil, raw); err != nil {
return StatusMessage{}, errors.Wrap(err, "create annotation")
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, errors.Wrap(err, "unmarshal response message")
}
Expand All @@ -38,13 +43,17 @@ func (r *Client) PatchAnnotation(ctx context.Context, id uint, a PatchAnnotation
raw []byte
resp StatusMessage
err error
code int
)
if raw, err = json.Marshal(a); err != nil {
return StatusMessage{}, errors.Wrap(err, "marshal request")
}
if raw, _, err = r.patch(ctx, fmt.Sprintf("api/annotations/%d", id), nil, raw); err != nil {
if raw, code, err = r.patch(ctx, fmt.Sprintf("api/annotations/%d", id), nil, raw); err != nil {
return StatusMessage{}, errors.Wrap(err, "patch annotation")
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, errors.Wrap(err, "unmarshal response message")
}
Expand All @@ -58,15 +67,19 @@ func (r *Client) GetAnnotations(ctx context.Context, params ...GetAnnotationsPar
err error
resp []AnnotationResponse
requestParams = make(url.Values)
code int
)

for _, p := range params {
p(requestParams)
}

if raw, _, err = r.get(ctx, "api/annotations", requestParams); err != nil {
if raw, code, err = r.get(ctx, "api/annotations", requestParams); err != nil {
return nil, errors.Wrap(err, "get annotations")
}
if code != http.StatusOK {
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return nil, errors.Wrap(err, "unmarshal response message")
}
Expand All @@ -79,11 +92,15 @@ func (r *Client) DeleteAnnotation(ctx context.Context, id uint) (StatusMessage,
raw []byte
err error
resp StatusMessage
code int
)

if raw, _, err = r.delete(ctx, fmt.Sprintf("api/annotations/%d", id)); err != nil {
if raw, code, err = r.delete(ctx, fmt.Sprintf("api/annotations/%d", id)); err != nil {
return StatusMessage{}, errors.Wrap(err, "delete annotation")
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, errors.Wrap(err, "unmarshal response message")
}
Expand Down
11 changes: 11 additions & 0 deletions rest-common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sdk

import "errors"

var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrNotAccessDenied = errors.New("access denied")
ErrNotAuthorized = errors.New("not authorized")
ErrCannotCreate = errors.New("cannot create; see body for details")
)
36 changes: 26 additions & 10 deletions rest-dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
Expand Down Expand Up @@ -167,7 +168,7 @@ func (r *Client) getRawDashboard(ctx context.Context, path string) ([]byte, Boar
if raw, code, err = r.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil); err != nil {
return nil, BoardProperties{}, err
}
if code != 200 {
if code != http.StatusOK {
return nil, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
dec := json.NewDecoder(bytes.NewReader(raw))
Expand Down Expand Up @@ -251,7 +252,7 @@ func (r *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoar
if raw, code, err = r.get(ctx, "api/search", q); err != nil {
return nil, err
}
if code != 200 {
if code != http.StatusOK {
return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &boards)
Expand Down Expand Up @@ -302,12 +303,19 @@ func (r *Client) SetDashboard(ctx context.Context, board Board, params SetDashbo
if raw, code, err = r.post(ctx, "api/dashboards/db", nil, raw); err != nil {
return StatusMessage{}, err
}
if err = json.Unmarshal(raw, &resp); err != nil {
return StatusMessage{}, err
}
if code != 200 {
return resp, fmt.Errorf("HTTP error %d: returns %s", code, *resp.Message)
switch code { // https://grafana.com/docs/grafana/latest/http_api/dashboard/#create--update-dashboard
case http.StatusOK:
err = json.Unmarshal(raw, &resp)
case http.StatusForbidden:
err = fmt.Errorf("database dashboard with uid %q %w", board.UID, ErrNotAccessDenied)
case http.StatusUnauthorized:
err = fmt.Errorf("database dashboard with uid %q %w", board.UID, ErrNotAuthorized)
case http.StatusPreconditionFailed:
err = fmt.Errorf("database dashboard with uid %q %w", board.UID, ErrCannotCreate)
default: // includes http.StatusBadRequest
err = fmt.Errorf("HTTP error %d: returns %s", code, raw)
}

return resp, nil
}

Expand All @@ -330,7 +338,7 @@ func (r *Client) SetRawDashboardWithParam(ctx context.Context, request RawBoardR
if err = json.Unmarshal(rawResp, &resp); err != nil {
return StatusMessage{}, err
}
if code != 200 {
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, *resp.Message)
}
return resp, nil
Expand Down Expand Up @@ -365,13 +373,17 @@ func (r *Client) DeleteDashboard(ctx context.Context, slug string) (StatusMessag
raw []byte
reply StatusMessage
err error
code int
)
if slug, isBoardFromDB = cleanPrefix(slug); !isBoardFromDB {
return StatusMessage{}, errors.New("only database dashboards (with 'db/' prefix in a slug) can be removed")
}
if raw, _, err = r.delete(ctx, fmt.Sprintf("api/dashboards/db/%s", slug)); err != nil {
if raw, code, err = r.delete(ctx, fmt.Sprintf("api/dashboards/db/%s", slug)); err != nil {
return StatusMessage{}, err
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &reply)
return reply, err
}
Expand All @@ -383,10 +395,14 @@ func (r *Client) DeleteDashboardByUID(ctx context.Context, uid string) (StatusMe
raw []byte
reply StatusMessage
err error
code int
)
if raw, _, err = r.delete(ctx, fmt.Sprintf("api/dashboards/uid/%s", uid)); err != nil {
if raw, code, err = r.delete(ctx, fmt.Sprintf("api/dashboards/uid/%s", uid)); err != nil {
return StatusMessage{}, err
}
if code != http.StatusOK {
return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
}
err = json.Unmarshal(raw, &reply)
return reply, err
}
Expand Down
Loading

0 comments on commit 4f854eb

Please sign in to comment.