blob: e6e1be7de6024213f3dec83a4efd67e009e27a08 [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 (
Tim Windelschmidt93b6fad2023-05-04 16:35:17 +020026 //go:embed templates/*.gohtml
Serge Bazanski77628312023-02-15 23:33:22 +010027 templateFS embed.FS
Tim Windelschmidt93b6fad2023-05-04 16:35:17 +020028 templates = template.Must(template.New("base.gohtml").Funcs(templateFuncs).ParseFS(templateFS, "templates/*"))
Serge Bazanski77628312023-02-15 23:33:22 +010029)
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
Tim Windelschmidt5832cd92023-06-13 13:09:55 +020039 // strictConsistency, when enabled, makes webug render its views with the
40 // freshest available data, potentially conflicting with online
41 // transactions. This should only be enabled during testing, as it tends to
42 // clog up the database query planner and make everything slow.
43 strictConsistency bool
Serge Bazanski77628312023-02-15 23:33:22 +010044}
45
46// curSchema returns the current cached BMDB schema.
47func (s *server) curSchema() *reflection.Schema {
48 s.muSchema.Lock()
49 defer s.muSchema.Unlock()
50 return s.schema
51}
52
53// schemaWorker runs a background goroutine which attempts to update the server's
54// cached BMDB schema every hour.
55func (s *server) schemaWorker(ctx context.Context) {
56 t := time.NewTicker(time.Hour)
57 defer t.Stop()
58
59 for {
60 // Wait for the timer to tick, or for the context to expire.
61 select {
62 case <-ctx.Done():
63 klog.Infof("Schema fetch worker exiting: %v", ctx.Err())
64 return
65 case <-t.C:
66 }
67
68 // Time to check the schema. Do that in an exponential backoff loop until
69 // successful.
70 bo := backoff.NewExponentialBackOff()
71 bo.MaxElapsedTime = 0
72 var schema *reflection.Schema
73 err := backoff.Retry(func() error {
74 var err error
75 schema, err = s.conn.Reflect(ctx)
76 if err != nil {
77 klog.Warningf("Failed to fetch new schema: %v", err)
78 return err
79 }
80 return nil
81 }, backoff.WithContext(bo, ctx))
82 // This will only happen due to context expiration.
83 if err != nil {
84 klog.Errorf("Giving up on schema fetch: %v", err)
85 continue
86 }
87
88 // Swap the current schema if necessary.
89 cur := s.curSchema().Version
90 new := schema.Version
91 if cur != new {
92 klog.Infof("Got new schema: %s -> %s", cur, new)
93 s.muSchema.Lock()
94 s.schema = schema
95 s.muSchema.Unlock()
96 }
97 }
98}
99
100// Register webug on an HTTP mux, using a BMDB connection pool.
101//
102// The given context will be used not only to time out the registration call, but
103// also used to run a BMDB schema fetching goroutine that will attempt to fetch
104// newer versions of the schema every hour.
105//
106// This is a low-level function useful when tying webug into an existing web
107// application. If you just want to run webug on a separate port that's
108// configured by flags, use Config and Config.RegisterFlags.
Tim Windelschmidt5832cd92023-06-13 13:09:55 +0200109func Register(ctx context.Context, conn *bmdb.Connection, mux *http.ServeMux, enableStrictConsistency bool) error {
Serge Bazanski77628312023-02-15 23:33:22 +0100110 schema, err := conn.Reflect(ctx)
111 if err != nil {
112 return fmt.Errorf("could not get BMDB schema for webug: %w", err)
113 }
114 s := server{
Tim Windelschmidt5832cd92023-06-13 13:09:55 +0200115 conn: conn,
116 schema: schema,
117 strictConsistency: enableStrictConsistency,
Serge Bazanski77628312023-02-15 23:33:22 +0100118 }
119 go s.schemaWorker(ctx)
120
121 type route struct {
122 pattern *regexp.Regexp
123 handler func(w http.ResponseWriter, r *http.Request, args ...string)
124 }
125
126 routes := []route{
127 {regexp.MustCompile(`^/$`), s.viewMachines},
128 {regexp.MustCompile(`^/machine/([a-fA-F0-9\-]+)$`), s.viewMachineDetail},
129 {regexp.MustCompile(`^/provider/([^/]+)/([^/]+)$`), s.viewProviderRedirect},
130 {regexp.MustCompile(`^/session/([^/]+)`), s.viewSession},
131 }
132
133 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
134 for _, route := range routes {
135 match := route.pattern.FindStringSubmatch(r.URL.Path)
136 if match == nil {
137 continue
138 }
139 route.handler(w, r, match[1:]...)
140 return
141 }
142 http.NotFound(w, r)
143 })
144 return nil
145}
146
147// Config describes the webug interface configuration. This should be embedded
148// inside your component's Config object.
149//
150// To configure, either set values or call RegisterFlags before flag.Parse.
151//
152// To run after configuration, call Start.
153type Config struct {
154 // If set, start a webug interface on an HTTP listener bound to the given address.
155 WebugListenAddress string
Tim Windelschmidt5832cd92023-06-13 13:09:55 +0200156
157 // Enables strict consistency
158 WebugDBFetchStrict bool
Serge Bazanski77628312023-02-15 23:33:22 +0100159}
160
161// RegisterFlags for webug interface.
162func (c *Config) RegisterFlags() {
163 flag.StringVar(&c.WebugListenAddress, "webug_listen_address", "", "Address to start BMDB webug on. If not set, webug will not be started.")
Tim Windelschmidt5832cd92023-06-13 13:09:55 +0200164 flag.BoolVar(&c.WebugDBFetchStrict, "webug_dbfetch_strict", false, "Enables strict consistency")
Serge Bazanski77628312023-02-15 23:33:22 +0100165}
166
167// Start the webug interface in the foreground if enabled. The returned error
168// will be either a configuration/connection error returned as soon as possible,
169// or a context expiration error.
170//
171// The given context will be used for all connections from the webug interface to
172// the given BMDB connection.
173func (c *Config) Start(ctx context.Context, conn *bmdb.Connection) error {
174 if c.WebugListenAddress == "" {
175 return nil
176 }
177 mux := http.NewServeMux()
Tim Windelschmidt5832cd92023-06-13 13:09:55 +0200178 if err := Register(ctx, conn, mux, c.WebugDBFetchStrict); err != nil {
Serge Bazanski77628312023-02-15 23:33:22 +0100179 return err
180 }
181
182 klog.Infof("Webug listening at %s", c.WebugListenAddress)
183 return http.ListenAndServe(c.WebugListenAddress, mux)
184}