github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/main.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "errors" 21 "fmt" 22 "os" 23 "runtime" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/containerd/log" 29 "github.com/containerd/nerdctl/pkg/config" 30 ncdefaults "github.com/containerd/nerdctl/pkg/defaults" 31 "github.com/containerd/nerdctl/pkg/errutil" 32 "github.com/containerd/nerdctl/pkg/logging" 33 "github.com/containerd/nerdctl/pkg/rootlessutil" 34 "github.com/containerd/nerdctl/pkg/version" 35 "github.com/fatih/color" 36 "github.com/pelletier/go-toml/v2" 37 38 "github.com/spf13/cobra" 39 "github.com/spf13/pflag" 40 ) 41 42 const ( 43 Category = "category" 44 Management = "management" 45 ) 46 47 var ( 48 // To print Bold Text 49 Bold = color.New(color.Bold).SprintfFunc() 50 ) 51 52 // usage was derived from https://github.com/spf13/cobra/blob/v1.2.1/command.go#L491-L514 53 func usage(c *cobra.Command) error { 54 s := "Usage: " 55 if c.Runnable() { 56 s += c.UseLine() + "\n" 57 } else { 58 s += c.CommandPath() + " [command]\n" 59 } 60 s += "\n" 61 if len(c.Aliases) > 0 { 62 s += "Aliases: " + c.NameAndAliases() + "\n" 63 } 64 if c.HasExample() { 65 s += "Example:\n" 66 s += c.Example + "\n" 67 } 68 69 var managementCommands, nonManagementCommands []*cobra.Command 70 for _, f := range c.Commands() { 71 f := f 72 if f.Hidden { 73 continue 74 } 75 if f.Annotations[Category] == Management { 76 managementCommands = append(managementCommands, f) 77 } else { 78 nonManagementCommands = append(nonManagementCommands, f) 79 } 80 } 81 printCommands := func(title string, commands []*cobra.Command) string { 82 if len(commands) == 0 { 83 return "" 84 } 85 var longest int 86 for _, f := range commands { 87 if l := len(f.Name()); l > longest { 88 longest = l 89 } 90 } 91 92 title = Bold(title) 93 t := title + ":\n" 94 for _, f := range commands { 95 t += " " 96 t += f.Name() 97 t += strings.Repeat(" ", longest-len(f.Name())) 98 t += " " + f.Short + "\n" 99 } 100 t += "\n" 101 return t 102 } 103 s += printCommands("Management commands", managementCommands) 104 s += printCommands("Commands", nonManagementCommands) 105 106 s += Bold("Flags") + ":\n" 107 s += c.LocalFlags().FlagUsages() + "\n" 108 109 if c == c.Root() { 110 s += "Run '" + c.CommandPath() + " COMMAND --help' for more information on a command.\n" 111 } else { 112 s += "See also '" + c.Root().CommandPath() + " --help' for the global flags such as '--namespace', '--snapshotter', and '--cgroup-manager'." 113 } 114 fmt.Fprintln(c.OutOrStdout(), s) 115 return nil 116 } 117 118 func main() { 119 if err := xmain(); err != nil { 120 errutil.HandleExitCoder(err) 121 log.L.Fatal(err) 122 } 123 } 124 125 func xmain() error { 126 if len(os.Args) == 3 && os.Args[1] == logging.MagicArgv1 { 127 // containerd runtime v2 logging plugin mode. 128 // "binary://BIN?KEY=VALUE" URI is parsed into Args {BIN, KEY, VALUE}. 129 return logging.Main(os.Args[2]) 130 } 131 // nerdctl CLI mode 132 app, err := newApp() 133 if err != nil { 134 return err 135 } 136 return app.Execute() 137 } 138 139 func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, error) { 140 cfg := config.New() 141 if r, err := os.Open(tomlPath); err == nil { 142 log.L.Debugf("Loading config from %q", tomlPath) 143 defer r.Close() 144 dec := toml.NewDecoder(r).DisallowUnknownFields() // set Strict to detect typo 145 if err := dec.Decode(cfg); err != nil { 146 return nil, fmt.Errorf("failed to load nerdctl config (not daemon config) from %q (Hint: don't mix up daemon's `config.toml` with `nerdctl.toml`): %w", tomlPath, err) 147 } 148 log.L.Debugf("Loaded config %+v", cfg) 149 } else { 150 log.L.WithError(err).Debugf("Not loading config from %q", tomlPath) 151 if !errors.Is(err, os.ErrNotExist) { 152 return nil, err 153 } 154 } 155 aliasToBeInherited := pflag.NewFlagSet(rootCmd.Name(), pflag.ExitOnError) 156 157 rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode") 158 rootCmd.PersistentFlags().Bool("debug-full", cfg.DebugFull, "debug mode (with full output)") 159 // -a is aliases (conflicts with nerdctl images -a) 160 AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`) 161 // -n is aliases (conflicts with nerdctl logs -n) 162 AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`) 163 rootCmd.RegisterFlagCompletionFunc("namespace", shellCompleteNamespaceNames) 164 AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter") 165 rootCmd.RegisterFlagCompletionFunc("snapshotter", shellCompleteSnapshotterNames) 166 rootCmd.RegisterFlagCompletionFunc("storage-driver", shellCompleteSnapshotterNames) 167 AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory") 168 AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory") 169 rootCmd.PersistentFlags().String("data-root", cfg.DataRoot, "Root directory of persistent nerdctl state (managed by nerdctl, not by containerd)") 170 rootCmd.PersistentFlags().String("cgroup-manager", cfg.CgroupManager, `Cgroup manager to use ("cgroupfs"|"systemd")`) 171 rootCmd.RegisterFlagCompletionFunc("cgroup-manager", shellCompleteCgroupManagerNames) 172 rootCmd.PersistentFlags().Bool("insecure-registry", cfg.InsecureRegistry, "skips verifying HTTPS certs, and allows falling back to plain HTTP") 173 // hosts-dir is defined as StringSlice, not StringArray, to allow specifying "--hosts-dir=/etc/containerd/certs.d,/etc/docker/certs.d" 174 rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains <HOST:PORT>/hosts.toml (containerd style) or <HOST:PORT>/{ca.cert, cert.pem, key.pem} (docker style)") 175 // Experimental enable experimental feature, see in https://github.com/containerd/nerdctl/blob/main/docs/experimental.md 176 AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md") 177 AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host") 178 return aliasToBeInherited, nil 179 } 180 181 func newApp() (*cobra.Command, error) { 182 183 tomlPath := ncdefaults.NerdctlTOML() 184 if v, ok := os.LookupEnv("NERDCTL_TOML"); ok { 185 tomlPath = v 186 } 187 188 short := "nerdctl is a command line interface for containerd" 189 long := fmt.Sprintf(`%s 190 191 Config file ($NERDCTL_TOML): %s 192 `, short, tomlPath) 193 var rootCmd = &cobra.Command{ 194 Use: "nerdctl", 195 Short: short, 196 Long: long, 197 Version: strings.TrimPrefix(version.GetVersion(), "v"), 198 SilenceUsage: true, 199 SilenceErrors: true, 200 TraverseChildren: true, // required for global short hands like -a, -H, -n 201 } 202 203 rootCmd.SetUsageFunc(usage) 204 aliasToBeInherited, err := initRootCmdFlags(rootCmd, tomlPath) 205 if err != nil { 206 return nil, err 207 } 208 209 rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 210 globalOptions, err := processRootCmdFlags(cmd) 211 if err != nil { 212 return err 213 } 214 debug := globalOptions.DebugFull 215 if !debug { 216 debug = globalOptions.Debug 217 } 218 if debug { 219 log.SetLevel(log.DebugLevel.String()) 220 } 221 address := globalOptions.Address 222 if strings.Contains(address, "://") && !strings.HasPrefix(address, "unix://") { 223 return fmt.Errorf("invalid address %q", address) 224 } 225 cgroupManager := globalOptions.CgroupManager 226 if runtime.GOOS == "linux" { 227 switch cgroupManager { 228 case "systemd", "cgroupfs", "none": 229 default: 230 return fmt.Errorf("invalid cgroup-manager %q (supported values: \"systemd\", \"cgroupfs\", \"none\")", cgroupManager) 231 } 232 } 233 if appNeedsRootlessParentMain(cmd, args) { 234 // reexec /proc/self/exe with `nsenter` into RootlessKit namespaces 235 return rootlessutil.ParentMain(globalOptions.HostGatewayIP) 236 } 237 return nil 238 } 239 rootCmd.RunE = unknownSubcommandAction 240 rootCmd.AddCommand( 241 newCreateCommand(), 242 // #region Run & Exec 243 newRunCommand(), 244 newUpdateCommand(), 245 newExecCommand(), 246 // #endregion 247 248 // #region Container management 249 newPsCommand(), 250 newLogsCommand(), 251 newPortCommand(), 252 newStopCommand(), 253 newStartCommand(), 254 newDiffCommand(), 255 newRestartCommand(), 256 newKillCommand(), 257 newRmCommand(), 258 newPauseCommand(), 259 newUnpauseCommand(), 260 newCommitCommand(), 261 newWaitCommand(), 262 newRenameCommand(), 263 newAttachCommand(), 264 // #endregion 265 266 // Build 267 newBuildCommand(), 268 269 // #region Image management 270 newImagesCommand(), 271 newPullCommand(), 272 newPushCommand(), 273 newLoadCommand(), 274 newSaveCommand(), 275 newTagCommand(), 276 newRmiCommand(), 277 newHistoryCommand(), 278 // #endregion 279 280 // #region System 281 newEventsCommand(), 282 newInfoCommand(), 283 newVersionCommand(), 284 // #endregion 285 286 // Inspect 287 newInspectCommand(), 288 289 // stats 290 newTopCommand(), 291 newStatsCommand(), 292 293 // #region Management 294 newContainerCommand(), 295 newImageCommand(), 296 newNetworkCommand(), 297 newVolumeCommand(), 298 newSystemCommand(), 299 newNamespaceCommand(), 300 newBuilderCommand(), 301 // #endregion 302 303 // Internal 304 newInternalCommand(), 305 306 // login 307 newLoginCommand(), 308 309 // Logout 310 newLogoutCommand(), 311 312 // Compose 313 newComposeCommand(), 314 315 // IPFS 316 newIPFSCommand(), 317 ) 318 addApparmorCommand(rootCmd) 319 addCpCommand(rootCmd) 320 321 // add aliasToBeInherited to subCommand(s) InheritedFlags 322 for _, subCmd := range rootCmd.Commands() { 323 subCmd.InheritedFlags().AddFlagSet(aliasToBeInherited) 324 } 325 return rootCmd, nil 326 } 327 328 func globalFlags(cmd *cobra.Command) (string, []string) { 329 args0, err := os.Executable() 330 if err != nil { 331 log.L.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0]) 332 args0 = os.Args[0] 333 } 334 if len(os.Args) < 2 { 335 return args0, nil 336 } 337 338 rootCmd := cmd.Root() 339 flagSet := rootCmd.Flags() 340 args := []string{} 341 flagSet.VisitAll(func(f *pflag.Flag) { 342 key := f.Name 343 val := f.Value.String() 344 if f.Changed { 345 args = append(args, "--"+key+"="+val) 346 } 347 }) 348 return args0, args 349 } 350 351 // unknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail 352 // https://github.com/containerd/nerdctl/issues/487 353 // 354 // Ideally this should be implemented in Cobra itself. 355 func unknownSubcommandAction(cmd *cobra.Command, args []string) error { 356 if len(args) == 0 { 357 return cmd.Help() 358 } 359 // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662 360 msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name()) 361 if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 { 362 msg += "\n\nDid you mean this?\n" 363 for _, s := range suggestions { 364 msg += fmt.Sprintf("\t%v\n", s) 365 } 366 } 367 return errors.New(msg) 368 } 369 370 // AddStringFlag is similar to cmd.Flags().String but supports aliases and env var 371 func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) { 372 if env != "" { 373 usage = fmt.Sprintf("%s [$%s]", usage, env) 374 } 375 if envV, ok := os.LookupEnv(env); ok { 376 value = envV 377 } 378 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 379 p := new(string) 380 flags := cmd.Flags() 381 flags.StringVar(p, name, value, usage) 382 for _, a := range aliases { 383 if len(a) == 1 { 384 // pflag doesn't support short-only flags, so we have to register long one as well here 385 flags.StringVarP(p, a, a, value, aliasesUsage) 386 } else { 387 flags.StringVar(p, a, value, aliasesUsage) 388 } 389 } 390 } 391 392 // AddIntFlag is similar to cmd.Flags().Int but supports aliases and env var 393 func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, env, usage string) { 394 if env != "" { 395 usage = fmt.Sprintf("%s [$%s]", usage, env) 396 } 397 if envV, ok := os.LookupEnv(env); ok { 398 v, err := strconv.ParseInt(envV, 10, 64) 399 if err != nil { 400 log.L.WithError(err).Warnf("Invalid int value for `%s`", env) 401 } 402 value = int(v) 403 } 404 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 405 p := new(int) 406 flags := cmd.Flags() 407 flags.IntVar(p, name, value, usage) 408 for _, a := range aliases { 409 if len(a) == 1 { 410 // pflag doesn't support short-only flags, so we have to register long one as well here 411 flags.IntVarP(p, a, a, value, aliasesUsage) 412 } else { 413 flags.IntVar(p, a, value, aliasesUsage) 414 } 415 } 416 } 417 418 // AddDurationFlag is similar to cmd.Flags().Duration but supports aliases and env var 419 func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value time.Duration, env, usage string) { 420 if env != "" { 421 usage = fmt.Sprintf("%s [$%s]", usage, env) 422 } 423 if envV, ok := os.LookupEnv(env); ok { 424 var err error 425 value, err = time.ParseDuration(envV) 426 if err != nil { 427 log.L.WithError(err).Warnf("Invalid duration value for `%s`", env) 428 } 429 } 430 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 431 p := new(time.Duration) 432 flags := cmd.Flags() 433 flags.DurationVar(p, name, value, usage) 434 for _, a := range aliases { 435 if len(a) == 1 { 436 // pflag doesn't support short-only flags, so we have to register long one as well here 437 flags.DurationVarP(p, a, a, value, aliasesUsage) 438 } else { 439 flags.DurationVar(p, a, value, aliasesUsage) 440 } 441 } 442 } 443 444 // AddPersistentStringFlag is similar to AddStringFlag but persistent. 445 // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". 446 func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) { 447 if env != "" { 448 usage = fmt.Sprintf("%s [$%s]", usage, env) 449 } 450 if envV, ok := os.LookupEnv(env); ok { 451 value = envV 452 } 453 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 454 p := new(string) 455 456 // flags is full set of flag(s) 457 // flags can redefine alias already used in subcommands 458 flags := cmd.Flags() 459 for _, a := range aliases { 460 if len(a) == 1 { 461 // pflag doesn't support short-only flags, so we have to register long one as well here 462 flags.StringVarP(p, a, a, value, aliasesUsage) 463 } else { 464 flags.StringVar(p, a, value, aliasesUsage) 465 } 466 // non-persistent flags are not added to the InheritedFlags, so we should add them manually 467 f := flags.Lookup(a) 468 aliasToBeInherited.AddFlag(f) 469 } 470 471 // localFlags are local to the rootCmd 472 localFlags := cmd.LocalFlags() 473 for _, a := range localAliases { 474 if len(a) == 1 { 475 // pflag doesn't support short-only flags, so we have to register long one as well here 476 localFlags.StringVarP(p, a, a, value, aliasesUsage) 477 } else { 478 localFlags.StringVar(p, a, value, aliasesUsage) 479 } 480 } 481 482 // persistentFlags cannot redefine alias already used in subcommands 483 persistentFlags := cmd.PersistentFlags() 484 persistentFlags.StringVar(p, name, value, usage) 485 for _, a := range persistentAliases { 486 if len(a) == 1 { 487 // pflag doesn't support short-only flags, so we have to register long one as well here 488 persistentFlags.StringVarP(p, a, a, value, aliasesUsage) 489 } else { 490 persistentFlags.StringVar(p, a, value, aliasesUsage) 491 } 492 } 493 } 494 495 // AddPersistentBoolFlag is similar to AddBoolFlag but persistent. 496 // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". 497 func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) { 498 if env != "" { 499 usage = fmt.Sprintf("%s [$%s]", usage, env) 500 } 501 if envV, ok := os.LookupEnv(env); ok { 502 var err error 503 value, err = strconv.ParseBool(envV) 504 if err != nil { 505 log.L.WithError(err).Warnf("Invalid boolean value for `%s`", env) 506 } 507 } 508 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 509 p := new(bool) 510 flags := cmd.Flags() 511 for _, a := range nonPersistentAliases { 512 if len(a) == 1 { 513 // pflag doesn't support short-only flags, so we have to register long one as well here 514 flags.BoolVarP(p, a, a, value, aliasesUsage) 515 } else { 516 flags.BoolVar(p, a, value, aliasesUsage) 517 } 518 } 519 520 persistentFlags := cmd.PersistentFlags() 521 persistentFlags.BoolVar(p, name, value, usage) 522 for _, a := range aliases { 523 if len(a) == 1 { 524 // pflag doesn't support short-only flags, so we have to register long one as well here 525 persistentFlags.BoolVarP(p, a, a, value, aliasesUsage) 526 } else { 527 persistentFlags.BoolVar(p, a, value, aliasesUsage) 528 } 529 } 530 } 531 532 // AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent. 533 // See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". 534 func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) { 535 if env != "" { 536 usage = fmt.Sprintf("%s [$%s]", usage, env) 537 } 538 if envV, ok := os.LookupEnv(env); ok { 539 value = []string{envV} 540 } 541 aliasesUsage := fmt.Sprintf("Alias of --%s", name) 542 p := new([]string) 543 flags := cmd.Flags() 544 for _, a := range nonPersistentAliases { 545 if len(a) == 1 { 546 // pflag doesn't support short-only flags, so we have to register long one as well here 547 flags.StringArrayVarP(p, a, a, value, aliasesUsage) 548 } else { 549 flags.StringArrayVar(p, a, value, aliasesUsage) 550 } 551 } 552 553 persistentFlags := cmd.PersistentFlags() 554 persistentFlags.StringArrayVar(p, name, value, usage) 555 for _, a := range aliases { 556 if len(a) == 1 { 557 // pflag doesn't support short-only flags, so we have to register long one as well here 558 persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage) 559 } else { 560 persistentFlags.StringArrayVar(p, a, value, aliasesUsage) 561 } 562 } 563 } 564 565 func checkExperimental(feature string) func(cmd *cobra.Command, args []string) error { 566 return func(cmd *cobra.Command, args []string) error { 567 globalOptions, err := processRootCmdFlags(cmd) 568 if err != nil { 569 return err 570 } 571 if !globalOptions.Experimental { 572 return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature) 573 } 574 return nil 575 } 576 } 577 578 // IsExactArgs returns an error if there is not the exact number of args 579 func IsExactArgs(number int) cobra.PositionalArgs { 580 return func(cmd *cobra.Command, args []string) error { 581 if len(args) == number { 582 return nil 583 } 584 return fmt.Errorf( 585 "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", 586 cmd.CommandPath(), 587 number, 588 "argument(s)", 589 cmd.CommandPath(), 590 cmd.UseLine(), 591 cmd.Short, 592 ) 593 } 594 }