diff --git a/cmd/nerdctl/build.go b/cmd/nerdctl/build.go index b85900a5f8..53b2036ab9 100644 --- a/cmd/nerdctl/build.go +++ b/cmd/nerdctl/build.go @@ -28,6 +28,7 @@ import ( "path/filepath" + "github.com/compose-spec/compose-go/types" "github.com/containerd/containerd/errdefs" dockerreference "github.com/containerd/containerd/reference/docker" "github.com/containerd/nerdctl/pkg/buildkitutil" @@ -39,7 +40,7 @@ import ( "github.com/spf13/cobra" ) -func newBuildCommand() *cobra.Command { +func newBuildCommand(cfg *types.BuildConfig) *cobra.Command { var buildCommand = &cobra.Command{ Use: "build [flags] PATH", Short: "Build an image from a Dockerfile. Needs buildkitd to be running.", @@ -66,7 +67,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/build_test.go b/cmd/nerdctl/build_test.go index df0f600104..6b53e0b71a 100644 --- a/cmd/nerdctl/build_test.go +++ b/cmd/nerdctl/build_test.go @@ -23,6 +23,7 @@ import ( "strings" "testing" + ncdefaults "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -420,3 +421,53 @@ CMD ["echo", "nerdctl-build-notag-string"] base.Cmd("images").AssertOutContains("") base.Cmd("image", "prune", "--force", "--all").AssertOK() } + +func TestBuildWithConfigFile(t *testing.T) { + testutil.DockerIncompatible(t) + testutil.RequiresBuild(t) + base := testutil.NewBase(t) + defer base.Cmd("builder", "prune").AssertOK() + + 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] +build = {Platforms=["linux/amd64", "linux/arm64"]} +`), 0755) + assert.NilError(t, err) + + if len(base.Env) == 0 { + base.Env = os.Environ() + } + base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) + + imageName := testutil.Identifier(t) + defer base.Cmd("rmi", imageName).Run() + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "dummy"] + `, testutil.CommonImage) + + buildCtx, err := createBuildContext(dockerfile) + assert.NilError(t, err) + defer os.RemoveAll(buildCtx) + + base.Cmd("build", "-t", imageName, buildCtx).AssertOK() + testCases := map[string]string{ + "amd64": "x86_64", + "arm64": "aarch64", + } + for plat, expectedUnameM := range testCases { + t.Logf("Testing %q (%q)", plat, expectedUnameM) + cmd := base.Cmd("run", "--rm", "--platform="+plat, imageName, "uname", "-m") + cmd.AssertOutExactly(expectedUnameM + "\n") + } +} diff --git a/cmd/nerdctl/container.go b/cmd/nerdctl/container.go index 6c0a763f14..99b9680fb8 100644 --- a/cmd/nerdctl/container.go +++ b/cmd/nerdctl/container.go @@ -17,10 +17,11 @@ package main import ( + "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" ) -func newContainerCommand() *cobra.Command { +func newContainerCommand(cfg *types.ServiceConfig) *cobra.Command { containerCommand := &cobra.Command{ Annotations: map[string]string{Category: Management}, Use: "container", @@ -29,9 +30,10 @@ func newContainerCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } + containerCommand.AddCommand( - newCreateCommand(), - newRunCommand(), + newCreateCommand(cfg), + newRunCommand(cfg), newUpdateCommand(), newExecCommand(), containerLsCommand(), diff --git a/cmd/nerdctl/create.go b/cmd/nerdctl/create.go index 7a5c9f063c..ae1a76da16 100644 --- a/cmd/nerdctl/create.go +++ b/cmd/nerdctl/create.go @@ -20,10 +20,11 @@ import ( "fmt" "runtime" + "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" ) -func newCreateCommand() *cobra.Command { +func newCreateCommand(cfg *types.ServiceConfig) *cobra.Command { shortHelp := "Create a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." longHelp := shortHelp switch runtime.GOOS { @@ -45,7 +46,7 @@ func newCreateCommand() *cobra.Command { SilenceErrors: true, } createCommand.Flags().SetInterspersed(false) - setCreateFlags(createCommand) + setCreateFlags(createCommand, cfg) return createCommand } diff --git a/cmd/nerdctl/image.go b/cmd/nerdctl/image.go index afae193633..ed14dd99fd 100644 --- a/cmd/nerdctl/image.go +++ b/cmd/nerdctl/image.go @@ -17,10 +17,11 @@ package main import ( + "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" ) -func newImageCommand() *cobra.Command { +func newImageCommand(cfg *types.BuildConfig) *cobra.Command { cmd := &cobra.Command{ Annotations: map[string]string{Category: Management}, Use: "image", @@ -30,7 +31,7 @@ func newImageCommand() *cobra.Command { SilenceErrors: true, } cmd.AddCommand( - newBuildCommand(), + newBuildCommand(cfg), // commitCommand is in "container", not in "image" imageLsCommand(), newHistoryCommand(), diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 062440e2e9..943f6f0e8c 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -24,11 +24,13 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/types" "github.com/containerd/containerd" "github.com/containerd/containerd/defaults" "github.com/containerd/containerd/namespaces" ncdefaults "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/logging" + "github.com/containerd/nerdctl/pkg/reflectutil" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/version" "github.com/fatih/color" @@ -136,18 +138,70 @@ 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 { + defaultConfig, ok := v.(map[string]interface{}) + if !ok { + return fmt.Errorf("parse ConfigTemplates error") + } + + res := make(map[string]types.ServiceConfig) + for namespace, vl := range defaultConfig { + 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: "", + } +} + +func warnUnknownFields(svc types.ServiceConfig, namespace string) { + if unknown := reflectutil.UnknownNonEmptyFields(&svc, + "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, @@ -166,23 +220,24 @@ func NewConfig() *Config { InsecureRegistry: false, HostsDir: ncdefaults.HostsDirs(), Experimental: true, + DefaultConfig: make(map[string]types.ServiceConfig), } } -func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, error) { +func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, map[string]types.ServiceConfig, 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 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) + return nil, 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 nil, err + return nil, nil, err } } aliasToBeInherited := pflag.NewFlagSet(rootCmd.Name(), pflag.ExitOnError) @@ -207,7 +262,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, 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/main/docs/experimental.md AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md") - return aliasToBeInherited, nil + return aliasToBeInherited, cfg.DefaultConfig, nil } func newApp() (*cobra.Command, error) { @@ -232,10 +287,23 @@ Config file ($NERDCTL_TOML): %s } rootCmd.SetUsageFunc(usage) - aliasToBeInherited, err := initRootCmdFlags(rootCmd, tomlPath) + aliasToBeInherited, defaultConfigs, err := initRootCmdFlags(rootCmd, tomlPath) + if err != nil { + return nil, err + } + + ns, err := rootCmd.Flags().GetString("namespace") if err != nil { return nil, err } + namespaceCfg, ok := defaultConfigs[ns] + 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") @@ -274,9 +342,9 @@ Config file ($NERDCTL_TOML): %s } rootCmd.RunE = unknownSubcommandAction rootCmd.AddCommand( - newCreateCommand(), + newCreateCommand(&namespaceCfg), // #region Run & Exec - newRunCommand(), + newRunCommand(&namespaceCfg), newUpdateCommand(), newExecCommand(), // #endregion @@ -298,7 +366,7 @@ Config file ($NERDCTL_TOML): %s // #endregion // Build - newBuildCommand(), + newBuildCommand(namespaceCfg.Build), // #region Image management newImagesCommand(), @@ -325,8 +393,8 @@ Config file ($NERDCTL_TOML): %s newStatsCommand(), // #region Management - newContainerCommand(), - newImageCommand(), + newContainerCommand(&namespaceCfg), + newImageCommand(namespaceCfg.Build), newNetworkCommand(), newVolumeCommand(), newSystemCommand(), diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go index 8d0e73d26f..40c6fb5911 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/rootlessutil" @@ -63,7 +65,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 { @@ -86,14 +88,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") @@ -118,7 +120,7 @@ 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