diff --git a/pkg/grafana/alertgroup-proxy.go b/pkg/grafana/alertgroup-proxy.go index 2b29f578..dd6f97dc 100644 --- a/pkg/grafana/alertgroup-proxy.go +++ b/pkg/grafana/alertgroup-proxy.go @@ -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, @@ -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") diff --git a/pkg/grafana/dashboard-proxy.go b/pkg/grafana/dashboard-proxy.go index 11358de3..b2b2f200 100644 --- a/pkg/grafana/dashboard-proxy.go +++ b/pkg/grafana/dashboard-proxy.go @@ -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, @@ -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 != "" { @@ -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 } diff --git a/pkg/grafana/datasource-proxy.go b/pkg/grafana/datasource-proxy.go index 226e7953..bef441d6 100644 --- a/pkg/grafana/datasource-proxy.go +++ b/pkg/grafana/datasource-proxy.go @@ -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, @@ -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") diff --git a/pkg/grafana/folder-proxy.go b/pkg/grafana/folder-proxy.go index ef40a3ee..d7cf2cdf 100644 --- a/pkg/grafana/folder-proxy.go +++ b/pkg/grafana/folder-proxy.go @@ -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, @@ -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") diff --git a/pkg/grafana/library-element-proxy.go b/pkg/grafana/library-element-proxy.go index 11ab9a61..5193bd44 100644 --- a/pkg/grafana/library-element-proxy.go +++ b/pkg/grafana/library-element-proxy.go @@ -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", @@ -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") diff --git a/pkg/grizzly/handler.go b/pkg/grizzly/handler.go index 88bc2ea2..6081d9b9 100644 --- a/pkg/grizzly/handler.go +++ b/pkg/grizzly/handler.go @@ -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 @@ -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 } @@ -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 } diff --git a/pkg/grizzly/server.go b/pkg/grizzly/server.go index 2010ef58..b0355a39 100644 --- a/pkg/grizzly/server.go +++ b/pkg/grizzly/server.go @@ -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 { @@ -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 { @@ -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) @@ -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)) @@ -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) } @@ -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) @@ -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) }