Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support argument --mac-address for nerdctl run command #1407

Merged
merged 1 commit into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ Network flags:
- :whale: `-h, --hostname`: Container host name
- :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip)
- :whale: `--ip`: Specific static IP address(es) to use
- :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not
check if manually specified MAC addresses are unique. Supports network
type `bridge` and `macvlan`

Resource flags:
- :whale: `--cpus`: Number of CPUs
Expand Down
73 changes: 73 additions & 0 deletions cmd/nerdctl/create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package main

import (
"fmt"
"testing"

"github.com/containerd/nerdctl/pkg/testutil"
"github.com/containerd/nerdctl/pkg/testutil/nettestutil"
)

func TestCreate(t *testing.T) {
Expand All @@ -33,3 +35,74 @@ func TestCreate(t *testing.T) {
base.Cmd("start", tID).AssertOK()
base.Cmd("logs", tID).AssertOutContains("foo")
}

func TestCreateWithMACAddress(t *testing.T) {
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
networkBridge := "testNetworkBridge" + tID
networkMACvlan := "testNetworkMACvlan" + tID
networkIPvlan := "testNetworkIPvlan" + tID
base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
t.Cleanup(func() {
base.Cmd("network", "rm", networkBridge).Run()
base.Cmd("network", "rm", networkMACvlan).Run()
base.Cmd("network", "rm", networkIPvlan).Run()
})
tests := []struct {
Network string
WantErr bool
Expect string
}{
{"host", true, "conflicting options"},
{"none", true, "can't open '/sys/class/net/eth0/address'"},
{"container:whatever" + tID, true, "conflicting options"},
{"bridge", false, ""},
{networkBridge, false, ""},
{networkMACvlan, false, ""},
{networkIPvlan, true, "not support"},
}
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
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" {
cmd.AssertOK()
} else {
cmd.AssertFail()
}
}
}
}
11 changes: 8 additions & 3 deletions cmd/nerdctl/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func setCreateFlags(cmd *cobra.Command) {
// FIXME: not support IPV6 yet
cmd.Flags().String("ip", "", "IPv4 address to assign to the container")
cmd.Flags().StringP("hostname", "h", "", "Container host name")
cmd.Flags().String("mac-address", "", "MAC address to assign to the container")
// #endregion

cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private")`)
Expand Down Expand Up @@ -573,7 +574,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
opts = append(opts, withCustomEtcHostname(hostnamePath))
}

netOpts, netSlice, ipAddress, ports, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
netOpts, netSlice, ipAddress, ports, macAddress, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -656,7 +657,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
return nil, nil, err
}
}
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ipAddress, ports, logURI, anonVolumes, pidFile, platform, mountPoints)
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ipAddress, ports, logURI, anonVolumes, pidFile, platform, mountPoints, macAddress)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -998,7 +999,7 @@ func withStop(stopSignal string, stopTimeout int, ensuredImage *imgutil.EnsuredI
}
}

func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts, networks []string, ipAddress string, ports []gocni.PortMapping, logURI string, anonVolumes []string, pidFile, platform string, mountPoints []*mountutil.Processed) (containerd.NewContainerOpts, error) {
func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts, networks []string, ipAddress string, ports []gocni.PortMapping, logURI string, anonVolumes []string, pidFile, platform string, mountPoints []*mountutil.Processed, macAddress string) (containerd.NewContainerOpts, error) {
m := make(map[string]string)
m[labels.Namespace] = ns
if name != "" {
Expand Down Expand Up @@ -1056,6 +1057,10 @@ func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts
m[labels.Mounts] = string(mountPointsJSON)
}

if macAddress != "" {
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
m[labels.MACAddress] = macAddress
}

return containerd.WithAdditionalContainerLabels(m), nil
}

Expand Down
73 changes: 54 additions & 19 deletions cmd/nerdctl/run_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"io/fs"
"net"
"path/filepath"
"runtime"
"strings"
Expand Down Expand Up @@ -110,75 +111,88 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C
}
}

func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, error) {
func generateNetOpts(cmd *cobra.Command, 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
return nil, nil, "", nil, "", err
}
ipAddress, err := cmd.Flags().GetString("ip")
if err != nil {
return nil, nil, "", nil, err
return nil, nil, "", nil, "", err
}
netSlice, err := getNetworkSlice(cmd)
if err != nil {
return nil, nil, "", nil, err
return nil, nil, "", nil, "", err
}

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)
}

macAddress, err := getMACAddress(cmd, netSlice)
if err != nil {
return nil, nil, "", nil, "", err
}

ports := make([]gocni.PortMapping, 0)
netType, err := nettype.Detect(netSlice)
if err != nil {
return nil, nil, "", nil, err
return nil, nil, "", nil, "", err
}

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:
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
// We only verify flags and generate resolv.conf here.
// The actual network is configured in the oci hook.
if err := verifyCNINetwork(cmd, netSlice); err != nil {
return nil, nil, "", nil, err
if err := verifyCNINetwork(cmd, netSlice, macAddress); 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
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
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
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
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:<id|name>\"", netSlice[0])
return nil, nil, "", nil, "", fmt.Errorf("invalid network: %s, should be \"container:<id|name>\"", netSlice[0])
}
containerName := network[1]
client, ctx, cancel, err := newClient(cmd)
if err != nil {
return nil, nil, "", nil, err
return nil, nil, "", nil, "", err
}
defer cancel()

Expand Down Expand Up @@ -224,15 +238,15 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]
}
n, err := walker.Walk(ctx, containerName)
if err != nil {
return nil, nil, "", nil, err
return nil, nil, "", nil, "", err
}
if n == 0 {
return nil, nil, "", nil, fmt.Errorf("no such container: %s", containerName)
return nil, nil, "", nil, "", fmt.Errorf("no such container: %s", containerName)
}
default:
return nil, nil, "", nil, fmt.Errorf("unexpected network type %v", netType)
return nil, nil, "", nil, "", fmt.Errorf("unexpected network type %v", netType)
}
return opts, netSlice, ipAddress, ports, nil
return opts, netSlice, ipAddress, ports, macAddress, nil
}

func getContainerNetNSPath(ctx context.Context, c containerd.Container) (string, error) {
Expand All @@ -250,7 +264,7 @@ func getContainerNetNSPath(ctx context.Context, c containerd.Container) (string,
return fmt.Sprintf("/proc/%d/ns/net", task.Pid()), nil
}

func verifyCNINetwork(cmd *cobra.Command, netSlice []string) error {
func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string) error {
cniPath, err := cmd.Flags().GetString("cni-path")
if err != nil {
return err
Expand All @@ -263,12 +277,19 @@ func verifyCNINetwork(cmd *cobra.Command, netSlice []string) error {
if err != nil {
return err
}
macValidNetworks := []string{"bridge", "macvlan"}
netMap := e.NetworkMap()
for _, netstr := range netSlice {
_, ok := netMap[netstr]
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 nil
}
Expand Down Expand Up @@ -360,3 +381,17 @@ func buildResolvConf(cmd *cobra.Command, resolvConfPath string) error {
}
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
}
46 changes: 46 additions & 0 deletions cmd/nerdctl/run_network_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,49 @@ func TestSharedNetworkStack(t *testing.T) {
base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80").
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
}

func TestRunContainerWithMACAddress(t *testing.T) {
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
networkBridge := "testNetworkBridge" + tID
networkMACvlan := "testNetworkMACvlan" + tID
networkIPvlan := "testNetworkIPvlan" + tID
base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
t.Cleanup(func() {
base.Cmd("network", "rm", networkBridge).Run()
base.Cmd("network", "rm", networkMACvlan).Run()
base.Cmd("network", "rm", networkIPvlan).Run()
})
tests := []struct {
Network string
WantErr bool
Expect string
}{
{"host", true, "conflicting options"},
{"none", true, "can't open '/sys/class/net/eth0/address'"},
{"container:whatever" + tID, true, "conflicting options"},
{"bridge", false, ""},
{networkBridge, false, ""},
{networkMACvlan, false, ""},
{networkIPvlan, true, "not support"},
}
for _, test := range tests {
macAddress, err := nettestutil.GenerateMACAddress()
if err != nil {
t.Errorf("failed to generate MAC address: %s", err)
}
if test.Expect == "" && !test.WantErr {
test.Expect = macAddress
}
cmd := base.Cmd("run", "--rm", "--network", test.Network, "--mac-address", macAddress, testutil.CommonImage, "cat", "/sys/class/net/eth0/address")
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
if test.WantErr {
cmd.AssertFail()
cmd.AssertCombinedOutContains(test.Expect)
} else {
cmd.AssertOK()
cmd.AssertOutContains(test.Expect)
}
}
}
2 changes: 2 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const (

// StopTimeout is seconds to wait for stop a container.
StopTimout = Prefix + "stop-timeout"

MACAddress = Prefix + "mac-address"
)

var ShellCompletions = []string{
Expand Down
Loading