Skip to content

Commit

Permalink
Let resource handlers configure specific routes to proxy/mock
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Nov 15, 2024
1 parent 3742e9f commit 5651865
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 71 deletions.
13 changes: 12 additions & 1 deletion pkg/grafana/alertgroup-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (c *alertRuleProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/alerting/grafana/%s/view", uid)
}

func (c *alertRuleProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
func (c *alertRuleProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
Expand All @@ -41,6 +41,17 @@ func (c *alertRuleProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizz
}
}

func (c *alertRuleProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{
ProxyGet: []string{
"/api/v1/ngalert",
},
ProxyPost: []string{
"/api/v1/eval",
},
}
}

func (c *alertRuleProxyConfigurator) alertRuleGroupJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
folderUID := chi.URLParam(r, "folder_uid")
Expand Down
22 changes: 20 additions & 2 deletions pkg/grafana/dashboard-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (c *dashboardProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/d/%s/slug", uid)
}

func (c *dashboardProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
func (c *dashboardProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
Expand All @@ -48,6 +48,24 @@ func (c *dashboardProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizz
}
}

func (c *dashboardProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{
ProxyGet: []string{
"/api/datasources/proxy/*",
"/api/datasources/*",
"/api/plugins/*",
},
ProxyPost: []string{
"/api/datasources/proxy/*",
"/api/ds/query",
},
MockGet: map[string]string{
"/api/annotations": "[]",
"/api/access-control/user/actions": `{"dashboards:write": true}`,
},
}
}

func (c *dashboardProxyConfigurator) resourceFromQueryParameterMiddleware(s grizzly.Server, parameterName string, next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if fromFilePath := r.URL.Query().Get(parameterName); fromFilePath != "" {
Expand Down Expand Up @@ -115,7 +133,7 @@ func (c *dashboardProxyConfigurator) dashboardJSONPostHandler(s grizzly.Server)

resource.SetSpec(resp.Dashboard)

if err := s.UpdateResource(uid, resource); err != nil {
if err := s.UpdateResource(resource); err != nil {
httputils.Error(w, err.Error(), err, http.StatusInternalServerError)
return
}
Expand Down
14 changes: 13 additions & 1 deletion pkg/grafana/datasource-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (c *datasourceProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/connections/datasources/edit/%s", uid)
}

func (c *datasourceProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
func (c *datasourceProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
Expand All @@ -35,6 +35,18 @@ func (c *datasourceProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []griz
}
}

func (c *datasourceProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{
ProxyGet: []string{
"/api/instance/plugins",
"/api/instance/provisioned-plugins",
"/api/plugins",
"/api/plugin-proxy/*",
"/api/usage/datasource/*",
},
}
}

func (c *datasourceProxyConfigurator) datasourceJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid := chi.URLParam(r, "uid")
Expand Down
6 changes: 5 additions & 1 deletion pkg/grafana/folder-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (c *folderProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/dashboards/f/%s/", uid)
}

func (c *folderProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
func (c *folderProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: http.MethodGet,
Expand All @@ -35,6 +35,10 @@ func (c *folderProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.
}
}

func (c *folderProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{}
}

func (c *folderProxyConfigurator) folderJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
folderUID := chi.URLParam(r, "folder_uid")
Expand Down
6 changes: 5 additions & 1 deletion pkg/grafana/library-element-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (c *libraryElementProxyConfigurator) ProxyURL(uid string) string {
return fmt.Sprintf("/api/library-elements/%s", uid)
}

func (c *libraryElementProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
func (c *libraryElementProxyConfigurator) Endpoints(s grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: "GET",
Expand All @@ -31,6 +31,10 @@ func (c *libraryElementProxyConfigurator) GetProxyEndpoints(s grizzly.Server) []
}
}

func (c *libraryElementProxyConfigurator) StaticEndpoints() grizzly.StaticProxyConfig {
return grizzly.StaticProxyConfig{}
}

func (c *libraryElementProxyConfigurator) libraryElementJSONGetHandler(s grizzly.Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid := chi.URLParam(r, "uid")
Expand Down
37 changes: 32 additions & 5 deletions pkg/grizzly/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ type Handler interface {
// Prepare gets a resource ready for dispatch to the remote endpoint
Prepare(existing *Resource, resource Resource) *Resource

// Retrieves a UID for a resource
// GetUID retrieves a UID for a resource
GetUID(resource Resource) (string, error)

// GetSpecUID retrieves a UID from the spec of a raw resource
GetSpecUID(resource Resource) (string, error)

// Get retrieves JSON for a resource from an endpoint, by UID
// GetByUID retrieves JSON for a resource from an endpoint, by UID
GetByUID(UID string) (*Resource, error)

// GetRemote retrieves a remote equivalent of a remote resource
Expand All @@ -94,7 +94,7 @@ type Handler interface {
// UsesFolders identifies whether this resource lives within a folder
UsesFolders() bool

// Detects whether a spec-only resource is of this kind
// Detect whether a spec-only resource is of this kind
Detect(map[string]any) bool
}

Expand Down Expand Up @@ -124,12 +124,39 @@ type HTTPEndpoint struct {
Handler http.HandlerFunc
}

// StaticProxyConfig holds some static configuration to apply to the proxy.
// This allows resource handlers to declare routes to proxy or mock that are
// specific to them.
type StaticProxyConfig struct {
// ProxyGet holds a list of routes to proxy when using the GET HTTP
// method.
// Example: /public/*
ProxyGet []string

// ProxyPost holds a list of routes to proxy when using the POST HTTP
// method.
// Example: /api/v1/eval
ProxyPost []string

// MockGet holds a map associating URLs to a mock response that they should
// return for GET requests.
// Note: the response is expected to be JSON.
MockGet map[string]string

// MockPost holds a map associating URLs to a mock response that they should
// return for POST requests.
// Note: the response is expected to be JSON.
MockPost map[string]string
}

// ProxyConfigurator describes a proxy endpoints that can be used to view/edit
// resources live via a proxied UI.
type ProxyConfigurator interface {
// GetProxyEndpoints registers HTTP handlers for proxy events
GetProxyEndpoints(p Server) []HTTPEndpoint
// Endpoints registers HTTP handlers for proxy events
Endpoints(p Server) []HTTPEndpoint

// ProxyURL returns a URL path for a resource on the proxy
ProxyURL(uid string) string

StaticEndpoints() StaticProxyConfig
}
115 changes: 55 additions & 60 deletions pkg/grizzly/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,59 +100,61 @@ func (s *Server) SetFormatting(onlySpec bool, outputFormat string) {
s.OutputFormat = outputFormat
}

var mustProxyGET = []string{
"/public/*",
"/api/datasources/proxy/*",
"/api/datasources/*",
"/api/plugins",
"/api/plugins/*",
"/api/plugin-proxy/*",
"/api/instance/plugins",
"/api/instance/provisioned-plugins",
"/api/usage/datasource/*",
"/api/v1/ngalert",
"/avatar/*",
}
var mustProxyPOST = []string{
"/api/datasources/proxy/*",
"/api/ds/query",
"/api/v1/eval",
}
var blockJSONget = map[string]string{
"/api/ma/events": "[]",
"/api/live/publish": "[]",
"/api/live/list": "[]",
"/api/user/orgs": "[]",
"/api/annotations": "[]",
"/api/search": "[]",
"/api/usage/*": "[]",
"/api/frontend/assets": "{}",
"/api/org/preferences": "{}",

"/api/access-control/user/actions": `{"dashboards:write": true}`,
"/api/prometheus/grafana/api/v1/rules": `{
func (s *Server) staticProxyConfig() StaticProxyConfig {
return StaticProxyConfig{
ProxyGet: []string{
"/public/*",
"/avatar/*",
},
MockGet: map[string]string{
"/api/ma/events": "[]",
"/api/live/publish": "[]",
"/api/live/list": "[]",
"/api/user/orgs": "[]",
"/api/search": "[]",
"/api/usage/*": "[]",
"/api/frontend/assets": "{}",
"/api/org/preferences": "{}",

"/api/prometheus/grafana/api/v1/rules": `{
"status": "success",
"data": { "groups": [] }
}`,
"/api/folders": "[]",
"/api/recording-rules/writer": `{
"/api/folders": "[]",
"/api/recording-rules/writer": `{
"id": "cojWep7Vz",
"data_source_uid": "grafanacloud-prom",
"remote_write_path": "/api/prom/push"
}`,

"/apis/banners.grafana.app/v0alpha1/namespaces/{stack}/announcement-banners": `{
"/apis/banners.grafana.app/v0alpha1/namespaces/{stack}/announcement-banners": `{
"kind": "AnnouncementBannerList",
"apiVersion": "banners.grafana.app/v0alpha1",
"metadata": {"resourceVersion": "29"}
}`,
},
MockPost: map[string]string{
"/api/frontend-metrics": "[]",
"/api/search-v2": "[]",
"/api/live/publish": "{}",
"/api/ma/events": "null",
},
}
}

var blockJSONpost = map[string]string{
"/api/frontend-metrics": "[]",
"/api/search-v2": "[]",
"/api/live/publish": "{}",
"/api/ma/events": "null",
func (s *Server) applyStaticProxyConfig(r chi.Router, config StaticProxyConfig) {
for _, pattern := range config.ProxyGet {
r.Get(pattern, s.ProxyRequestHandler)
}
for _, pattern := range config.ProxyPost {
r.Post(pattern, s.ProxyRequestHandler)
}
for pattern, response := range config.MockGet {
r.Get(pattern, s.mockHandler(response))
}
for pattern, response := range config.MockPost {
r.Post(pattern, s.mockHandler(response))
}
}

func (s *Server) Start() error {
Expand All @@ -171,6 +173,8 @@ func (s *Server) Start() error {
r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: logger.DecorateAtLevel(log.StandardLogger(), log.DebugLevel), NoColor: !color}))
r.Handle("/grizzly/assets/*", http.StripPrefix("/grizzly/assets/", http.FileServer(http.FS(assetsFS))))

s.applyStaticProxyConfig(r, s.staticProxyConfig())

for _, handler := range s.Registry.Handlers {
proxyConfigProvider, ok := handler.(ProxyConfiguratorProvider)
if !ok {
Expand All @@ -180,7 +184,7 @@ func (s *Server) Start() error {
log.WithField("handler", handler.Kind()).Debug("registering proxy configuration")

proxyConfig := proxyConfigProvider.ProxyConfigurator()
for _, endpoint := range proxyConfig.GetProxyEndpoints(*s) {
for _, endpoint := range proxyConfig.Endpoints(*s) {
switch endpoint.Method {
case http.MethodGet:
r.Get(endpoint.URL, endpoint.Handler)
Expand All @@ -190,19 +194,10 @@ func (s *Server) Start() error {
return fmt.Errorf("unknown endpoint method %s for handler %s", endpoint.Method, handler.Kind())
}
}

s.applyStaticProxyConfig(r, proxyConfig.StaticEndpoints())
}
for _, pattern := range mustProxyGET {
r.Get(pattern, s.ProxyRequestHandler)
}
for _, pattern := range mustProxyPOST {
r.Post(pattern, s.ProxyRequestHandler)
}
for pattern, response := range blockJSONget {
r.Get(pattern, s.blockHandler(response))
}
for pattern, response := range blockJSONpost {
r.Post(pattern, s.blockHandler(response))
}

r.Get("/", s.rootHandler)
r.Get("/grizzly/{kind}/{name}", s.iframeHandler)
r.Get("/livereload", livereload.Handler(upgrader))
Expand Down Expand Up @@ -248,7 +243,7 @@ func (s *Server) Start() error {
}
}

fmt.Printf("Listening on %s\n", s.url("/"))
log.Infof("Listening on %s\n", s.url("/"))
return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
}

Expand Down Expand Up @@ -349,7 +344,7 @@ func (s *Server) executeWatchScript() ([]byte, error) {
return stdout.Bytes(), nil
}

func (s *Server) blockHandler(response string) http.HandlerFunc {
func (s *Server) mockHandler(response string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -409,25 +404,25 @@ func (s *Server) rootHandler(w http.ResponseWriter, _ *http.Request) {
"CurrentContext": s.CurrentContext,
}
if err := templates.ExecuteTemplate(w, "proxy/index.html.tmpl", templateVars); err != nil {
httputils.Error(w, "Error while executing template", err, 500)
httputils.Error(w, "Error while executing template", err, http.StatusInternalServerError)
return
}
}

func (s *Server) UpdateResource(name string, resource Resource) error {
func (s *Server) UpdateResource(resource Resource) error {
out, _, _, err := Format(s.Registry, s.ResourcePath, &resource, resource.Source.Format, !resource.Source.WithEnvelope)
if err != nil {
return fmt.Errorf("error formatting content: %s", err)
}

existing, found := s.Resources.Find(NewResourceRef("Dashboard", name))
existing, found := s.Resources.Find(resource.Ref())
if !found {
return fmt.Errorf("dashboard with UID %s not found", name)
return fmt.Errorf("%s not found", resource.Ref())
}

if !existing.Source.Rewritable {
return fmt.Errorf("the source for this dashboard is not rewritable")
return fmt.Errorf("the source for this %s is not rewritable", resource.Kind())
}

return os.WriteFile(existing.Source.Path, out, 0644)
return WriteFile(existing.Source.Path, out)
}

0 comments on commit 5651865

Please sign in to comment.