Skip to content

Commit

Permalink
Support Datasources in grr serve
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Nov 14, 2024
1 parent eca189d commit e9b18b5
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 71 deletions.
69 changes: 11 additions & 58 deletions pkg/grafana/dashboard-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,22 +259,22 @@ func (h *DashboardHandler) Detect(data map[string]any) bool {
func (h *DashboardHandler) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: "GET",
Method: http.MethodGet,
URL: "/d/{uid}/{slug}",
Handler: h.resourceFromQueryParameterMiddleware(s, "grizzly_from_file", h.RootDashboardPageHandler(s)),
Handler: h.resourceFromQueryParameterMiddleware(s, "grizzly_from_file", authenticateAndProxyHandler(s, h.Provider)),
},
{
Method: "GET",
Method: http.MethodGet,
URL: "/api/dashboards/uid/{uid}",
Handler: h.DashboardJSONGetHandler(s),
},
{
Method: "POST",
Method: http.MethodPost,
URL: "/api/dashboards/db",
Handler: h.DashboardJSONPostHandler(s),
},
{
Method: "POST",
Method: http.MethodPost,
URL: "/api/dashboards/db/",
Handler: h.DashboardJSONPostHandler(s),
},
Expand All @@ -294,64 +294,17 @@ func (h *DashboardHandler) resourceFromQueryParameterMiddleware(s grizzly.Server
}
}

func (h *DashboardHandler) RootDashboardPageHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
config := h.Provider.(ClientProvider).Config()
if config.URL == "" {
grizzly.SendError(w, "Error: No Grafana URL configured", fmt.Errorf("no Grafana URL configured"), 400)
return
}
req, err := http.NewRequest("GET", config.URL+r.URL.Path, nil)
if err != nil {
grizzly.SendError(w, http.StatusText(500), err, 500)
return
}

if config.User != "" {
req.SetBasicAuth(config.User, config.Token)
} else if config.Token != "" {
req.Header.Set("Authorization", "Bearer "+config.Token)
}

req.Header.Set("User-Agent", s.UserAgent)

client := &http.Client{}
resp, err := client.Do(req)

if err == nil {
body, _ := io.ReadAll(resp.Body)
writeOrLog(w, body)
return
}

msg := ""
if config.Token == "" {
msg += "<p><b>Warning:</b> No service account token specified.</p>"
}

if resp.StatusCode == 302 {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, "%s<p>Authentication error</p>", msg)
} else {
body, _ := io.ReadAll(resp.Body)
w.WriteHeader(resp.StatusCode)
fmt.Fprintf(w, "%s%s", msg, string(body))
}
}
}

func (h *DashboardHandler) DashboardJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid := chi.URLParam(r, "uid")
if uid == "" {
grizzly.SendError(w, "No UID specified", fmt.Errorf("no UID specified within the URL"), 400)
grizzly.SendError(w, "No UID specified", fmt.Errorf("no UID specified within the URL"), http.StatusBadRequest)
return
}

resource, found := s.Resources.Find(grizzly.NewResourceRef("Dashboard", uid))
if !found {
grizzly.SendError(w, fmt.Sprintf("Dashboard with UID %s not found", uid), fmt.Errorf("dashboard with UID %s not found", uid), 404)
grizzly.SendError(w, fmt.Sprintf("Dashboard with UID %s not found", uid), fmt.Errorf("dashboard with UID %s not found", uid), http.StatusNotFound)
return
}
if resource.GetSpecValue("version") == nil {
Expand Down Expand Up @@ -379,26 +332,26 @@ func (h *DashboardHandler) DashboardJSONPostHandler(s grizzly.Server) http.Handl
content, _ := io.ReadAll(r.Body)
err := json.Unmarshal(content, &resp)
if err != nil {
grizzly.SendError(w, "Error parsing JSON", err, 400)
grizzly.SendError(w, "Error parsing JSON", err, http.StatusBadRequest)
return
}
uid, ok := resp.Dashboard["uid"].(string)
if !ok || uid == "" {
grizzly.SendError(w, "Dashboard has no UID", fmt.Errorf("dashboard has no UID"), 400)
grizzly.SendError(w, "Dashboard has no UID", fmt.Errorf("dashboard has no UID"), http.StatusBadRequest)
return
}
resource, ok := s.Resources.Find(grizzly.NewResourceRef(h.Kind(), uid))
if !ok {
err := fmt.Errorf("unknown dashboard: %s", uid)
grizzly.SendError(w, err.Error(), err, 400)
grizzly.SendError(w, err.Error(), err, http.StatusBadRequest)
return
}

resource.SetSpec(resp.Dashboard)

err = s.UpdateResource(uid, resource)
if err != nil {
grizzly.SendError(w, err.Error(), err, 500)
grizzly.SendError(w, err.Error(), err, http.StatusInternalServerError)
return
}
jout := map[string]interface{}{
Expand Down
74 changes: 71 additions & 3 deletions pkg/grafana/datasource-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import (
"strconv"
"strings"

"github.com/go-chi/chi"
"github.com/go-openapi/runtime"
"github.com/grafana/grafana-openapi-client-go/client/datasources"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grizzly/pkg/grizzly"
)

const DatasourceKind = "Datasource"

// DatasourceHandler is a Grizzly Handler for Grafana datasources
type DatasourceHandler struct {
grizzly.BaseHandler
Expand All @@ -23,7 +26,7 @@ type DatasourceHandler struct {
// NewDatasourceHandler returns a new Grizzly Handler for Grafana datasources
func NewDatasourceHandler(provider grizzly.Provider) *DatasourceHandler {
return &DatasourceHandler{
BaseHandler: grizzly.NewBaseHandler(provider, "Datasource", false),
BaseHandler: grizzly.NewBaseHandler(provider, DatasourceKind, false),
}
}

Expand Down Expand Up @@ -77,6 +80,67 @@ func (h *DatasourceHandler) GetSpecUID(resource grizzly.Resource) (string, error
}
}

func (h *DatasourceHandler) ProxyURL(uid string) string {
return fmt.Sprintf("/connections/datasources/edit/%s", uid)
}

func (h *DatasourceHandler) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
URL: "/connections/datasources/edit/{uid}",
Handler: authenticateAndProxyHandler(s, h.Provider),
},
{
Method: http.MethodGet,
URL: "/api/datasources/uid/{uid}",
Handler: h.DatasourceJSONGetHandler(s),
},
}
}

func (h *DatasourceHandler) DatasourceJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid := chi.URLParam(r, "uid")
if uid == "" {
grizzly.SendError(w, "No UID specified", fmt.Errorf("no UID specified within the URL"), http.StatusBadRequest)
return
}

resource, found := s.Resources.Find(grizzly.NewResourceRef(DatasourceKind, uid))
if !found {
grizzly.SendError(w, fmt.Sprintf("Datasource with UID %s not found", uid), fmt.Errorf("datasource with UID %s not found", uid), http.StatusNotFound)
return
}

// These values are required for the page to load properly.
if resource.GetSpecValue("version") == nil {
resource.SetSpecValue("version", 1)
}
if resource.GetSpecValue("id") == nil {
resource.SetSpecValue("id", 1)
}

// we don't support saving datasources via the proxy yet
resource.SetSpecValue("readOnly", true)

// to remove some "missing permissions warning" and enable some features
resource.SetSpecValue("accessControl", map[string]any{
"datasources.caching:read": true,
"datasources.caching:write": false,
"datasources.id:read": true,
"datasources.permissions:read": true,
"datasources.permissions:write": true,
"datasources:delete": false,
"datasources:query": true,
"datasources:read": true,
"datasources:write": true,
})

writeJSONOrLog(w, resource.Spec())
}
}

// GetByUID retrieves JSON for a resource from an endpoint, by UID
func (h *DatasourceHandler) GetByUID(uid string) (*grizzly.Resource, error) {
return h.getRemoteDatasource(uid)
Expand Down Expand Up @@ -151,11 +215,12 @@ func (h *DatasourceHandler) getRemoteDatasourceList() ([]string, error) {
return nil, err
}

datasourcesOk, err := client.Datasources.GetDataSources()
response, err := client.Datasources.GetDataSources()
if err != nil {
return nil, err
}
datasources := datasourcesOk.GetPayload()

datasources := response.GetPayload()

uids := make([]string, len(datasources))
for i, datasource := range datasources {
Expand All @@ -176,10 +241,12 @@ func (h *DatasourceHandler) postDatasource(resource grizzly.Resource) error {
if err != nil {
return err
}

client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return err
}

_, err = client.Datasources.AddDataSource(&datasource, nil)
return err
}
Expand All @@ -202,6 +269,7 @@ func (h *DatasourceHandler) putDatasource(resource grizzly.Resource) error {
if err != nil {
return err
}

client, err := h.Provider.(ClientProvider).Client()
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions pkg/grafana/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ func writeJSONOrLog(w http.ResponseWriter, content any) {
log.Errorf("error marshalling response to JSON: %v", err)
}

w.Header().Set("Content-Type", "application/json")
writeOrLog(w, responseJSON)
}
53 changes: 53 additions & 0 deletions pkg/grafana/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package grafana

import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"

gclient "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grizzly/pkg/grizzly"
)

var (
Expand Down Expand Up @@ -45,3 +49,52 @@ func structToMap(s interface{}) (map[string]interface{}, error) {

return result, nil
}

func authenticateAndProxyHandler(s grizzly.Server, provider grizzly.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")

config := provider.(ClientProvider).Config()
if config.URL == "" {
grizzly.SendError(w, "Error: No Grafana URL configured", fmt.Errorf("no Grafana URL configured"), 400)
return
}

req, err := http.NewRequest(http.MethodGet, config.URL+r.URL.Path, nil)
if err != nil {
grizzly.SendError(w, http.StatusText(http.StatusInternalServerError), err, http.StatusInternalServerError)
return
}

if config.User != "" {
req.SetBasicAuth(config.User, config.Token)
} else if config.Token != "" {
req.Header.Set("Authorization", "Bearer "+config.Token)
}

req.Header.Set("User-Agent", s.UserAgent)

client := &http.Client{}
resp, err := client.Do(req)

if err == nil {
body, _ := io.ReadAll(resp.Body)
writeOrLog(w, body)
return
}

msg := ""
if config.Token == "" {
msg += "<p><b>Warning:</b> No service account token specified.</p>"
}

if resp.StatusCode == http.StatusFound {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, "%s<p>Authentication error</p>", msg)
} else {
body, _ := io.ReadAll(resp.Body)
w.WriteHeader(resp.StatusCode)
fmt.Fprintf(w, "%s%s", msg, string(body))
}
}
}
31 changes: 22 additions & 9 deletions pkg/grizzly/embed/templates/proxy/index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<main>
<div>
{{ if ne (len .ParseErrors) 0 }}
{{ if ne (len .ParseErrors) 0 }}
<h1>Errors</h1>

{{ range .ParseErrors }}
Expand All @@ -20,18 +20,31 @@
</li>
{{ end }}
{{ end }}
{{ end }}
<h1>Available dashboards</h1>
{{ end }}

<h1>Dashboards</h1>

<ul>
{{ range .Resources }}
{{ if eq .Kind "Dashboard" }}
<ul>
{{ range (.Resources.OfKind "Dashboard").AsList }}
<li>
<a href="/grizzly/{{.Kind}}/{{.Name}}">{{ .Spec.title }}</a>
<a href="/grizzly/{{ .Kind }}/{{ .Name }}">{{ .Spec.title }}</a>
</li>
{{ else }}
<li>No dashboards.</li>
{{ end }}
{{ end }}
</ul>
</ul>

<h1>Datasources</h1>

<ul>
{{ range (.Resources.OfKind "Datasource").AsList }}
<li>
<a href="/grizzly/{{ .Kind }}/{{ .Name }}">{{ .Spec.name }}</a>
</li>
{{ else }}
<li>No datasources.</li>
{{ end }}
</ul>
</div>
</main>
</body>
Expand Down
6 changes: 6 additions & 0 deletions pkg/grizzly/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ func (r Resources) Filter(predicate func(Resource) bool) Resources {
return NewResources(filtered...)
}

func (r Resources) OfKind(kind string) Resources {
return r.Filter(func(resource Resource) bool {
return resource.Kind() == kind
})
}

func (r Resources) ForEach(callback func(Resource) error) error {
for pair := r.collection.Oldest(); pair != nil; pair = pair.Next() {
if err := callback(pair.Value); err != nil {
Expand Down
Loading

0 comments on commit e9b18b5

Please sign in to comment.