From 1b89db4dff4ad3a72591ac661e94801792b5e338 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Mon, 14 Feb 2022 17:28:52 -0800 Subject: [PATCH] tsweb: add DebugHandler.FlagSet For runtime-modifiable values. Signed-off-by: Josh Bleecher Snyder --- cmd/tailscaled/depaware.txt | 3 + tsweb/debug.go | 167 ++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 5ecec59dd..2e18d1f9b 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -353,6 +353,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+ hash/maphash from go4.org/mem html from net/http/pprof+ + html/template from tailscale.com/tsweb io from bufio+ io/fs from crypto/rand+ io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+ @@ -391,6 +392,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de sync/atomic from context+ syscall from crypto/rand+ text/tabwriter from runtime/pprof + text/template from html/template + text/template/parse from html/template+ time from compress/gzip+ unicode from bytes+ unicode/utf16 from crypto/x509+ diff --git a/tsweb/debug.go b/tsweb/debug.go index 54fd188cb..39a3bb25c 100644 --- a/tsweb/debug.go +++ b/tsweb/debug.go @@ -6,14 +6,20 @@ package tsweb import ( "expvar" + "flag" "fmt" "html" + "html/template" "io" + "log" "net/http" "net/http/pprof" "net/url" "os" "runtime" + "sort" + "strings" + "sync" "tailscale.com/version" ) @@ -33,6 +39,9 @@ type DebugHandler struct { kvs []func(io.Writer) // output one
  • ...
  • each, see KV() urls []string // one
  • ...
  • block with link each sections []func(io.Writer, *http.Request) // invoked in registration order prior to outputting + flagmu sync.Mutex // flagmu protects access to flagset and flagc + flagset *flag.FlagSet // runtime-modifiable flags, may be nil + flagc chan map[string]interface{} // DebugHandler sends new flag values on flagc when the flags have been modified } // Debugger returns the DebugHandler registered on mux at /debug/, @@ -139,3 +148,161 @@ func gcHandler(w http.ResponseWriter, r *http.Request) { runtime.GC() w.Write([]byte("Done.\n")) } + +// FlagSet returns a FlagSet that can be used to add runtime-modifiable flags to d. +// Calling code should add flags to fs, but not retain the values directly. +// Modifications to fs will be delivered via c. +// Maps sent to c will be keyed on the flag name, and contain the new value. +// Only modified values will be sent on c. +// +// Sample usage: +// flagset, flagc := debug.FlagSet() +// flagset.Int("max", 0, "maximum number of bars") +// flagset.String("s", "qux", "default name for new foos") +// go func() { +// for change := range flagc { +// // TODO: handle change, which will contain values for keys "max" and/or "s" +// } +// }() +func (d *DebugHandler) FlagSet() (fs *flag.FlagSet, c chan map[string]interface{}) { + d.flagmu.Lock() + defer d.flagmu.Unlock() + if d.flagset == nil { + d.flagset = flag.NewFlagSet("debug", flag.ContinueOnError) + d.flagc = make(chan map[string]interface{}) + d.Handle("flags", "Runtime flags", http.HandlerFunc(d.handleFlags)) + } + return d.flagset, d.flagc +} + +type copiedFlag struct { + Name string + Value string + Usage string +} + +func copyFlags(fs *flag.FlagSet) []copiedFlag { + var all []copiedFlag + fs.VisitAll(func(f *flag.Flag) { + all = append(all, copiedFlag{Name: f.Name, Value: f.Value.String(), Usage: f.Usage}) + }) + return all +} + +func (d *DebugHandler) handleFlags(w http.ResponseWriter, r *http.Request) { + d.flagmu.Lock() + defer d.flagmu.Unlock() + + var userError string + var modified string + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Make a copy of existing values, in case we need to roll back. + all := copyFlags(d.flagset) + // Set inbound values. + changed := make(map[string][2]string) + rollback := false + for k, v := range r.PostForm { + if len(v) != 1 { + userError = fmt.Sprintf("multiple values for name %q: %q", k, v) + rollback = true + break + } + f := d.flagset.Lookup(k) + if f == nil { + userError = fmt.Sprintf("unknown name %q", k) + rollback = true + break + } + prev := f.Value.String() + new := strings.TrimSpace(v[0]) + if prev == new { + continue + } + err := d.flagset.Set(k, new) + if err != nil { + userError = fmt.Sprintf("parsing value %q for name %q: %v", new, k, err) + rollback = true + break + } + changed[k] = [2]string{prev, new} + } + if rollback { + for _, f := range all { + d.flagset.Set(f.Name, f.Value) + } + } else { + // Generate description of modifications. + var names []string + for k := range changed { + names = append(names, k) + } + sort.Strings(names) + buf := new(strings.Builder) + for i, k := range names { + if i != 0 { + buf.WriteString("; ") + } + pn := changed[k] + fmt.Fprintf(buf, "%q: %v → %v", k, pn[0], pn[1]) + } + modified = buf.String() + vals := make(map[string]interface{}) + for _, k := range names { + vals[k] = d.flagset.Lookup(k).Value.(flag.Getter).Get() + } + + d.flagc <- vals + // TODO: post modifications to Slack, along with attribution + } + } + + dot := &struct { + Error string + Modified string + Flags []copiedFlag + }{ + Error: userError, + Modified: modified, + Flags: copyFlags(d.flagset), + } + err := flagsTemplate.Execute(w, dot) + if err != nil { + log.Print(err) + } +} + +var ( + flagsTemplate = template.Must(template.New("flags").Parse(` + + + +{{if .Error}} +

    Error: {{.Error}}

    +{{end}} + +{{if .Modified}} +

    Modified: {{.Modified}}

    +{{end}} + +

    Modifiable runtime flags

    + +

    Warning! Modifying these values will take effect immediately and impact the running service

    + +
    + + +{{range .Flags}} + +{{end}} +
    Name Value Usage
    {{.Name}} {{.Usage}}
    + +
    + + +`)) +)