Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let resource handlers configure specific routes to proxy/mock #528

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Loading