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