From 66a7bce801909608578909e656e358ec54dc356e Mon Sep 17 00:00:00 2001 From: Steffen Uhlig Date: Sat, 7 Aug 2021 18:18:52 +0200 Subject: [PATCH 1/2] Return HTTP response codes as typed errors 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) } ``` --- README.md | 31 ++++++----- rest-admin.go | 19 +++++-- rest-alertnotification.go | 17 +++--- rest-annotation.go | 25 +++++++-- rest-common.go | 11 ++++ rest-dashboard.go | 68 ++++++++++++++++-------- rest-dashboard_integration_test.go | 9 +++- rest-datasource.go | 61 +++++++++++++++------- rest-folder.go | 13 ++--- rest-get_health.go | 15 ++++-- rest-org.go | 84 +++++++++++++++++++++++------- rest-user.go | 16 +++--- 12 files changed, 264 insertions(+), 105 deletions(-) create mode 100644 rest-common.go diff --git a/README.md b/README.md index 60187e94..a43ab906 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 ./... + ``` diff --git a/rest-admin.go b/rest-admin.go index 583066c4..5ff364a2 100644 --- a/rest-admin.go +++ b/rest-admin.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" ) // CreateUser creates a new global user. @@ -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 } @@ -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 } @@ -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 } diff --git a/rest-alertnotification.go b/rest-alertnotification.go index 4cc336b5..ddce56ed 100644 --- a/rest-alertnotification.go +++ b/rest-alertnotification.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" ) // GetAllAlertNotifications gets all alert notification channels. @@ -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) @@ -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) @@ -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) @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/rest-annotation.go b/rest-annotation.go index a5c7c3d8..97975b42 100644 --- a/rest-annotation.go +++ b/rest-annotation.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "net/url" "strconv" "time" @@ -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") } @@ -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") } @@ -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") } @@ -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") } diff --git a/rest-common.go b/rest-common.go new file mode 100644 index 00000000..9750b91a --- /dev/null +++ b/rest-common.go @@ -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") +) diff --git a/rest-dashboard.go b/rest-dashboard.go index 653844c8..05dc02b6 100644 --- a/rest-dashboard.go +++ b/rest-dashboard.go @@ -24,6 +24,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "net/url" "strconv" "strings" @@ -161,21 +162,27 @@ func (r *Client) getRawDashboard(ctx context.Context, path string) ([]byte, Boar Meta BoardProperties `json:"meta"` Board json.RawMessage `json:"dashboard"` } - code int - err error + code int + err error + boardBytes []byte ) if raw, code, err = r.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil); err != nil { return nil, BoardProperties{}, err } - if code != 200 { - return nil, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw) - } - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.UseNumber() - if err := dec.Decode(&result); err != nil { - return nil, BoardProperties{}, errors.Wrap(err, "unmarshal board") + + switch code { + case http.StatusOK: + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.UseNumber() + err = dec.Decode(&result) + boardBytes = []byte(result.Board) + case http.StatusNotFound: + err = fmt.Errorf("dashboard with path %q %w", path, ErrNotFound) + default: + err = fmt.Errorf("HTTP error %d: returns %s", code, raw) } - return []byte(result.Board), result.Meta, err + + return boardBytes, result.Meta, err } // GetRawDashboardByUID loads a dashboard and its metadata from Grafana by dashboard uid. @@ -251,7 +258,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) @@ -302,13 +309,20 @@ 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 + + return resp, err } //SetRawDashboardWithParam sends the serialized along with request parameters @@ -330,7 +344,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 @@ -365,13 +379,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 } @@ -383,11 +401,19 @@ 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 } - err = json.Unmarshal(raw, &reply) + switch code { + case http.StatusOK: + err = json.Unmarshal(raw, &reply) + case http.StatusNotFound: + err = fmt.Errorf("dashboard with UID %q %w", uid, ErrNotFound) + default: + err = fmt.Errorf("HTTP error %d: returns %s", code, raw) + } return reply, err } diff --git a/rest-dashboard_integration_test.go b/rest-dashboard_integration_test.go index dd2eef9d..91c11976 100644 --- a/rest-dashboard_integration_test.go +++ b/rest-dashboard_integration_test.go @@ -3,6 +3,7 @@ package sdk_test import ( "context" "encoding/json" + "errors" "io/ioutil" "testing" @@ -30,7 +31,9 @@ func Test_Dashboard_CRUD(t *testing.T) { board.ID = 1234 board.Title = "barfoo" - if _, err = client.DeleteDashboard(ctx, board.UpdateSlug()); err != nil { + _, err = client.DeleteDashboardByUID(ctx, board.UID) + + if !errors.Is(err, sdk.ErrNotFound) { t.Fatal(err) } @@ -85,7 +88,9 @@ func Test_Dashboard_CRUD_By_UID(t *testing.T) { board.Title = "foobar" //Cleanup if Already exists - if _, err = client.DeleteDashboardByUID(ctx, board.UID); err != nil { + _, err = client.DeleteDashboardByUID(ctx, board.UID) + + if !errors.Is(err, sdk.ErrNotFound) { t.Fatal(err) } diff --git a/rest-datasource.go b/rest-datasource.go index f5feee5d..42f04163 100644 --- a/rest-datasource.go +++ b/rest-datasource.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" ) // GetAllDatasources gets all datasources. @@ -37,7 +38,7 @@ func (r *Client) GetAllDatasources(ctx context.Context) ([]Datasource, error) { if raw, code, err = r.get(ctx, "api/datasources", 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, &ds) @@ -56,10 +57,14 @@ func (r *Client) GetDatasource(ctx context.Context, id uint) (Datasource, error) if raw, code, err = r.get(ctx, fmt.Sprintf("api/datasources/%d", id), nil); err != nil { return ds, err } - if code != 200 { - return ds, fmt.Errorf("HTTP error %d: returns %s", code, raw) + switch code { + case http.StatusOK: + err = json.Unmarshal(raw, &ds) + case http.StatusNotFound: + err = fmt.Errorf("data source with id %q %w", id, ErrNotFound) + default: + err = fmt.Errorf("HTTP error %d: returns %s", code, raw) } - err = json.Unmarshal(raw, &ds) return ds, err } @@ -75,7 +80,7 @@ func (r *Client) GetDatasourceByName(ctx context.Context, name string) (Datasour if raw, code, err = r.get(ctx, fmt.Sprintf("api/datasources/name/%s", name), nil); err != nil { return ds, err } - if code != 200 { + if code != http.StatusOK { return ds, fmt.Errorf("HTTP error %d: returns %s", code, raw) } err = json.Unmarshal(raw, &ds) @@ -89,17 +94,23 @@ func (r *Client) CreateDatasource(ctx context.Context, ds Datasource) (StatusMes raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(ds); err != nil { return StatusMessage{}, err } - if raw, _, err = r.post(ctx, "api/datasources", nil, raw); err != nil { + if raw, code, err = r.post(ctx, "api/datasources", nil, raw); err != nil { return StatusMessage{}, err } - if err = json.Unmarshal(raw, &resp); err != nil { - return StatusMessage{}, err + switch code { + case http.StatusOK: + err = json.Unmarshal(raw, &resp) + case http.StatusConflict: + err = fmt.Errorf("data source with name %q %w", ds.Name, ErrAlreadyExists) + default: + err = fmt.Errorf("HTTP status code %d: returns %s", code, raw) } - return resp, nil + return resp, err } // UpdateDatasource updates a datasource from data passed in argument. @@ -109,17 +120,23 @@ func (r *Client) UpdateDatasource(ctx context.Context, ds Datasource) (StatusMes raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(ds); err != nil { - return StatusMessage{}, err + return resp, err } - if raw, _, err = r.put(ctx, fmt.Sprintf("api/datasources/%d", ds.ID), nil, raw); err != nil { - return StatusMessage{}, err + if raw, code, err = r.put(ctx, fmt.Sprintf("api/datasources/%d", ds.ID), nil, raw); err != nil { + return resp, err } - if err = json.Unmarshal(raw, &resp); err != nil { - return StatusMessage{}, err + switch code { + case http.StatusOK: + err = json.Unmarshal(raw, &resp) + case http.StatusNotFound: + err = fmt.Errorf("data source with name %q %w", ds.Name, ErrNotFound) + default: + err = fmt.Errorf("HTTP status code %d: returns %s", code, raw) } - return resp, nil + return resp, err } // DeleteDatasource deletes an existing datasource by ID. @@ -129,10 +146,14 @@ func (r *Client) DeleteDatasource(ctx context.Context, id uint) (StatusMessage, raw []byte reply StatusMessage err error + code int ) - if raw, _, err = r.delete(ctx, fmt.Sprintf("api/datasources/%d", id)); err != nil { + if raw, code, err = r.delete(ctx, fmt.Sprintf("api/datasources/%d", id)); 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 } @@ -144,10 +165,14 @@ func (r *Client) DeleteDatasourceByName(ctx context.Context, name string) (Statu raw []byte reply StatusMessage err error + code int ) - if raw, _, err = r.delete(ctx, fmt.Sprintf("api/datasources/name/%s", name)); err != nil { + if raw, code, err = r.delete(ctx, fmt.Sprintf("api/datasources/name/%s", name)); 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 } @@ -164,7 +189,7 @@ func (r *Client) GetDatasourceTypes(ctx context.Context) (map[string]DatasourceT if raw, code, err = r.get(ctx, "api/datasources/plugins", 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, &dsTypes) diff --git a/rest-folder.go b/rest-folder.go index da4dc589..4a59b87f 100644 --- a/rest-folder.go +++ b/rest-folder.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "net/url" "strconv" ) @@ -45,7 +46,7 @@ func (r *Client) GetAllFolders(ctx context.Context, params ...GetFolderParams) ( if raw, code, err = r.get(ctx, "api/folders", requestParams); 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, &fs) @@ -64,7 +65,7 @@ func (r *Client) GetFolderByUID(ctx context.Context, UID string) (Folder, error) if raw, code, err = r.get(ctx, fmt.Sprintf("api/folders/%s", UID), nil); err != nil { return f, err } - if code != 200 { + if code != http.StatusOK { return f, fmt.Errorf("HTTP error %d: returns %s", code, raw) } err = json.Unmarshal(raw, &f) @@ -87,7 +88,7 @@ func (r *Client) CreateFolder(ctx context.Context, f Folder) (Folder, error) { if raw, code, err = r.post(ctx, "api/folders", nil, raw); err != nil { return rf, err } - if code != 200 { + if code != http.StatusOK { return rf, fmt.Errorf("HTTP error %d: returns %s", code, raw) } err = json.Unmarshal(raw, &rf) @@ -110,7 +111,7 @@ func (r *Client) UpdateFolderByUID(ctx context.Context, f Folder) (Folder, error if raw, code, err = r.put(ctx, fmt.Sprintf("api/folders/%s", f.UID), nil, raw); err != nil { return rf, err } - if code != 200 { + if code != http.StatusOK { return f, fmt.Errorf("HTTP error %d: returns %s", code, raw) } err = json.Unmarshal(raw, &rf) @@ -128,7 +129,7 @@ func (r *Client) DeleteFolderByUID(ctx context.Context, UID string) (bool, error if raw, code, err = r.delete(ctx, fmt.Sprintf("api/folders/%s", UID)); err != nil { return false, err } - if code != 200 { + if code != http.StatusOK { return false, fmt.Errorf("HTTP error %d: returns %s", code, raw) } return true, err @@ -149,7 +150,7 @@ func (r *Client) GetFolderByID(ctx context.Context, ID int) (Folder, error) { if raw, code, err = r.get(ctx, fmt.Sprintf("api/folders/id/%d", ID), nil); err != nil { return f, err } - if code != 200 { + if code != http.StatusOK { return f, fmt.Errorf("HTTP error %d: returns %s", code, raw) } err = json.Unmarshal(raw, &f) diff --git a/rest-get_health.go b/rest-get_health.go index aa1b0e1d..5e407039 100644 --- a/rest-get_health.go +++ b/rest-get_health.go @@ -22,6 +22,8 @@ package sdk import ( "context" "encoding/json" + "fmt" + "net/http" ) // HealthResponse represents the health of grafana server @@ -35,14 +37,17 @@ type HealthResponse struct { // Reflects GET BaseURL API call. func (r *Client) GetHealth(ctx context.Context) (HealthResponse, error) { var ( - raw []byte - err error + raw []byte + err error + health HealthResponse + code int ) - if raw, _, err = r.get(ctx, "/api/health", nil); err != nil { + if raw, code, err = r.get(ctx, "/api/health", nil); err != nil { return HealthResponse{}, err } - - health := HealthResponse{} + if code != http.StatusOK { + return HealthResponse{}, fmt.Errorf("HTTP error %d: returns %s", code, raw) + } if err := json.Unmarshal(raw, &health); err != nil { return HealthResponse{}, err } diff --git a/rest-org.go b/rest-org.go index 20116bd2..dd658666 100644 --- a/rest-org.go +++ b/rest-org.go @@ -34,13 +34,17 @@ func (r *Client) CreateOrg(ctx context.Context, org Org) (StatusMessage, error) raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(org); err != nil { return StatusMessage{}, err } - if raw, _, err = r.post(ctx, "api/orgs", nil, raw); err != nil { + if raw, code, err = r.post(ctx, "api/orgs", 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 } @@ -59,7 +63,6 @@ func (r *Client) GetAllOrgs(ctx context.Context) ([]Org, error) { if raw, code, err = r.get(ctx, "api/orgs", nil); err != nil { return orgs, err } - if code != http.StatusOK { return orgs, fmt.Errorf("HTTP error %d: returns %s", code, raw) } @@ -106,7 +109,6 @@ func (r *Client) GetOrgById(ctx context.Context, oid uint) (Org, error) { if raw, code, err = r.get(ctx, fmt.Sprintf("api/orgs/%d", oid), nil); err != nil { return org, err } - if code != http.StatusOK { return org, fmt.Errorf("HTTP error %d: returns %s", code, raw) } @@ -130,7 +132,6 @@ func (r *Client) GetOrgByOrgName(ctx context.Context, name string) (Org, error) if raw, code, err = r.get(ctx, fmt.Sprintf("api/orgs/name/%s", name), nil); err != nil { return org, err } - if code != http.StatusOK { return org, fmt.Errorf("HTTP error %d: returns %s", code, raw) } @@ -149,13 +150,17 @@ func (r *Client) UpdateActualOrg(ctx context.Context, org Org) (StatusMessage, e raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(org); err != nil { return StatusMessage{}, err } - if raw, _, err = r.put(ctx, "api/org", nil, raw); err != nil { + if raw, code, err = r.put(ctx, "api/org", 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 } @@ -169,13 +174,17 @@ func (r *Client) UpdateOrg(ctx context.Context, org Org, oid uint) (StatusMessag raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(org); err != nil { return StatusMessage{}, err } - if raw, _, err = r.put(ctx, fmt.Sprintf("api/orgs/%d", oid), nil, raw); err != nil { + if raw, code, err = r.put(ctx, fmt.Sprintf("api/orgs/%d", 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 } @@ -189,10 +198,14 @@ func (r *Client) DeleteOrg(ctx context.Context, oid uint) (StatusMessage, error) raw []byte resp StatusMessage err error + code int ) - if raw, _, err = r.delete(ctx, fmt.Sprintf("api/orgs/%d", oid)); err != nil { + if raw, code, err = r.delete(ctx, fmt.Sprintf("api/orgs/%d", oid)); 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 } @@ -252,13 +265,17 @@ func (r *Client) AddActualOrgUser(ctx context.Context, userRole UserRole) (Statu raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(userRole); err != nil { return StatusMessage{}, err } - if raw, _, err = r.post(ctx, "api/org/users", nil, raw); err != nil { + if raw, code, err = r.post(ctx, "api/org/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 } @@ -272,13 +289,17 @@ func (r *Client) UpdateActualOrgUser(ctx context.Context, user UserRole, uid uin 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, fmt.Sprintf("api/org/users/%d", uid), nil, raw); err != nil { + if raw, code, err = r.post(ctx, fmt.Sprintf("api/org/users/%d", uid), 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 } @@ -292,10 +313,14 @@ func (r *Client) DeleteActualOrgUser(ctx context.Context, uid uint) (StatusMessa raw []byte reply StatusMessage err error + code int ) - if raw, _, err = r.delete(ctx, fmt.Sprintf("api/org/users/%d", uid)); err != nil { + if raw, code, err = r.delete(ctx, fmt.Sprintf("api/org/users/%d", 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 } @@ -307,13 +332,17 @@ func (r *Client) AddOrgUser(ctx context.Context, user UserRole, oid uint) (Statu raw []byte reply StatusMessage err error + code int ) if raw, err = json.Marshal(user); err != nil { return StatusMessage{}, err } - if raw, _, err = r.post(ctx, fmt.Sprintf("api/orgs/%d/users", oid), nil, raw); err != nil { + if raw, code, err = r.post(ctx, fmt.Sprintf("api/orgs/%d/users", oid), 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 } @@ -325,13 +354,17 @@ func (r *Client) UpdateOrgUser(ctx context.Context, user UserRole, oid, uid uint raw []byte reply StatusMessage err error + code int ) if raw, err = json.Marshal(user); err != nil { return StatusMessage{}, err } - if raw, _, err = r.patch(ctx, fmt.Sprintf("api/orgs/%d/users/%d", oid, uid), nil, raw); err != nil { + if raw, code, err = r.patch(ctx, fmt.Sprintf("api/orgs/%d/users/%d", oid, 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 } @@ -343,10 +376,14 @@ func (r *Client) DeleteOrgUser(ctx context.Context, oid, uid uint) (StatusMessag raw []byte reply StatusMessage err error + code int ) - if raw, _, err = r.delete(ctx, fmt.Sprintf("api/orgs/%d/users/%d", oid, uid)); err != nil { + if raw, code, err = r.delete(ctx, fmt.Sprintf("api/orgs/%d/users/%d", oid, 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 } @@ -358,13 +395,17 @@ func (r *Client) UpdateActualOrgPreferences(ctx context.Context, prefs Preferenc raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(prefs); err != nil { return StatusMessage{}, err } - if raw, _, err = r.put(ctx, "api/org/preferences/", nil, raw); err != nil { + if raw, code, err = r.put(ctx, "api/org/preferences/", 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 } @@ -377,13 +418,12 @@ func (r *Client) GetActualOrgPreferences(ctx context.Context) (Preferences, erro var ( raw []byte pref Preferences - code int err error + code int ) if raw, code, err = r.get(ctx, "/api/org/preferences", nil); err != nil { return pref, err } - if code != http.StatusOK { return pref, fmt.Errorf("HTTP error %d: returns %s", code, raw) } @@ -402,13 +442,17 @@ func (r *Client) UpdateActualOrgAddress(ctx context.Context, address Address) (S raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(address); err != nil { return StatusMessage{}, err } - if raw, _, err = r.put(ctx, "api/org/address", nil, raw); err != nil { + if raw, code, err = r.put(ctx, "api/org/address", 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 } @@ -422,13 +466,17 @@ func (r *Client) UpdateOrgAddress(ctx context.Context, address Address, oid uint raw []byte resp StatusMessage err error + code int ) if raw, err = json.Marshal(address); err != nil { return StatusMessage{}, err } - if raw, _, err = r.put(ctx, fmt.Sprintf("api/orgs/%d/address", oid), nil, raw); err != nil { + if raw, code, err = r.put(ctx, fmt.Sprintf("api/orgs/%d/address", 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 } diff --git a/rest-user.go b/rest-user.go index 1b4dd4ce..fb07ad0f 100644 --- a/rest-user.go +++ b/rest-user.go @@ -24,6 +24,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "net/url" ) @@ -39,7 +40,7 @@ func (r *Client) GetActualUser(ctx context.Context) (User, error) { if raw, code, err = r.get(ctx, "api/user", nil); err != nil { return user, err } - if code != 200 { + if code != http.StatusOK { return user, fmt.Errorf("HTTP error %d: returns %s", code, raw) } dec := json.NewDecoder(bytes.NewReader(raw)) @@ -62,7 +63,7 @@ func (r *Client) GetUser(ctx context.Context, id uint) (User, error) { if raw, code, err = r.get(ctx, fmt.Sprintf("api/users/%d", id), nil); err != nil { return user, err } - if code != 200 { + if code != http.StatusOK { return user, fmt.Errorf("HTTP error %d: returns %s", code, raw) } dec := json.NewDecoder(bytes.NewReader(raw)) @@ -88,7 +89,7 @@ func (r *Client) GetAllUsers(ctx context.Context) ([]User, error) { if raw, code, err = r.get(ctx, "api/users", params); err != nil { return users, err } - if code != 200 { + if code != http.StatusOK { return users, fmt.Errorf("HTTP error %d: returns %s", code, raw) } dec := json.NewDecoder(bytes.NewReader(raw)) @@ -134,7 +135,7 @@ func (r *Client) SearchUsersWithPaging(ctx context.Context, query *string, perpa if raw, code, err = r.get(ctx, "api/users/search", params); err != nil { return pageUsers, err } - if code != 200 { + if code != http.StatusOK { return pageUsers, fmt.Errorf("HTTP error %d: returns %s", code, raw) } dec := json.NewDecoder(bytes.NewReader(raw)) @@ -152,11 +153,14 @@ func (r *Client) SwitchActualUserContext(ctx context.Context, oid uint) (StatusM raw []byte resp StatusMessage err error + code int ) - - if raw, _, err = r.post(ctx, fmt.Sprintf("/api/user/using/%d", oid), nil, raw); err != nil { + if raw, code, err = r.post(ctx, fmt.Sprintf("/api/user/using/%d", 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 } From 5b5b4d63f8027c1e35f2cd4750d35d4cc524d5d9 Mon Sep 17 00:00:00 2001 From: Steffen Uhlig Date: Sat, 11 Sep 2021 14:03:09 +0200 Subject: [PATCH 2/2] Extract http status code handling --- rest-common.go | 25 +++++++++++++++++++++++- rest-dashboard.go | 47 +++++++++++++++++++--------------------------- rest-datasource.go | 40 +++++++++++++++++++-------------------- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/rest-common.go b/rest-common.go index 9750b91a..7e13c061 100644 --- a/rest-common.go +++ b/rest-common.go @@ -1,6 +1,10 @@ package sdk -import "errors" +import ( + "errors" + "fmt" + "net/http" +) var ( ErrNotFound = errors.New("not found") @@ -9,3 +13,22 @@ var ( 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) + } +} diff --git a/rest-dashboard.go b/rest-dashboard.go index 05dc02b6..db049b76 100644 --- a/rest-dashboard.go +++ b/rest-dashboard.go @@ -166,22 +166,20 @@ func (r *Client) getRawDashboard(ctx context.Context, path string) ([]byte, Boar err error boardBytes []byte ) + if raw, code, err = r.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil); err != nil { return nil, BoardProperties{}, err } - switch code { - case http.StatusOK: - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.UseNumber() - err = dec.Decode(&result) - boardBytes = []byte(result.Board) - case http.StatusNotFound: - err = fmt.Errorf("dashboard with path %q %w", path, ErrNotFound) - default: - err = fmt.Errorf("HTTP error %d: returns %s", code, raw) + if code != http.StatusOK { + return nil, BoardProperties{}, httpStatusCodeError(code, fmt.Sprintf("dashboard with path %q", path), raw) } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.UseNumber() + err = dec.Decode(&result) + boardBytes = []byte(result.Board) + return boardBytes, result.Meta, err } @@ -309,19 +307,13 @@ 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 } - 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) + + if code != http.StatusOK { + return StatusMessage{}, httpStatusCodeError(code, fmt.Sprintf("database dashboard with uid %q", board.UID), raw) } + err = json.Unmarshal(raw, &resp) + return resp, err } @@ -406,14 +398,13 @@ func (r *Client) DeleteDashboardByUID(ctx context.Context, uid string) (StatusMe if raw, code, err = r.delete(ctx, fmt.Sprintf("api/dashboards/uid/%s", uid)); err != nil { return StatusMessage{}, err } - switch code { - case http.StatusOK: - err = json.Unmarshal(raw, &reply) - case http.StatusNotFound: - err = fmt.Errorf("dashboard with UID %q %w", uid, ErrNotFound) - default: - err = fmt.Errorf("HTTP error %d: returns %s", code, raw) + + if code != http.StatusOK { + return StatusMessage{}, httpStatusCodeError(code, fmt.Sprintf("dashboard with uid %q", uid), raw) } + + err = json.Unmarshal(raw, &reply) + return reply, err } diff --git a/rest-datasource.go b/rest-datasource.go index 42f04163..38d589d2 100644 --- a/rest-datasource.go +++ b/rest-datasource.go @@ -54,17 +54,17 @@ func (r *Client) GetDatasource(ctx context.Context, id uint) (Datasource, error) code int err error ) + if raw, code, err = r.get(ctx, fmt.Sprintf("api/datasources/%d", id), nil); err != nil { return ds, err } - switch code { - case http.StatusOK: - err = json.Unmarshal(raw, &ds) - case http.StatusNotFound: - err = fmt.Errorf("data source with id %q %w", id, ErrNotFound) - default: - err = fmt.Errorf("HTTP error %d: returns %s", code, raw) + + if code != http.StatusOK { + return ds, httpStatusCodeError(code, fmt.Sprintf("data source with id %d", id), raw) } + + err = json.Unmarshal(raw, &ds) + return ds, err } @@ -102,14 +102,13 @@ func (r *Client) CreateDatasource(ctx context.Context, ds Datasource) (StatusMes if raw, code, err = r.post(ctx, "api/datasources", nil, raw); err != nil { return StatusMessage{}, err } - switch code { - case http.StatusOK: - err = json.Unmarshal(raw, &resp) - case http.StatusConflict: - err = fmt.Errorf("data source with name %q %w", ds.Name, ErrAlreadyExists) - default: - err = fmt.Errorf("HTTP status code %d: returns %s", code, raw) + + if code != http.StatusOK { + return StatusMessage{}, httpStatusCodeError(code, fmt.Sprintf("data source with name %q", ds.Name), raw) } + + err = json.Unmarshal(raw, &resp) + return resp, err } @@ -128,14 +127,13 @@ func (r *Client) UpdateDatasource(ctx context.Context, ds Datasource) (StatusMes if raw, code, err = r.put(ctx, fmt.Sprintf("api/datasources/%d", ds.ID), nil, raw); err != nil { return resp, err } - switch code { - case http.StatusOK: - err = json.Unmarshal(raw, &resp) - case http.StatusNotFound: - err = fmt.Errorf("data source with name %q %w", ds.Name, ErrNotFound) - default: - err = fmt.Errorf("HTTP status code %d: returns %s", code, raw) + + if code != http.StatusOK { + return StatusMessage{}, httpStatusCodeError(code, fmt.Sprintf("data source with name %q", ds.Name), raw) } + + err = json.Unmarshal(raw, &resp) + return resp, err }