From 89a78b4d1e8eca31a59c0885430c6e4bb045f019 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Mon, 20 Mar 2023 10:52:15 +0200 Subject: [PATCH] Refactor run command networking options for Windows support. Refactor the loading and application of the networking-related arguments for the `nerdctl run` command in order to enable Windows container networking support through CNI. To facilitate this, the following major changes were made: * moved all networking-related container spec definitions to pkg/containerutil/container_network_manager_* * refactored network-related argiment loading and application in cmd/nerdctl/container_run.go * enabled some of the networking-related tests on Windows Signed-off-by: Nashwan Azhari --- cmd/nerdctl/container_create.go | 13 +- cmd/nerdctl/container_create_linux_test.go | 71 +-- cmd/nerdctl/container_run.go | 141 +++-- cmd/nerdctl/container_run_network.go | 358 ++--------- .../container_run_network_base_test.go | 231 ++++++++ .../container_run_network_linux_test.go | 196 +----- .../container_run_network_windows_test.go | 186 ++++++ cmd/nerdctl/image_encrypt_linux_test.go | 1 + go.mod | 1 + go.sum | 1 + pkg/api/types/container_network_types.go | 54 ++ pkg/cmd/container/remove.go | 26 + .../container_network_manager.go | 558 ++++++++++++++++++ .../container_network_manager_linux.go | 171 ++++++ .../container_network_manager_other.go | 56 ++ .../container_network_manager_test.go | 117 ++++ .../container_network_manager_windows.go | 193 ++++++ pkg/containerutil/containerutil.go | 11 + pkg/nsutil/nsutil.go | 47 ++ pkg/nsutil/nsutil_test.go | 59 ++ pkg/ocihook/ocihook.go | 6 + pkg/testutil/testutil.go | 2 +- pkg/testutil/testutil_freebsd.go | 11 +- pkg/testutil/testutil_linux.go | 7 + pkg/testutil/testutil_windows.go | 12 + 25 files changed, 1945 insertions(+), 584 deletions(-) create mode 100644 cmd/nerdctl/container_run_network_base_test.go create mode 100644 cmd/nerdctl/container_run_network_windows_test.go create mode 100644 pkg/api/types/container_network_types.go create mode 100644 pkg/containerutil/container_network_manager.go create mode 100644 pkg/containerutil/container_network_manager_linux.go create mode 100644 pkg/containerutil/container_network_manager_other.go create mode 100644 pkg/containerutil/container_network_manager_test.go create mode 100644 pkg/containerutil/container_network_manager_windows.go create mode 100644 pkg/nsutil/nsutil.go create mode 100644 pkg/nsutil/nsutil_test.go diff --git a/cmd/nerdctl/container_create.go b/cmd/nerdctl/container_create.go index 4c47c2dae98..7b56ba9a02c 100644 --- a/cmd/nerdctl/container_create.go +++ b/cmd/nerdctl/container_create.go @@ -21,6 +21,7 @@ import ( "runtime" "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/containerutil" "github.com/spf13/cobra" ) @@ -73,7 +74,17 @@ func createAction(cmd *cobra.Command, args []string) error { return err } - container, gc, err := createContainer(ctx, cmd, client, globalOptions, args, platform, false, flagT, true) + netFlags, err := loadNetworkFlags(cmd) + if err != nil { + return fmt.Errorf("failed to load networking flags: %s", err) + } + + netManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netFlags) + if err != nil { + return err + } + + container, gc, err := createContainer(ctx, cmd, client, netManager, globalOptions, args, platform, false, flagT, true) if err != nil { if gc != nil { gc() diff --git a/cmd/nerdctl/container_create_linux_test.go b/cmd/nerdctl/container_create_linux_test.go index fb1268fb43a..403d463de8d 100644 --- a/cmd/nerdctl/container_create_linux_test.go +++ b/cmd/nerdctl/container_create_linux_test.go @@ -67,45 +67,48 @@ func TestCreateWithMACAddress(t *testing.T) { } for i, test := range tests { containerName := fmt.Sprintf("%s_%d", tID, i) - macAddress, err := nettestutil.GenerateMACAddress() - if err != nil { - t.Errorf("failed to generate MAC address: %s", err) - } - if test.Expect == "" && !test.WantErr { - test.Expect = macAddress - } - t.Cleanup(func() { - base.Cmd("rm", "-f", containerName).Run() - }) - cmd := base.Cmd("create", "--network", test.Network, "--mac-address", macAddress, "--name", containerName, testutil.CommonImage, "cat", "/sys/class/net/eth0/address") - if !test.WantErr { - cmd.AssertOK() - base.Cmd("start", containerName).AssertOK() - cmd = base.Cmd("logs", containerName) - cmd.AssertOK() - cmd.AssertOutContains(test.Expect) - } else { - if (testutil.GetTarget() == testutil.Docker && test.Network == networkIPvlan) || test.Network == "none" { - // 1. unlike nerdctl - // when using network ipvlan in Docker - // it delays fail on executing start command - // 2. start on network none will success in both - // nerdctl and Docker + testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) + t.Run(testName, func(tt *testing.T) { + macAddress, err := nettestutil.GenerateMACAddress() + if err != nil { + tt.Errorf("failed to generate MAC address: %s", err) + } + if test.Expect == "" && !test.WantErr { + test.Expect = macAddress + } + tt.Cleanup(func() { + base.Cmd("rm", "-f", containerName).Run() + }) + cmd := base.Cmd("create", "--network", test.Network, "--mac-address", macAddress, "--name", containerName, testutil.CommonImage, "cat", "/sys/class/net/eth0/address") + if !test.WantErr { cmd.AssertOK() - cmd = base.Cmd("start", containerName) + base.Cmd("start", containerName).AssertOK() + cmd = base.Cmd("logs", containerName) + cmd.AssertOK() + cmd.AssertOutContains(test.Expect) + } else { + if (testutil.GetTarget() == testutil.Docker && test.Network == networkIPvlan) || test.Network == "none" { + // 1. unlike nerdctl + // when using network ipvlan in Docker + // it delays fail on executing start command + // 2. start on network none will success in both + // nerdctl and Docker + cmd.AssertOK() + cmd = base.Cmd("start", containerName) + if test.Network == "none" { + // we check the result on logs command + cmd.AssertOK() + cmd = base.Cmd("logs", containerName) + } + } + cmd.AssertCombinedOutContains(test.Expect) if test.Network == "none" { - // we check the result on logs command cmd.AssertOK() - cmd = base.Cmd("logs", containerName) + } else { + cmd.AssertFail() } } - cmd.AssertCombinedOutContains(test.Expect) - if test.Network == "none" { - cmd.AssertOK() - } else { - cmd.AssertFail() - } - } + }) } } diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index 1ed290a2a79..bdcf399bdf1 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -41,6 +41,7 @@ import ( "github.com/containerd/nerdctl/pkg/cmd/container" "github.com/containerd/nerdctl/pkg/cmd/image" "github.com/containerd/nerdctl/pkg/consoleutil" + "github.com/containerd/nerdctl/pkg/containerutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/errutil" "github.com/containerd/nerdctl/pkg/flagutil" @@ -328,7 +329,17 @@ func runAction(cmd *cobra.Command, args []string) error { return errors.New("flags -d and --rm cannot be specified together") } - c, gc, err := createContainer(ctx, cmd, client, globalOptions, args, platform, flagI, flagT, flagD) + netFlags, err := loadNetworkFlags(cmd) + if err != nil { + return fmt.Errorf("failed to load networking flags: %s", err) + } + + netManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netFlags) + if err != nil { + return err + } + + c, gc, err := createContainer(ctx, cmd, client, netManager, globalOptions, args, platform, flagI, flagT, flagD) if err != nil { if gc != nil { defer gc() @@ -339,6 +350,14 @@ func runAction(cmd *cobra.Command, args []string) error { id := c.ID() if rm && !flagD { defer func() { + // NOTE: OCI hooks (which are used for CNI network setup/teardown on Linux) + // are not currently supported on Windows, so we must explicitly call + // network setup/cleanup from the main nerdctl executable. + if runtime.GOOS == "windows" { + if err := netManager.CleanupNetworking(ctx, c); err != nil { + logrus.Warnf("failed to clean up container networking: %s", err) + } + } if err := container.RemoveContainer(ctx, c, globalOptions, true, true); err != nil { logrus.WithError(err).Warnf("failed to remove container %s", id) } @@ -406,7 +425,7 @@ func runAction(cmd *cobra.Command, args []string) error { } // FIXME: split to smaller functions -func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd.Client, globalOptions types.GlobalCommandOptions, args []string, platform string, flagI, flagT, flagD bool) (containerd.Container, func(), error) { +func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd.Client, netManager containerutil.NetworkOptionsManager, globalOptions types.GlobalCommandOptions, args []string, platform string, flagI, flagT, flagD bool) (containerd.Container, func(), error) { // simulate the behavior of double dash newArg := []string{} if len(args) >= 2 && args[1] == "--" { @@ -439,7 +458,7 @@ func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd return nil, nil, err } - stateDir, err := getContainerStateDirPath(cmd, globalOptions, dataStore, id) + stateDir, err := containerutil.ContainerStateDirPath(globalOptions, dataStore, id) if err != nil { return nil, nil, err } @@ -577,52 +596,40 @@ func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd } cOpts = append(cOpts, withStop(stopSignal, stopTimeout, ensuredImage)) - hostname := id[0:12] - customHostname, err := cmd.Flags().GetString("hostname") - if err != nil { - return nil, nil, err - } - - uts, err := cmd.Flags().GetString("uts") + err = netManager.VerifyNetworkOptions(ctx) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to verify networking settings: %s", err) } - if customHostname != "" { - // Docker considers this a validation error so keep compat. - if uts != "" { - return nil, nil, errors.New("conflicting options: hostname and UTS mode") - } - hostname = customHostname - } - if uts == "" { - opts = append(opts, oci.WithHostname(hostname)) - internalLabels.hostname = hostname - // `/etc/hostname` does not exist on FreeBSD - if runtime.GOOS == "linux" { - hostnamePath := filepath.Join(stateDir, "hostname") - if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { - return nil, nil, err - } - opts = append(opts, withCustomEtcHostname(hostnamePath)) - } - } - - netOpts, netSlice, ipAddress, ports, macAddress, err := generateNetOpts(cmd, globalOptions, dataStore, stateDir, globalOptions.Namespace, id) + netOpts, netNewContainerOpts, err := netManager.ContainerNetworkingOpts(ctx, id) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to generate networking spec options: %s", err) } - internalLabels.networks = netSlice - internalLabels.ipAddress = ipAddress - internalLabels.ports = ports - internalLabels.macAddress = macAddress opts = append(opts, netOpts...) + cOpts = append(cOpts, netNewContainerOpts...) - hookOpt, err := withNerdctlOCIHook(cmd, id) + netLabelOpts, err := netManager.InternalNetworkingOptionLabels(ctx) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to generate internal networking labels: %s", err) + } + // TODO(aznashwan): more formal way to load net opts into internalLabels: + internalLabels.hostname = netLabelOpts.Hostname + internalLabels.ports = netLabelOpts.PortMappings + internalLabels.ipAddress = netLabelOpts.IPAddress + internalLabels.networks = netLabelOpts.NetworkSlice + internalLabels.macAddress = netLabelOpts.MACAddress + + // NOTE: OCI hooks are currently not supported on Windows so we skip setting them altogether. + // The OCI hooks we define (whose logic can be found in pkg/ocihook) primarily + // perform network setup and teardown when using CNI networking. + // On Windows, we are forced to set up and tear down the networking from within nerdctl. + if runtime.GOOS != "windows" { + hookOpt, err := withNerdctlOCIHook(cmd, id) + if err != nil { + return nil, nil, err + } + opts = append(opts, hookOpt) } - opts = append(opts, hookOpt) user, err := cmd.Flags().GetString("user") if err != nil { @@ -734,31 +741,50 @@ func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd var s specs.Spec spec := containerd.WithSpec(&s, opts...) + cOpts = append(cOpts, spec) - container, err := client.NewContainer(ctx, id, cOpts...) - if err != nil { + container, containerErr := client.NewContainer(ctx, id, cOpts...) + var netSetupErr error + // NOTE: on non-Windows platforms, network setup is performed by OCI hooks. + // Seeing as though Windows does not currently support OCI hooks, we must explicitly + // perform network setup/teardown in the main nerdctl executable. + if containerErr == nil && runtime.GOOS == "windows" { + netSetupErr = netManager.SetupNetworking(ctx, id) + logrus.WithError(netSetupErr).Warnf("networking setup error has occurred") + } + + if containerErr != nil || netSetupErr != nil { gcContainer := func() { - var isErr bool - if errE := os.RemoveAll(stateDir); errE != nil { - isErr = true + if containerErr == nil { + netGcErr := netManager.CleanupNetworking(ctx, container) + if netGcErr != nil { + logrus.WithError(netGcErr).Warnf("failed to revert container %q networking settings", id) + } } + + if rmErr := os.RemoveAll(stateDir); rmErr != nil { + logrus.WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, stateDir) + } + if name != "" { var errE error if containerNameStore, errE = namestore.New(dataStore, globalOptions.Namespace); errE != nil { - isErr = true + logrus.WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id) } if errE = containerNameStore.Release(name, id); errE != nil { - isErr = true + logrus.WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id) } - - } - if isErr { - logrus.Warnf("failed to remove container %q", id) } } - return nil, gcContainer, err + + returnedError := containerErr + if netSetupErr != nil { + returnedError = netSetupErr // mutually exclusive + } + return nil, gcContainer, returnedError } + return container, nil, nil } @@ -974,17 +1000,6 @@ func withNerdctlOCIHook(cmd *cobra.Command, id string) (oci.SpecOpts, error) { }, nil } -func getContainerStateDirPath(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, dataStore, id string) (string, error) { - - if globalOptions.Namespace == "" { - return "", errors.New("namespace is required") - } - if strings.Contains(globalOptions.Namespace, "/") { - return "", errors.New("namespace with '/' is unsupported") - } - return filepath.Join(dataStore, "containers", globalOptions.Namespace, id), nil -} - func withContainerLabels(cmd *cobra.Command) ([]containerd.NewContainerOpts, error) { labelMap, err := readKVStringsMapfFromLabel(cmd) if err != nil { diff --git a/cmd/nerdctl/container_run_network.go b/cmd/nerdctl/container_run_network.go index f03407d1f77..8fc178c4bf7 100644 --- a/cmd/nerdctl/container_run_network.go +++ b/cmd/nerdctl/container_run_network.go @@ -17,43 +17,25 @@ package main import ( - "context" - "errors" - "fmt" - "io/fs" "net" - "path/filepath" - "runtime" - "strings" - "github.com/containerd/containerd/containers" - "github.com/containerd/containerd/oci" gocni "github.com/containerd/go-cni" "github.com/containerd/nerdctl/pkg/api/types" - "github.com/containerd/nerdctl/pkg/clientutil" - "github.com/containerd/nerdctl/pkg/containerutil" - "github.com/containerd/nerdctl/pkg/dnsutil" - "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" - "github.com/containerd/nerdctl/pkg/idutil/containerwalker" - "github.com/containerd/nerdctl/pkg/mountutil" - "github.com/containerd/nerdctl/pkg/netutil" - "github.com/containerd/nerdctl/pkg/netutil/nettype" "github.com/containerd/nerdctl/pkg/portutil" - "github.com/containerd/nerdctl/pkg/resolvconf" - "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/strutil" - "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func getNetworkSlice(cmd *cobra.Command) ([]string, error) { +func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) { + netOpts := types.NetworkOptions{} + + // --net/--network= ... var netSlice = []string{} var networkSet = false if cmd.Flags().Lookup("network").Changed { network, err := cmd.Flags().GetStringSlice("network") if err != nil { - return nil, err + return netOpts, err } netSlice = append(netSlice, network...) networkSet = true @@ -61,7 +43,7 @@ func getNetworkSlice(cmd *cobra.Command) ([]string, error) { if cmd.Flags().Lookup("net").Changed { net, err := cmd.Flags().GetStringSlice("net") if err != nil { - return nil, err + return netOpts, err } netSlice = append(netSlice, net...) networkSet = true @@ -70,310 +52,98 @@ func getNetworkSlice(cmd *cobra.Command) ([]string, error) { if !networkSet { network, err := cmd.Flags().GetStringSlice("network") if err != nil { - return nil, err + return netOpts, err } netSlice = append(netSlice, network...) } - return netSlice, nil -} - -func withCustomResolvConf(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - s.Mounts = append(s.Mounts, specs.Mount{ - Destination: "/etc/resolv.conf", - Type: "bind", - Source: src, - Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable - }) - return nil - } -} + netOpts.NetworkSlice = strutil.DedupeStrSlice(netSlice) -func withCustomEtcHostname(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - s.Mounts = append(s.Mounts, specs.Mount{ - Destination: "/etc/hostname", - Type: "bind", - Source: src, - Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable - }) - return nil + // --mac-address= + macAddress, err := cmd.Flags().GetString("mac-address") + if err != nil { + return netOpts, err } -} - -func withCustomHosts(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - s.Mounts = append(s.Mounts, specs.Mount{ - Destination: "/etc/hosts", - Type: "bind", - Source: src, - Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable - }) - return nil + if macAddress != "" { + if _, err := net.ParseMAC(macAddress); err != nil { + return netOpts, err + } } -} + netOpts.MACAddress = macAddress -func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, error) { - opts := []oci.SpecOpts{} - portSlice, err := cmd.Flags().GetStringSlice("publish") - if err != nil { - return nil, nil, "", nil, "", err - } + // --ip= ipAddress, err := cmd.Flags().GetString("ip") if err != nil { - return nil, nil, "", nil, "", err - } - netSlice, err := getNetworkSlice(cmd) - if err != nil { - return nil, nil, "", nil, "", err + return netOpts, err } + netOpts.IPAddress = ipAddress - if (len(netSlice) == 0) && (ipAddress != "") { - logrus.Warnf("You have assign an IP address %s but no network, So we will use the default network", ipAddress) + // -h/--hostname= + hostName, err := cmd.Flags().GetString("hostname") + if err != nil { + return netOpts, err } + netOpts.Hostname = hostName - macAddress, err := getMACAddress(cmd, netSlice) + // --dns= ... + dnsSlice, err := cmd.Flags().GetStringSlice("dns") if err != nil { - return nil, nil, "", nil, "", err + return netOpts, err } + netOpts.DNSServers = strutil.DedupeStrSlice(dnsSlice) - ports := make([]gocni.PortMapping, 0) - netType, err := nettype.Detect(netSlice) + // --dns-search= ... + dnsSearchSlice, err := cmd.Flags().GetStringSlice("dns-search") if err != nil { - return nil, nil, "", nil, "", err + return netOpts, err } + netOpts.DNSSearchDomains = strutil.DedupeStrSlice(dnsSearchSlice) - switch netType { - case nettype.None: - // NOP - // Docker compatible: if macAddress is specified, set MAC address shall - // not work but run command will success - case nettype.Host: - if macAddress != "" { - return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode") - } - opts = append(opts, oci.WithHostNamespace(specs.NetworkNamespace), oci.WithHostHostsFile, oci.WithHostResolvconf) - case nettype.CNI: - // We only verify flags and generate resolv.conf here. - // The actual network is configured in the oci hook. - if err := verifyCNINetwork(cmd, netSlice, macAddress, globalOptions); err != nil { - return nil, nil, "", nil, "", err - } - - if runtime.GOOS == "linux" { - resolvConfPath := filepath.Join(stateDir, "resolv.conf") - if err := buildResolvConf(cmd, resolvConfPath); err != nil { - return nil, nil, "", nil, "", err - } - - // the content of /etc/hosts is created in OCI Hook - etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id) - if err != nil { - return nil, nil, "", nil, "", err - } - opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) - for _, p := range portSlice { - pm, err := portutil.ParseFlagP(p) - if err != nil { - return nil, nil, "", pm, "", err - } - ports = append(ports, pm...) - } - } - case nettype.Container: - if macAddress != "" { - return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode") - } - if err := verifyContainerNetwork(cmd, netSlice); err != nil { - return nil, nil, "", nil, "", err - } - network := strings.Split(netSlice[0], ":") - if len(network) != 2 { - return nil, nil, "", nil, "", fmt.Errorf("invalid network: %s, should be \"container:\"", netSlice[0]) - } - containerName := network[1] - client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) - if err != nil { - return nil, nil, "", nil, "", err - } - defer cancel() - - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple containers found with prefix: %s", containerName) - } - containerID := found.Container.ID() - - conStateDir, err := getContainerStateDirPath(cmd, globalOptions, dataStore, containerID) - if err != nil { - return err - } - - s, err := found.Container.Spec(ctx) - if err != nil { - return err - } - hostname := s.Hostname - hostnamePath := filepath.Join(conStateDir, "hostname") - resolvConfPath := filepath.Join(conStateDir, "resolv.conf") - etcHostsPath := hostsstore.HostsPath(dataStore, ns, containerID) - netNSPath, err := containerutil.ContainerNetNSPath(ctx, found.Container) - if err != nil { - return err - } - opts = append(opts, - oci.WithLinuxNamespace(specs.LinuxNamespace{ - Type: specs.NetworkNamespace, - Path: netNSPath, - }), - withCustomResolvConf(resolvConfPath), - withCustomHosts(etcHostsPath), - oci.WithHostname(hostname), - withCustomEtcHostname(hostnamePath), - ) - // stored in labels with key "nerdctl/networks" - netSlice = []string{fmt.Sprintf("container:%s", containerID)} - return nil - }, - } - n, err := walker.Walk(ctx, containerName) - if err != nil { - return nil, nil, "", nil, "", err - } - if n == 0 { - return nil, nil, "", nil, "", fmt.Errorf("no such container: %s", containerName) - } - default: - return nil, nil, "", nil, "", fmt.Errorf("unexpected network type %v", netType) - } - return opts, netSlice, ipAddress, ports, macAddress, nil -} + // --dns-opt/--dns-option= ... + dnsOptions := []string{} -func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string, globalOptions types.GlobalCommandOptions) error { - e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + dnsOptFlags, err := cmd.Flags().GetStringSlice("dns-opt") if err != nil { - return err + return netOpts, err } - macValidNetworks := []string{"bridge", "macvlan"} - netMap, err := e.NetworkMap() + dnsOptions = append(dnsOptions, dnsOptFlags...) + + dnsOptionFlags, err := cmd.Flags().GetStringSlice("dns-option") if err != nil { - return err - } - for _, netstr := range netSlice { - netConfig, ok := netMap[netstr] - if !ok { - return fmt.Errorf("network %s not found", netstr) - } - // if MAC address is specified, the type of the network - // must be one of macValidNetworks - netType := netConfig.Plugins[0].Network.Type - if macAddress != "" && !strutil.InStringSlice(macValidNetworks, netType) { - return fmt.Errorf("%s interfaces on network %s do not support --mac-address", netType, netstr) - } + return netOpts, err } - return nil -} + dnsOptions = append(dnsOptions, dnsOptionFlags...) -func verifyContainerNetwork(cmd *cobra.Command, netSlice []string) error { - if cmd.Flags().Changed("publish") { - return fmt.Errorf("conflicting options: port publishing and the container type network mode") - } - if cmd.Flags().Changed("hostname") { - return fmt.Errorf("conflicting options: hostname and the network mode") - } - if cmd.Flags().Changed("dns") { - return fmt.Errorf("conflicting options: dns and the network mode") - } - if cmd.Flags().Changed("add-host") { - return fmt.Errorf("conflicting options: custom host-to-IP mapping and the network mode") - } - if runtime.GOOS != "linux" { - return fmt.Errorf("currently '--network=container:' can only works on linux") - } - if len(netSlice) > 1 { - return fmt.Errorf("only one network allowed using '--network=container:'") - } - return nil -} + netOpts.DNSResolvConfOptions = strutil.DedupeStrSlice(dnsOptions) -func buildResolvConf(cmd *cobra.Command, resolvConfPath string) error { - dnsValue, err := cmd.Flags().GetStringSlice("dns") + // --add-host= ... + addHostFlags, err := cmd.Flags().GetStringSlice("add-host") if err != nil { - return err + return netOpts, err } - dnsSearchValue, err := cmd.Flags().GetStringSlice("dns-search") + netOpts.AddHost = addHostFlags + + // --uts= + utsNamespace, err := cmd.Flags().GetString("uts") if err != nil { - return err - } - var dnsOptionValue []string - if dnsOpts, err := cmd.Flags().GetStringSlice("dns-opt"); err == nil { - dnsOptionValue = append(dnsOptionValue, dnsOpts...) - } else { - return err - } - if dnsOpts, err := cmd.Flags().GetStringSlice("dns-option"); err == nil { - dnsOptionValue = append(dnsOptionValue, dnsOpts...) - } else { - return err + return netOpts, err } + netOpts.UTSNamespace = utsNamespace - slirp4Dns := []string{} - if rootlessutil.IsRootlessChild() { - slirp4Dns, err = dnsutil.GetSlirp4netnsDNS() - if err != nil { - return err - } + // -p/--publish=127.0.0.1:80:8080/tcp ... + portSlice, err := cmd.Flags().GetStringSlice("publish") + if err != nil { + return netOpts, err } - - var ( - nameServers = strutil.DedupeStrSlice(dnsValue) - searchDomains = strutil.DedupeStrSlice(dnsSearchValue) - dnsOptions = strutil.DedupeStrSlice(dnsOptionValue) - ) - - if len(nameServers) == 0 || len(searchDomains) == 0 || len(dnsOptions) == 0 { - conf, err := resolvconf.Get() + portSlice = strutil.DedupeStrSlice(portSlice) + portMappings := []gocni.PortMapping{} + for _, p := range portSlice { + pm, err := portutil.ParseFlagP(p) if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return err - } - // if resolvConf file does't exist, using default resolvers - conf = &resolvconf.File{} - logrus.WithError(err).Debug("resolvConf file doesn't exist") - } - conf, err = resolvconf.FilterResolvDNS(conf.Content, true) - if err != nil { - return err - } - if len(searchDomains) == 0 { - searchDomains = resolvconf.GetSearchDomains(conf.Content) - } - if len(nameServers) == 0 { - nameServers = resolvconf.GetNameservers(conf.Content, resolvconf.IPv4) - } - if len(dnsOptions) == 0 { - dnsOptions = resolvconf.GetOptions(conf.Content) + return netOpts, err } + portMappings = append(portMappings, pm...) } + netOpts.PortMappings = portMappings - if _, err := resolvconf.Build(resolvConfPath, append(slirp4Dns, nameServers...), searchDomains, dnsOptions); err != nil { - return err - } - return nil -} - -func getMACAddress(cmd *cobra.Command, netSlice []string) (string, error) { - macAddress, err := cmd.Flags().GetString("mac-address") - if err != nil { - return "", err - } - if macAddress == "" { - return "", nil - } - if _, err := net.ParseMAC(macAddress); err != nil { - return "", err - } - return macAddress, nil + return netOpts, nil } diff --git a/cmd/nerdctl/container_run_network_base_test.go b/cmd/nerdctl/container_run_network_base_test.go new file mode 100644 index 00000000000..effaa7406da --- /dev/null +++ b/cmd/nerdctl/container_run_network_base_test.go @@ -0,0 +1,231 @@ +//go:build linux || windows + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + "net" + "strings" + "testing" + + "github.com/containerd/nerdctl/pkg/testutil" + "github.com/containerd/nerdctl/pkg/testutil/nettestutil" + "gotest.tools/v3/assert" +) + +// Tests various port mapping argument combinations by starting an nginx container and +// verifying its connectivity and that its serves its index.html from the external +// host IP as well as through the loopback interface. +// `loopbackIsolationEnabled` indicates whether the test should expect connections between +// the loopback interface and external host interface to succeed or not. +func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet string, loopbackIsolationEnabled bool) { + expectedIsolationErr := "" + if loopbackIsolationEnabled { + expectedIsolationErr = testutil.ExpectedConnectionRefusedError + } + + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(t, err) + type testCase struct { + listenIP net.IP + connectIP net.IP + hostPort string + containerPort string + connectURLPort int + runShouldSuccess bool + err string + } + lo := net.ParseIP("127.0.0.1") + zeroIP := net.ParseIP("0.0.0.0") + testCases := []testCase{ + { + listenIP: lo, + connectIP: lo, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + runShouldSuccess: true, + }, + { + // for https://github.com/containerd/nerdctl/issues/88 + listenIP: hostIP, + connectIP: hostIP, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + runShouldSuccess: true, + }, + { + listenIP: hostIP, + connectIP: lo, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + err: expectedIsolationErr, + runShouldSuccess: true, + }, + { + listenIP: lo, + connectIP: hostIP, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + err: expectedIsolationErr, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: lo, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: hostIP, + hostPort: "8080", + containerPort: "80", + connectURLPort: 8080, + runShouldSuccess: true, + }, + { + listenIP: lo, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "79-84", + connectURLPort: 7001, + runShouldSuccess: true, + }, + { + listenIP: hostIP, + connectIP: hostIP, + hostPort: "7000-7005", + containerPort: "79-84", + connectURLPort: 7001, + runShouldSuccess: true, + }, + { + listenIP: hostIP, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "79-84", + connectURLPort: 7001, + err: expectedIsolationErr, + runShouldSuccess: true, + }, + { + listenIP: lo, + connectIP: hostIP, + hostPort: "7000-7005", + containerPort: "79-84", + connectURLPort: 7001, + err: expectedIsolationErr, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: hostIP, + hostPort: "7000-7005", + containerPort: "79-84", + connectURLPort: 7001, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "80-85", + connectURLPort: 7001, + err: "error after 30 attempts", + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "80", + connectURLPort: 7000, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "80", + connectURLPort: 7005, + err: testutil.ExpectedConnectionRefusedError, + runShouldSuccess: true, + }, + { + listenIP: zeroIP, + connectIP: lo, + hostPort: "7000-7005", + containerPort: "79-85", + connectURLPort: 7005, + err: "invalid ranges specified for container and host Ports", + runShouldSuccess: false, + }, + } + + tID := testutil.Identifier(t) + for i, tc := range testCases { + i := i + tc := tc + tcName := fmt.Sprintf("%+v", tc) + t.Run(tcName, func(t *testing.T) { + testContainerName := fmt.Sprintf("%s-%d", tID, i) + base := testutil.NewBase(t) + defer base.Cmd("rm", "-f", testContainerName).Run() + pFlag := fmt.Sprintf("%s:%s:%s", tc.listenIP.String(), tc.hostPort, tc.containerPort) + connectURL := fmt.Sprintf("http://%s:%d", tc.connectIP.String(), tc.connectURLPort) + t.Logf("pFlag=%q, connectURL=%q", pFlag, connectURL) + cmd := base.Cmd("run", "-d", + "--name", testContainerName, + "-p", pFlag, + nginxImage) + if tc.runShouldSuccess { + cmd.AssertOK() + } else { + cmd.AssertFail() + return + } + + resp, err := nettestutil.HTTPGet(connectURL, 30, false) + if tc.err != "" { + assert.ErrorContains(t, err, tc.err) + return + } + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(respBody), nginxIndexHTMLSnippet)) + }) + } + +} + +func valuesOfMapStringString(m map[string]string) map[string]struct{} { + res := make(map[string]struct{}) + for _, v := range m { + res[v] = struct{}{} + } + return res +} diff --git a/cmd/nerdctl/container_run_network_linux_test.go b/cmd/nerdctl/container_run_network_linux_test.go index db7c4e6e96e..a311d16ee3c 100644 --- a/cmd/nerdctl/container_run_network_linux_test.go +++ b/cmd/nerdctl/container_run_network_linux_test.go @@ -19,7 +19,6 @@ package main import ( "fmt" "io" - "net" "regexp" "runtime" "strings" @@ -103,13 +102,15 @@ func TestRunHostLookup(t *testing.T) { // Create nginx containers for name, netName := range m { - base.Cmd("run", + cmd := base.Cmd("run", "-d", "--name", name, "--hostname", name+"-foobar", "--net", netName, testutil.NginxAlpineImage, - ).AssertOK() + ) + t.Logf("creating host lookup testing container with command: %q", strings.Join(cmd.Command, " ")) + cmd.AssertOK() } testWget := func(srcContainer, targetHostname string, expected bool) { @@ -136,18 +137,11 @@ func TestRunHostLookup(t *testing.T) { testWget("c1-in-n0", "c0-in-n0-foobar.n0", true) } -func valuesOfMapStringString(m map[string]string) map[string]struct{} { - res := make(map[string]struct{}) - for _, v := range m { - res[v] = struct{}{} - } - return res -} - func TestRunPortWithNoHostPort(t *testing.T) { if rootlessutil.IsRootless() { t.Skip("Auto port assign is not supported rootless mode yet") } + type testCase struct { containerPort string runShouldSuccess bool @@ -217,185 +211,7 @@ func TestRunPortWithNoHostPort(t *testing.T) { } func TestRunPort(t *testing.T) { - hostIP, err := nettestutil.NonLoopbackIPv4() - assert.NilError(t, err) - type testCase struct { - listenIP net.IP - connectIP net.IP - hostPort string - containerPort string - connectURLPort int - runShouldSuccess bool - err string - } - lo := net.ParseIP("127.0.0.1") - zeroIP := net.ParseIP("0.0.0.0") - testCases := []testCase{ - { - listenIP: lo, - connectIP: lo, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - runShouldSuccess: true, - }, - { - // for https://github.com/containerd/nerdctl/issues/88 - listenIP: hostIP, - connectIP: hostIP, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - runShouldSuccess: true, - }, - { - listenIP: hostIP, - connectIP: lo, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - err: "connection refused", - runShouldSuccess: true, - }, - { - listenIP: lo, - connectIP: hostIP, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - err: "connection refused", - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: lo, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: hostIP, - hostPort: "8080", - containerPort: "80", - connectURLPort: 8080, - runShouldSuccess: true, - }, - { - listenIP: lo, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "79-84", - connectURLPort: 7001, - runShouldSuccess: true, - }, - { - listenIP: hostIP, - connectIP: hostIP, - hostPort: "7000-7005", - containerPort: "79-84", - connectURLPort: 7001, - runShouldSuccess: true, - }, - { - listenIP: hostIP, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "79-84", - connectURLPort: 7001, - err: "connection refused", - runShouldSuccess: true, - }, - { - listenIP: lo, - connectIP: hostIP, - hostPort: "7000-7005", - containerPort: "79-84", - connectURLPort: 7001, - err: "connection refused", - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: hostIP, - hostPort: "7000-7005", - containerPort: "79-84", - connectURLPort: 7001, - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "80-85", - connectURLPort: 7001, - err: "error after 30 attempts", - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "80", - connectURLPort: 7000, - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "80", - connectURLPort: 7005, - err: "connection refused", - runShouldSuccess: true, - }, - { - listenIP: zeroIP, - connectIP: lo, - hostPort: "7000-7005", - containerPort: "79-85", - connectURLPort: 7005, - err: "invalid ranges specified for container and host Ports", - runShouldSuccess: false, - }, - } - - tID := testutil.Identifier(t) - for i, tc := range testCases { - i := i - tc := tc - tcName := fmt.Sprintf("%+v", tc) - t.Run(tcName, func(t *testing.T) { - testContainerName := fmt.Sprintf("%s-%d", tID, i) - base := testutil.NewBase(t) - defer base.Cmd("rm", "-f", testContainerName).Run() - pFlag := fmt.Sprintf("%s:%s:%s", tc.listenIP.String(), tc.hostPort, tc.containerPort) - connectURL := fmt.Sprintf("http://%s:%d", tc.connectIP.String(), tc.connectURLPort) - t.Logf("pFlag=%q, connectURL=%q", pFlag, connectURL) - cmd := base.Cmd("run", "-d", - "--name", testContainerName, - "-p", pFlag, - testutil.NginxAlpineImage) - if tc.runShouldSuccess { - cmd.AssertOK() - } else { - cmd.AssertFail() - return - } - - resp, err := nettestutil.HTTPGet(connectURL, 30, false) - if tc.err != "" { - assert.ErrorContains(t, err, tc.err) - return - } - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) - }) - } - + baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true) } func TestRunWithInvalidPortThenCleanUp(t *testing.T) { diff --git a/cmd/nerdctl/container_run_network_windows_test.go b/cmd/nerdctl/container_run_network_windows_test.go new file mode 100644 index 00000000000..3ae5bcf2db3 --- /dev/null +++ b/cmd/nerdctl/container_run_network_windows_test.go @@ -0,0 +1,186 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/Microsoft/hcsshim" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/nerdctl/pkg/testutil" + "gotest.tools/v3/assert" +) + +// TestRunInternetConnectivity tests Internet connectivity by pinging github.com. +func TestRunInternetConnectivity(t *testing.T) { + base := testutil.NewBase(t) + + type testCase struct { + args []string + } + testCases := []testCase{ + { + args: []string{"--net", "nat"}, + }, + } + for _, tc := range testCases { + tc := tc // IMPORTANT + name := "default" + if len(tc.args) > 0 { + name = strings.Join(tc.args, "_") + } + t.Run(name, func(t *testing.T) { + args := []string{"run", "--rm"} + args = append(args, tc.args...) + // TODO(aznashwan): smarter way to ensure internet connectivity is working. + args = append(args, testutil.CommonImage, "ping github.com") + cmd := base.Cmd(args...) + cmd.AssertOutContains("Reply from") + }) + } +} + +func TestRunPort(t *testing.T) { + // NOTE: currently no isolation between the loopback and host namespaces on Windows. + baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, false) +} + +// Checks whether an HNS endpoint with a name matching exists. +func listHnsEndpointsRegex(hnsEndpointNameRegex string) ([]hcsshim.HNSEndpoint, error) { + r, err := regexp.Compile(hnsEndpointNameRegex) + if err != nil { + return nil, err + } + hnsEndpoints, err := hcsshim.HNSListEndpointRequest() + if err != nil { + return nil, fmt.Errorf("failed to list HNS endpoints for request: %s", err) + } + + res := []hcsshim.HNSEndpoint{} + for _, endp := range hnsEndpoints { + if r.Match([]byte(endp.Name)) { + res = append(res, endp) + } + } + return res, nil +} + +// Asserts whether the container with the provided has any HNS endpoints with the expected +// naming format (`${container_id}_${network_name}`) for all of the provided network names. +// The container ID can be a regex. +func assertHnsEndpointsExistence(t *testing.T, shouldExist bool, containerIDRegex string, networkNames ...string) { + for _, netName := range networkNames { + endpointName := fmt.Sprintf("%s_%s", containerIDRegex, netName) + + testName := fmt.Sprintf("hns_endpoint_%s_shouldExist_%t", endpointName, shouldExist) + t.Run(testName, func(t *testing.T) { + matchingEndpoints, err := listHnsEndpointsRegex(endpointName) + assert.NilError(t, err) + if shouldExist { + assert.Equal(t, len(matchingEndpoints), 1) + assert.Equal(t, matchingEndpoints[0].Name, endpointName) + } else { + assert.Equal(t, len(matchingEndpoints), 0) + } + }) + } +} + +// Tests whether HNS endpoints are properly created and managed throughout the lifecycle of a container. +func TestHnsEndpointsExistDuringContainerLifecycle(t *testing.T) { + base := testutil.NewBase(t) + + testNet, err := getTestingNetwork() + assert.NilError(t, err) + + tID := testutil.Identifier(t) + defer base.Cmd("rm", "-f", tID).Run() + cmd := base.Cmd( + "create", + "--name", tID, + "--net", testNet.Name, + testutil.CommonImage, + "bash", "-c", + // NOTE: the BusyBox image used in Windows testing's `sleep` binary + // does not support the `infinity` argument. + "tail", "-f", + ) + t.Logf("Creating HNS lifecycle test container with command: %q", strings.Join(cmd.Command, " ")) + containerId := strings.TrimSpace(cmd.Run().Stdout()) + t.Logf("HNS endpoint lifecycle test container ID: %q", containerId) + + // HNS endpoints should be allocated on container creation. + assertHnsEndpointsExistence(t, true, containerId, testNet.Name) + + // Starting and stopping the container should NOT affect/change the endpoints. + base.Cmd("start", containerId).AssertOK() + assertHnsEndpointsExistence(t, true, containerId, testNet.Name) + + base.Cmd("stop", containerId).AssertOK() + assertHnsEndpointsExistence(t, true, containerId, testNet.Name) + + // Removing the container should remove the HNS endpoints. + base.Cmd("rm", containerId).AssertOK() + assertHnsEndpointsExistence(t, false, containerId, testNet.Name) +} + +// Returns a network to be used for testing. +// Note: currently hardcoded to return the default network, as `network create` +// does not work on Windows. +func getTestingNetwork() (*netutil.NetworkConfig, error) { + // NOTE: cannot currently `nerdctl network create` on Windows so we use a pre-existing network: + cniEnv, err := netutil.NewCNIEnv(defaults.CNIPath(), defaults.CNINetConfPath()) + if err != nil { + return nil, err + } + + return cniEnv.GetDefaultNetworkConfig() +} + +// Tests whether HNS endpoints are properly removed when running `run --rm`. +func TestHnsEndpointsRemovedAfterAttachedRun(t *testing.T) { + base := testutil.NewBase(t) + + testNet, err := getTestingNetwork() + assert.NilError(t, err) + + // NOTE: because we cannot set/obtain the ID of the container to check for the exact HNS + // endpoint name, we record the number of HNS endpoints on the testing network and + // ensure it remains constant until after the test. + existingEndpoints, err := listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) + assert.NilError(t, err) + originalEndpointsCount := len(existingEndpoints) + + tID := testutil.Identifier(t) + base.Cmd( + "run", + "--name", + tID, + "--rm", + "--net", testNet.Name, + testutil.CommonImage, + "ipconfig", "/all", + ).AssertOK() + + existingEndpoints, err = listHnsEndpointsRegex(fmt.Sprintf(".*_%s", testNet.Name)) + assert.NilError(t, err) + assert.Equal(t, originalEndpointsCount, len(existingEndpoints), "the number of HNS endpoints should equal pre-test amount") +} diff --git a/cmd/nerdctl/image_encrypt_linux_test.go b/cmd/nerdctl/image_encrypt_linux_test.go index 5d181c6540a..9236a5ca33a 100644 --- a/cmd/nerdctl/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image_encrypt_linux_test.go @@ -101,6 +101,7 @@ func rmiAll(base *testutil.Base) { base.T.Logf("Pruning all images (again?)") imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + base.T.Logf("pruning following images: %+v", imageIDs) base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() } } diff --git a/go.mod b/go.mod index 5b194262249..367546f6c0c 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,7 @@ require ( github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/symlink v0.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect diff --git a/go.sum b/go.sum index 11b9dcbab16..3c37103f68e 100644 --- a/go.sum +++ b/go.sum @@ -716,6 +716,7 @@ github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= diff --git a/pkg/api/types/container_network_types.go b/pkg/api/types/container_network_types.go new file mode 100644 index 00000000000..c3c48912af8 --- /dev/null +++ b/pkg/api/types/container_network_types.go @@ -0,0 +1,54 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package types + +import ( + gocni "github.com/containerd/go-cni" +) + +// Struct defining networking-related options. +type NetworkOptions struct { + // --net/--network= ... + NetworkSlice []string + + // --mac-address= + MACAddress string + + // --ip= + IPAddress string + + // -h/--hostname= + Hostname string + + // --dns= ... + DNSServers []string + + // --dns-opt/--dns-option= ... + DNSResolvConfOptions []string + + // --dns-search= ... + DNSSearchDomains []string + + // --add-host= ... + AddHost []string + + // --uts= + UTSNamespace string + + // -p/--publish=127.0.0.1:80:8080/tcp ... + PortMappings []gocni.PortMapping +} diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index e5f7f69620d..c9e7f8955ce 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "os" + "runtime" "syscall" "github.com/containerd/containerd" @@ -31,6 +32,7 @@ import ( "github.com/containerd/nerdctl/pkg/api/types" "github.com/containerd/nerdctl/pkg/clientutil" "github.com/containerd/nerdctl/pkg/cmd/volume" + "github.com/containerd/nerdctl/pkg/containerutil" "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/labels" @@ -164,6 +166,30 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions return err } + // NOTE: on non-Windows platforms, network cleanup is performed by OCI hooks. + // Seeing as though Windows does not currently support OCI hooks, we must explicitly + // perform the network cleanup from the main nerdctl executable. + if runtime.GOOS == "windows" { + spec, err := c.Spec(ctx) + if err != nil { + return err + } + + netOpts, err := containerutil.NetworkOptionsFromSpec(spec) + if err != nil { + return fmt.Errorf("failed to load container networking options from specs: %s", err) + } + + networkManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netOpts) + if err != nil { + return fmt.Errorf("failed to instantiate network options manager: %s", err) + } + + if err := networkManager.CleanupNetworking(ctx, c); err != nil { + logrus.WithError(retErr).Warnf("failed to clean up container networking: %s", err) + } + } + switch status.Status { case containerd.Created, containerd.Stopped: if _, err := task.Delete(ctx); err != nil && !errdefs.IsNotFound(err) { diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go new file mode 100644 index 00000000000..a111cb43989 --- /dev/null +++ b/pkg/containerutil/container_network_manager.go @@ -0,0 +1,558 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/pkg/netns" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/mountutil" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/nerdctl/pkg/netutil/nettype" + "github.com/containerd/nerdctl/pkg/strutil" + "github.com/opencontainers/runtime-spec/specs-go" +) + +const ( + UtsNamespaceHost = "host" +) + +func withCustomResolvConf(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/resolv.conf", + Type: "bind", + Source: src, + Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable + }) + return nil + } +} + +func withCustomEtcHostname(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/hostname", + Type: "bind", + Source: src, + Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable + }) + return nil + } +} + +func withCustomHosts(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/hosts", + Type: "bind", + Source: src, + Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable + }) + return nil + } +} + +// types.NetworkOptionsManager is an interface for reading/setting networking +// options for containers based on the provided command flags. +type NetworkOptionsManager interface { + // Returns a copy of the internal types.NetworkOptions. + NetworkOptions() types.NetworkOptions + + // Verifies that the internal network settings are correct. + VerifyNetworkOptions(context.Context) error + + // Performs setup actions required for the container with the given ID. + SetupNetworking(context.Context, string) error + + // Performs any required cleanup actions for the given container. + // Should only be called to revert any setup steps performed in SetupNetworking. + CleanupNetworking(context.Context, containerd.Container) error + + // Returns the set of NetworkingOptions which should be set as labels on the container. + // + // These options can potentially differ from the actual networking options + // that the NetworkOptionsManager was initially instantiated with. + // E.g: in container networking mode, the label will be normalized to an ID: + // `--net=container:myContainer` => `--net=container:`. + InternalNetworkingOptionLabels(context.Context) (types.NetworkOptions, error) + + // Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent + // the network specs which need to be applied to the container with the given ID. + ContainerNetworkingOpts(context.Context, string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) +} + +// Returns a types.NetworkOptionsManager based on the provided command's flags. +func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOpts types.NetworkOptions) (NetworkOptionsManager, error) { + netType, err := nettype.Detect(netOpts.NetworkSlice) + if err != nil { + return nil, err + } + + var manager NetworkOptionsManager + switch netType { + case nettype.None: + manager = &noneNetworkManager{globalOptions, netOpts} + case nettype.Host: + manager = &hostNetworkManager{globalOptions, netOpts} + case nettype.Container: + manager = &containerNetworkManager{globalOptions, netOpts} + case nettype.CNI: + manager = &cniNetworkManager{globalOptions, netOpts, nil} + default: + return nil, fmt.Errorf("unexpected container networking type: %q", netType) + } + + return manager, nil +} + +// No-op types.NetworkOptionsManager for network-less containers. +type noneNetworkManager struct { + globalOptions types.GlobalCommandOptions + netOpts types.NetworkOptions +} + +// Returns a copy of the internal types.NetworkOptions. +func (m *noneNetworkManager) NetworkOptions() types.NetworkOptions { + return m.netOpts +} + +// Verifies that the internal network settings are correct. +func (m *noneNetworkManager) VerifyNetworkOptions(_ context.Context) error { + // No options to verify if no network settings are provided. + return nil +} + +// Performs setup actions required for the container with the given ID. +func (m *noneNetworkManager) SetupNetworking(_ context.Context, _ string) error { + return nil +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in SetupNetworking. +func (m *noneNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { + return nil +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *noneNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { + return m.netOpts, nil +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + // No options to return if no network settings are provided. + return []oci.SpecOpts{}, []containerd.NewContainerOpts{}, nil +} + +// types.NetworkOptionsManager implementation for container networking settings. +type containerNetworkManager struct { + globalOptions types.GlobalCommandOptions + netOpts types.NetworkOptions +} + +// Returns a copy of the internal types.NetworkOptions. +func (m *containerNetworkManager) NetworkOptions() types.NetworkOptions { + return m.netOpts +} + +// Verifies that the internal network settings are correct. +func (m *containerNetworkManager) VerifyNetworkOptions(_ context.Context) error { + // TODO: check host OS, not client-side OS. + if runtime.GOOS != "linux" { + return errors.New("container networking mode is currently only supported on Linux") + } + + if m.netOpts.NetworkSlice != nil && len(m.netOpts.NetworkSlice) > 1 { + return errors.New("conflicting options: only one network specification is allowed when using '--network=container:'") + } + + nonZeroParams := nonZeroMapValues(map[string]interface{}{ + "--hostname": m.netOpts.Hostname, + "--mac-address": m.netOpts.MACAddress, + // NOTE: an empty slice still counts as a non-zero value so we check its length: + "-p/--publish": len(m.netOpts.PortMappings) != 0, + "--dns": len(m.netOpts.DNSServers) != 0, + "--add-host": len(m.netOpts.AddHost) != 0, + }) + + if len(nonZeroParams) != 0 { + return fmt.Errorf("conflicting options: the following arguments are not supported when using `--network=container:`: %s", nonZeroParams) + } + + return nil +} + +// Returns the relevant paths of the `hostname`, `resolv.conf`, and `hosts` files +// in the datastore of the container with the given ID. +func (m *containerNetworkManager) getContainerNetworkFilePaths(containerID string) (string, string, string, error) { + dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address) + if err != nil { + return "", "", "", err + } + conStateDir, err := ContainerStateDirPath(m.globalOptions, dataStore, containerID) + if err != nil { + return "", "", "", err + } + + hostnamePath := filepath.Join(conStateDir, "hostname") + resolvConfPath := filepath.Join(conStateDir, "resolv.conf") + etcHostsPath := hostsstore.HostsPath(dataStore, m.globalOptions.Namespace, containerID) + + return hostnamePath, resolvConfPath, etcHostsPath, nil +} + +// Performs setup actions required for the container with the given ID. +func (m *containerNetworkManager) SetupNetworking(_ context.Context, _ string) error { + // NOTE: container networking simply reuses network config files from the + // bridged container so there are no setup/teardown steps required. + return nil +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in SetupNetworking. +func (m *containerNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { + // NOTE: container networking simply reuses network config files from the + // bridged container so there are no setup/teardown steps required. + return nil +} + +// Searches for and returns the networking container for the given network argument. +func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.Context, containerNetArg string) (containerd.Container, error) { + netItems := strings.Split(containerNetArg, ":") + if len(netItems) < 2 { + return nil, fmt.Errorf("container networking argument format must be 'container:', got: %q", containerNetArg) + } + containerName := netItems[1] + + client, ctxt, cancel, err := clientutil.NewClient(ctx, m.globalOptions.Namespace, m.globalOptions.Address) + if err != nil { + return nil, err + } + defer cancel() + + var foundContainer containerd.Container + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("container networking: multiple containers found with prefix: %s", containerName) + } + foundContainer = found.Container + return nil + }, + } + n, err := walker.Walk(ctxt, containerName) + if err != nil { + return nil, err + } + if n == 0 { + return nil, fmt.Errorf("container networking: could not find container: %s", containerName) + } + + return foundContainer, nil +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *containerNetworkManager) InternalNetworkingOptionLabels(ctx context.Context) (types.NetworkOptions, error) { + opts := m.netOpts + if m.netOpts.NetworkSlice == nil || len(m.netOpts.NetworkSlice) != 1 { + return opts, fmt.Errorf("conflicting options: exactly one network specification is allowed when using '--network=container:'") + } + + container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0]) + if err != nil { + return opts, err + } + containerID := container.ID() + opts.NetworkSlice = []string{fmt.Sprintf("container:%s", containerID)} + return opts, nil +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *containerNetworkManager) ContainerNetworkingOpts(ctx context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + opts := []oci.SpecOpts{} + cOpts := []containerd.NewContainerOpts{} + + container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0]) + if err != nil { + return nil, nil, err + } + containerID := container.ID() + + s, err := container.Spec(ctx) + if err != nil { + return nil, nil, err + } + hostname := s.Hostname + + netNSPath, err := ContainerNetNSPath(ctx, container) + if err != nil { + return nil, nil, err + } + + hostnamePath, resolvConfPath, etcHostsPath, err := m.getContainerNetworkFilePaths(containerID) + if err != nil { + return nil, nil, err + } + + opts = append(opts, + oci.WithLinuxNamespace(specs.LinuxNamespace{ + Type: specs.NetworkNamespace, + Path: netNSPath, + }), + withCustomResolvConf(resolvConfPath), + withCustomHosts(etcHostsPath), + oci.WithHostname(hostname), + withCustomEtcHostname(hostnamePath), + ) + + return opts, cOpts, nil +} + +// types.NetworkOptionsManager implementation for host networking settings. +type hostNetworkManager struct { + globalOptions types.GlobalCommandOptions + netOpts types.NetworkOptions +} + +// Returns a copy of the internal types.NetworkOptions. +func (m *hostNetworkManager) NetworkOptions() types.NetworkOptions { + return m.netOpts +} + +// Verifies that the internal network settings are correct. +func (m *hostNetworkManager) VerifyNetworkOptions(_ context.Context) error { + // TODO: check host OS, not client-side OS. + if runtime.GOOS == "windows" { + return errors.New("cannot use host networking on Windows") + } + + if m.netOpts.MACAddress != "" { + return errors.New("conflicting options: mac-address and the network mode") + } + + return validateUtsSettings(m.netOpts) +} + +// Performs setup actions required for the container with the given ID. +func (m *hostNetworkManager) SetupNetworking(_ context.Context, _ string) error { + // NOTE: there are no setup steps required for host networking. + return nil +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in SetupNetworking. +func (m *hostNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { + // NOTE: there are no setup steps required for host networking. + return nil +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *hostNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { + opts := m.netOpts + // Cannot have a MAC address in host networking mode. + opts.MACAddress = "" + return opts, nil +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + + cOpts := []containerd.NewContainerOpts{} + specs := []oci.SpecOpts{ + oci.WithHostNamespace(specs.NetworkNamespace), + oci.WithHostHostsFile, + oci.WithHostResolvconf, + } + + // `/etc/hostname` does not exist on FreeBSD + if runtime.GOOS == "linux" && m.netOpts.UTSNamespace != UtsNamespaceHost { + // If no hostname is set, default to first 12 characters of the container ID. + hostname := m.netOpts.Hostname + if hostname == "" { + hostname = containerID + if len(hostname) > 12 { + hostname = hostname[0:12] + } + } + m.netOpts.Hostname = hostname + + hostnameOpts, err := writeEtcHostnameForContainer(m.globalOptions, m.netOpts.Hostname, containerID) + if err != nil { + return nil, nil, err + } + if hostnameOpts != nil { + specs = append(specs, hostnameOpts...) + } + } + + return specs, cOpts, nil +} + +// types.NetworkOptionsManager implementation for CNI networking settings. +// This is a more specialized and OS-dependendant networking model so this +// struct provides different implementations on different platforms. +type cniNetworkManager struct { + globalOptions types.GlobalCommandOptions + netOpts types.NetworkOptions + netNs *netns.NetNS +} + +// Returns a copy of the internal types.NetworkOptions. +func (m *cniNetworkManager) NetworkOptions() types.NetworkOptions { + return m.netOpts +} + +func validateUtsSettings(netOpts types.NetworkOptions) error { + utsNamespace := netOpts.UTSNamespace + if utsNamespace == "" { + return nil + } + + // Docker considers this a validation error so keep compat. + // https://docs.docker.com/engine/reference/run/#uts-settings---uts + if utsNamespace == UtsNamespaceHost && netOpts.Hostname != "" { + return fmt.Errorf("conflicting options: cannot set a --hostname with --uts=host") + } + + return nil +} + +// Writes the provided hostname string in a "hostname" file in the Container's +// Nerdctl-managed datastore and returns the oci.SpecOpts required in the container +// spec for the file to be mounted under /etc/hostname in the new container. +// If the hostname is empty, the leading 12 characters of the containerID +func writeEtcHostnameForContainer(globalOptions types.GlobalCommandOptions, hostname string, containerID string) ([]oci.SpecOpts, error) { + if containerID == "" { + return nil, fmt.Errorf("container ID is required for setting up hostname file") + } + + dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address) + if err != nil { + return nil, err + } + + stateDir, err := ContainerStateDirPath(globalOptions, dataStore, containerID) + if err != nil { + return nil, err + } + + hostnamePath := filepath.Join(stateDir, "hostname") + if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { + return nil, err + } + + return []oci.SpecOpts{oci.WithHostname(hostname), withCustomEtcHostname(hostnamePath)}, nil +} + +// Loads all available networks and verifies that every selected network +// from the networkSlice is of a type within supportedTypes. +func verifyNetworkTypes(env *netutil.CNIEnv, networkSlice []string, supportedTypes []string) (map[string]*netutil.NetworkConfig, error) { + netMap, err := env.NetworkMap() + if err != nil { + return nil, err + } + + res := make(map[string]*netutil.NetworkConfig, len(networkSlice)) + for _, netstr := range networkSlice { + netConfig, ok := netMap[netstr] + if !ok { + return nil, fmt.Errorf("network %s not found", netstr) + } + netType := netConfig.Plugins[0].Network.Type + if supportedTypes != nil && !strutil.InStringSlice(supportedTypes, netType) { + return nil, fmt.Errorf("network type %q is not supported for network mapping %q, must be one of: %v", netType, netstr, supportedTypes) + } + + res[netstr] = netConfig + } + + return res, nil +} + +// Returns the NetworkOptions used in a container's creation from its spec.Annotations. +func NetworkOptionsFromSpec(spec *specs.Spec) (types.NetworkOptions, error) { + opts := types.NetworkOptions{} + + if spec == nil { + return opts, fmt.Errorf("cannot determine networking options from nil spec") + } + if spec.Annotations == nil { + return opts, fmt.Errorf("cannot determine networking options from nil spec.Annotations") + } + + opts.Hostname = spec.Hostname + + if macAddress, ok := spec.Annotations[labels.MACAddress]; ok { + opts.MACAddress = macAddress + } + + if ipAddress, ok := spec.Annotations[labels.IPAddress]; ok { + opts.IPAddress = ipAddress + } + + var networks []string + networksJSON := spec.Annotations[labels.Networks] + if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil { + return opts, err + } + opts.NetworkSlice = networks + + if portsJSON := spec.Annotations[labels.Ports]; portsJSON != "" { + if err := json.Unmarshal([]byte(portsJSON), &opts.PortMappings); err != nil { + return opts, err + } + } + + return opts, nil +} + +// Returns a lslice of keys of the values in the map that are invalid or are a non-zero-value +// for their respective type. (e.g. anything other than a `""` for string type) +// Note that the zero-values for innately pointer-types slices/maps/chans are `nil`, +// and NOT a zero-length container value like `[]Any{}`. +func nonZeroMapValues(values map[string]interface{}) []string { + nonZero := []string{} + + for k, v := range values { + if !reflect.ValueOf(v).IsZero() { + nonZero = append(nonZero, k) + } + } + + return nonZero +} diff --git a/pkg/containerutil/container_network_manager_linux.go b/pkg/containerutil/container_network_manager_linux.go new file mode 100644 index 00000000000..8cfb3683a6f --- /dev/null +++ b/pkg/containerutil/container_network_manager_linux.go @@ -0,0 +1,171 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "context" + "errors" + "io/fs" + "path/filepath" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/clientutil" + "github.com/containerd/nerdctl/pkg/dnsutil" + "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/nerdctl/pkg/resolvconf" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/sirupsen/logrus" +) + +// 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()) + if err != nil { + return err + } + + if m.netOpts.MACAddress != "" { + macValidNetworks := []string{"bridge", "macvlan"} + if _, err := verifyNetworkTypes(e, m.netOpts.NetworkSlice, macValidNetworks); err != nil { + return err + } + } + + return validateUtsSettings(m.netOpts) +} + +// Performs setup actions required for the container with the given ID. +func (m *cniNetworkManager) SetupNetworking(_ context.Context, _ string) error { + // NOTE: on non-Windows systems which support OCI hooks, CNI networking setup + // is performed via createRuntime and postCreate hooks whose logic can + // be found in the pkg/ocihook package. + return nil +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in setupNetworking. +func (m *cniNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { + // NOTE: on non-Windows systems which support OCI hooks, CNI networking setup + // is performed via createRuntime and postCreate hooks whose logic can + // be found in the pkg/ocihook package. + return nil +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *cniNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { + return m.netOpts, nil +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *cniNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + opts := []oci.SpecOpts{} + cOpts := []containerd.NewContainerOpts{} + + dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address) + if err != nil { + return nil, nil, err + } + + stateDir, err := ContainerStateDirPath(m.globalOptions, dataStore, containerID) + if err != nil { + return nil, nil, err + } + + resolvConfPath := filepath.Join(stateDir, "resolv.conf") + if err := m.buildResolvConf(resolvConfPath); err != nil { + return nil, nil, err + } + + // the content of /etc/hosts is created in OCI Hook + etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, m.globalOptions.Namespace, containerID) + if err != nil { + return nil, nil, err + } + opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) + + if m.netOpts.UTSNamespace != UtsNamespaceHost { + // If no hostname is set, default to first 12 characters of the container ID. + hostname := m.netOpts.Hostname + if hostname == "" { + hostname = containerID + if len(hostname) > 12 { + hostname = hostname[0:12] + } + } + m.netOpts.Hostname = hostname + + hostnameOpts, err := writeEtcHostnameForContainer(m.globalOptions, m.netOpts.Hostname, containerID) + if err != nil { + return nil, nil, err + } + if hostnameOpts != nil { + opts = append(opts, hostnameOpts...) + } + } + + return opts, cOpts, nil +} + +func (m *cniNetworkManager) buildResolvConf(resolvConfPath string) error { + var err error + slirp4Dns := []string{} + if rootlessutil.IsRootlessChild() { + slirp4Dns, err = dnsutil.GetSlirp4netnsDNS() + if err != nil { + return err + } + } + + var ( + nameServers = m.netOpts.DNSServers + searchDomains = m.netOpts.DNSSearchDomains + dnsOptions = m.netOpts.DNSResolvConfOptions + ) + + // Use host defaults if any DNS settings are missing: + if len(nameServers) == 0 || len(searchDomains) == 0 || len(dnsOptions) == 0 { + conf, err := resolvconf.Get() + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + // if resolvConf file does't exist, using default resolvers + conf = &resolvconf.File{} + logrus.WithError(err).Debugf("resolvConf file doesn't exist on host") + } + conf, err = resolvconf.FilterResolvDNS(conf.Content, true) + if err != nil { + return err + } + if len(nameServers) == 0 { + nameServers = resolvconf.GetNameservers(conf.Content, resolvconf.IPv4) + } + if len(searchDomains) == 0 { + searchDomains = resolvconf.GetSearchDomains(conf.Content) + } + if len(dnsOptions) == 0 { + dnsOptions = resolvconf.GetOptions(conf.Content) + } + } + + _, err = resolvconf.Build(resolvConfPath, append(slirp4Dns, nameServers...), searchDomains, dnsOptions) + return err +} diff --git a/pkg/containerutil/container_network_manager_other.go b/pkg/containerutil/container_network_manager_other.go new file mode 100644 index 00000000000..500872f57c3 --- /dev/null +++ b/pkg/containerutil/container_network_manager_other.go @@ -0,0 +1,56 @@ +//go:build darwin || freebsd || netbsd || openbsd + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "context" + "fmt" + "runtime" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/pkg/api/types" +) + +// Verifies that the internal network settings are correct. +func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error { + return fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS) +} + +// Performs setup actions required for the container with the given ID. +func (m *cniNetworkManager) SetupNetworking(_ context.Context, _ string) error { + return fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS) +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in setupNetworking. +func (m *cniNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error { + return fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS) +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *cniNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { + return m.netOpts, fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS) +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *cniNetworkManager) ContainerNetworkingOpts(_ context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + return []oci.SpecOpts{}, []containerd.NewContainerOpts{}, fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS) +} diff --git a/pkg/containerutil/container_network_manager_test.go b/pkg/containerutil/container_network_manager_test.go new file mode 100644 index 00000000000..e7ce8cb00fb --- /dev/null +++ b/pkg/containerutil/container_network_manager_test.go @@ -0,0 +1,117 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func TestZeroMapValues(t *testing.T) { + emptyString := "" + testCases := []struct { + key string + value interface{} + shouldBeZero bool + }{ + { + key: "false", + value: false, + shouldBeZero: true, + }, + { + key: "true", + value: true, + shouldBeZero: false, + }, + { + key: "zeroInt", + value: int(0), + shouldBeZero: true, + }, + { + key: "nonZeroInt", + value: int(1), + shouldBeZero: false, + }, + { + key: "zeroString", + value: "", + shouldBeZero: true, + }, + { + key: "nonZeroString", + value: "non-zero", + shouldBeZero: false, + }, + { + key: "nilPointer", + value: (*string)(nil), + shouldBeZero: true, + }, + { + key: "pointerToEmpty", + value: &emptyString, + // technically just a nil pointer check, so any value should be non-Zero: + shouldBeZero: false, + }, + { + key: "nilSlice", + value: []string(nil), + shouldBeZero: true, + }, + { + key: "emptySlice", + value: []string{}, + shouldBeZero: false, + }, + { + key: "nonEmptySlice", + value: []string{"non-empty"}, + shouldBeZero: false, + }, + { + key: "nilMap", + value: map[string]int(nil), + shouldBeZero: true, + }, + { + key: "emptyMap", + value: map[string]int{}, + shouldBeZero: false, + }, + { + key: "nonEmptyMap", + value: map[string]int{"non-empty": 42}, + shouldBeZero: false, + }, + } + + for _, tc := range testCases { + testName := fmt.Sprintf("%s=%t", tc.key, tc.shouldBeZero) + t.Run(testName, func(tt *testing.T) { + result := nonZeroMapValues(map[string]interface{}{tc.key: tc.value}) + if tc.shouldBeZero { + assert.Equal(tt, len(result), 0) + } else { + assert.Equal(tt, len(result), 1) + } + }) + } +} diff --git a/pkg/containerutil/container_network_manager_windows.go b/pkg/containerutil/container_network_manager_windows.go new file mode 100644 index 00000000000..88abaeb6073 --- /dev/null +++ b/pkg/containerutil/container_network_manager_windows.go @@ -0,0 +1,193 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerutil + +import ( + "context" + "fmt" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/pkg/netns" + gocni "github.com/containerd/go-cni" + + "github.com/containerd/nerdctl/pkg/api/types" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/nerdctl/pkg/ocihook" +) + +// 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()) + if err != nil { + return err + } + + // NOTE: only currently supported network type on Windows is nat: + validNetworkTypes := []string{"nat"} + if _, err := verifyNetworkTypes(e, m.netOpts.NetworkSlice, validNetworkTypes); err != nil { + return err + } + + nonZeroArgs := nonZeroMapValues(map[string]interface{}{ + "--hostname": m.netOpts.Hostname, + "--uts": m.netOpts.UTSNamespace, + // NOTE: IP and MAC settings are currently ignored on Windows. + "--ip-address": m.netOpts.IPAddress, + "--mac-address": m.netOpts.MACAddress, + // NOTE: zero-length slices count as a non-zero-value so we explicitly check length: + "--dns-opt/--dns-option": len(m.netOpts.DNSResolvConfOptions) != 0, + "--dns-servers": len(m.netOpts.DNSServers) != 0, + "--dns-search": len(m.netOpts.DNSSearchDomains) != 0, + "--add-host": len(m.netOpts.AddHost) != 0, + }) + if len(nonZeroArgs) != 0 { + return fmt.Errorf("the following networking arguments are not supported on Windows: %+v", nonZeroArgs) + } + + return nil +} + +func (m *cniNetworkManager) getCNI() (gocni.CNI, error) { + e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork()) + if err != nil { + return nil, fmt.Errorf("failed to instantiate CNI env: %s", err) + } + + cniOpts := []gocni.Opt{ + gocni.WithPluginDir([]string{m.globalOptions.CNIPath}), + gocni.WithPluginConfDir(m.globalOptions.CNINetConfPath), + } + + if netMap, err := verifyNetworkTypes(e, m.netOpts.NetworkSlice, nil); err == nil { + for _, netConf := range netMap { + cniOpts = append(cniOpts, gocni.WithConfListBytes(netConf.Bytes)) + } + } else { + return nil, err + } + + return gocni.New(cniOpts...) +} + +// Performs setup actions required for the container with the given ID. +func (m *cniNetworkManager) SetupNetworking(ctx context.Context, containerID string) error { + cni, err := m.getCNI() + if err != nil { + return fmt.Errorf("failed to get container networking for setup: %s", err) + } + + netNs, err := m.setupNetNs() + if err != nil { + return err + } + + _, err = cni.Setup(ctx, containerID, netNs.GetPath(), m.getCNINamespaceOpts()...) + return err +} + +// Performs any required cleanup actions for the given container. +// Should only be called to revert any setup steps performed in setupNetworking. +func (m *cniNetworkManager) CleanupNetworking(ctx context.Context, container containerd.Container) error { + containerID := container.ID() + cni, err := m.getCNI() + if err != nil { + return fmt.Errorf("failed to get container networking for cleanup: %s", err) + } + + spec, err := container.Spec(ctx) + if err != nil { + return fmt.Errorf("failed to get container specs for networking cleanup: %s", err) + } + + netNsId, found := spec.Annotations[ocihook.NetworkNamespace] + if !found { + return fmt.Errorf("no %q annotation present on container with ID %s", ocihook.NetworkNamespace, containerID) + } + + return cni.Remove(ctx, containerID, netNsId, m.getCNINamespaceOpts()...) +} + +// Returns the set of NetworkingOptions which should be set as labels on the container. +func (m *cniNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) { + return m.netOpts, nil +} + +// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent +// the network specs which need to be applied to the container with the given ID. +func (m *cniNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + ns, err := m.setupNetNs() + if err != nil { + return nil, nil, err + } + + opts := []oci.SpecOpts{ + oci.WithWindowNetworksAllowUnqualifiedDNSQuery(), + oci.WithWindowsNetworkNamespace(ns.GetPath()), + } + + cOpts := []containerd.NewContainerOpts{ + containerd.WithAdditionalContainerLabels( + map[string]string{ + ocihook.NetworkNamespace: ns.GetPath(), + }, + ), + } + + return opts, cOpts, nil +} + +// Returns the string path to a network namespace. +func (m *cniNetworkManager) setupNetNs() (*netns.NetNS, error) { + if m.netNs != nil { + return m.netNs, nil + } + + // NOTE: the baseDir argument to NewNetNS is ignored on Windows. + ns, err := netns.NewNetNS("") + if err != nil { + return nil, err + } + + m.netNs = ns + return ns, err +} + +// Returns the []gocni.NamespaceOpts to be used for CNI setup/teardown. +func (m *cniNetworkManager) getCNINamespaceOpts() []gocni.NamespaceOpts { + opts := []gocni.NamespaceOpts{ + gocni.WithLabels(map[string]string{ + // allow loose CNI argument verification + // FYI: https://github.com/containernetworking/cni/issues/560 + "IgnoreUnknown": "1", + }), + } + + if m.netOpts.MACAddress != "" { + opts = append(opts, gocni.WithArgs("MAC", m.netOpts.MACAddress)) + } + + if m.netOpts.IPAddress != "" { + opts = append(opts, gocni.WithArgs("IP", m.netOpts.IPAddress)) + } + + if m.netOpts.PortMappings != nil { + opts = append(opts, gocni.WithCapabilityPortMap(m.netOpts.PortMappings)) + } + + return opts +} diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 4a2e7f24005..5bc2c4e62c9 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "path" + "path/filepath" "strconv" "strings" "time" @@ -31,10 +32,12 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" "github.com/containerd/containerd/runtime/restart" + "github.com/containerd/nerdctl/pkg/api/types" "github.com/containerd/nerdctl/pkg/consoleutil" "github.com/containerd/nerdctl/pkg/errutil" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/nsutil" "github.com/containerd/nerdctl/pkg/portutil" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/signalutil" @@ -438,3 +441,11 @@ func Unpause(ctx context.Context, client *containerd.Client, id string) error { return fmt.Errorf("container %s is not paused", id) } } + +// Returns the path to the Nerdctl-managed state directory for the container with the given ID. +func ContainerStateDirPath(globalOptions types.GlobalCommandOptions, dataStore, id string) (string, error) { + if err := nsutil.ValidateNamespaceName(globalOptions.Namespace); err != nil { + return "", fmt.Errorf("invalid namespace name %q for determining state dir of container %q: %s", globalOptions.Namespace, id, err) + } + return filepath.Join(dataStore, "containers", globalOptions.Namespace, id), nil +} diff --git a/pkg/nsutil/nsutil.go b/pkg/nsutil/nsutil.go new file mode 100644 index 00000000000..9cde4583c87 --- /dev/null +++ b/pkg/nsutil/nsutil.go @@ -0,0 +1,47 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package nsutil provides utilities for namespaces. +package nsutil + +import ( + "fmt" + "strings" +) + +// Ensures the provided namespace name is valid. +// Namespace names cannot be path-like strings or pre-defined aliases such as "..". +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names +func ValidateNamespaceName(nsName string) error { + if nsName == "" { + return fmt.Errorf("namespace name cannot be empty") + } + + // Slash and '$' for POSIX and backslash and '%' for Windows. + pathSeparators := "/\\%$" + if strings.ContainsAny(nsName, pathSeparators) { + return fmt.Errorf("namespace name cannot contain any special characters (%q): %s", pathSeparators, nsName) + } + + specialAliases := []string{".", "..", "~"} + for _, alias := range specialAliases { + if nsName == alias { + return fmt.Errorf("namespace name cannot be special path alias %q", alias) + } + } + + return nil +} diff --git a/pkg/nsutil/nsutil_test.go b/pkg/nsutil/nsutil_test.go new file mode 100644 index 00000000000..4c3645faf6e --- /dev/null +++ b/pkg/nsutil/nsutil_test.go @@ -0,0 +1,59 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package nsutil_test + +import ( + "testing" + + "github.com/containerd/nerdctl/pkg/nsutil" + "gotest.tools/v3/assert" +) + +func TestValidateNamespaceName(t *testing.T) { + testCases := []struct { + inputs []string + errSubstr string + }{ + { + []string{"test", "test-hyphen", ".start.dot", "mid.dot", "end.dot."}, + "", + }, + { + []string{".", "..", "~"}, + "namespace name cannot be special path alias", + }, + { + []string{"$$", "a$VARiable", "a%VAR%iable", "\\.", "\\%", "\\$"}, + "namespace name cannot contain any special characters", + }, + { + []string{"/start", "mid/dle", "end/", "\\start", "mid\\dle", "end\\"}, + "namespace name cannot contain any special characters", + }, + } + + for _, tc := range testCases { + for _, input := range tc.inputs { + err := nsutil.ValidateNamespaceName(input) + if tc.errSubstr == "" { + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, tc.errSubstr) + } + } + } +} diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 343e6dfd1c9..f38b4b20206 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -50,6 +50,9 @@ const ( // spec.State.Pid. // This is mostly used for VM based runtime, where the spec.State PID does not // necessarily lives in the created container networking namespace. + // + // On Windows, this label will contain the UUID of a namespace managed by + // the Host Compute Network Service (HCN) API. NetworkNamespace = labels.Prefix + "network-namespace" ) @@ -161,6 +164,9 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin if err != nil { return nil, err } + if o.cni == nil { + logrus.Warnf("no CNI network could be loaded from the provided network names: %v", networks) + } default: return nil, fmt.Errorf("unexpected network type %v", netType) } diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index e26b56d0b18..7eee6efc3e1 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -382,7 +382,7 @@ func (c *Cmd) AssertOutContains(s string) { func (c *Cmd) AssertCombinedOutContains(s string) { c.Base.T.Helper() res := c.Run() - assert.Assert(c.Base.T, strings.Contains(res.Combined(), s)) + assert.Assert(c.Base.T, strings.Contains(res.Combined(), s), fmt.Sprintf("expected output to contain %q: %q", s, res.Combined())) } // AssertOutContainsAll checks if command output contains All strings in `strs`. diff --git a/pkg/testutil/testutil_freebsd.go b/pkg/testutil/testutil_freebsd.go index 2068d8ef789..91bb973c11e 100644 --- a/pkg/testutil/testutil_freebsd.go +++ b/pkg/testutil/testutil_freebsd.go @@ -16,4 +16,13 @@ package testutil -const CommonImage = "docker.io/knast/freebsd:13-STABLE" +const ( + CommonImage = "docker.io/knast/freebsd:13-STABLE" + + // This error string is expected when attempting to connect to a TCP socket + // for a service which actively refuses the connection. + // (e.g. attempting to connect using http to an https endpoint). + // It should be "connection refused" as per the TCP RFC. + // https://www.rfc-editor.org/rfc/rfc793 + ExpectedConnectionRefusedError = "connection refused" +) diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index 65cb3f5c8eb..10c97de0f97 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -42,6 +42,13 @@ var ( NonDistBlobDigest = "sha256:be691b1535726014cdf3b715ff39361b19e121ca34498a9ceea61ad776b9c215" CommonImage = AlpineImage + + // This error string is expected when attempting to connect to a TCP socket + // for a service which actively refuses the connection. + // (e.g. attempting to connect using http to an https endpoint). + // It should be "connection refused" as per the TCP RFC. + // https://www.rfc-editor.org/rfc/rfc793 + ExpectedConnectionRefusedError = "connection refused" ) const ( diff --git a/pkg/testutil/testutil_windows.go b/pkg/testutil/testutil_windows.go index 1893c60f5bb..734431a3225 100644 --- a/pkg/testutil/testutil_windows.go +++ b/pkg/testutil/testutil_windows.go @@ -26,4 +26,16 @@ const ( // use gcr.io/k8s-staging-e2e-test-images/busybox:1.29-2-windows-amd64-ltsc2022 locally on windows 11 // https://github.com/microsoft/Windows-Containers/issues/179 CommonImage = WindowsNano + + // NOTE(aznashwan): the upstream e2e Nginx test image is actually based on BusyBox. + NginxAlpineImage = "registry.k8s.io/e2e-test-images/nginx:1.14-2" + NginxAlpineIndexHTMLSnippet = "Welcome to nginx!" + + // This error string is expected when attempting to connect to a TCP socket + // for a service which actively refuses the connection. + // (e.g. attempting to connect using http to an https endpoint). + // It should be "connection refused" as per the TCP RFC, but it is the + // below string constant on Windows. + // https://www.rfc-editor.org/rfc/rfc793 + ExpectedConnectionRefusedError = "No connection could be made because the target machine actively refused it." )