From 5bb7e0307c2e6c7c7e676fb3acaf54c98aa190cc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 7 Nov 2022 13:04:10 -0800 Subject: [PATCH] cmd/tailscale, ipn/ipnlocal: add debug command to write to StateStore for dev Not for end users (unless directed by support). Mostly for ease of development for some upcoming webserver work. Change-Id: I43acfed217514567acb3312367b24d620e739f88 Signed-off-by: Brad Fitzpatrick --- client/tailscale/localclient.go | 13 +++++++++++++ cmd/tailscale/cli/debug.go | 24 ++++++++++++++++++++++++ ipn/ipnlocal/local.go | 12 ++++++++++++ ipn/localapi/localapi.go | 18 ++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 648c8cc11..b03da5df3 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -348,6 +348,19 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error { return nil } +// SetDevStoreKeyValue set a statestore key/value. It's only meant for development. +// The schema (including when keys are re-read) is not a stable interface. +func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error { + body, err := lc.send(ctx, "POST", "/localapi/v0/dev-set-state-store?"+(url.Values{ + "key": {key}, + "value": {value}, + }).Encode(), 200, nil) + if err != nil { + return fmt.Errorf("error %w: %s", err, body) + } + return nil +} + // SetComponentDebugLogging sets component's debug logging enabled for // the provided duration. If the duration is in the past, the debug logging // is disabled. diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 1293d10e9..de68a7ef6 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -144,6 +144,16 @@ var debugCmd = &ffcli.Command{ return fs })(), }, + { + Name: "dev-store-set", + Exec: runDevStoreSet, + ShortHelp: "set a key/value pair during development", + FlagSet: (func() *flag.FlagSet { + fs := newFlagSet("store-set") + fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger") + return fs + })(), + }, }, } @@ -546,3 +556,17 @@ func runDebugComponentLogs(ctx context.Context, args []string) error { } return nil } + +var devStoreSetArgs struct { + danger bool +} + +func runDevStoreSet(ctx context.Context, args []string) error { + if len(args) != 2 { + return errors.New("usage: dev-store-set --danger ") + } + if !devStoreSetArgs.danger { + return errors.New("this command is dangerous; use --danger to proceed") + } + return localClient.SetDevStoreKeyValue(ctx, args[0], args[1]) +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index ee30a9cde..a9e3e3ef4 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3966,3 +3966,15 @@ func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) { doctor.RunChecks(ctx, logf, checks...) } + +// SetDevStateStore updates the LocalBackend's state storage to the provided values. +// +// It's meant only for development. +func (b *LocalBackend) SetDevStateStore(key, value string) error { + if b.store == nil { + return errors.New("no state store") + } + err := b.store.WriteState(ipn.StateKey(key), []byte(value)) + b.logf("SetDevStateStore(%q, %q) = %v", key, value, err) + return err +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 8895770c8..0a5b5c027 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -61,6 +61,7 @@ var handler = map[string]localAPIHandler{ "component-debug-logging": (*Handler).serveComponentDebugLogging, "debug": (*Handler).serveDebug, "derpmap": (*Handler).serveDERPMap, + "dev-set-state-store": (*Handler).serveDevSetStateStore, "dial": (*Handler).serveDial, "file-targets": (*Handler).serveFileTargets, "goroutines": (*Handler).serveGoroutines, @@ -401,6 +402,23 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "done\n") } +func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "debug access denied", http.StatusForbidden) + return + } + if r.Method != "POST" { + http.Error(w, "POST required", http.StatusMethodNotAllowed) + return + } + if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "done\n") +} + func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden)