blob: 91888d54fc9d9a6351aebbb1b906685c8ec0e78b [file] [log] [blame]
Serge Bazanski77628312023-02-15 23:33:22 +01001// Package webug implements a web-based debug/troubleshooting/introspection
2// system for the BMDB. It's optimized for use by developers and trained
3// operators, prioritizing information density, fast navigation and heavy
4// interlinking.
5package webug
6
7import (
8 "context"
9 "embed"
10 "flag"
11 "fmt"
12 "html/template"
13 "net/http"
14 "regexp"
15 "sync"
16 "time"
17
18 "github.com/cenkalti/backoff/v4"
19 "k8s.io/klog/v2"
20
21 "source.monogon.dev/cloud/bmaas/bmdb"
22 "source.monogon.dev/cloud/bmaas/bmdb/reflection"
23)
24
25var (
26 //go:embed templates/*html
27 templateFS embed.FS
28 templates = template.Must(template.New("base.html").Funcs(templateFuncs).ParseFS(templateFS, "templates/*"))
29)
30
31// server holds the state of an active webug interface.
32type server struct {
33 // connection pool to BMDB.
34 conn *bmdb.Connection
35 // schema retrieved from BMDB.
36 schema *reflection.Schema
37 // muSchema locks schema for updates.
38 muSchema sync.RWMutex
39}
40
41// curSchema returns the current cached BMDB schema.
42func (s *server) curSchema() *reflection.Schema {
43 s.muSchema.Lock()
44 defer s.muSchema.Unlock()
45 return s.schema
46}
47
48// schemaWorker runs a background goroutine which attempts to update the server's
49// cached BMDB schema every hour.
50func (s *server) schemaWorker(ctx context.Context) {
51 t := time.NewTicker(time.Hour)
52 defer t.Stop()
53
54 for {
55 // Wait for the timer to tick, or for the context to expire.
56 select {
57 case <-ctx.Done():
58 klog.Infof("Schema fetch worker exiting: %v", ctx.Err())
59 return
60 case <-t.C:
61 }
62
63 // Time to check the schema. Do that in an exponential backoff loop until
64 // successful.
65 bo := backoff.NewExponentialBackOff()
66 bo.MaxElapsedTime = 0
67 var schema *reflection.Schema
68 err := backoff.Retry(func() error {
69 var err error
70 schema, err = s.conn.Reflect(ctx)
71 if err != nil {
72 klog.Warningf("Failed to fetch new schema: %v", err)
73 return err
74 }
75 return nil
76 }, backoff.WithContext(bo, ctx))
77 // This will only happen due to context expiration.
78 if err != nil {
79 klog.Errorf("Giving up on schema fetch: %v", err)
80 continue
81 }
82
83 // Swap the current schema if necessary.
84 cur := s.curSchema().Version
85 new := schema.Version
86 if cur != new {
87 klog.Infof("Got new schema: %s -> %s", cur, new)
88 s.muSchema.Lock()
89 s.schema = schema
90 s.muSchema.Unlock()
91 }
92 }
93}
94
95// Register webug on an HTTP mux, using a BMDB connection pool.
96//
97// The given context will be used not only to time out the registration call, but
98// also used to run a BMDB schema fetching goroutine that will attempt to fetch
99// newer versions of the schema every hour.
100//
101// This is a low-level function useful when tying webug into an existing web
102// application. If you just want to run webug on a separate port that's
103// configured by flags, use Config and Config.RegisterFlags.
104func Register(ctx context.Context, conn *bmdb.Connection, mux *http.ServeMux) error {
105 schema, err := conn.Reflect(ctx)
106 if err != nil {
107 return fmt.Errorf("could not get BMDB schema for webug: %w", err)
108 }
109 s := server{
110 conn: conn,
111 schema: schema,
112 }
113 go s.schemaWorker(ctx)
114
115 type route struct {
116 pattern *regexp.Regexp
117 handler func(w http.ResponseWriter, r *http.Request, args ...string)
118 }
119
120 routes := []route{
121 {regexp.MustCompile(`^/$`), s.viewMachines},
122 {regexp.MustCompile(`^/machine/([a-fA-F0-9\-]+)$`), s.viewMachineDetail},
123 {regexp.MustCompile(`^/provider/([^/]+)/([^/]+)$`), s.viewProviderRedirect},
124 {regexp.MustCompile(`^/session/([^/]+)`), s.viewSession},
125 }
126
127 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
128 for _, route := range routes {
129 match := route.pattern.FindStringSubmatch(r.URL.Path)
130 if match == nil {
131 continue
132 }
133 route.handler(w, r, match[1:]...)
134 return
135 }
136 http.NotFound(w, r)
137 })
138 return nil
139}
140
141// Config describes the webug interface configuration. This should be embedded
142// inside your component's Config object.
143//
144// To configure, either set values or call RegisterFlags before flag.Parse.
145//
146// To run after configuration, call Start.
147type Config struct {
148 // If set, start a webug interface on an HTTP listener bound to the given address.
149 WebugListenAddress string
150}
151
152// RegisterFlags for webug interface.
153func (c *Config) RegisterFlags() {
154 flag.StringVar(&c.WebugListenAddress, "webug_listen_address", "", "Address to start BMDB webug on. If not set, webug will not be started.")
155}
156
157// Start the webug interface in the foreground if enabled. The returned error
158// will be either a configuration/connection error returned as soon as possible,
159// or a context expiration error.
160//
161// The given context will be used for all connections from the webug interface to
162// the given BMDB connection.
163func (c *Config) Start(ctx context.Context, conn *bmdb.Connection) error {
164 if c.WebugListenAddress == "" {
165 return nil
166 }
167 mux := http.NewServeMux()
168 if err := Register(ctx, conn, mux); err != nil {
169 return err
170 }
171
172 klog.Infof("Webug listening at %s", c.WebugListenAddress)
173 return http.ListenAndServe(c.WebugListenAddress, mux)
174}