Skip to content

Commit

Permalink
feat: Support API requests
Browse files Browse the repository at this point in the history
* Support auth from Authorization header

* First cookies will be checked, if not found, Auth header will be checked

* If any of them are successful, headers will be forwarded for client reqs

Signed-off-by: Mahendra Paipuri <[email protected]>
  • Loading branch information
mahendrapaipuri committed Apr 18, 2024
1 parent 3f56681 commit a151d11
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 29 deletions.
2 changes: 1 addition & 1 deletion pkg/plugin/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type App struct {
httpClient *http.Client
grafanaAppUrl string
config *Config
newGrafanaClient func(client *http.Client, grafanaAppURL string, cookie string, variables url.Values, layout string, panels string) GrafanaClient
newGrafanaClient func(client *http.Client, grafanaAppURL string, headers http.Header, variables url.Values, layout string, panels string) GrafanaClient
newReport func(logger log.Logger, grafanaClient GrafanaClient, config *ReportConfig) (Report, error)
}

Expand Down
34 changes: 21 additions & 13 deletions pkg/plugin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ type grafanaClient struct {
url string
getDashEndpoint func(dashUID string) string
getPanelEndpoint func(dashUID string, vals url.Values) string
cookies string
headers http.Header
queryParams url.Values
layout string
dashboardMode string
}

var getPanelRetrySleepTime = time.Duration(10) * time.Second

// NewClient creates a new Grafana Client. If cookies is the non-empty string,
// cookie will be forwarded in the requests.
// NewClient creates a new Grafana Client. Cookies and Authorization headers, if found,
// will be forwarded in the requests
// queryParams are Grafana template variable url values of the form var-{name}={value}, e.g. var-host=dev
func NewGrafanaClient(
client *http.Client,
grafanaAppURL string,
cookie string,
headers http.Header,
queryParams url.Values,
layout string,
dashboardMode string,
Expand All @@ -60,13 +60,25 @@ func NewGrafanaClient(
grafanaAppURL,
getDashEndpoint,
getPanelEndpoint,
cookie,
headers,
queryParams,
layout,
dashboardMode,
}
}

// Add auth specific header to request
func (g grafanaClient) forwardAuthHeader(r *http.Request) *http.Request {
// If incoming request has cookies formward them
// If cookie is not foun, try Authorization header that is used in API requests
if g.headers.Get(backend.CookiesHeaderName) != "" {
r.Header.Set(backend.CookiesHeaderName, g.headers.Get(backend.CookiesHeaderName))
} else if g.headers.Get("Authorization") != "" {
r.Header.Set("Authorization", g.headers.Get("Authorization"))
}
return r
}

func (g grafanaClient) GetDashboard(dashUID string) (Dashboard, error) {
dashURL := g.getDashEndpoint(dashUID)

Expand All @@ -76,10 +88,8 @@ func (g grafanaClient) GetDashboard(dashUID string) (Dashboard, error) {
return Dashboard{}, fmt.Errorf("error creating request for %s: %v", dashURL, err)
}

// If incoming request has cookies formward them
if g.cookies != "" {
req.Header.Set(backend.CookiesHeaderName, g.cookies)
}
// Forward auth headers
req = g.forwardAuthHeader(req)

// Make request
resp, err := g.client.Do(req)
Expand Down Expand Up @@ -113,10 +123,8 @@ func (g grafanaClient) GetPanelPNG(p Panel, dashUID string, t TimeRange) (io.Rea
return nil, fmt.Errorf("error creating request for %s: %v", panelURL, err)
}

// Forward cookies from incoming request
if g.cookies != "" {
req.Header.Set(backend.CookiesHeaderName, g.cookies)
}
// Forward auth headers
req = g.forwardAuthHeader(req)

// Make request
resp, err := g.client.Do(req)
Expand Down
23 changes: 16 additions & 7 deletions pkg/plugin/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/grafana/grafana-plugin-sdk-go/backend"
. "github.com/smartystreets/goconvey/convey"
)

Expand All @@ -20,19 +21,26 @@ func init() {
func TestGrafanaClientFetchesDashboard(t *testing.T) {
Convey("When fetching a Dashboard", t, func() {
requestURI := ""
requestCookie := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestURI = r.RequestURI
requestCookie = r.Header.Get(backend.CookiesHeaderName)
w.Write([]byte(`{"dashboard": {"title": "foo"}}`))
}))
defer ts.Close()

Convey("When using the Grafana client", func() {
grf := NewGrafanaClient(&testClient, ts.URL, "", url.Values{}, "simple", "default")
headers := http.Header{}
headers.Add(backend.CookiesHeaderName, "cookie")
grf := NewGrafanaClient(&testClient, ts.URL, headers, url.Values{}, "simple", "default")
grf.GetDashboard("rYy7Paekz")

Convey("It should use the v5 dashboards endpoint", func() {
So(requestURI, ShouldEqual, "/api/dashboards/uid/rYy7Paekz")
})
Convey("It should use cookie", func() {
So(requestCookie, ShouldEqual, "cookie")
})
})

})
Expand All @@ -49,7 +57,8 @@ func TestGrafanaClientFetchesPanelPNG(t *testing.T) {
}))
defer ts.Close()

cookies := "1234"
headers := http.Header{}
headers.Add("Authorization", "token")
variables := url.Values{}
variables.Add("var-host", "servername")
variables.Add("var-port", "adapter")
Expand All @@ -59,7 +68,7 @@ func TestGrafanaClientFetchesPanelPNG(t *testing.T) {
pngEndpoint string
}{
"client": {
NewGrafanaClient(&testClient, ts.URL, cookies, variables, "simple", "default"),
NewGrafanaClient(&testClient, ts.URL, headers, variables, "simple", "default"),
"/render/d-solo/testDash/_",
},
}
Expand All @@ -85,7 +94,7 @@ func TestGrafanaClientFetchesPanelPNG(t *testing.T) {
})

Convey("The client should insert auth token should in request header", func() {
So(requestHeaders.Get("Cookie"), ShouldContainSubstring, cookies)
So(requestHeaders.Get("Authorization"), ShouldEqual, "token")
})

Convey("The client should pass variables in the request parameters", func() {
Expand All @@ -104,7 +113,7 @@ func TestGrafanaClientFetchesPanelPNG(t *testing.T) {
pngEndpoint string
}{
"client": {
NewGrafanaClient(&testClient, ts.URL, cookies, variables, "grid", "default"),
NewGrafanaClient(&testClient, ts.URL, headers, variables, "grid", "default"),
"/render/d-solo/testDash/_",
},
}
Expand Down Expand Up @@ -138,7 +147,7 @@ func TestGrafanaClientFetchPanelPNGErrorHandling(t *testing.T) {
}))
defer ts.Close()

grf := NewGrafanaClient(&testClient, ts.URL, "", url.Values{}, "simple", "default")
grf := NewGrafanaClient(&testClient, ts.URL, http.Header{}, url.Values{}, "simple", "default")

_, err := grf.GetPanelPNG(
Panel{44, "singlestat", "title", GridPos{0, 0, 0, 0}},
Expand All @@ -157,7 +166,7 @@ func TestGrafanaClientFetchPanelPNGErrorHandling(t *testing.T) {
}))
defer ts.Close()

grf := NewGrafanaClient(&testClient, ts.URL, "", url.Values{}, "simple", "default")
grf := NewGrafanaClient(&testClient, ts.URL, http.Header{}, url.Values{}, "simple", "default")

_, err := grf.GetPanelPNG(
Panel{44, "singlestat", "title", GridPos{0, 0, 0, 0}},
Expand Down
7 changes: 1 addition & 6 deletions pkg/plugin/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strconv"
"strings"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
)
Expand Down Expand Up @@ -150,15 +149,11 @@ func (a *App) handleReport(w http.ResponseWriter, req *http.Request) {
}
}

// Get current user cookie and we will use this cookie in API request to get
// dashboard JSON models and panel PNGs
cookie := req.Header.Get(backend.CookiesHeaderName)

// Make a new Grafana client to get dashboard JSON model and Panel PNGs
grafanaClient := a.newGrafanaClient(
a.httpClient,
a.grafanaAppUrl,
cookie,
req.Header,
variables,
layout,
dashboardMode,
Expand Down
4 changes: 2 additions & 2 deletions pkg/plugin/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ func TestReportResource(t *testing.T) {
Convey("When the report handler is called", t, func() {
var clientVars url.Values
// mock new grafana client function to capture and validate its input parameters
app.newGrafanaClient = func(client *http.Client, url string, cookie string, variables url.Values, layout string, panels string) GrafanaClient {
app.newGrafanaClient = func(client *http.Client, url string, headers http.Header, variables url.Values, layout string, panels string) GrafanaClient {
clientVars = variables
return NewGrafanaClient(&testClient, url, "", clientVars, "simple", "default")
return NewGrafanaClient(&testClient, url, http.Header{}, clientVars, "simple", "default")
}
//mock new report function to capture and validate its input parameters
var repDashName string
Expand Down
19 changes: 19 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ to set these values.

## Using plugin

### Using Grafana web UI

The prerequisite is the user must have at least `Viewer` role on the dashboard that they
want to create a PDF report. After the user authenticates with Grafana, creating a
dashboard report is done by visiting the following end point
Expand Down Expand Up @@ -221,6 +223,23 @@ Following steps will configure a dashboard link to create PDF report for that da
Now there should be link in the right top corner of dashboard named `Report` and clicking
this link will create a new PDF report of the dashboard.

### Using Grafana API

The plugin can generate reports programmatically using Grafana API by using
[Grafana service accounts](https://grafana.com/docs/grafana/latest/administration/service-accounts/).

Once a service account is created with appropriate permissions by following
[Grafana docs](https://grafana.com/docs/grafana/latest/administration/service-accounts/#to-create-a-service-account),
generate an [API token](https://grafana.com/docs/grafana/latest/administration/service-accounts/#add-a-token-to-a-service-account-in-grafana)
from the service account. Once the token has been generated, reports can be created using

```
$ curl --output=report.pdf -H "Authorization: Bearer <supersecrettoken>" "https://example.grafana.com/api/plugins/mahendrapaipuri-dashboardreporter-app/resources/report?dashUid=<UID of dashboard>"
```

The above example shows on how to generate report using `curl` but this can be done with
any HTTP client of your favorite programming language.

## Examples

Here are the example reports that are generated out of the test dashboards
Expand Down

0 comments on commit a151d11

Please sign in to comment.