diff --git a/cmd/grr/config.go b/cmd/grr/config.go index 948595c6..d344f089 100644 --- a/cmd/grr/config.go +++ b/cmd/grr/config.go @@ -33,6 +33,7 @@ type Opts struct { ProxyPort int CanSave bool Watch bool + WatchScript string } func configPathCmd() *cli.Command { diff --git a/cmd/grr/workflow.go b/cmd/grr/workflow.go index 0b3a9eb4..a99faec4 100644 --- a/cmd/grr/workflow.go +++ b/cmd/grr/workflow.go @@ -376,12 +376,16 @@ func serveCmd(registry grizzly.Registry) *cli.Command { } resourcesPath := "" - if len(args) > 0 { - resourcesPath = args[0] - } watchPaths := []string{resourcesPath} - if len(args) > 1 { - watchPaths = args[1:] + if opts.WatchScript != "" { + watchPaths = args + } else { + if len(args) > 0 { + resourcesPath = args[0] + } + if len(args) > 1 { + watchPaths = args[1:] + } } targets := currentContext.GetTargets(opts.Targets) @@ -405,6 +409,9 @@ func serveCmd(registry grizzly.Registry) *cli.Command { server.SetFormatting(onlySpec, format) if opts.Watch { server.Watch(watchPaths) + if opts.WatchScript != "" { + server.WatchScript(opts.WatchScript) + } } if opts.OpenBrowser { server.OpenBrowser() @@ -414,6 +421,7 @@ func serveCmd(registry grizzly.Registry) *cli.Command { cmd.Flags().BoolVarP(&opts.Watch, "watch", "w", false, "Watch filesystem for changes") cmd.Flags().BoolVarP(&opts.OpenBrowser, "open-browser", "b", false, "Open Grizzly in default browser") cmd.Flags().IntVarP(&opts.ProxyPort, "port", "p", 8080, "Port on which the server will listen") + cmd.Flags().StringVarP(&opts.WatchScript, "script", "S", "", "Script to execute on filesystem change") cmd = initialiseOnlySpec(cmd, &opts) return initialiseCmd(cmd, &opts) } diff --git a/docs/content/server.md b/docs/content/server.md index 40d4e824..bceb5640 100644 --- a/docs/content/server.md +++ b/docs/content/server.md @@ -64,19 +64,13 @@ grr serve -w examples/grr.jsonnet examples/*.*sonnet examples/vendor ### Reviewing changes to code in other languages in Grafana The [Grafana Foundation SDK](https://github.com/grafana/grafana-foundation-sdk) provides libraries in a range of languages that can be used to render Grafana dashboards. Watching changes to these with Grizzly -is a two stage process, currently requiring an additional tool to watch for changes to source code and -render your dashboard(s) to files. One such tool is [entr](https://github.com/eradman/entr), which can be -used like so (with the Foundation SDK's TypeScript support): +is also possible. ``` git clone https://github.com/grafana/grafana-foundation-sdk cd grafana-foundation-sdk/examples/typescript/red-method npm install -find . | entr -s 'npm run -s dev > ts.json' -``` -Then, in another window: -``` -grr serve -w ts.json +grr serve -w -S 'npm run -s dev' . ``` Finally, open the Grizzly server at [http://localhost:8080](http://localhost:8080) and select the Red Method dashboard. diff --git a/examples/array-of-resources.jsonnet b/examples/array-of-resources.jsonnet new file mode 100644 index 00000000..4af815e7 --- /dev/null +++ b/examples/array-of-resources.jsonnet @@ -0,0 +1,14 @@ +local dashboard(uid, title) = { + uid: uid, + title: title, + tags: ['templated'], + timezone: 'browser', + schemaVersion: 17, + panels: [], +}; + +[ + dashboard("dashboard-1", "Dashboard 1"), + dashboard("dashboard-2", "Dashboard 2"), + dashboard("dashboard-3", "Dashboard 3"), +] diff --git a/pkg/grizzly/server.go b/pkg/grizzly/server.go index 04680b75..1ee0b39e 100644 --- a/pkg/grizzly/server.go +++ b/pkg/grizzly/server.go @@ -1,12 +1,14 @@ package grizzly import ( + "bytes" "errors" "fmt" "io/fs" "net/http" "net/http/httputil" "os" + "os/exec" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -31,6 +33,7 @@ type Server struct { UserAgent string ResourcePath string WatchPaths []string + watchScript string OnlySpec bool OutputFormat string watch bool @@ -85,6 +88,10 @@ func (s *Server) Watch(watchPaths []string) { s.WatchPaths = watchPaths } +func (s *Server) WatchScript(script string) { + s.watchScript = script +} + func (s *Server) SetFormatting(onlySpec bool, outputFormat string) { s.OnlySpec = onlySpec s.OutputFormat = outputFormat @@ -174,10 +181,19 @@ func (s *Server) Start() error { r.Get("/grizzly/{kind}/{name}", s.IframeHandler) r.Get("/api/live/ws", livereload.LiveReloadHandlerFunc(upgrader)) - if _, err := s.ParseResources(s.ResourcePath); err != nil { + if s.watchScript != "" { + var b []byte + b, err = s.executeWatchScript() + if err != nil { + return err + } + _, err = s.ParseBytes(b) + } else { + _, err = s.ParseResources(s.ResourcePath) + } + if err != nil { fmt.Print(err) } - if s.openBrowser { browser, err := NewBrowserInterface(s.Registry, s.ResourcePath, s.port) if err != nil { @@ -217,6 +233,26 @@ func (s *Server) ParseResources(resourcePath string) (Resources, error) { return resources, err } +func (s *Server) ParseBytes(b []byte) (Resources, error) { + f, err := os.CreateTemp(".", fmt.Sprintf("*.%s", s.OutputFormat)) + if err != nil { + return Resources{}, err + } + defer os.Remove(f.Name()) + _, err = f.Write(b) + if err != nil { + return Resources{}, err + } + err = f.Close() + if err != nil { + return Resources{}, err + } + resources, err := s.parser.Parse(f.Name(), s.parserOpts) + s.parserErr = err + s.Resources.Merge(resources) + return resources, err +} + func (s *Server) URL(path string) string { if len(path) == 0 || path[0] != '/' { path = "/" + path @@ -226,10 +262,19 @@ func (s *Server) URL(path string) string { } func (s *Server) updateWatchedResource(name string) error { - if !s.parser.Accept(name) { - return nil + var resources Resources + var err error + + if s.watchScript != "" { + var b []byte + b, err = s.executeWatchScript() + if err != nil { + return err + } + resources, err = s.ParseBytes(b) + } else { + resources, err = s.ParseResources(s.ResourcePath) } - resources, err := s.ParseResources(s.ResourcePath) if errors.As(err, &UnrecognisedFormatError{}) { uerr := err.(UnrecognisedFormatError) log.Printf("Skipping %s", uerr.File) @@ -239,6 +284,7 @@ func (s *Server) updateWatchedResource(name string) error { log.Error("Error: ", err) return err } + for _, resource := range resources.AsList() { handler, err := s.Registry.GetHandler(resource.Kind()) if err != nil { @@ -257,6 +303,22 @@ func (s *Server) updateWatchedResource(name string) error { return nil } +func (s *Server) executeWatchScript() ([]byte, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := exec.Command("sh", "-c", s.watchScript) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return nil, err + } + if stderr.Len() > 0 { + log.Errorf("%s", stderr.String()) + } + return stdout.Bytes(), nil +} + func (s *Server) blockHandler(response string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")