diff --git a/cmd/nerdctl/completion.go b/cmd/nerdctl/completion.go index 15dd83a3260..ebd8d47c8bd 100644 --- a/cmd/nerdctl/completion.go +++ b/cmd/nerdctl/completion.go @@ -109,7 +109,7 @@ func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string, excludeMap[ex] = struct{}{} } - e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace)) if err != nil { return nil, cobra.ShellCompDirectiveError } diff --git a/cmd/nerdctl/network_inspect_test.go b/cmd/nerdctl/network_inspect_test.go index bab22c998eb..b605b5c5020 100644 --- a/cmd/nerdctl/network_inspect_test.go +++ b/cmd/nerdctl/network_inspect_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "gotest.tools/v3/assert" ) @@ -66,3 +67,31 @@ func TestNetworkInspect(t *testing.T) { } assert.DeepEqual(base.T, expectedIPAM, got.IPAM) } + +func TestNetworkWithNamespace(t *testing.T) { + testutil.DockerIncompatible(t) + + t.Parallel() + + tID := testutil.Identifier(t) + base := testutil.NewBase(t) + baseOther := testutil.NewBaseWithNamespace(t, tID) + + tearDown := func() { + base.Cmd("network", "rm", tID).Run() + baseOther.Cmd("network", "prune", "-f").Run() + } + tearDown() + t.Cleanup(tearDown) + + base.Cmd("network", "create", tID).AssertOK() + + // Other namespace cannot inspect, prune, see, or remove this network + baseOther.Cmd("network", "inspect", tID).AssertFail() + baseOther.Cmd("network", "prune", "-f").AssertOutNotContains(tID) + baseOther.Cmd("network", "ls").AssertOutNotContains(tID) + + if !rootlessutil.IsRootless() { + baseOther.Cmd("network", "remove", tID).AssertFail() + } +} diff --git a/cmd/nerdctl/network_remove_linux_test.go b/cmd/nerdctl/network_remove_linux_test.go index 80ccc504069..9ac5267a46a 100644 --- a/cmd/nerdctl/network_remove_linux_test.go +++ b/cmd/nerdctl/network_remove_linux_test.go @@ -25,30 +25,6 @@ import ( "gotest.tools/v3/assert" ) -func TestNetworkRemoveInOtherNamespace(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("test skipped for remove rootless network") - } - if testutil.GetTarget() == testutil.Docker { - t.Skip("test skipped for docker") - } - // --namespace=nerdctl-test - base := testutil.NewBase(t) - // --namespace=nerdctl-other - baseOther := testutil.NewBaseWithNamespace(t, "nerdctl-other") - networkName := testutil.Identifier(t) - - base.Cmd("network", "create", networkName).AssertOK() - defer base.Cmd("network", "rm", networkName).AssertOK() - - tID := testutil.Identifier(t) - base.Cmd("run", "-d", "--net", networkName, "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", tID).Run() - - // delete network in namespace nerdctl-other - baseOther.Cmd("network", "rm", networkName).AssertFail() -} - func TestNetworkRemove(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("test skipped for remove rootless network") diff --git a/docs/cni.md b/docs/cni.md index 02ec79f1cce..67313891604 100644 --- a/docs/cni.md +++ b/docs/cni.md @@ -116,7 +116,16 @@ For example: ## Custom networks You can also customize your CNI network by providing configuration files. -For example you have one configuration file(`/etc/cni/net.d/10-mynet.conf`) + +When rootful, the expected root location is `/etc/cni/net.d`. +For rootless, the expected root location is `~/.config/cni/net.d/` + +Configuration files (like `10-mynet.conf`) can be placed either in the root location, +or under a subfolder. +If in the root location, this network will be available to all nerdctl namespaces. +If placed in a subfolder, it will be available only to the identically named namespace. + +For example, you have one configuration file(`/etc/cni/net.d/10-mynet.conf`) for `bridge` network: ```json @@ -138,7 +147,7 @@ for `bridge` network: ``` This will configure a new CNI network with the name `mynet`, and you can use -this network to create a container: +this network to create a container in any namespace: ```console # nerdctl run -it --net mynet --rm alpine ip addr show diff --git a/docs/dir.md b/docs/dir.md index 7bf5418a1e0..4843eadb6bc 100644 --- a/docs/dir.md +++ b/docs/dir.md @@ -65,5 +65,9 @@ Data volume Can be overridden with `nerdctl --cni-netconfpath=` flag and environment variable `$NETCONFPATH`. +At the top-level of , network (files) are shared accross all namespaces. +Sub-folders inside are only available to the namespace bearing the same name, +and its networks definitions are private. + Files: - `nerdctl-.conflist`: CNI conf list created by nerdctl diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go index 515a91a8a7f..b0ea6ac3ca9 100644 --- a/pkg/cmd/compose/compose.go +++ b/pkg/cmd/compose/compose.go @@ -41,7 +41,7 @@ import ( // New returns a new *composer.Composer. func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, options composer.Options, stdout, stderr io.Writer) (*composer.Composer, error) { - cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace), netutil.WithDefaultNetwork()) if err != nil { return nil, err } diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go index 20c9bd9efed..d185ff09be3 100644 --- a/pkg/cmd/container/kill.go +++ b/pkg/cmd/container/kill.go @@ -152,7 +152,7 @@ func cleanupNetwork(ctx context.Context, container containerd.Container, globalO case nettype.Host, nettype.None, nettype.Container: // NOP case nettype.CNI: - e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithDefaultNetwork()) + e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithNamespace(globalOpts.Namespace), netutil.WithDefaultNetwork()) if err != nil { return err } diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index 3115a127640..43f1f96d6ff 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -33,7 +33,7 @@ func Create(options types.NetworkCreateOptions, stdout io.Writer) error { options.Subnets = []string{""} } - e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace)) if err != nil { return err } diff --git a/pkg/cmd/network/inspect.go b/pkg/cmd/network/inspect.go index 9ef9581b4a4..1d680642856 100644 --- a/pkg/cmd/network/inspect.go +++ b/pkg/cmd/network/inspect.go @@ -32,7 +32,7 @@ import ( func Inspect(ctx context.Context, options types.NetworkInspectOptions) error { globalOptions := options.GOptions - e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace)) if err != nil { return err diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go index ab7c2696ee2..b2075356d8c 100644 --- a/pkg/cmd/network/list.go +++ b/pkg/cmd/network/list.go @@ -65,7 +65,7 @@ func List(ctx context.Context, options types.NetworkListOptions) error { } } - e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace)) if err != nil { return err } diff --git a/pkg/cmd/network/prune.go b/pkg/cmd/network/prune.go index 4595e6fe2b4..9294583947c 100644 --- a/pkg/cmd/network/prune.go +++ b/pkg/cmd/network/prune.go @@ -28,7 +28,7 @@ import ( ) func Prune(ctx context.Context, client *containerd.Client, options types.NetworkPruneOptions) error { - e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace)) if err != nil { return err } diff --git a/pkg/cmd/network/remove.go b/pkg/cmd/network/remove.go index a2fc73c0e17..133a2b47bba 100644 --- a/pkg/cmd/network/remove.go +++ b/pkg/cmd/network/remove.go @@ -27,7 +27,7 @@ import ( ) func Remove(ctx context.Context, client *containerd.Client, options types.NetworkRemoveOptions) error { - e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath) + e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace)) if err != nil { return err } diff --git a/pkg/containerutil/container_network_manager_linux.go b/pkg/containerutil/container_network_manager_linux.go index e4d058f856f..534040f362d 100644 --- a/pkg/containerutil/container_network_manager_linux.go +++ b/pkg/containerutil/container_network_manager_linux.go @@ -39,7 +39,7 @@ type cniNetworkManagerPlatform struct { // Verifies that the internal network settings are correct. func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error { - e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork()) if err != nil { return err } diff --git a/pkg/containerutil/container_network_manager_windows.go b/pkg/containerutil/container_network_manager_windows.go index ba7304c30f5..399ac70bacb 100644 --- a/pkg/containerutil/container_network_manager_windows.go +++ b/pkg/containerutil/container_network_manager_windows.go @@ -36,7 +36,7 @@ type cniNetworkManagerPlatform struct { // Verifies that the internal network settings are correct. func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error { - e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork()) if err != nil { return err } @@ -67,7 +67,7 @@ func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error { } func (m *cniNetworkManager) getCNI() (gocni.CNI, error) { - e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork()) if err != nil { return nil, fmt.Errorf("failed to instantiate CNI env: %s", err) } diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 5691e27e8d6..10a4b99148b 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -46,6 +46,7 @@ import ( type CNIEnv struct { Path string NetconfPath string + Namespace string } type CNIEnvOpt func(e *CNIEnv) error @@ -132,6 +133,16 @@ func WithDefaultNetwork() CNIEnvOpt { } } +func WithNamespace(namespace string) CNIEnvOpt { + return func(e *CNIEnv) error { + if err := os.MkdirAll(filepath.Join(e.NetconfPath, namespace), 0755); err != nil { + return err + } + e.Namespace = namespace + return nil + } +} + func NewCNIEnv(cniPath, cniConfPath string, opts ...CNIEnvOpt) (*CNIEnv, error) { e := CNIEnv{ Path: cniPath, @@ -193,7 +204,10 @@ func (e *CNIEnv) FilterNetworks(filterf func(*NetworkConfig) bool) ([]*NetworkCo } func (e *CNIEnv) getConfigPathForNetworkName(netName string) string { - return filepath.Join(e.NetconfPath, "nerdctl-"+netName+".conflist") + if netName == DefaultNetworkName || e.Namespace == "" { + return filepath.Join(e.NetconfPath, "nerdctl-"+netName+".conflist") + } + return filepath.Join(e.NetconfPath, e.Namespace, "nerdctl-"+netName+".conflist") } func (e *CNIEnv) usedSubnets() ([]*net.IPNet, error) { @@ -404,10 +418,18 @@ func (e *CNIEnv) writeNetworkConfig(net *NetworkConfig) error { // networkConfigList loads config from dir if dir exists. func (e *CNIEnv) networkConfigList() ([]*NetworkConfig, error) { l := []*NetworkConfig{} - fileNames, err := libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) + common, err := libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"}) if err != nil { return nil, err } + namespaced := []string{} + if e.Namespace != "" { + namespaced, err = libcni.ConfFiles(filepath.Join(e.NetconfPath, e.Namespace), []string{".conf", ".conflist", ".json"}) + if err != nil { + return nil, err + } + } + fileNames := append(common, namespaced...) sort.Strings(fileNames) for _, fileName := range fileNames { var lcl *libcni.NetworkConfigList diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 4ab13a7ce78..d023de39b40 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -152,7 +152,7 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin case nettype.Host, nettype.None, nettype.Container: // NOP case nettype.CNI: - e, err := netutil.NewCNIEnv(cniPath, cniNetconfPath, netutil.WithDefaultNetwork()) + e, err := netutil.NewCNIEnv(cniPath, cniNetconfPath, netutil.WithNamespace(namespace), netutil.WithDefaultNetwork()) if err != nil { return nil, err }