diff --git a/pkg/grafana/dashboard-handler.go b/pkg/grafana/dashboard-handler.go
index d6d1dbca..c2860940 100644
--- a/pkg/grafana/dashboard-handler.go
+++ b/pkg/grafana/dashboard-handler.go
@@ -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),
},
@@ -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 += "
Warning: No service account token specified.
"
- }
-
- if resp.StatusCode == 302 {
- w.WriteHeader(http.StatusUnauthorized)
- fmt.Fprintf(w, "%sAuthentication error
", 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 {
@@ -379,18 +332,18 @@ 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
}
@@ -398,7 +351,7 @@ func (h *DashboardHandler) DashboardJSONPostHandler(s grizzly.Server) http.Handl
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{}{
diff --git a/pkg/grafana/datasource-handler.go b/pkg/grafana/datasource-handler.go
index 1c550da4..85151e6e 100644
--- a/pkg/grafana/datasource-handler.go
+++ b/pkg/grafana/datasource-handler.go
@@ -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
@@ -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),
}
}
@@ -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)
@@ -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 {
@@ -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
}
@@ -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
diff --git a/pkg/grafana/errors.go b/pkg/grafana/errors.go
index 625e671f..54c58dee 100644
--- a/pkg/grafana/errors.go
+++ b/pkg/grafana/errors.go
@@ -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)
}
diff --git a/pkg/grafana/utils.go b/pkg/grafana/utils.go
index d7120a46..2a46c13d 100644
--- a/pkg/grafana/utils.go
+++ b/pkg/grafana/utils.go
@@ -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 (
@@ -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"), http.StatusBadRequest)
+ 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 += "Warning: No service account token specified.
"
+ }
+
+ if resp.StatusCode == http.StatusFound {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprintf(w, "%sAuthentication error
", msg)
+ } else {
+ body, _ := io.ReadAll(resp.Body)
+ w.WriteHeader(resp.StatusCode)
+ fmt.Fprintf(w, "%s%s", msg, string(body))
+ }
+ }
+}
diff --git a/pkg/grizzly/embed/templates/proxy/index.html.tmpl b/pkg/grizzly/embed/templates/proxy/index.html.tmpl
index 641e48a7..544f293e 100644
--- a/pkg/grizzly/embed/templates/proxy/index.html.tmpl
+++ b/pkg/grizzly/embed/templates/proxy/index.html.tmpl
@@ -10,7 +10,7 @@
- {{ if ne (len .ParseErrors) 0 }}
+ {{ if ne (len .ParseErrors) 0 }}
Errors
{{ range .ParseErrors }}
@@ -20,18 +20,31 @@
{{ end }}
{{ end }}
- {{ end }}
-
Available dashboards
+ {{ end }}
+
+
Dashboards
-
- {{ range .Resources }}
- {{ if eq .Kind "Dashboard" }}
+
+
+
+
Datasources
+
+
+ {{ range (.Resources.OfKind "Datasource").AsList }}
+ -
+ {{ .Spec.name }}
+
+ {{ else }}
+ - No datasources.
+ {{ end }}
+