| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame^] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 4 | // 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. |
| 8 | package webug |
| 9 | |
| 10 | import ( |
| 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 | |
| 28 | var ( |
| Tim Windelschmidt | 93b6fad | 2023-05-04 16:35:17 +0200 | [diff] [blame] | 29 | //go:embed templates/*.gohtml |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 30 | templateFS embed.FS |
| Tim Windelschmidt | 93b6fad | 2023-05-04 16:35:17 +0200 | [diff] [blame] | 31 | templates = template.Must(template.New("base.gohtml").Funcs(templateFuncs).ParseFS(templateFS, "templates/*")) |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 32 | ) |
| 33 | |
| 34 | // server holds the state of an active webug interface. |
| 35 | type 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 Windelschmidt | 3810567 | 2024-04-11 01:37:29 +0200 | [diff] [blame] | 42 | // 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 Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 45 | // clog up the database query planner and make everything slow. |
| 46 | strictConsistency bool |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 47 | } |
| 48 | |
| 49 | // curSchema returns the current cached BMDB schema. |
| 50 | func (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. |
| 58 | func (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 Windelschmidt | 3810567 | 2024-04-11 01:37:29 +0200 | [diff] [blame] | 93 | newVer := schema.Version |
| 94 | if cur != newVer { |
| 95 | klog.Infof("Got new schema: %s -> %s", cur, newVer) |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 96 | 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 Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 112 | func Register(ctx context.Context, conn *bmdb.Connection, mux *http.ServeMux, enableStrictConsistency bool) error { |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 113 | 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 Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 118 | conn: conn, |
| 119 | schema: schema, |
| 120 | strictConsistency: enableStrictConsistency, |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 121 | } |
| 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. |
| 156 | type Config struct { |
| 157 | // If set, start a webug interface on an HTTP listener bound to the given address. |
| 158 | WebugListenAddress string |
| Tim Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 159 | |
| 160 | // Enables strict consistency |
| 161 | WebugDBFetchStrict bool |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 162 | } |
| 163 | |
| 164 | // RegisterFlags for webug interface. |
| 165 | func (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 Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 167 | flag.BoolVar(&c.WebugDBFetchStrict, "webug_dbfetch_strict", false, "Enables strict consistency") |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 168 | } |
| 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. |
| 176 | func (c *Config) Start(ctx context.Context, conn *bmdb.Connection) error { |
| 177 | if c.WebugListenAddress == "" { |
| 178 | return nil |
| 179 | } |
| 180 | mux := http.NewServeMux() |
| Tim Windelschmidt | 5832cd9 | 2023-06-13 13:09:55 +0200 | [diff] [blame] | 181 | if err := Register(ctx, conn, mux, c.WebugDBFetchStrict); err != nil { |
| Serge Bazanski | 7762831 | 2023-02-15 23:33:22 +0100 | [diff] [blame] | 182 | return err |
| 183 | } |
| 184 | |
| 185 | klog.Infof("Webug listening at %s", c.WebugListenAddress) |
| 186 | return http.ListenAndServe(c.WebugListenAddress, mux) |
| 187 | } |