diff --git a/cmd/tailscale/cli/serve_legacy.go b/cmd/tailscale/cli/serve_legacy.go index 1bd28ad4f..6058f2ee0 100644 --- a/cmd/tailscale/cli/serve_legacy.go +++ b/cmd/tailscale/cli/serve_legacy.go @@ -164,6 +164,7 @@ type serveEnv struct { tcp string // TCP port tlsTerminatedTCP string // a TLS terminated TCP port subcmd serveMode // subcommand + yes bool // update without prompt lc localServeClient // localClient interface, specific to serve diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index 97b082045..26ce1f29c 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -128,7 +128,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command { fs.StringVar(&e.http, "http", "", "Expose an HTTP server at the specified port") fs.StringVar(&e.tcp, "tcp", "", "Expose a TCP forwarder to forward raw TCP packets at the specified port") fs.StringVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", "", "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port") - + fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts") }), UsageFunc: usageFunc, Subcommands: []*ffcli.Command{ @@ -679,13 +679,40 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u return errors.New("cannot remove web handler; currently serving TCP") } - hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort)))) - if !sc.WebHandlerExists(hp, mount) { + portStr := strconv.Itoa(int(srvPort)) + hp := ipn.HostPort(net.JoinHostPort(dnsName, portStr)) + + var targetExists bool + var mounts []string + // mount is deduced from e.setPath but it is ambiguous as + // to whether the user explicitly passed "/" or it was defaulted to. + if e.setPath == "" { + targetExists = sc.Web[hp] != nil && len(sc.Web[hp].Handlers) > 0 + if targetExists { + for mount := range sc.Web[hp].Handlers { + mounts = append(mounts, mount) + } + } + } else { + targetExists = sc.WebHandlerExists(hp, mount) + mounts = []string{mount} + } + + if !targetExists { return errors.New("error: handler does not exist") } + if len(mounts) > 1 { + msg := fmt.Sprintf("Are you sure you want to delete %d handlers under port %s?", len(mounts), portStr) + if !e.yes && !promptYesNo(msg) { + return nil + } + } + // delete existing handler, then cascade delete if empty - delete(sc.Web[hp].Handlers, mount) + for _, m := range mounts { + delete(sc.Web[hp].Handlers, m) + } if len(sc.Web[hp].Handlers) == 0 { delete(sc.Web, hp) delete(sc.TCP, srvPort) diff --git a/cmd/tailscale/cli/serve_v2_test.go b/cmd/tailscale/cli/serve_v2_test.go index 4ebf40be1..028e66cd2 100644 --- a/cmd/tailscale/cli/serve_v2_test.go +++ b/cmd/tailscale/cli/serve_v2_test.go @@ -725,6 +725,40 @@ type step struct { wantErr: anyErr(), }) + add(step{ + command: cmd("serve reset"), + want: &ipn.ServeConfig{}, + }) + + // start two handlers and turn them off in one command + add(step{ + command: cmd("serve --https=4545 --set-path=/foo --bg localhost:3000"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ + "/foo": {Proxy: "http://127.0.0.1:3000"}, + }}, + }, + }, + }) + add(step{ + command: cmd("serve --https=4545 --set-path=/bar --bg localhost:3000"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ + "/foo": {Proxy: "http://127.0.0.1:3000"}, + "/bar": {Proxy: "http://127.0.0.1:3000"}, + }}, + }, + }, + }) + add(step{ + command: cmd("serve --https=4545 --bg --yes localhost:3000 off"), + want: &ipn.ServeConfig{}, + }) + lc := &fakeLocalServeClient{} // And now run the steps above. for i, st := range steps { diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go index 020ac4d59..145f87deb 100644 --- a/cmd/tailscale/cli/update.go +++ b/cmd/tailscale/cli/update.go @@ -82,7 +82,14 @@ func confirmUpdate(ver string) bool { return false } - fmt.Printf("This will update Tailscale from %v to %v. Continue? [y/n] ", version.Short(), ver) + msg := fmt.Sprintf("This will update Tailscale from %v to %v. Continue?", version.Short(), ver) + return promptYesNo(msg) +} + +// PromptYesNo takes a question and prompts the user to answer the +// question with a yes or no. It appends a [y/n] to the message. +func promptYesNo(msg string) bool { + fmt.Print(msg + " [y/n] ") var resp string fmt.Scanln(&resp) resp = strings.ToLower(resp)