github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/command/system/info.go (about) 1 package system 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "sort" 8 "strings" 9 10 "github.com/docker/cli/cli" 11 pluginmanager "github.com/docker/cli/cli-plugins/manager" 12 "github.com/docker/cli/cli/command" 13 "github.com/docker/cli/cli/debug" 14 "github.com/docker/cli/templates" 15 "github.com/docker/docker/api/types" 16 "github.com/docker/docker/api/types/swarm" 17 "github.com/docker/docker/api/types/versions" 18 "github.com/docker/go-units" 19 "github.com/spf13/cobra" 20 ) 21 22 type infoOptions struct { 23 format string 24 } 25 26 type clientInfo struct { 27 Debug bool 28 Context string 29 Plugins []pluginmanager.Plugin 30 Warnings []string 31 } 32 33 type info struct { 34 // This field should/could be ServerInfo but is anonymous to 35 // preserve backwards compatibility in the JSON rendering 36 // which has ServerInfo immediately within the top-level 37 // object. 38 *types.Info `json:",omitempty"` 39 ServerErrors []string `json:",omitempty"` 40 41 ClientInfo *clientInfo `json:",omitempty"` 42 ClientErrors []string `json:",omitempty"` 43 } 44 45 // NewInfoCommand creates a new cobra.Command for `docker info` 46 func NewInfoCommand(dockerCli command.Cli) *cobra.Command { 47 var opts infoOptions 48 49 cmd := &cobra.Command{ 50 Use: "info [OPTIONS]", 51 Short: "Display system-wide information", 52 Args: cli.NoArgs, 53 RunE: func(cmd *cobra.Command, args []string) error { 54 return runInfo(cmd, dockerCli, &opts) 55 }, 56 } 57 58 flags := cmd.Flags() 59 60 flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") 61 62 return cmd 63 } 64 65 func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error { 66 var info info 67 68 ctx := context.Background() 69 if dinfo, err := dockerCli.Client().Info(ctx); err == nil { 70 info.Info = &dinfo 71 } else { 72 info.ServerErrors = append(info.ServerErrors, err.Error()) 73 } 74 75 info.ClientInfo = &clientInfo{ 76 Context: dockerCli.CurrentContext(), 77 Debug: debug.IsEnabled(), 78 } 79 if plugins, err := pluginmanager.ListPlugins(dockerCli, cmd.Root()); err == nil { 80 info.ClientInfo.Plugins = plugins 81 } else { 82 info.ClientErrors = append(info.ClientErrors, err.Error()) 83 } 84 85 if opts.format == "" { 86 return prettyPrintInfo(dockerCli, info) 87 } 88 return formatInfo(dockerCli, info, opts.format) 89 } 90 91 func prettyPrintInfo(dockerCli command.Cli, info info) error { 92 fmt.Fprintln(dockerCli.Out(), "Client:") 93 if info.ClientInfo != nil { 94 prettyPrintClientInfo(dockerCli, *info.ClientInfo) 95 } 96 for _, err := range info.ClientErrors { 97 fmt.Fprintln(dockerCli.Out(), "ERROR:", err) 98 } 99 100 fmt.Fprintln(dockerCli.Out()) 101 fmt.Fprintln(dockerCli.Out(), "Server:") 102 if info.Info != nil { 103 for _, err := range prettyPrintServerInfo(dockerCli, *info.Info) { 104 info.ServerErrors = append(info.ServerErrors, err.Error()) 105 } 106 } 107 for _, err := range info.ServerErrors { 108 fmt.Fprintln(dockerCli.Out(), "ERROR:", err) 109 } 110 111 if len(info.ServerErrors) > 0 || len(info.ClientErrors) > 0 { 112 return fmt.Errorf("errors pretty printing info") 113 } 114 return nil 115 } 116 117 func prettyPrintClientInfo(dockerCli command.Cli, info clientInfo) { 118 fmt.Fprintln(dockerCli.Out(), " Context: ", info.Context) 119 fmt.Fprintln(dockerCli.Out(), " Debug Mode:", info.Debug) 120 121 if len(info.Plugins) > 0 { 122 fmt.Fprintln(dockerCli.Out(), " Plugins:") 123 for _, p := range info.Plugins { 124 if p.Err == nil { 125 var version string 126 if p.Version != "" { 127 version = ", " + p.Version 128 } 129 fmt.Fprintf(dockerCli.Out(), " %s: %s (%s%s)\n", p.Name, p.ShortDescription, p.Vendor, version) 130 } else { 131 info.Warnings = append(info.Warnings, fmt.Sprintf("WARNING: Plugin %q is not valid: %s", p.Path, p.Err)) 132 } 133 } 134 } 135 136 if len(info.Warnings) > 0 { 137 fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n")) 138 } 139 } 140 141 // nolint: gocyclo 142 func prettyPrintServerInfo(dockerCli command.Cli, info types.Info) []error { 143 var errs []error 144 145 fmt.Fprintln(dockerCli.Out(), " Containers:", info.Containers) 146 fmt.Fprintln(dockerCli.Out(), " Running:", info.ContainersRunning) 147 fmt.Fprintln(dockerCli.Out(), " Paused:", info.ContainersPaused) 148 fmt.Fprintln(dockerCli.Out(), " Stopped:", info.ContainersStopped) 149 fmt.Fprintln(dockerCli.Out(), " Images:", info.Images) 150 fprintlnNonEmpty(dockerCli.Out(), " Server Version:", info.ServerVersion) 151 fprintlnNonEmpty(dockerCli.Out(), " Storage Driver:", info.Driver) 152 if info.DriverStatus != nil { 153 for _, pair := range info.DriverStatus { 154 fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) 155 } 156 } 157 if info.SystemStatus != nil { 158 for _, pair := range info.SystemStatus { 159 fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) 160 } 161 } 162 fprintlnNonEmpty(dockerCli.Out(), " Logging Driver:", info.LoggingDriver) 163 fprintlnNonEmpty(dockerCli.Out(), " Cgroup Driver:", info.CgroupDriver) 164 fprintlnNonEmpty(dockerCli.Out(), " Cgroup Version:", info.CgroupVersion) 165 166 fmt.Fprintln(dockerCli.Out(), " Plugins:") 167 fmt.Fprintln(dockerCli.Out(), " Volume:", strings.Join(info.Plugins.Volume, " ")) 168 fmt.Fprintln(dockerCli.Out(), " Network:", strings.Join(info.Plugins.Network, " ")) 169 170 if len(info.Plugins.Authorization) != 0 { 171 fmt.Fprintln(dockerCli.Out(), " Authorization:", strings.Join(info.Plugins.Authorization, " ")) 172 } 173 174 fmt.Fprintln(dockerCli.Out(), " Log:", strings.Join(info.Plugins.Log, " ")) 175 176 fmt.Fprintln(dockerCli.Out(), " Swarm:", info.Swarm.LocalNodeState) 177 printSwarmInfo(dockerCli, info) 178 179 if len(info.Runtimes) > 0 { 180 fmt.Fprint(dockerCli.Out(), " Runtimes:") 181 for name := range info.Runtimes { 182 fmt.Fprintf(dockerCli.Out(), " %s", name) 183 } 184 fmt.Fprint(dockerCli.Out(), "\n") 185 fmt.Fprintln(dockerCli.Out(), " Default Runtime:", info.DefaultRuntime) 186 } 187 188 if info.OSType == "linux" { 189 fmt.Fprintln(dockerCli.Out(), " Init Binary:", info.InitBinary) 190 191 for _, ci := range []struct { 192 Name string 193 Commit types.Commit 194 }{ 195 {"containerd", info.ContainerdCommit}, 196 {"runc", info.RuncCommit}, 197 {"init", info.InitCommit}, 198 } { 199 fmt.Fprintf(dockerCli.Out(), " %s version: %s", ci.Name, ci.Commit.ID) 200 if ci.Commit.ID != ci.Commit.Expected { 201 fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected) 202 } 203 fmt.Fprint(dockerCli.Out(), "\n") 204 } 205 if len(info.SecurityOptions) != 0 { 206 if kvs, err := types.DecodeSecurityOptions(info.SecurityOptions); err != nil { 207 errs = append(errs, err) 208 } else { 209 fmt.Fprintln(dockerCli.Out(), " Security Options:") 210 for _, so := range kvs { 211 fmt.Fprintln(dockerCli.Out(), " "+so.Name) 212 for _, o := range so.Options { 213 switch o.Key { 214 case "profile": 215 fmt.Fprintln(dockerCli.Out(), " Profile:", o.Value) 216 } 217 } 218 } 219 } 220 } 221 } 222 223 // Isolation only has meaning on a Windows daemon. 224 if info.OSType == "windows" { 225 fmt.Fprintln(dockerCli.Out(), " Default Isolation:", info.Isolation) 226 } 227 228 fprintlnNonEmpty(dockerCli.Out(), " Kernel Version:", info.KernelVersion) 229 fprintlnNonEmpty(dockerCli.Out(), " Operating System:", info.OperatingSystem) 230 fprintlnNonEmpty(dockerCli.Out(), " OSType:", info.OSType) 231 fprintlnNonEmpty(dockerCli.Out(), " Architecture:", info.Architecture) 232 fmt.Fprintln(dockerCli.Out(), " CPUs:", info.NCPU) 233 fmt.Fprintln(dockerCli.Out(), " Total Memory:", units.BytesSize(float64(info.MemTotal))) 234 fprintlnNonEmpty(dockerCli.Out(), " Name:", info.Name) 235 fprintlnNonEmpty(dockerCli.Out(), " ID:", info.ID) 236 fmt.Fprintln(dockerCli.Out(), " Docker Root Dir:", info.DockerRootDir) 237 fmt.Fprintln(dockerCli.Out(), " Debug Mode:", info.Debug) 238 239 if info.Debug { 240 fmt.Fprintln(dockerCli.Out(), " File Descriptors:", info.NFd) 241 fmt.Fprintln(dockerCli.Out(), " Goroutines:", info.NGoroutines) 242 fmt.Fprintln(dockerCli.Out(), " System Time:", info.SystemTime) 243 fmt.Fprintln(dockerCli.Out(), " EventsListeners:", info.NEventsListener) 244 } 245 246 fprintlnNonEmpty(dockerCli.Out(), " HTTP Proxy:", info.HTTPProxy) 247 fprintlnNonEmpty(dockerCli.Out(), " HTTPS Proxy:", info.HTTPSProxy) 248 fprintlnNonEmpty(dockerCli.Out(), " No Proxy:", info.NoProxy) 249 250 if info.IndexServerAddress != "" { 251 u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username 252 if len(u) > 0 { 253 fmt.Fprintln(dockerCli.Out(), " Username:", u) 254 } 255 fmt.Fprintln(dockerCli.Out(), " Registry:", info.IndexServerAddress) 256 } 257 258 if info.Labels != nil { 259 fmt.Fprintln(dockerCli.Out(), " Labels:") 260 for _, lbl := range info.Labels { 261 fmt.Fprintln(dockerCli.Out(), " "+lbl) 262 } 263 } 264 265 fmt.Fprintln(dockerCli.Out(), " Experimental:", info.ExperimentalBuild) 266 fprintlnNonEmpty(dockerCli.Out(), " Cluster Store:", info.ClusterStore) 267 fprintlnNonEmpty(dockerCli.Out(), " Cluster Advertise:", info.ClusterAdvertise) 268 269 if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) { 270 fmt.Fprintln(dockerCli.Out(), " Insecure Registries:") 271 for _, registry := range info.RegistryConfig.IndexConfigs { 272 if !registry.Secure { 273 fmt.Fprintln(dockerCli.Out(), " "+registry.Name) 274 } 275 } 276 277 for _, registry := range info.RegistryConfig.InsecureRegistryCIDRs { 278 mask, _ := registry.Mask.Size() 279 fmt.Fprintf(dockerCli.Out(), " %s/%d\n", registry.IP.String(), mask) 280 } 281 } 282 283 if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 { 284 fmt.Fprintln(dockerCli.Out(), " Registry Mirrors:") 285 for _, mirror := range info.RegistryConfig.Mirrors { 286 fmt.Fprintln(dockerCli.Out(), " "+mirror) 287 } 288 } 289 290 fmt.Fprintln(dockerCli.Out(), " Live Restore Enabled:", info.LiveRestoreEnabled) 291 if info.ProductLicense != "" { 292 fmt.Fprintln(dockerCli.Out(), " Product License:", info.ProductLicense) 293 } 294 295 if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 { 296 fmt.Fprintln(dockerCli.Out(), " Default Address Pools:") 297 for _, pool := range info.DefaultAddressPools { 298 fmt.Fprintf(dockerCli.Out(), " Base: %s, Size: %d\n", pool.Base, pool.Size) 299 } 300 } 301 302 fmt.Fprint(dockerCli.Out(), "\n") 303 304 printServerWarnings(dockerCli, info) 305 return errs 306 } 307 308 // nolint: gocyclo 309 func printSwarmInfo(dockerCli command.Cli, info types.Info) { 310 if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked { 311 return 312 } 313 fmt.Fprintln(dockerCli.Out(), " NodeID:", info.Swarm.NodeID) 314 if info.Swarm.Error != "" { 315 fmt.Fprintln(dockerCli.Out(), " Error:", info.Swarm.Error) 316 } 317 fmt.Fprintln(dockerCli.Out(), " Is Manager:", info.Swarm.ControlAvailable) 318 if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { 319 fmt.Fprintln(dockerCli.Out(), " ClusterID:", info.Swarm.Cluster.ID) 320 fmt.Fprintln(dockerCli.Out(), " Managers:", info.Swarm.Managers) 321 fmt.Fprintln(dockerCli.Out(), " Nodes:", info.Swarm.Nodes) 322 var strAddrPool strings.Builder 323 if info.Swarm.Cluster.DefaultAddrPool != nil { 324 for _, p := range info.Swarm.Cluster.DefaultAddrPool { 325 strAddrPool.WriteString(p + " ") 326 } 327 fmt.Fprintln(dockerCli.Out(), " Default Address Pool:", strAddrPool.String()) 328 fmt.Fprintln(dockerCli.Out(), " SubnetSize:", info.Swarm.Cluster.SubnetSize) 329 } 330 if info.Swarm.Cluster.DataPathPort > 0 { 331 fmt.Fprintln(dockerCli.Out(), " Data Path Port:", info.Swarm.Cluster.DataPathPort) 332 } 333 fmt.Fprintln(dockerCli.Out(), " Orchestration:") 334 335 taskHistoryRetentionLimit := int64(0) 336 if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil { 337 taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit 338 } 339 fmt.Fprintln(dockerCli.Out(), " Task History Retention Limit:", taskHistoryRetentionLimit) 340 fmt.Fprintln(dockerCli.Out(), " Raft:") 341 fmt.Fprintln(dockerCli.Out(), " Snapshot Interval:", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) 342 if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil { 343 fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots) 344 } 345 fmt.Fprintln(dockerCli.Out(), " Heartbeat Tick:", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) 346 fmt.Fprintln(dockerCli.Out(), " Election Tick:", info.Swarm.Cluster.Spec.Raft.ElectionTick) 347 fmt.Fprintln(dockerCli.Out(), " Dispatcher:") 348 fmt.Fprintln(dockerCli.Out(), " Heartbeat Period:", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod)) 349 fmt.Fprintln(dockerCli.Out(), " CA Configuration:") 350 fmt.Fprintln(dockerCli.Out(), " Expiry Duration:", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry)) 351 fmt.Fprintln(dockerCli.Out(), " Force Rotate:", info.Swarm.Cluster.Spec.CAConfig.ForceRotate) 352 if caCert := strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert); caCert != "" { 353 fmt.Fprintf(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", caCert) 354 } 355 if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 { 356 fmt.Fprintln(dockerCli.Out(), " External CAs:") 357 for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs { 358 fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL) 359 } 360 } 361 fmt.Fprintln(dockerCli.Out(), " Autolock Managers:", info.Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers) 362 fmt.Fprintln(dockerCli.Out(), " Root Rotation In Progress:", info.Swarm.Cluster.RootRotationInProgress) 363 } 364 fmt.Fprintln(dockerCli.Out(), " Node Address:", info.Swarm.NodeAddr) 365 if len(info.Swarm.RemoteManagers) > 0 { 366 managers := []string{} 367 for _, entry := range info.Swarm.RemoteManagers { 368 managers = append(managers, entry.Addr) 369 } 370 sort.Strings(managers) 371 fmt.Fprintln(dockerCli.Out(), " Manager Addresses:") 372 for _, entry := range managers { 373 fmt.Fprintf(dockerCli.Out(), " %s\n", entry) 374 } 375 } 376 } 377 378 func printServerWarnings(dockerCli command.Cli, info types.Info) { 379 if versions.LessThan(dockerCli.Client().ClientVersion(), "1.42") { 380 printSecurityOptionsWarnings(dockerCli, info) 381 } 382 if len(info.Warnings) > 0 { 383 fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n")) 384 return 385 } 386 // daemon didn't return warnings. Fallback to old behavior 387 printStorageDriverWarnings(dockerCli, info) 388 printServerWarningsLegacy(dockerCli, info) 389 } 390 391 // printSecurityOptionsWarnings prints warnings based on the security options 392 // returned by the daemon. 393 // DEPRECATED: warnings are now generated by the daemon, and returned in 394 // info.Warnings. This function is used to provide backward compatibility with 395 // daemons that do not provide these warnings. No new warnings should be added 396 // here. 397 func printSecurityOptionsWarnings(dockerCli command.Cli, info types.Info) { 398 if info.OSType == "windows" { 399 return 400 } 401 kvs, _ := types.DecodeSecurityOptions(info.SecurityOptions) 402 for _, so := range kvs { 403 if so.Name != "seccomp" { 404 continue 405 } 406 for _, o := range so.Options { 407 if o.Key == "profile" && o.Value != "default" && o.Value != "builtin" { 408 _, _ = fmt.Fprintln(dockerCli.Err(), "WARNING: You're not using the default seccomp profile") 409 } 410 } 411 } 412 } 413 414 // printServerWarningsLegacy generates warnings based on information returned by the daemon. 415 // DEPRECATED: warnings are now generated by the daemon, and returned in 416 // info.Warnings. This function is used to provide backward compatibility with 417 // daemons that do not provide these warnings. No new warnings should be added 418 // here. 419 func printServerWarningsLegacy(dockerCli command.Cli, info types.Info) { 420 if info.OSType == "windows" { 421 return 422 } 423 if !info.MemoryLimit { 424 fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") 425 } 426 if !info.SwapLimit { 427 fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") 428 } 429 if !info.OomKillDisable && info.CgroupVersion != "2" { 430 fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") 431 } 432 if !info.CPUCfsQuota { 433 fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") 434 } 435 if !info.CPUCfsPeriod { 436 fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") 437 } 438 if !info.CPUShares { 439 fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") 440 } 441 if !info.CPUSet { 442 fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") 443 } 444 if !info.IPv4Forwarding { 445 fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") 446 } 447 if !info.BridgeNfIptables { 448 fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") 449 } 450 if !info.BridgeNfIP6tables { 451 fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") 452 } 453 } 454 455 // printStorageDriverWarnings generates warnings based on storage-driver information 456 // returned by the daemon. 457 // DEPRECATED: warnings are now generated by the daemon, and returned in 458 // info.Warnings. This function is used to provide backward compatibility with 459 // daemons that do not provide these warnings. No new warnings should be added 460 // here. 461 func printStorageDriverWarnings(dockerCli command.Cli, info types.Info) { 462 if info.OSType == "windows" { 463 return 464 } 465 if info.DriverStatus == nil { 466 return 467 } 468 for _, pair := range info.DriverStatus { 469 if pair[0] == "Data loop file" { 470 fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is "+ 471 "strongly discouraged for production use.\n "+ 472 "Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.\n", info.Driver) 473 } 474 if pair[0] == "Supports d_type" && pair[1] == "false" { 475 backingFs := getBackingFs(info) 476 477 msg := fmt.Sprintf("WARNING: %s: the backing %s filesystem is formatted without d_type support, which leads to incorrect behavior.\n", info.Driver, backingFs) 478 if backingFs == "xfs" { 479 msg += " Reformat the filesystem with ftype=1 to enable d_type support.\n" 480 } 481 msg += " Running without d_type support will not be supported in future releases." 482 fmt.Fprintln(dockerCli.Err(), msg) 483 } 484 } 485 } 486 487 func getBackingFs(info types.Info) string { 488 if info.DriverStatus == nil { 489 return "" 490 } 491 492 for _, pair := range info.DriverStatus { 493 if pair[0] == "Backing Filesystem" { 494 return pair[1] 495 } 496 } 497 return "" 498 } 499 500 func formatInfo(dockerCli command.Cli, info info, format string) error { 501 // Ensure slice/array fields render as `[]` not `null` 502 if info.ClientInfo != nil && info.ClientInfo.Plugins == nil { 503 info.ClientInfo.Plugins = make([]pluginmanager.Plugin, 0) 504 } 505 506 tmpl, err := templates.Parse(format) 507 if err != nil { 508 return cli.StatusError{StatusCode: 64, 509 Status: "Template parsing error: " + err.Error()} 510 } 511 err = tmpl.Execute(dockerCli.Out(), info) 512 dockerCli.Out().Write([]byte{'\n'}) 513 return err 514 } 515 516 func fprintlnNonEmpty(w io.Writer, label, value string) { 517 if value != "" { 518 fmt.Fprintln(w, label, value) 519 } 520 }