Skip to content
This repository was archived by the owner on Jan 15, 2024. It is now read-only.

Commit aae2f42

Browse files
Merge branch 'master' into data-source-http-headers
2 parents d1db715 + ff02ac6 commit aae2f42

File tree

11 files changed

+414
-34
lines changed

11 files changed

+414
-34
lines changed

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
# DEPRECATED
1+
# Grafana HTTP API Client for Go
22

3-
:warning: :warning: :warning:
3+
This library provides a low-level client to access Grafana [HTTP API](https://grafana.com/docs/grafana/latest/http_api/).
44

5-
**This repository is no longer being maintained.**
6-
7-
We're in the process of creating our next generation API clients.
8-
9-
In the interim, further changes to this repository should only be to support
10-
[grafana/terraform-provider-grafana](https://github.com/grafana/terraform-provider-grafana).
11-
12-
---
13-
14-
Grafana HTTP API Client for Go
5+
:warning: This repository is still active but not under heavy development.
6+
Contributions to this library offering support for the [Terraform provider for Grafana](https://github.com/grafana/terraform-provider-grafana) will be prioritized over generic ones.
157

168
## Tests
179

annotation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type GraphiteAnnotation struct {
3838
// Annotations fetches the annotations queried with the params it's passed
3939
func (c *Client) Annotations(params url.Values) ([]Annotation, error) {
4040
result := []Annotation{}
41-
err := c.request("GET", "/api/annotation", params, nil, &result)
41+
err := c.request("GET", "/api/annotations", params, nil, &result)
4242
if err != nil {
4343
return nil, err
4444
}

api_key.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package gapi
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/url"
8+
"strconv"
9+
"time"
10+
)
11+
12+
type CreateAPIKeyRequest struct {
13+
Name string `json:"name"`
14+
Role string `json:"role"`
15+
SecondsToLive int64 `json:"secondsToLive,omitempty"`
16+
}
17+
18+
type CreateAPIKeyResponse struct {
19+
// ID field only returned after Grafana v7.
20+
ID int64 `json:"id,omitempty"`
21+
Name string `json:"name"`
22+
Key string `json:"key"`
23+
}
24+
25+
type GetAPIKeysResponse struct {
26+
ID int64 `json:"id"`
27+
Name string `json:"name"`
28+
Role string `json:"role"`
29+
Expiration time.Time `json:"expiration,omitempty"`
30+
}
31+
32+
type DeleteAPIKeyResponse struct {
33+
Message string `json:"message"`
34+
}
35+
36+
// CreateAPIKey creates a new Grafana API key.
37+
func (c *Client) CreateAPIKey(request CreateAPIKeyRequest) (CreateAPIKeyResponse, error) {
38+
response := CreateAPIKeyResponse{}
39+
40+
data, err := json.Marshal(request)
41+
if err != nil {
42+
return response, err
43+
}
44+
45+
err = c.request("POST", "/api/auth/keys", nil, bytes.NewBuffer(data), &response)
46+
return response, err
47+
}
48+
49+
// GetAPIKeys retrieves a list of all API keys.
50+
func (c *Client) GetAPIKeys(includeExpired bool) ([]*GetAPIKeysResponse, error) {
51+
response := make([]*GetAPIKeysResponse, 0)
52+
53+
query := url.Values{}
54+
query.Add("includeExpired", strconv.FormatBool(includeExpired))
55+
56+
err := c.request("GET", "/api/auth/keys", query, nil, &response)
57+
return response, err
58+
}
59+
60+
// DeleteAPIKey deletes the Grafana API key with the specified ID.
61+
func (c *Client) DeleteAPIKey(id int64) (DeleteAPIKeyResponse, error) {
62+
response := DeleteAPIKeyResponse{}
63+
64+
path := fmt.Sprintf("/api/auth/keys/%d", id)
65+
err := c.request("DELETE", path, nil, nil, &response)
66+
return response, err
67+
}

api_key_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package gapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gobs/pretty"
7+
)
8+
9+
const (
10+
createAPIKeyJSON = `{"name":"key-name", "key":"mock-api-key"}`
11+
deleteAPIKeyJSON = `{"message":"API key deleted"}`
12+
13+
getAPIKeysJSON = `[
14+
{
15+
"id": 1,
16+
"name": "key-name-2",
17+
"role": "Viewer"
18+
},
19+
{
20+
"id": 2,
21+
"name": "key-name-2",
22+
"role": "Admin",
23+
"expiration": "2021-10-30T10:52:03+03:00"
24+
}
25+
]`
26+
)
27+
28+
func TestCreateAPIKey(t *testing.T) {
29+
server, client := gapiTestTools(t, 200, createAPIKeyJSON)
30+
defer server.Close()
31+
32+
req := CreateAPIKeyRequest{
33+
Name: "key-name",
34+
Role: "Viewer",
35+
SecondsToLive: 0,
36+
}
37+
38+
res, err := client.CreateAPIKey(req)
39+
if err != nil {
40+
t.Error(err)
41+
}
42+
43+
t.Log(pretty.PrettyFormat(res))
44+
}
45+
46+
func TestDeleteAPIKey(t *testing.T) {
47+
server, client := gapiTestTools(t, 200, deleteAPIKeyJSON)
48+
defer server.Close()
49+
50+
res, err := client.DeleteAPIKey(int64(1))
51+
if err != nil {
52+
t.Error(err)
53+
}
54+
55+
t.Log(pretty.PrettyFormat(res))
56+
}
57+
58+
func TestGetAPIKeys(t *testing.T) {
59+
server, client := gapiTestTools(t, 200, getAPIKeysJSON)
60+
defer server.Close()
61+
62+
res, err := client.GetAPIKeys(true)
63+
if err != nil {
64+
t.Error(err)
65+
}
66+
67+
t.Log(pretty.PrettyFormat(res))
68+
}

client.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os"
1313
"path"
1414
"strconv"
15+
"time"
1516

1617
"github.com/hashicorp/go-cleanhttp"
1718
)
@@ -33,6 +34,8 @@ type Config struct {
3334
Client *http.Client
3435
// OrgID provides an optional organization ID, ignored when using APIKey, BasicAuth defaults to last used org
3536
OrgID int64
37+
// NumRetries contains the number of attempted retries
38+
NumRetries int
3639
}
3740

3841
// New creates a new Grafana client.
@@ -59,18 +62,49 @@ func New(baseURL string, cfg Config) (*Client, error) {
5962
}
6063

6164
func (c *Client) request(method, requestPath string, query url.Values, body io.Reader, responseStruct interface{}) error {
62-
r, err := c.newRequest(method, requestPath, query, body)
63-
if err != nil {
64-
return err
65-
}
65+
var (
66+
req *http.Request
67+
resp *http.Response
68+
err error
69+
bodyContents []byte
70+
)
71+
72+
// retry logic
73+
for n := 0; n <= c.config.NumRetries; n++ {
74+
req, err = c.newRequest(method, requestPath, query, body)
75+
if err != nil {
76+
return err
77+
}
6678

67-
resp, err := c.client.Do(r)
68-
if err != nil {
69-
return err
70-
}
71-
defer resp.Body.Close()
79+
// Wait a bit if that's not the first request
80+
if n != 0 {
81+
time.Sleep(time.Second * 5)
82+
}
83+
84+
resp, err = c.client.Do(req)
7285

73-
bodyContents, err := ioutil.ReadAll(resp.Body)
86+
// If err is not nil, retry again
87+
// That's either caused by client policy, or failure to speak HTTP (such as network connectivity problem). A
88+
// non-2xx status code doesn't cause an error.
89+
if err != nil {
90+
continue
91+
}
92+
93+
defer resp.Body.Close()
94+
95+
// read the body (even on non-successful HTTP status codes), as that's what the unit tests expect
96+
bodyContents, err = ioutil.ReadAll(resp.Body)
97+
98+
// if there was an error reading the body, try again
99+
if err != nil {
100+
continue
101+
}
102+
103+
// Exit the loop if we have something final to return. This is anything < 500, if it's not a 429.
104+
if resp.StatusCode < http.StatusInternalServerError && resp.StatusCode != http.StatusTooManyRequests {
105+
break
106+
}
107+
}
74108
if err != nil {
75109
return err
76110
}
@@ -79,6 +113,7 @@ func (c *Client) request(method, requestPath string, query url.Values, body io.R
79113
log.Printf("response status %d with body %v", resp.StatusCode, string(bodyContents))
80114
}
81115

116+
// check status code.
82117
if resp.StatusCode >= 400 {
83118
return fmt.Errorf("status: %d, body: %v", resp.StatusCode, string(bodyContents))
84119
}

client_test.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestNew_invalidURL(t *testing.T) {
5858

5959
expected := "parse \"://my-grafana.com\": missing protocol scheme"
6060
if err.Error() != expected {
61-
t.Errorf("expected error: %v; got: %s", expected, err.Error())
61+
t.Errorf("expected error: %v; got: %s", expected, err)
6262
}
6363
}
6464

@@ -68,7 +68,7 @@ func TestRequest_200(t *testing.T) {
6868

6969
err := client.request("GET", "/foo", url.Values{}, nil, nil)
7070
if err != nil {
71-
t.Errorf(err.Error())
71+
t.Error(err)
7272
}
7373
}
7474

@@ -78,7 +78,7 @@ func TestRequest_201(t *testing.T) {
7878

7979
err := client.request("GET", "/foo", url.Values{}, nil, nil)
8080
if err != nil {
81-
t.Errorf(err.Error())
81+
t.Error(err)
8282
}
8383
}
8484

@@ -89,7 +89,7 @@ func TestRequest_400(t *testing.T) {
8989
expected := `status: 400, body: {"foo":"bar"}`
9090
err := client.request("GET", "/foo", url.Values{}, nil, nil)
9191
if err.Error() != expected {
92-
t.Errorf("expected error: %v; got: %s", expected, err.Error())
92+
t.Errorf("expected error: %v; got: %s", expected, err)
9393
}
9494
}
9595

@@ -100,7 +100,23 @@ func TestRequest_500(t *testing.T) {
100100
expected := `status: 500, body: {"foo":"bar"}`
101101
err := client.request("GET", "/foo", url.Values{}, nil, nil)
102102
if err.Error() != expected {
103-
t.Errorf("expected error: %v; got: %s", expected, err.Error())
103+
t.Errorf("expected error: %v; got: %s", expected, err)
104+
}
105+
}
106+
107+
func TestRequest_badURL(t *testing.T) {
108+
server, client := gapiTestTools(t, 200, `{"foo":"bar"}`)
109+
baseURL, err := url.Parse("bad-url")
110+
if err != nil {
111+
t.Fatal(err)
112+
}
113+
client.baseURL = *baseURL
114+
defer server.Close()
115+
116+
expected := `Get "bad-url/foo": unsupported protocol scheme ""`
117+
err = client.request("GET", "/foo", url.Values{}, nil, nil)
118+
if err.Error() != expected {
119+
t.Errorf("expected error: %v; got: %s", expected, err)
104120
}
105121
}
106122

@@ -113,7 +129,7 @@ func TestRequest_200Unmarshal(t *testing.T) {
113129
}{}
114130
err := client.request("GET", "/foo", url.Values{}, nil, &result)
115131
if err != nil {
116-
t.Fatal(err.Error())
132+
t.Fatal(err)
117133
}
118134

119135
if result.Foo != "bar" {
@@ -130,7 +146,7 @@ func TestRequest_200UnmarshalPut(t *testing.T) {
130146
}
131147
data, err := json.Marshal(u)
132148
if err != nil {
133-
t.Fatal(err.Error())
149+
t.Fatal(err)
134150
}
135151

136152
result := struct {
@@ -140,7 +156,7 @@ func TestRequest_200UnmarshalPut(t *testing.T) {
140156
q.Add("a", "b")
141157
err = client.request("PUT", "/foo", q, bytes.NewBuffer(data), &result)
142158
if err != nil {
143-
t.Errorf(err.Error())
159+
t.Error(err)
144160
}
145161

146162
if result.Name != "mike" {

dashboard.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Dashboard struct {
2828
Model map[string]interface{} `json:"dashboard"`
2929
Folder int64 `json:"folderId"`
3030
Overwrite bool `json:"overwrite"`
31+
32+
// This is only used when creating a new dashboard, it will always be empty when getting a dashboard.
33+
Message string `json:"message"`
3134
}
3235

3336
// SaveDashboard is a deprecated method for saving a Grafana dashboard. Use NewDashboard.

datasource.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
// DataSource represents a Grafana data source.
1313
type DataSource struct {
1414
ID int64 `json:"id,omitempty"`
15+
UID string `json:"uid,omitempty"`
1516
Name string `json:"name"`
1617
Type string `json:"type"`
1718
URL string `json:"url"`
@@ -84,7 +85,8 @@ type JSONData struct {
8485
TimeInterval string `json:"timeInterval,omitempty"`
8586

8687
// Used by Elasticsearch
87-
EsVersion int64 `json:"esVersion,omitempty"`
88+
// From Grafana 8.x esVersion is the semantic version of Elasticsearch.
89+
EsVersion string `json:"esVersion,omitempty"`
8890
TimeField string `json:"timeField,omitempty"`
8991
Interval string `json:"interval,omitempty"`
9092
LogMessageField string `json:"logMessageField,omitempty"`

0 commit comments

Comments
 (0)