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

Add live reload functionality #386

Merged
merged 11 commits into from
May 1, 2024
1 change: 1 addition & 0 deletions cmd/grr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Opts struct {
OpenBrowser bool
ProxyPort int
CanSave bool
Watch bool
}

func configPathCmd() *cli.Command {
Expand Down
38 changes: 8 additions & 30 deletions cmd/grr/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,26 +276,6 @@ func applyCmd(registry grizzly.Registry) *cli.Command {
return initialiseCmd(cmd, &opts)
}

type jsonnetWatchParser struct {
resourcePath string
registry grizzly.Registry
resourceKind string
folderUID string
targets []string
jsonnetPaths []string
}

func (p *jsonnetWatchParser) Name() string {
return p.resourcePath
}

func (p *jsonnetWatchParser) Parse() (grizzly.Resources, error) {
return grizzly.DefaultParser(p.registry, p.targets, p.jsonnetPaths, grizzly.ParserContinueOnError(true)).Parse(p.resourcePath, grizzly.ParserOptions{
DefaultResourceKind: p.resourceKind,
DefaultFolderUID: p.folderUID,
})
}

func watchCmd(registry grizzly.Registry) *cli.Command {
cmd := &cli.Command{
Use: "watch <dir-to-watch> <resource-path>",
Expand All @@ -315,20 +295,17 @@ func watchCmd(registry grizzly.Registry) *cli.Command {
return err
}
targets := currentContext.GetTargets(opts.Targets)
parser := &jsonnetWatchParser{
resourcePath: args[1],
registry: registry,
resourceKind: resourceKind,
folderUID: folderUID,
targets: targets,
jsonnetPaths: opts.JsonnetPaths,
}

watchDir := args[0]

trailRecorder := grizzly.NewWriterRecorder(os.Stdout, grizzly.EventToPlainText)

return grizzly.Watch(registry, watchDir, parser, trailRecorder)
parser := grizzly.DefaultParser(registry, targets, opts.JsonnetPaths, grizzly.ParserContinueOnError(true))
parserOpts := grizzly.ParserOptions{
DefaultResourceKind: resourceKind,
DefaultFolderUID: folderUID,
}
return grizzly.Watch(registry, watchDir, parser, parserOpts, trailRecorder)
}
return initialiseCmd(cmd, &opts)
}
Expand Down Expand Up @@ -414,8 +391,9 @@ func serveCmd(registry grizzly.Registry) *cli.Command {
return err
}

return grizzly.Serve(registry, parser, parserOpts, resourcesPath, opts.ProxyPort, opts.OpenBrowser, onlySpec, format)
return grizzly.Serve(registry, parser, parserOpts, resourcesPath, opts.ProxyPort, opts.OpenBrowser, opts.Watch, onlySpec, format)
}
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 = initialiseOnlySpec(cmd, &opts)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/go-openapi/runtime v0.28.0
github.com/gobwas/glob v0.2.3
github.com/google/go-jsonnet v0.20.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/grafana/grafana-openapi-client-go v0.0.0-20240325012504-4958bdd139e7
github.com/grafana/synthetic-monitoring-agent v0.23.1
Expand Down Expand Up @@ -50,7 +51,6 @@ require (
github.com/go-openapi/validate v0.24.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand Down
36 changes: 12 additions & 24 deletions pkg/grafana/dashboard-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/go-chi/chi"
Expand Down Expand Up @@ -256,8 +255,8 @@ func (h *DashboardHandler) Detect(data map[string]any) bool {
return true
}

func (h *DashboardHandler) GetProxyEndpoints(p grizzly.Server) []grizzly.ProxyEndpoint {
return []grizzly.ProxyEndpoint{
func (h *DashboardHandler) GetProxyEndpoints(p grizzly.Server) []grizzly.HTTPEndpoint {
return []grizzly.HTTPEndpoint{
{
Method: "GET",
URL: "/d/{uid}/{slug}",
Expand All @@ -284,7 +283,7 @@ func (h *DashboardHandler) GetProxyEndpoints(p grizzly.Server) []grizzly.ProxyEn
func (h *DashboardHandler) resourceFromQueryParameterMiddleware(p 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 != "" {
if err := p.ParseResources(fromFilePath); err != nil {
if _, err := p.ParseResources(fromFilePath); err != nil {
grizzly.SendError(w, "could not parse resource", fmt.Errorf("could not parse resource"), http.StatusBadRequest)
return
}
Expand All @@ -307,7 +306,13 @@ func (h *DashboardHandler) RootDashboardPageHandler(p grizzly.Server) http.Handl
grizzly.SendError(w, http.StatusText(500), err, 500)
return
}
req.Header.Set("Authorization", "Bearer "+config.Token)

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", p.UserAgent)

client := &http.Client{}
Expand Down Expand Up @@ -396,28 +401,11 @@ func (h *DashboardHandler) DashboardJSONPostHandler(p grizzly.Server) http.Handl
resource.SetMetadata("name", uid)
resource.SetSpecString("uid", uid)

out, _, _, err := grizzly.Format(p.Registry, p.ResourcePath, &resource, p.OutputFormat, p.OnlySpec)
if err != nil {
grizzly.SendError(w, "Error formatting content", err, 500)
return
}

existing, found := p.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), 500)
return
}
if !existing.Source.Rewritable {
grizzly.SendError(w, "The source for this dashboard is not rewritable", fmt.Errorf("the source for this dashboard is not rewritable"), 400)
return
}

err = os.WriteFile(existing.Source.Path, out, 0644)
err = p.UpdateResource(uid, resource)
if err != nil {
grizzly.SendError(w, fmt.Sprintf("Error writing file: %s", err), err, 500)
grizzly.SendError(w, err.Error(), err, 500)
return
}

jout := map[string]interface{}{
"id": 1,
"slug": "slug",
Expand Down
77 changes: 77 additions & 0 deletions pkg/grizzly/browser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package grizzly

import (
"fmt"
"os"
"os/exec"
"runtime"
)

type BrowserInterface struct {
registry Registry
port int
isDir bool
}

func NewBrowserInterface(registry Registry, resourcePath string, port int) (*BrowserInterface, error) {
stat, err := os.Stat(resourcePath)
if err != nil {
return nil, err
}

return &BrowserInterface{
registry: registry,
isDir: stat.IsDir(),
port: port,
}, nil
}

func (i BrowserInterface) Open(resources Resources) error {
path := "/"

if i.isDir {
if resources.Len() == 0 {
return fmt.Errorf("no resources found to proxy")
} else if resources.Len() == 1 {
resource := resources.First()
handler, err := i.registry.GetHandler(resource.Kind())
if err != nil {
return err
}
proxyHandler, ok := handler.(ProxyHandler)
if !ok {
uid, err := handler.GetUID(resource)
if err != nil {
return err
}
return fmt.Errorf("kind %s (for resource %s) does not support proxying", resource.Kind(), uid)
}
proxyURL, err := proxyHandler.ProxyURL(resource)
if err != nil {
return err
}
path = proxyURL
}
}

if len(path) == 0 || path[0] != '/' {
path = "/" + path
}

url := fmt.Sprintf("http://localhost:%d%s", i.port, path)
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
return err
}
return nil
}
14 changes: 14 additions & 0 deletions pkg/grizzly/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,17 @@ type APIErr struct {
func (e APIErr) Error() string {
return fmt.Sprintf("Failed to parse Grafana response: %s.\n\nResponse:\n%s", e.Err, string(e.Body))
}

type UnrecognisedFormatError struct {
File string
}

func (e UnrecognisedFormatError) Error() string {
return fmt.Sprintf("unrecognized format for %s", e.File)
}

func NewUnrecognisedFormatError(file string) UnrecognisedFormatError {
return UnrecognisedFormatError{
File: file,
}
}
6 changes: 3 additions & 3 deletions pkg/grizzly/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,16 @@ type ListenHandler interface {
Listen(UID, filename string) error
}

type ProxyEndpoint struct {
type HTTPEndpoint struct {
Method string
URL string
Handler func(http.ResponseWriter, *http.Request)
Handler http.HandlerFunc
}

// ProxyHandler describes a handler that can be used to edit resources live via a proxied UI
type ProxyHandler interface {
// RegisterHandlers registers HTTP handlers for proxy events
GetProxyEndpoints(p Server) []ProxyEndpoint
GetProxyEndpoints(p Server) []HTTPEndpoint

// ProxyURL returns a URL path for a resource on the proxy
ProxyURL(Resource) (string, error)
Expand Down
Loading
Loading