From 1ae6a97a7313b3412dc89618efffad3181a07997 Mon Sep 17 00:00:00 2001 From: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:13:25 -0400 Subject: [PATCH] cmd/tailscale/cli: add advertise command to advertise a node as service proxy to tailnet (#16620) This commit adds a advertise subcommand for tailscale serve, that would declare the node as a service proxy for a service. This command only adds the service to node's list of advertised service, but doesn't modify the list of services currently advertised. Fixes tailscale/corp#28016 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> --- cmd/tailscale/cli/serve_v2.go | 33 ++++++++++++++++++++++++++---- cmd/tailscale/cli/serve_v2_test.go | 14 ++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index 056bfabb0..91a236970 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -220,6 +220,16 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command { LongHelp: "Remove all handlers configured for the specified service.", Exec: e.runServeClear, }, + { + Name: "advertise", + ShortUsage: fmt.Sprintf("tailscale %s advertise ", info.Name), + ShortHelp: "Advertise this node as a service proxy to the tailnet", + LongHelp: "Advertise this node as a service proxy to the tailnet. This command is used\n" + + "to make the current node be considered as a service host for a service. This is\n" + + "useful to bring a service back after it has been drained. (i.e. after running \n" + + "`tailscale serve drain `). This is not needed if you are using `tailscale serve` to initialize a service.", + Exec: e.runServeAdvertise, + }, }, } } @@ -401,7 +411,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { return err } if forService { - e.addServiceToPrefs(ctx, svcName.String()) + e.addServiceToPrefs(ctx, svcName) } target := "" if len(args) > 0 { @@ -442,16 +452,16 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { } } -func (e *serveEnv) addServiceToPrefs(ctx context.Context, serviceName string) error { +func (e *serveEnv) addServiceToPrefs(ctx context.Context, serviceName tailcfg.ServiceName) error { prefs, err := e.lc.GetPrefs(ctx) if err != nil { return fmt.Errorf("error getting prefs: %w", err) } advertisedServices := prefs.AdvertiseServices - if slices.Contains(advertisedServices, serviceName) { + if slices.Contains(advertisedServices, serviceName.String()) { return nil // already advertised } - advertisedServices = append(advertisedServices, serviceName) + advertisedServices = append(advertisedServices, serviceName.String()) _, err = e.lc.EditPrefs(ctx, &ipn.MaskedPrefs{ AdvertiseServicesSet: true, Prefs: ipn.Prefs{ @@ -526,6 +536,21 @@ func (e *serveEnv) runServeClear(ctx context.Context, args []string) error { return e.lc.SetServeConfig(ctx, sc) } +func (e *serveEnv) runServeAdvertise(ctx context.Context, args []string) error { + if len(args) == 0 { + return fmt.Errorf("error: missing service name argument") + } + if len(args) != 1 { + fmt.Fprintf(Stderr, "error: invalid number of arguments\n\n") + return errHelp + } + svc := tailcfg.ServiceName(args[0]) + if err := svc.Validate(); err != nil { + return fmt.Errorf("invalid service name: %w", err) + } + return e.addServiceToPrefs(ctx, svc) +} + const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration" // validateConfig checks if the serve config is valid to serve the type wanted on the port. diff --git a/cmd/tailscale/cli/serve_v2_test.go b/cmd/tailscale/cli/serve_v2_test.go index 95bf5b101..1deeaf3ea 100644 --- a/cmd/tailscale/cli/serve_v2_test.go +++ b/cmd/tailscale/cli/serve_v2_test.go @@ -1167,24 +1167,24 @@ func TestCleanURLPath(t *testing.T) { func TestAddServiceToPrefs(t *testing.T) { tests := []struct { name string - dnsName string + svcName tailcfg.ServiceName startServices []string expected []string }{ { name: "add service to empty prefs", - dnsName: "svc:foo", + svcName: "svc:foo", expected: []string{"svc:foo"}, }, { name: "add service to existing prefs", - dnsName: "svc:bar", + svcName: "svc:bar", startServices: []string{"svc:foo"}, expected: []string{"svc:foo", "svc:bar"}, }, { name: "add existing service to prefs", - dnsName: "svc:foo", + svcName: "svc:foo", startServices: []string{"svc:foo"}, expected: []string{"svc:foo"}, }, @@ -1200,12 +1200,12 @@ func TestAddServiceToPrefs(t *testing.T) { }, }) e := &serveEnv{lc: lc, bg: bgBoolFlag{true, false}} - err := e.addServiceToPrefs(ctx, tt.dnsName) + err := e.addServiceToPrefs(ctx, tt.svcName) if err != nil { - t.Fatalf("addServiceToPrefs(%q) returned unexpected error: %v", tt.dnsName, err) + t.Fatalf("addServiceToPrefs(%q) returned unexpected error: %v", tt.svcName, err) } if !slices.Equal(lc.prefs.AdvertiseServices, tt.expected) { - t.Errorf("addServiceToPrefs(%q) = %v, want %v", tt.dnsName, lc.prefs.AdvertiseServices, tt.expected) + t.Errorf("addServiceToPrefs(%q) = %v, want %v", tt.svcName, lc.prefs.AdvertiseServices, tt.expected) } }) }