github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/cmd/status.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  
    11  	"github.com/spf13/cobra"
    12  	empty "google.golang.org/protobuf/types/known/emptypb"
    13  
    14  	"github.com/telepresenceio/telepresence/rpc/v2/connector"
    15  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/ann"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/daemon"
    19  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/global"
    20  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/output"
    21  	"github.com/telepresenceio/telepresence/v2/pkg/client/scout"
    22  	"github.com/telepresenceio/telepresence/v2/pkg/ioutil"
    23  	"github.com/telepresenceio/telepresence/v2/pkg/iputil"
    24  )
    25  
    26  type StatusInfo struct {
    27  	RootDaemon     RootDaemonStatus     `json:"root_daemon" yaml:"root_daemon"`
    28  	UserDaemon     UserDaemonStatus     `json:"user_daemon" yaml:"user_daemon"`
    29  	TrafficManager TrafficManagerStatus `json:"traffic_manager" yaml:"traffic_manager"`
    30  }
    31  
    32  type MultiConnectStatusInfo struct {
    33  	extendedInfo ioutil.WriterTos
    34  	statusInfos  []ioutil.WriterTos
    35  }
    36  
    37  type SingleConnectStatusInfo struct {
    38  	extendedInfo ioutil.WriterTos
    39  	statusInfo   ioutil.WriterTos
    40  }
    41  
    42  type RootDaemonStatus struct {
    43  	Running              bool             `json:"running,omitempty" yaml:"running,omitempty"`
    44  	Name                 string           `json:"name,omitempty" yaml:"name,omitempty"`
    45  	Version              string           `json:"version,omitempty" yaml:"version,omitempty"`
    46  	APIVersion           int32            `json:"api_version,omitempty" yaml:"api_version,omitempty"`
    47  	DNS                  *client.DNSSnake `json:"dns,omitempty" yaml:"dns,omitempty"`
    48  	*client.RoutingSnake `yaml:",inline"`
    49  }
    50  
    51  type UserDaemonStatus struct {
    52  	Running           bool                     `json:"running,omitempty" yaml:"running,omitempty"`
    53  	InDocker          bool                     `json:"in_docker,omitempty" yaml:"in_docker,omitempty"`
    54  	Name              string                   `json:"name,omitempty" yaml:"name,omitempty"`
    55  	DaemonPort        int                      `json:"daemon_port,omitempty" yaml:"daemon_port,omitempty"`
    56  	ContainerNetwork  string                   `json:"container_network,omitempty" yaml:"container_network,omitempty"`
    57  	Hostname          string                   `json:"hostname,omitempty" yaml:"hostname,omitempty"`
    58  	ExposedPorts      []string                 `json:"exposedPorts,omitempty" yaml:"exposedPorts,omitempty"`
    59  	Version           string                   `json:"version,omitempty" yaml:"version,omitempty"`
    60  	Executable        string                   `json:"executable,omitempty" yaml:"executable,omitempty"`
    61  	InstallID         string                   `json:"install_id,omitempty" yaml:"install_id,omitempty"`
    62  	Status            string                   `json:"status,omitempty" yaml:"status,omitempty"`
    63  	Error             string                   `json:"error,omitempty" yaml:"error,omitempty"`
    64  	KubernetesServer  string                   `json:"kubernetes_server,omitempty" yaml:"kubernetes_server,omitempty"`
    65  	KubernetesContext string                   `json:"kubernetes_context,omitempty" yaml:"kubernetes_context,omitempty"`
    66  	Namespace         string                   `json:"namespace,omitempty" yaml:"namespace,omitempty"`
    67  	ManagerNamespace  string                   `json:"manager_namespace,omitempty" yaml:"manager_namespace,omitempty"`
    68  	MappedNamespaces  []string                 `json:"mapped_namespaces,omitempty" yaml:"mapped_namespaces,omitempty"`
    69  	Intercepts        []ConnectStatusIntercept `json:"intercepts,omitempty" yaml:"intercepts,omitempty"`
    70  	versionName       string
    71  }
    72  
    73  type ContainerizedDaemonStatus struct {
    74  	*UserDaemonStatus    `yaml:",inline"`
    75  	DNS                  *client.DNSSnake `json:"dns,omitempty" yaml:"dns,omitempty"`
    76  	*client.RoutingSnake `yaml:",inline"`
    77  }
    78  
    79  type TrafficManagerStatus struct {
    80  	Name         string `json:"name,omitempty" yaml:"name,omitempty"`
    81  	Version      string `json:"version,omitempty" yaml:"version,omitempty"`
    82  	TrafficAgent string `json:"traffic_agent,omitempty" yaml:"traffic_agent,omitempty"`
    83  	extendedInfo ioutil.KeyValueProvider
    84  }
    85  
    86  type ConnectStatusIntercept struct {
    87  	Name   string `json:"name,omitempty" yaml:"name,omitempty"`
    88  	Client string `json:"client,omitempty" yaml:"client,omitempty"`
    89  }
    90  
    91  const (
    92  	multiDaemonFlag = "multi-daemon"
    93  	jsonFlag        = "json"
    94  )
    95  
    96  func statusCmd() *cobra.Command {
    97  	cmd := &cobra.Command{
    98  		Use:  "status",
    99  		Args: cobra.NoArgs,
   100  
   101  		Short:             "Show connectivity status",
   102  		RunE:              run,
   103  		PersistentPreRunE: fixFlag,
   104  		Annotations: map[string]string{
   105  			ann.UserDaemon: ann.Optional,
   106  		},
   107  	}
   108  	flags := cmd.Flags()
   109  	flags.Bool(multiDaemonFlag, false, "always use multi-daemon output format, even if there's only one daemon connected")
   110  	flags.BoolP(jsonFlag, "j", false, "output as json object")
   111  	flags.Lookup(jsonFlag).Hidden = true
   112  	return cmd
   113  }
   114  
   115  func fixFlag(cmd *cobra.Command, _ []string) error {
   116  	flags := cmd.Flags()
   117  	json, err := flags.GetBool(jsonFlag)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	rootCmd := cmd.Parent()
   122  	if json {
   123  		if err = rootCmd.PersistentFlags().Set(global.FlagOutput, "json"); err != nil {
   124  			return err
   125  		}
   126  	}
   127  	return rootCmd.PersistentPreRunE(cmd, flags.Args())
   128  }
   129  
   130  // status will retrieve connectivity status from the daemon and print it on stdout.
   131  func run(cmd *cobra.Command, _ []string) error {
   132  	var mdErr daemon.MultipleDaemonsError
   133  	err := connect.InitCommand(cmd)
   134  	if err != nil {
   135  		if !errors.As(err, &mdErr) {
   136  			return err
   137  		}
   138  	}
   139  	ctx := cmd.Context()
   140  
   141  	var sis []ioutil.WriterTos
   142  	if len(mdErr) > 0 {
   143  		sis = make([]ioutil.WriterTos, len(mdErr))
   144  		for i, info := range mdErr {
   145  			ud, err := connect.ExistingDaemon(ctx, info)
   146  			if err != nil {
   147  				return err
   148  			}
   149  			sis[i], err = getStatusInfo(daemon.WithUserClient(ctx, ud), info)
   150  			ud.Conn.Close()
   151  			if err != nil {
   152  				return err
   153  			}
   154  		}
   155  	} else {
   156  		si, err := getStatusInfo(ctx, nil)
   157  		if err != nil {
   158  			return err
   159  		}
   160  		sis = []ioutil.WriterTos{si}
   161  	}
   162  
   163  	sx, err := GetStatusInfo(ctx)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	multiFormat := len(sis) > 1
   169  	if !multiFormat {
   170  		multiFormat, _ = cmd.Flags().GetBool(multiDaemonFlag)
   171  	}
   172  	var as ioutil.WriterTos
   173  	if multiFormat {
   174  		as = &MultiConnectStatusInfo{
   175  			extendedInfo: sx,
   176  			statusInfos:  sis,
   177  		}
   178  	} else {
   179  		as = &SingleConnectStatusInfo{
   180  			extendedInfo: sx,
   181  			statusInfo:   sis[0],
   182  		}
   183  	}
   184  
   185  	if output.WantsFormatted(cmd) {
   186  		output.Object(ctx, &as, true)
   187  	} else {
   188  		_, _ = ioutil.WriteAllTo(cmd.OutOrStdout(), as.WriterTos()...)
   189  	}
   190  	return nil
   191  }
   192  
   193  // GetStatusInfo may return an extended struct
   194  //
   195  //nolint:gochecknoglobals // extension point
   196  var GetStatusInfo = func(ctx context.Context) (ioutil.WriterTos, error) {
   197  	return nil, nil
   198  }
   199  
   200  // GetTrafficManagerStatusExtras may return an extended struct
   201  //
   202  //nolint:gochecknoglobals // extension point
   203  var GetTrafficManagerStatusExtras = func(context.Context, *daemon.UserClient) ioutil.KeyValueProvider {
   204  	return nil
   205  }
   206  
   207  func (s *StatusInfo) WriterTos() []io.WriterTo {
   208  	if s.UserDaemon.InDocker {
   209  		return []io.WriterTo{
   210  			&ContainerizedDaemonStatus{
   211  				UserDaemonStatus: &s.UserDaemon,
   212  				DNS:              s.RootDaemon.DNS,
   213  				RoutingSnake:     s.RootDaemon.RoutingSnake,
   214  			},
   215  			&s.TrafficManager,
   216  		}
   217  	}
   218  	return []io.WriterTo{&s.UserDaemon, &s.RootDaemon, &s.TrafficManager}
   219  }
   220  
   221  func (s *StatusInfo) MarshalJSON() ([]byte, error) {
   222  	return json.Marshal(s.toMap())
   223  }
   224  
   225  func (s *StatusInfo) MarshalYAML() (any, error) {
   226  	return s.toMap(), nil
   227  }
   228  
   229  func (s *StatusInfo) toMap() map[string]any {
   230  	if s.UserDaemon.InDocker {
   231  		return map[string]any{
   232  			"daemon": &ContainerizedDaemonStatus{
   233  				UserDaemonStatus: &s.UserDaemon,
   234  				DNS:              s.RootDaemon.DNS,
   235  				RoutingSnake:     s.RootDaemon.RoutingSnake,
   236  			},
   237  			"traffic_manager": &s.TrafficManager,
   238  		}
   239  	}
   240  	return map[string]any{
   241  		"user_daemon":     &s.UserDaemon,
   242  		"root_daemon":     &s.RootDaemon,
   243  		"traffic_manager": &s.TrafficManager,
   244  	}
   245  }
   246  
   247  func getStatusInfo(ctx context.Context, di *daemon.Info) (*StatusInfo, error) {
   248  	wt := &StatusInfo{}
   249  	userD := daemon.GetUserClient(ctx)
   250  	if userD == nil {
   251  		return wt, nil
   252  	}
   253  	ctx = scout.NewReporter(ctx, "cli")
   254  	us := &wt.UserDaemon
   255  	us.InstallID = scout.InstallID(ctx)
   256  	us.Running = true
   257  	v, err := userD.Version(ctx, &empty.Empty{})
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	us.Version = v.Version
   262  	us.versionName = v.Name
   263  	us.Executable = v.Executable
   264  	us.Name = userD.DaemonID.Name
   265  
   266  	if userD.Containerized() {
   267  		us.InDocker = true
   268  		us.DaemonPort = userD.DaemonPort()
   269  		if di != nil {
   270  			us.Hostname = di.Hostname
   271  			us.ExposedPorts = di.ExposedPorts
   272  		}
   273  		us.ContainerNetwork = "container:" + userD.DaemonID.ContainerName()
   274  		if us.versionName == "" {
   275  			us.versionName = "Daemon"
   276  		}
   277  	} else if us.versionName == "" {
   278  		us.versionName = "User daemon"
   279  	}
   280  
   281  	status, err := userD.Status(ctx, &empty.Empty{})
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	switch status.Error {
   286  	case connector.ConnectInfo_UNSPECIFIED, connector.ConnectInfo_ALREADY_CONNECTED:
   287  		us.Status = "Connected"
   288  		us.KubernetesServer = status.ClusterServer
   289  		us.KubernetesContext = status.ClusterContext
   290  		for _, icept := range status.GetIntercepts().GetIntercepts() {
   291  			us.Intercepts = append(us.Intercepts, ConnectStatusIntercept{
   292  				Name:   icept.Spec.Name,
   293  				Client: icept.Spec.Client,
   294  			})
   295  		}
   296  		us.Namespace = status.Namespace
   297  		us.ManagerNamespace = status.ManagerNamespace
   298  		us.MappedNamespaces = status.MappedNamespaces
   299  	case connector.ConnectInfo_UNAUTHORIZED:
   300  		us.Status = "Not authorized to connect"
   301  		us.Error = status.ErrorText
   302  	case connector.ConnectInfo_UNAUTHENTICATED:
   303  		us.Status = "Not logged in"
   304  		us.Error = status.ErrorText
   305  	case connector.ConnectInfo_MUST_RESTART:
   306  		us.Status = "Connected, but must restart"
   307  	case connector.ConnectInfo_DISCONNECTED:
   308  		us.Status = "Not connected"
   309  	case connector.ConnectInfo_CLUSTER_FAILED:
   310  		us.Status = "Not connected, error talking to cluster"
   311  		us.Error = status.ErrorText
   312  	case connector.ConnectInfo_TRAFFIC_MANAGER_FAILED:
   313  		us.Status = "Not connected, error talking to in-cluster Telepresence traffic-manager"
   314  		us.Error = status.ErrorText
   315  	}
   316  
   317  	rStatus := status.DaemonStatus
   318  	if rStatus != nil {
   319  		rs := &wt.RootDaemon
   320  		rs.Running = true
   321  		rs.Name = rStatus.Version.Name
   322  		if rs.Name == "" {
   323  			rs.Name = "Root Daemon"
   324  		}
   325  		rs.Version = rStatus.Version.Version
   326  		rs.APIVersion = rStatus.Version.ApiVersion
   327  		if obc := rStatus.OutboundConfig; obc != nil {
   328  			rs.DNS = &client.DNSSnake{}
   329  			dns := obc.Dns
   330  			if dns.LocalIp != nil {
   331  				// Local IP is only set when the overriding resolver is used
   332  				rs.DNS.LocalIP = dns.LocalIp
   333  			}
   334  			rs.DNS.Error = dns.Error
   335  			rs.DNS.RemoteIP = dns.RemoteIp
   336  			rs.DNS.ExcludeSuffixes = dns.ExcludeSuffixes
   337  			rs.DNS.IncludeSuffixes = dns.IncludeSuffixes
   338  			rs.DNS.Excludes = dns.Excludes
   339  			rs.DNS.Mappings.FromRPC(dns.Mappings)
   340  			rs.DNS.LookupTimeout = dns.LookupTimeout.AsDuration()
   341  			rs.RoutingSnake = &client.RoutingSnake{}
   342  			for _, subnet := range rStatus.Subnets {
   343  				rs.RoutingSnake.Subnets = append(rs.RoutingSnake.Subnets, (*iputil.Subnet)(iputil.IPNetFromRPC(subnet)))
   344  			}
   345  			for _, subnet := range obc.AlsoProxySubnets {
   346  				rs.RoutingSnake.AlsoProxy = append(rs.RoutingSnake.AlsoProxy, (*iputil.Subnet)(iputil.IPNetFromRPC(subnet)))
   347  			}
   348  			for _, subnet := range obc.NeverProxySubnets {
   349  				rs.RoutingSnake.NeverProxy = append(rs.RoutingSnake.NeverProxy, (*iputil.Subnet)(iputil.IPNetFromRPC(subnet)))
   350  			}
   351  			for _, subnet := range obc.AllowConflictingSubnets {
   352  				rs.RoutingSnake.AllowConflicting = append(rs.RoutingSnake.AllowConflicting, (*iputil.Subnet)(iputil.IPNetFromRPC(subnet)))
   353  			}
   354  		}
   355  	}
   356  
   357  	if v, err := userD.TrafficManagerVersion(ctx, &empty.Empty{}); err == nil {
   358  		tm := &wt.TrafficManager
   359  		tm.Name = v.Name
   360  		tm.Version = v.Version
   361  		if af, err := userD.AgentImageFQN(ctx, &empty.Empty{}); err == nil {
   362  			tm.TrafficAgent = af.FQN
   363  		}
   364  		tm.extendedInfo = GetTrafficManagerStatusExtras(ctx, userD)
   365  	}
   366  
   367  	return wt, nil
   368  }
   369  
   370  func (s *SingleConnectStatusInfo) WriterTos() []io.WriterTo {
   371  	var wts []io.WriterTo
   372  	if s.extendedInfo != nil {
   373  		wts = s.extendedInfo.WriterTos()
   374  	}
   375  	wts = append(wts, s.statusInfo.WriterTos()...)
   376  	return wts
   377  }
   378  
   379  func (s *SingleConnectStatusInfo) MarshalJSON() ([]byte, error) {
   380  	m, err := s.toMap()
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  	return json.Marshal(m)
   385  }
   386  
   387  func (s *SingleConnectStatusInfo) MarshalYAML() (any, error) {
   388  	return s.toMap()
   389  }
   390  
   391  func (s *SingleConnectStatusInfo) toMap() (map[string]any, error) {
   392  	m := make(map[string]any)
   393  	if s.extendedInfo != nil {
   394  		sx, err := json.Marshal(s.extendedInfo)
   395  		if err != nil {
   396  			return nil, err
   397  		}
   398  		if err = json.Unmarshal(sx, &m); err != nil {
   399  			return nil, err
   400  		}
   401  	}
   402  	sx, err := json.Marshal(s.statusInfo)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  	if err = json.Unmarshal(sx, &m); err != nil {
   407  		return nil, err
   408  	}
   409  	return m, nil
   410  }
   411  
   412  func (s *MultiConnectStatusInfo) MarshalJSON() ([]byte, error) {
   413  	m, err := s.toMap()
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  	return json.Marshal(m)
   418  }
   419  
   420  func (s *MultiConnectStatusInfo) MarshalYAML() (any, error) {
   421  	return s.toMap()
   422  }
   423  
   424  func (s *MultiConnectStatusInfo) toMap() (map[string]any, error) {
   425  	m := make(map[string]any)
   426  	if s.extendedInfo != nil {
   427  		sx, err := json.Marshal(s.extendedInfo)
   428  		if err != nil {
   429  			return nil, err
   430  		}
   431  		if err = json.Unmarshal(sx, &m); err != nil {
   432  			return nil, err
   433  		}
   434  	}
   435  	m["connections"] = s.statusInfos
   436  	return m, nil
   437  }
   438  
   439  func (s *MultiConnectStatusInfo) WriterTos() []io.WriterTo {
   440  	var wts []io.WriterTo
   441  	if s.extendedInfo != nil {
   442  		wts = s.extendedInfo.WriterTos()
   443  	}
   444  	for _, v := range s.statusInfos {
   445  		wts = append(wts, v.WriterTos()...)
   446  	}
   447  	return wts
   448  }
   449  
   450  func (cs *ContainerizedDaemonStatus) WriteTo(out io.Writer) (int64, error) {
   451  	n := 0
   452  	if cs.UserDaemonStatus.Running {
   453  		n += ioutil.Printf(out, "%s %s: Running\n", cs.UserDaemonStatus.versionName, cs.UserDaemonStatus.Name)
   454  		kvf := ioutil.DefaultKeyValueFormatter()
   455  		kvf.Prefix = "  "
   456  		kvf.Indent = "  "
   457  		cs.print(kvf)
   458  		if cs.DNS != nil {
   459  			printDNS(kvf, cs.DNS)
   460  		}
   461  		if cs.RoutingSnake != nil {
   462  			printRouting(kvf, cs.RoutingSnake)
   463  		}
   464  		n += kvf.Println(out)
   465  	} else {
   466  		n += ioutil.Println(out, "Daemon: Not running")
   467  	}
   468  	return int64(n), nil
   469  }
   470  
   471  func (ds *RootDaemonStatus) WriteTo(out io.Writer) (int64, error) {
   472  	n := 0
   473  	if ds.Running {
   474  		n += ioutil.Printf(out, "%s: Running\n", ds.Name)
   475  		kvf := ioutil.DefaultKeyValueFormatter()
   476  		kvf.Prefix = "  "
   477  		kvf.Indent = "  "
   478  		kvf.Add("Version", ds.Version)
   479  		if ds.DNS != nil {
   480  			printDNS(kvf, ds.DNS)
   481  		}
   482  		if ds.RoutingSnake != nil {
   483  			printRouting(kvf, ds.RoutingSnake)
   484  		}
   485  		n += kvf.Println(out)
   486  	} else {
   487  		n += ioutil.Println(out, "Root Daemon: Not running")
   488  	}
   489  	return int64(n), nil
   490  }
   491  
   492  func printDNS(kvf *ioutil.KeyValueFormatter, d *client.DNSSnake) {
   493  	dnsKvf := ioutil.DefaultKeyValueFormatter()
   494  	kvf.Indent = "  "
   495  	if d.Error != "" {
   496  		dnsKvf.Add("Error", d.Error)
   497  	}
   498  	if len(d.LocalIP) > 0 {
   499  		dnsKvf.Add("Local IP", d.LocalIP.String())
   500  	}
   501  	if len(d.RemoteIP) > 0 {
   502  		dnsKvf.Add("Remote IP", d.RemoteIP.String())
   503  	}
   504  	dnsKvf.Add("Exclude suffixes", fmt.Sprintf("%v", d.ExcludeSuffixes))
   505  	dnsKvf.Add("Include suffixes", fmt.Sprintf("%v", d.IncludeSuffixes))
   506  	if len(d.Excludes) > 0 {
   507  		dnsKvf.Add("Excludes", fmt.Sprintf("%v", d.Excludes))
   508  	}
   509  	if len(d.Mappings) > 0 {
   510  		mappingsKvf := ioutil.DefaultKeyValueFormatter()
   511  		for i := range d.Mappings {
   512  			mappingsKvf.Add(d.Mappings[i].Name, d.Mappings[i].AliasFor)
   513  		}
   514  		dnsKvf.Add("Mappings", "\n"+mappingsKvf.String())
   515  	}
   516  	dnsKvf.Add("Timeout", fmt.Sprintf("%v", d.LookupTimeout))
   517  	kvf.Add("DNS", "\n"+dnsKvf.String())
   518  }
   519  
   520  func printRouting(kvf *ioutil.KeyValueFormatter, r *client.RoutingSnake) {
   521  	printSubnets := func(title string, subnets []*iputil.Subnet) {
   522  		if len(subnets) == 0 {
   523  			return
   524  		}
   525  		out := &strings.Builder{}
   526  		ioutil.Printf(out, "(%d subnets)", len(subnets))
   527  		for _, subnet := range subnets {
   528  			ioutil.Printf(out, "\n- %s", subnet)
   529  		}
   530  		kvf.Add(title, out.String())
   531  	}
   532  	printSubnets("Subnets", r.Subnets)
   533  	printSubnets("Also Proxy", r.AlsoProxy)
   534  	printSubnets("Never Proxy", r.NeverProxy)
   535  	printSubnets("Allow conflicts for", r.AllowConflicting)
   536  }
   537  
   538  func (cs *UserDaemonStatus) WriteTo(out io.Writer) (int64, error) {
   539  	n := 0
   540  	if cs.Running {
   541  		n += ioutil.Printf(out, "%s: Running\n", cs.versionName)
   542  		kvf := ioutil.DefaultKeyValueFormatter()
   543  		kvf.Prefix = "  "
   544  		kvf.Indent = "  "
   545  		cs.print(kvf)
   546  		n += kvf.Println(out)
   547  	} else {
   548  		n += ioutil.Println(out, "User Daemon: Not running")
   549  	}
   550  	return int64(n), nil
   551  }
   552  
   553  func (cs *UserDaemonStatus) print(kvf *ioutil.KeyValueFormatter) {
   554  	kvf.Add("Version", cs.Version)
   555  	kvf.Add("Executable", cs.Executable)
   556  	kvf.Add("Install ID", cs.InstallID)
   557  	kvf.Add("Status", cs.Status)
   558  	if cs.Error != "" {
   559  		kvf.Add("Error", cs.Error)
   560  	}
   561  	kvf.Add("Kubernetes server", cs.KubernetesServer)
   562  	kvf.Add("Kubernetes context", cs.KubernetesContext)
   563  	if cs.ContainerNetwork != "" {
   564  		kvf.Add("Container network", cs.ContainerNetwork)
   565  	}
   566  	kvf.Add("Namespace", cs.Namespace)
   567  	kvf.Add("Manager namespace", cs.ManagerNamespace)
   568  	if len(cs.MappedNamespaces) > 0 {
   569  		kvf.Add("Mapped namespaces", fmt.Sprintf("%v", cs.MappedNamespaces))
   570  	}
   571  	if cs.Hostname != "" {
   572  		kvf.Add("Hostname", cs.Hostname)
   573  	}
   574  	if len(cs.ExposedPorts) > 0 {
   575  		kvf.Add("Exposed ports", fmt.Sprintf("%v", cs.ExposedPorts))
   576  	}
   577  	out := &strings.Builder{}
   578  	fmt.Fprintf(out, "%d total\n", len(cs.Intercepts))
   579  	if len(cs.Intercepts) > 0 {
   580  		subKvf := ioutil.DefaultKeyValueFormatter()
   581  		subKvf.Indent = "  "
   582  		for _, intercept := range cs.Intercepts {
   583  			subKvf.Add(intercept.Name, intercept.Client)
   584  		}
   585  		subKvf.Println(out)
   586  	}
   587  	kvf.Add("Intercepts", out.String())
   588  }
   589  
   590  func (ts *TrafficManagerStatus) MarshalJSON() ([]byte, error) {
   591  	m, err := ts.toMap()
   592  	if err != nil {
   593  		return nil, err
   594  	}
   595  	return json.Marshal(m)
   596  }
   597  
   598  func (ts *TrafficManagerStatus) MarshalYAML() (any, error) {
   599  	return ts.toMap()
   600  }
   601  
   602  func (ts *TrafficManagerStatus) toMap() (map[string]any, error) {
   603  	m := make(map[string]any)
   604  	if ts.extendedInfo != nil {
   605  		sx, err := json.Marshal(ts.extendedInfo)
   606  		if err != nil {
   607  			return nil, err
   608  		}
   609  		if err = json.Unmarshal(sx, &m); err != nil {
   610  			return nil, err
   611  		}
   612  	}
   613  	m["name"] = ts.Name
   614  	m["traffic_agent"] = ts.TrafficAgent
   615  	m["version"] = ts.Version
   616  	return m, nil
   617  }
   618  
   619  func (ts *TrafficManagerStatus) WriteTo(out io.Writer) (int64, error) {
   620  	n := 0
   621  	if ts.Name != "" {
   622  		n += ioutil.Printf(out, "%s: Connected\n", ts.Name)
   623  		kvf := ioutil.DefaultKeyValueFormatter()
   624  		kvf.Prefix = "  "
   625  		kvf.Indent = "  "
   626  		kvf.Add("Version", ts.Version)
   627  		if ts.TrafficAgent != "" {
   628  			kvf.Add("Traffic Agent", ts.TrafficAgent)
   629  		}
   630  		if ts.extendedInfo != nil {
   631  			ts.extendedInfo.AddTo(kvf)
   632  		}
   633  		n += kvf.Println(out)
   634  	} else {
   635  		n += ioutil.Println(out, "Traffic Manager: Not connected")
   636  	}
   637  	return int64(n), nil
   638  }