| // Package webug implements a web-based debug/troubleshooting/introspection | 
 | // system for the BMDB. It's optimized for use by developers and trained | 
 | // operators, prioritizing information density, fast navigation and heavy | 
 | // interlinking. | 
 | package webug | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"embed" | 
 | 	"flag" | 
 | 	"fmt" | 
 | 	"html/template" | 
 | 	"net/http" | 
 | 	"regexp" | 
 | 	"sync" | 
 | 	"time" | 
 |  | 
 | 	"github.com/cenkalti/backoff/v4" | 
 | 	"k8s.io/klog/v2" | 
 |  | 
 | 	"source.monogon.dev/cloud/bmaas/bmdb" | 
 | 	"source.monogon.dev/cloud/bmaas/bmdb/reflection" | 
 | ) | 
 |  | 
 | var ( | 
 | 	//go:embed templates/*html | 
 | 	templateFS embed.FS | 
 | 	templates  = template.Must(template.New("base.html").Funcs(templateFuncs).ParseFS(templateFS, "templates/*")) | 
 | ) | 
 |  | 
 | // server holds the state of an active webug interface. | 
 | type server struct { | 
 | 	// connection pool to BMDB. | 
 | 	conn *bmdb.Connection | 
 | 	// schema retrieved from BMDB. | 
 | 	schema *reflection.Schema | 
 | 	// muSchema locks schema for updates. | 
 | 	muSchema sync.RWMutex | 
 | } | 
 |  | 
 | // curSchema returns the current cached BMDB schema. | 
 | func (s *server) curSchema() *reflection.Schema { | 
 | 	s.muSchema.Lock() | 
 | 	defer s.muSchema.Unlock() | 
 | 	return s.schema | 
 | } | 
 |  | 
 | // schemaWorker runs a background goroutine which attempts to update the server's | 
 | // cached BMDB schema every hour. | 
 | func (s *server) schemaWorker(ctx context.Context) { | 
 | 	t := time.NewTicker(time.Hour) | 
 | 	defer t.Stop() | 
 |  | 
 | 	for { | 
 | 		// Wait for the timer to tick, or for the context to expire. | 
 | 		select { | 
 | 		case <-ctx.Done(): | 
 | 			klog.Infof("Schema fetch worker exiting: %v", ctx.Err()) | 
 | 			return | 
 | 		case <-t.C: | 
 | 		} | 
 |  | 
 | 		// Time to check the schema. Do that in an exponential backoff loop until | 
 | 		// successful. | 
 | 		bo := backoff.NewExponentialBackOff() | 
 | 		bo.MaxElapsedTime = 0 | 
 | 		var schema *reflection.Schema | 
 | 		err := backoff.Retry(func() error { | 
 | 			var err error | 
 | 			schema, err = s.conn.Reflect(ctx) | 
 | 			if err != nil { | 
 | 				klog.Warningf("Failed to fetch new schema: %v", err) | 
 | 				return err | 
 | 			} | 
 | 			return nil | 
 | 		}, backoff.WithContext(bo, ctx)) | 
 | 		// This will only happen due to context expiration. | 
 | 		if err != nil { | 
 | 			klog.Errorf("Giving up on schema fetch: %v", err) | 
 | 			continue | 
 | 		} | 
 |  | 
 | 		// Swap the current schema if necessary. | 
 | 		cur := s.curSchema().Version | 
 | 		new := schema.Version | 
 | 		if cur != new { | 
 | 			klog.Infof("Got new schema: %s -> %s", cur, new) | 
 | 			s.muSchema.Lock() | 
 | 			s.schema = schema | 
 | 			s.muSchema.Unlock() | 
 | 		} | 
 | 	} | 
 | } | 
 |  | 
 | // Register webug on an HTTP mux, using a BMDB connection pool. | 
 | // | 
 | // The given context will be used not only to time out the registration call, but | 
 | // also used to run a BMDB schema fetching goroutine that will attempt to fetch | 
 | // newer versions of the schema every hour. | 
 | // | 
 | // This is a low-level function useful when tying webug into an existing web | 
 | // application. If you just want to run webug on a separate port that's | 
 | // configured by flags, use Config and Config.RegisterFlags. | 
 | func Register(ctx context.Context, conn *bmdb.Connection, mux *http.ServeMux) error { | 
 | 	schema, err := conn.Reflect(ctx) | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("could not get BMDB schema for webug: %w", err) | 
 | 	} | 
 | 	s := server{ | 
 | 		conn:   conn, | 
 | 		schema: schema, | 
 | 	} | 
 | 	go s.schemaWorker(ctx) | 
 |  | 
 | 	type route struct { | 
 | 		pattern *regexp.Regexp | 
 | 		handler func(w http.ResponseWriter, r *http.Request, args ...string) | 
 | 	} | 
 |  | 
 | 	routes := []route{ | 
 | 		{regexp.MustCompile(`^/$`), s.viewMachines}, | 
 | 		{regexp.MustCompile(`^/machine/([a-fA-F0-9\-]+)$`), s.viewMachineDetail}, | 
 | 		{regexp.MustCompile(`^/provider/([^/]+)/([^/]+)$`), s.viewProviderRedirect}, | 
 | 		{regexp.MustCompile(`^/session/([^/]+)`), s.viewSession}, | 
 | 	} | 
 |  | 
 | 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | 
 | 		for _, route := range routes { | 
 | 			match := route.pattern.FindStringSubmatch(r.URL.Path) | 
 | 			if match == nil { | 
 | 				continue | 
 | 			} | 
 | 			route.handler(w, r, match[1:]...) | 
 | 			return | 
 | 		} | 
 | 		http.NotFound(w, r) | 
 | 	}) | 
 | 	return nil | 
 | } | 
 |  | 
 | // Config describes the webug interface configuration. This should be embedded | 
 | // inside your component's Config object. | 
 | // | 
 | // To configure, either set values or call RegisterFlags before flag.Parse. | 
 | // | 
 | // To run after configuration, call Start. | 
 | type Config struct { | 
 | 	// If set, start a webug interface on an HTTP listener bound to the given address. | 
 | 	WebugListenAddress string | 
 | } | 
 |  | 
 | // RegisterFlags for webug interface. | 
 | func (c *Config) RegisterFlags() { | 
 | 	flag.StringVar(&c.WebugListenAddress, "webug_listen_address", "", "Address to start BMDB webug on. If not set, webug will not be started.") | 
 | } | 
 |  | 
 | // Start the webug interface in the foreground if enabled. The returned error | 
 | // will be either a configuration/connection error returned as soon as possible, | 
 | // or a context expiration error. | 
 | // | 
 | // The given context will be used for all connections from the webug interface to | 
 | // the given BMDB connection. | 
 | func (c *Config) Start(ctx context.Context, conn *bmdb.Connection) error { | 
 | 	if c.WebugListenAddress == "" { | 
 | 		return nil | 
 | 	} | 
 | 	mux := http.NewServeMux() | 
 | 	if err := Register(ctx, conn, mux); err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	klog.Infof("Webug listening at %s", c.WebugListenAddress) | 
 | 	return http.ListenAndServe(c.WebugListenAddress, mux) | 
 | } |