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