diff --git a/cmd/nerdctl/build.go b/cmd/nerdctl/build.go index 0434c847e14..081b647d34b 100644 --- a/cmd/nerdctl/build.go +++ b/cmd/nerdctl/build.go @@ -38,7 +38,7 @@ import ( "github.com/spf13/cobra" ) -func newBuildCommand() *cobra.Command { +func newBuildCommand(cfg *BuildConfig) *cobra.Command { var buildCommand = &cobra.Command{ Use: "build", Short: "Build an image from a Dockerfile. Needs buildkitd to be running.", @@ -63,7 +63,7 @@ func newBuildCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" - buildCommand.Flags().StringSlice("platform", []string{}, "Set target platform for build (e.g., \"amd64\", \"arm64\")") + buildCommand.Flags().StringSlice("platform", cfg.Platform, "Set target platform for build (e.g., \"amd64\", \"arm64\")") buildCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) // #endregion diff --git a/cmd/nerdctl/container.go b/cmd/nerdctl/container.go index b8f3af6902e..cb1b4ce6a27 100644 --- a/cmd/nerdctl/container.go +++ b/cmd/nerdctl/container.go @@ -31,7 +31,7 @@ func newContainerCommand() *cobra.Command { } containerCommand.AddCommand( newCreateCommand(), - newRunCommand(), + newRunCommand(runConfig()), newUpdateCommand(), newExecCommand(), containerLsCommand(), diff --git a/cmd/nerdctl/create.go b/cmd/nerdctl/create.go index 5167e8445d5..1111d85f7ef 100644 --- a/cmd/nerdctl/create.go +++ b/cmd/nerdctl/create.go @@ -45,7 +45,7 @@ func newCreateCommand() *cobra.Command { SilenceErrors: true, } createCommand.Flags().SetInterspersed(false) - setCreateFlags(createCommand) + setCreateFlags(createCommand, runConfig()) return createCommand } diff --git a/cmd/nerdctl/image.go b/cmd/nerdctl/image.go index 0c7a2fa2f0b..22c7eb4ae1b 100644 --- a/cmd/nerdctl/image.go +++ b/cmd/nerdctl/image.go @@ -30,7 +30,7 @@ func newImageCommand() *cobra.Command { SilenceErrors: true, } cmd.AddCommand( - newBuildCommand(), + newBuildCommand(buildConfig()), // commitCommand is in "container", not in "image" imageLsCommand(), newHistoryCommand(), diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index d951f63ee95..4668fa0874b 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "github.com/containerd/nerdctl/pkg/netutil" "os" "runtime" "strings" @@ -87,17 +88,41 @@ func xmain() error { // Config corresponds to nerdctl.toml . // See docs/config.md . type Config struct { - Debug bool `toml:"debug"` - DebugFull bool `toml:"debug_full"` - Address string `toml:"address"` - Namespace string `toml:"namespace"` - Snapshotter string `toml:"snapshotter"` - CNIPath string `toml:"cni_path"` - CNINetConfPath string `toml:"cni_netconfpath"` - DataRoot string `toml:"data_root"` - CgroupManager string `toml:"cgroup_manager"` - InsecureRegistry bool `toml:"insecure_registry"` - HostsDir []string `toml:"hosts_dir"` + Debug bool `toml:"debug"` + DebugFull bool `toml:"debug_full"` + Address string `toml:"address"` + Namespace string `toml:"namespace"` + Snapshotter string `toml:"snapshotter"` + CNIPath string `toml:"cni_path"` + CNINetConfPath string `toml:"cni_netconfpath"` + DataRoot string `toml:"data_root"` + CgroupManager string `toml:"cgroup_manager"` + InsecureRegistry bool `toml:"insecure_registry"` + HostsDir []string `toml:"hosts_dir"` + RunConfig *RunConfig `toml:"default_run_config"` + BuildConfig *BuildConfig `toml:"default_build_config"` +} + +type RunConfig struct { + Platform string `toml:"platform"` + Network []string `toml:"network"` +} + +func runConfig() *RunConfig { + return &RunConfig{ + Platform: "", + Network: []string{netutil.DefaultNetworkName}, + } +} + +type BuildConfig struct { + Platform []string `toml:"platform"` +} + +func buildConfig() *BuildConfig { + return &BuildConfig{ + Platform: []string{}, + } } // NewConfig creates a default Config object statically, @@ -115,23 +140,25 @@ func NewConfig() *Config { CgroupManager: ncdefaults.CgroupManager(), InsecureRegistry: false, HostsDir: ncdefaults.HostsDirs(), + RunConfig: runConfig(), + BuildConfig: buildConfig(), } } -func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) error { +func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*Config, error) { cfg := NewConfig() if r, err := os.Open(tomlPath); err == nil { logrus.Debugf("Loading config from %q", tomlPath) defer r.Close() dec := toml.NewDecoder(r).Strict(true) // set Strict to detect typo if err := dec.Decode(cfg); err != nil { - return 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) + 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) } logrus.Debugf("Loaded config %+v", cfg) } else { logrus.WithError(err).Debugf("Not loading config from %q", tomlPath) if !errors.Is(err, os.ErrNotExist) { - return err + return nil, err } } rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode") @@ -152,7 +179,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) error { rootCmd.PersistentFlags().Bool("insecure-registry", cfg.InsecureRegistry, "skips verifying HTTPS certs, and allows falling back to plain HTTP") // hosts-dir is defined as StringSlice, not StringArray, to allow specifying "--hosts-dir=/etc/containerd/certs.d,/etc/docker/certs.d" rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains /hosts.toml (containerd style) or /{ca.cert, cert.pem, key.pem} (docker style)") - return nil + return cfg, nil } func newApp() (*cobra.Command, error) { @@ -176,7 +203,8 @@ Config file ($NERDCTL_TOML): %s TraverseChildren: true, // required for global short hands like -a, -H, -n } rootCmd.SetUsageTemplate(mainHelpTemplate) - if err := initRootCmdFlags(rootCmd, tomlPath); err != nil { + cfg, err := initRootCmdFlags(rootCmd, tomlPath) + if err != nil { return nil, err } @@ -219,7 +247,7 @@ Config file ($NERDCTL_TOML): %s rootCmd.AddCommand( newCreateCommand(), // #region Run & Exec - newRunCommand(), + newRunCommand(cfg.RunConfig), newUpdateCommand(), newExecCommand(), // #endregion @@ -241,7 +269,7 @@ Config file ($NERDCTL_TOML): %s // #endregion // Build - newBuildCommand(), + newBuildCommand(cfg.BuildConfig), // #region Image management newImagesCommand(), diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index 03a09360147..706d232c90f 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -17,8 +17,10 @@ package main import ( + "github.com/containerd/nerdctl/pkg/platformutil" "os" "path/filepath" + "strings" "testing" "github.com/containerd/containerd" @@ -88,3 +90,58 @@ version = 2 base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) base.Cmd("info").AssertFail() } + +// TestNerdctlSpecificConfig validates the configuration precedence [CLI, TOML, Default]. +func TestNerdctlSpecificConfig(t *testing.T) { + testutil.DockerIncompatible(t) + t.Parallel() + tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") + err := os.WriteFile(tomlPath, []byte(` +[default_run_config] +platform = "arm64" +network = ["host"] +`), 0400) + assert.NilError(t, err) + base := testutil.NewBase(t) + testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64") + + // [DEFAULT] + platformAmd64, err := platformutil.NormalizeString("amd64") + assert.NilError(t, err) + base.Cmd("run", testutil.CommonImage, "uname", "-m").AssertOutWithFunc(func(out string) error { + platformOut, err := platformutil.NormalizeString(strings.TrimSuffix(out, "\n")) + assert.NilError(t, err) + assert.Equal(t, platformAmd64, platformOut) + return nil + }) + + // [CLI, DEFAULT] + platformArm64, err := platformutil.NormalizeString("arm64") + assert.NilError(t, err) + base.Cmd("run", "--platform", "arm64", "--rm", testutil.CommonImage, "uname", "-m").AssertOutWithFunc(func(out string) error { + platformOut, err := platformutil.NormalizeString(strings.TrimSuffix(out, "\n")) + assert.NilError(t, err) + assert.Equal(t, platformArm64, platformOut) + return nil + }) + + // [TOML, DEFAULT] + if len(base.Env) == 0 { + base.Env = os.Environ() + } + base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) + base.Cmd("run", "--rm", testutil.CommonImage, "uname", "-m").AssertOutWithFunc(func(out string) error { + platformOut, err := platformutil.NormalizeString(strings.TrimSuffix(out, "\n")) + assert.NilError(t, err) + assert.Equal(t, platformArm64, platformOut) + return nil + }) + + // [CLI, TOML, DEFAULT] + base.Cmd("run", "--platform", "amd64", "--rm", testutil.CommonImage, "uname", "-m").AssertOutWithFunc(func(out string) error { + platformOut, err := platformutil.NormalizeString(strings.TrimSuffix(out, "\n")) + assert.NilError(t, err) + assert.Equal(t, platformAmd64, platformOut) + return nil + }) +} diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go index d66290fa799..df6ce18424e 100644 --- a/cmd/nerdctl/run.go +++ b/cmd/nerdctl/run.go @@ -62,7 +62,7 @@ const ( tiniInitBinary = "tini" ) -func newRunCommand() *cobra.Command { +func newRunCommand(cfg *RunConfig) *cobra.Command { shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." longHelp := shortHelp switch runtime.GOOS { @@ -85,14 +85,14 @@ func newRunCommand() *cobra.Command { } runCommand.Flags().SetInterspersed(false) - setCreateFlags(runCommand) + setCreateFlags(runCommand, cfg) runCommand.Flags().BoolP("detach", "d", false, "Run container in background and print container ID") return runCommand } -func setCreateFlags(cmd *cobra.Command) { +func setCreateFlags(cmd *cobra.Command, cfg *RunConfig) { // No "-h" alias for "--help", because "-h" for "--hostname". cmd.Flags().Bool("help", false, "show help") @@ -117,13 +117,13 @@ func setCreateFlags(cmd *cobra.Command) { // #endregion // #region platform flags - cmd.Flags().String("platform", "", "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms + cmd.Flags().String("platform", cfg.Platform, "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms cmd.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) // #endregion // #region network flags // network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2" - cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|)`) + cmd.Flags().StringSlice("network", cfg.Network, `Connect a container to a network ("bridge"|"host"|"none"|)`) cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return shellCompleteNetworkNames(cmd, []string{}) })