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