Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return HTTP response codes as typed errors #161

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 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 All @@ -136,3 +125,17 @@ https://github.com/grafana-tools/sdk
* [github.com/raintank/memo](https://github.com/raintank/memo) — send slack mentions to Grafana annotations.
* [github.com/retzkek/grafctl](https://github.com/retzkek/grafctl) — backup/restore/track dashboards with git.
* [github.com/grafana/grizzly](https://github.com/grafana/grizzly) — manage Grafana dashboards via CLI and libsonnet/jsonnet

## Running tests

* Unit tests:
```command
$ go test ./...
```

* Integration tests:

```command
$ GRAFANA_VERSION=6.7.1 docker-compose up -d
$ GRAFANA_INTEGRATION=1 go test ./...
```
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use httpStatusCodeError everywhere in all of these functions (in this and others)?

}
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
34 changes: 34 additions & 0 deletions rest-common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sdk

import (
"errors"
"fmt"
"net/http"
)

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")
)

func httpStatusCodeError(code int, message string, raw []byte) error {
switch code {
case http.StatusNotFound:
return fmt.Errorf("%s: %w", message, ErrNotFound)

case http.StatusForbidden:
return fmt.Errorf("%s: %w", message, ErrNotAccessDenied)

case http.StatusUnauthorized:
return fmt.Errorf("%s: %w", message, ErrNotAuthorized)

case http.StatusPreconditionFailed:
return fmt.Errorf("%s: %w", message, ErrCannotCreate)

default:
return fmt.Errorf("%s returned HTTP status code %d: %v", message, code, raw)
}
}
Loading