From b073d7c393231adde3be6343a44e842aa7f55fc7 Mon Sep 17 00:00:00 2001 From: Ravi Arnan Date: Sat, 8 Aug 2026 17:05:51 +0800 Subject: [PATCH] fix: do not require CNI plugins for compose projects that avoid CNI compose.New unconditionally passed netutil.WithDefaultNetwork to NewCNIEnv, so every compose command created nerdctl's default bridge network before parsing a single service. On a host without the CNI plugins installed this failed outright, even for projects whose services all use network_mode: host or none and so never touch CNI. The default network is not needed there. NetworkExists, the only consumer of the CNIEnv built in compose.New, is called exclusively with project-scoped network names, and external networks return before reaching it. Services that do attach to the default bridge still get it created on demand, because compose shells out to `nerdctl run`, which ensures the default network via cniNetworkManager. Fixes #4461 Signed-off-by: Ravi Arnan --- cmd/nerdctl/compose/compose_up_linux_test.go | 41 ++++++++++++++++++++ pkg/cmd/compose/compose.go | 5 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 97f06b37fe0..4ac9af1163b 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -1423,3 +1423,44 @@ services: testCase.Run(t) } + +func TestComposeUpNetworkModeHostWithoutCNIPlugins(t *testing.T) { + testCase := nerdtest.Setup() + + // --cni-path and --cni-netconfpath are nerdctl specific. + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` +services: + svc0: + image: %s + network_mode: host + command: "sleep infinity" +`, testutil.CommonImage) + + data.Labels().Set("composeYAML", data.Temp().Save(dockerComposeYAML, "compose.yaml")) + // An empty CNI_PATH and an empty netconf dir together mimic a host that never + // installed the CNI plugins. Services using host networking do not need them. + data.Labels().Set("cniPath", data.Temp().Dir("cni-bin")) + data.Labels().Set("cniNetConfPath", data.Temp().Dir("cni-netconf")) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "--cni-path", data.Labels().Get("cniPath"), + "--cni-netconfpath", data.Labels().Get("cniNetConfPath"), + "compose", "-f", data.Labels().Get("composeYAML"), "up", "-d") + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow( + "--cni-path", data.Labels().Get("cniPath"), + "--cni-netconfpath", data.Labels().Get("cniNetConfPath"), + "compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + + testCase.Run(t) +} diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index 146c4997b99..fbf00600f8e 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -52,7 +52,10 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op return nil, err } - cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace), netutil.WithDefaultNetwork(globalOptions.BridgeIP)) + // The default network is deliberately not created here. It is only needed by + // services that actually attach to it, and `nerdctl run` already creates it on + // demand. + cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace)) if err != nil { return nil, err }