mirror of
https://github.com/tailscale/tailscale.git
synced 2026-04-03 06:02:30 -04:00
tsnet,client,cmd/tailscale/cli: expose service details on all clients
This commit is contained in:
@@ -686,6 +686,18 @@ func (lc *Client) status(ctx context.Context, queryString string) (*ipnstate.Sta
|
||||
return decodeJSON[*ipnstate.Status](body)
|
||||
}
|
||||
|
||||
// GetServiceDetails returns the VIP services that the control plane has
|
||||
// approved this node to serve, including their names, assigned IP addresses,
|
||||
// ports, and annotations (e.g. proxy service configuration). Returns nil if
|
||||
// no service details are present.
|
||||
func (lc *Client) GetServiceDetails(ctx context.Context) ([]*tailcfg.ServiceDetail, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/service-details")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[[]*tailcfg.ServiceDetail](body)
|
||||
}
|
||||
|
||||
// IDToken is a request to get an OIDC ID token for an audience.
|
||||
// The token can be presented to any resource provider which offers OIDC
|
||||
// Federation.
|
||||
|
||||
@@ -269,6 +269,7 @@ func newRootCmd() *ffcli.Command {
|
||||
nilOrCall(maybeNetlockCmd),
|
||||
licensesCmd,
|
||||
exitNodeCmd(),
|
||||
serviceCmd(),
|
||||
nilOrCall(maybeUpdateCmd),
|
||||
whoisCmd,
|
||||
debugCmd(),
|
||||
|
||||
103
cmd/tailscale/cli/service.go
Normal file
103
cmd/tailscale/cli/service.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func serviceCmd() *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "service",
|
||||
ShortUsage: "tailscale service <subcommand> [flags]",
|
||||
ShortHelp: "Manage and inspect Tailscale VIP services",
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ShortUsage: "tailscale service list [--json]",
|
||||
ShortHelp: "List VIP services approved for this node",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'tailscale service list' command shows the VIP services that the control
|
||||
plane has approved this node to serve, including their assigned IP addresses,
|
||||
accepted ports, and any application-specific annotations.
|
||||
`),
|
||||
Exec: runServiceList,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("list")
|
||||
fs.BoolVar(&serviceArgs.json, "json", false, "output in JSON format")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var serviceArgs struct {
|
||||
json bool
|
||||
}
|
||||
|
||||
func runServiceList(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected non-flag arguments to 'tailscale service list'")
|
||||
}
|
||||
details, err := localClient.GetServiceDetails(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if serviceArgs.json {
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(details)
|
||||
return nil
|
||||
}
|
||||
if len(details) == 0 {
|
||||
printf("No VIP services configured for this node.\n")
|
||||
return nil
|
||||
}
|
||||
printServiceDetails(details)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printServiceDetails(details []*tailcfg.ServiceDetail) {
|
||||
w := tabwriter.NewWriter(Stdout, 0, 0, 3, ' ', 0)
|
||||
fmt.Fprintln(w, "NAME\tADDRS\tPORTS\t")
|
||||
fmt.Fprintln(w, "----\t-----\t-----\t")
|
||||
for _, svc := range details {
|
||||
addrs := make([]string, len(svc.Addrs))
|
||||
for i, a := range svc.Addrs {
|
||||
addrs[i] = a.String()
|
||||
}
|
||||
ports := make([]string, len(svc.Ports))
|
||||
for i, p := range svc.Ports {
|
||||
ports[i] = p.String()
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t\n",
|
||||
svc.Name,
|
||||
strings.Join(addrs, ", "),
|
||||
strings.Join(ports, ", "),
|
||||
)
|
||||
if len(svc.Annotations) > 0 {
|
||||
// Print annotations sorted by key, indented under the service row.
|
||||
keys := make([]string, 0, len(svc.Annotations))
|
||||
for k := range svc.Annotations {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(w, " %s: %s\t\t\t\n", k, svc.Annotations[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
@@ -564,6 +564,19 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
|
||||
return ip4, ip6
|
||||
}
|
||||
|
||||
// GetServiceDetails returns the VIP services that the control plane has
|
||||
// approved this node to serve, including their names, assigned IP addresses,
|
||||
// ports, and application-specific annotations (e.g. proxy service
|
||||
// configuration). Returns nil if the server is not running or no service
|
||||
// details have been received yet.
|
||||
func (s *Server) GetServiceDetails() []*tailcfg.ServiceDetail {
|
||||
nm := s.lb.NetMap()
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
return nm.GetVIPServiceDetails()
|
||||
}
|
||||
|
||||
// LogtailWriter returns an [io.Writer] that writes to Tailscale's logging service and will be only visible to Tailscale's
|
||||
// support team. Logs written there cannot be retrieved by the user. This method always returns a non-nil value.
|
||||
func (s *Server) LogtailWriter() io.Writer {
|
||||
|
||||
Reference in New Issue
Block a user