diff --git a/cmd/nerdctl/build.go b/cmd/nerdctl/build.go index ab21f8115cc..e0b576fe665 100644 --- a/cmd/nerdctl/build.go +++ b/cmd/nerdctl/build.go @@ -26,6 +26,8 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/types" + "path/filepath" "github.com/containerd/containerd/errdefs" @@ -39,7 +41,7 @@ import ( "github.com/spf13/cobra" ) -func newBuildCommand() *cobra.Command { +func newBuildCommand(cfg *types.BuildConfig) *cobra.Command { var buildCommand = &cobra.Command{ Use: "build", Short: "Build an image from a Dockerfile. Needs buildkitd to be running.", @@ -66,7 +68,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container // #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.Platforms, "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 6c0a763f14b..7281df75f5a 100644 --- a/cmd/nerdctl/container.go +++ b/cmd/nerdctl/container.go @@ -29,9 +29,11 @@ func newContainerCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } + + cfg := defaultConfigTemplate() containerCommand.AddCommand( newCreateCommand(), - newRunCommand(), + newRunCommand(&cfg), newUpdateCommand(), newExecCommand(), containerLsCommand(), diff --git a/cmd/nerdctl/create.go b/cmd/nerdctl/create.go index d92cec9ea45..1273ad78ba4 100644 --- a/cmd/nerdctl/create.go +++ b/cmd/nerdctl/create.go @@ -45,7 +45,8 @@ func newCreateCommand() *cobra.Command { SilenceErrors: true, } createCommand.Flags().SetInterspersed(false) - setCreateFlags(createCommand) + cfg := defaultConfigTemplate() + setCreateFlags(createCommand, &cfg) return createCommand } diff --git a/cmd/nerdctl/image.go b/cmd/nerdctl/image.go index afae1936335..9ff140f05ca 100644 --- a/cmd/nerdctl/image.go +++ b/cmd/nerdctl/image.go @@ -29,8 +29,9 @@ func newImageCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } + cfg := defaultConfigTemplate() cmd.AddCommand( - newBuildCommand(), + newBuildCommand(cfg.Build), // commitCommand is in "container", not in "image" imageLsCommand(), newHistoryCommand(), diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index e401944a4f2..9848469f33d 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -24,6 +24,9 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/types" + "github.com/containerd/nerdctl/pkg/reflectutil" + "github.com/containerd/containerd" "github.com/containerd/containerd/defaults" "github.com/containerd/containerd/namespaces" @@ -32,7 +35,6 @@ import ( "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/version" "github.com/pelletier/go-toml" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -88,18 +90,67 @@ 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"` - Experimental bool `toml:"experimental"` + 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"` + Experimental bool `toml:"experimental"` + DefaultConfig ConfigTemplates `toml:"default_config"` +} + +type ConfigTemplates map[string]types.ServiceConfig + +func (d *ConfigTemplates) UnmarshalTOML(v interface{}) error { + res := make(map[string]types.ServiceConfig) + for namespace, vl := range v.(map[string]interface{}) { + dcfg := defaultConfigTemplate() + tree, err := toml.TreeFromMap(vl.(map[string]interface{})) + if err != nil { + return err + } + if err = tree.Unmarshal(&dcfg); err != nil { + return err + } + warnUnknownFields(dcfg, namespace) + res[namespace] = dcfg + } + *d = res + return nil +} + +func defaultConfigTemplate() types.ServiceConfig { + return types.ServiceConfig{ + Build: &types.BuildConfig{ + Platforms: []string{}, + }, + Platform: "", + Net: "bridge", + } +} + +func warnUnknownFields(svc types.ServiceConfig, namespace string) { + if unknown := reflectutil.UnknownNonEmptyFields(&svc, + "Net", + "Build", + "Platform", + ); len(unknown) > 0 { + logrus.Warnf("Ignoring: [namespace %s] service %s: %+v", namespace, svc.Name, unknown) + } + + if svc.Build != nil { + if unknown := reflectutil.UnknownNonEmptyFields(svc.Build, + "Platforms", + ); len(unknown) > 0 { + logrus.Warnf("Ignoring: [namespace %s] service %s: build: %+v", namespace, svc.Name, unknown) + } + } } // NewConfig creates a default Config object statically, @@ -118,23 +169,24 @@ func NewConfig() *Config { InsecureRegistry: false, HostsDir: ncdefaults.HostsDirs(), Experimental: true, + DefaultConfig: make(map[string]types.ServiceConfig), } } -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") @@ -157,7 +209,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) error { rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains /hosts.toml (containerd style) or /{ca.cert, cert.pem, key.pem} (docker style)") // Experimental enable experimental feature, see in https://github.com/containerd/nerdctl/blob/master/docs/experimental.md AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/master/docs/experimental.md") - return nil + return cfg, nil } func newApp() (*cobra.Command, error) { @@ -181,10 +233,20 @@ 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 } + namespaceCfg, ok := cfg.DefaultConfig[cfg.Namespace] + if !ok { + namespaceCfg = defaultConfigTemplate() + } + size := len(namespaceCfg.Build.Platforms) + if size != 0 && namespaceCfg.Platform != namespaceCfg.Build.Platforms[size-1] { + return nil, fmt.Errorf("conflict platform config") + } + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { debug, err := cmd.Flags().GetBool("debug-full") if err != nil { @@ -224,7 +286,7 @@ Config file ($NERDCTL_TOML): %s rootCmd.AddCommand( newCreateCommand(), // #region Run & Exec - newRunCommand(), + newRunCommand(&namespaceCfg), newUpdateCommand(), newExecCommand(), // #endregion @@ -246,7 +308,7 @@ Config file ($NERDCTL_TOML): %s // #endregion // Build - newBuildCommand(), + newBuildCommand(namespaceCfg.Build), // #region Image management newImagesCommand(), diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index 03a09360147..3ba7870b492 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/containerd/containerd" + ncdefaults "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -88,3 +89,45 @@ version = 2 base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) base.Cmd("info").AssertFail() } + +// TestNerdctlConfigTemplate validates the execution of default template configurations [CLI, TOML, Default]. +func TestNerdctlConfigTemplate(t *testing.T) { + testutil.DockerIncompatible(t) + t.Parallel() + tomlPath := ncdefaults.NerdctlTOML() + err := os.MkdirAll(filepath.Dir(tomlPath), 0755) + assert.NilError(t, err) + defer func(path string) { + _ = os.Remove(path) + }(tomlPath) + err = os.WriteFile(tomlPath, []byte(` +namespace = "normal" +[default_config] + +[default_config.normal] +net = "test-network" +`), 0755) + assert.NilError(t, err) + base := testutil.NewBase(t) + networkName := "test-network" + networkSubnet := "172.0.0.0/16" + base.Cmd("network", "create", networkName, "--subnet", networkSubnet).AssertOK() + defer base.Cmd("network", "rm", networkName).Run() + testContainer := testutil.Identifier(t) + + if len(base.Env) == 0 { + base.Env = os.Environ() + } + base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) + + // [TOML, DEFAULT] + base.Cmd("run", "-d", "--name", testContainer, testutil.NginxAlpineImage).AssertOK() + defer base.Cmd("rm", "-f", testContainer).Run() + base.Cmd("inspect", "-f", "{{index .Config.Labels \"nerdctl/networks\" }}", testContainer).AssertOutExactly("[\"test-network\"]\n") + + // [CLI, TOML, DEFAULT] + testContainer1 := testContainer + "1" + base.Cmd("run", "-d", "--name", testContainer1, "--network", "host", testutil.NginxAlpineImage).AssertOK() + defer base.Cmd("rm", "-f", testContainer1).Run() + base.Cmd("inspect", "-f", "{{index .Config.Labels \"nerdctl/networks\" }}", testContainer1).AssertOutExactly("[\"host\"]\n") +} diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go index 5d2dffac273..b6882a49727 100644 --- a/cmd/nerdctl/run.go +++ b/cmd/nerdctl/run.go @@ -31,6 +31,9 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/types" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/console" "github.com/containerd/containerd" "github.com/containerd/containerd/cio" @@ -47,7 +50,6 @@ import ( "github.com/containerd/nerdctl/pkg/logging" "github.com/containerd/nerdctl/pkg/mountutil" "github.com/containerd/nerdctl/pkg/namestore" - "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/referenceutil" "github.com/containerd/nerdctl/pkg/strutil" @@ -62,7 +64,7 @@ const ( tiniInitBinary = "tini" ) -func newRunCommand() *cobra.Command { +func newRunCommand(cfg *types.ServiceConfig) *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 +87,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 *types.ServiceConfig) { // No "-h" alias for "--help", because "-h" for "--hostname". cmd.Flags().Bool("help", false, "show help") @@ -117,13 +119,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"|"container:"|)`) + cmd.Flags().StringSlice("network", []string{cfg.Net}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|)`) cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return shellCompleteNetworkNames(cmd, []string{}) }) diff --git a/go.mod b/go.mod index 4123554fa88..177540594e5 100644 --- a/go.mod +++ b/go.mod @@ -143,7 +143,7 @@ require ( github.com/opencontainers/selinux v1.10.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/philhofer/fwd v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e // indirect github.com/prometheus/client_golang v1.12.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect