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 }