Skip to content

Commit 37c681b

Browse files
authored
Merge pull request #1407 from yuchanns/issue-1382
Support argument `--mac-address` for nerdctl run command
2 parents 86f7cf4 + adca3e4 commit 37c681b

File tree

8 files changed

+227
-22
lines changed

8 files changed

+227
-22
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ Network flags:
397397
- :whale: `-h, --hostname`: Container host name
398398
- :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip)
399399
- :whale: `--ip`: Specific static IP address(es) to use
400+
- :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not
401+
check if manually specified MAC addresses are unique. Supports network
402+
type `bridge` and `macvlan`
400403

401404
Resource flags:
402405
- :whale: `--cpus`: Number of CPUs

cmd/nerdctl/create_linux_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package main
1818

1919
import (
20+
"fmt"
2021
"testing"
2122

2223
"github.com/containerd/nerdctl/pkg/testutil"
24+
"github.com/containerd/nerdctl/pkg/testutil/nettestutil"
2325
)
2426

2527
func TestCreate(t *testing.T) {
@@ -33,3 +35,74 @@ func TestCreate(t *testing.T) {
3335
base.Cmd("start", tID).AssertOK()
3436
base.Cmd("logs", tID).AssertOutContains("foo")
3537
}
38+
39+
func TestCreateWithMACAddress(t *testing.T) {
40+
base := testutil.NewBase(t)
41+
tID := testutil.Identifier(t)
42+
networkBridge := "testNetworkBridge" + tID
43+
networkMACvlan := "testNetworkMACvlan" + tID
44+
networkIPvlan := "testNetworkIPvlan" + tID
45+
base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
46+
base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
47+
base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
48+
t.Cleanup(func() {
49+
base.Cmd("network", "rm", networkBridge).Run()
50+
base.Cmd("network", "rm", networkMACvlan).Run()
51+
base.Cmd("network", "rm", networkIPvlan).Run()
52+
})
53+
tests := []struct {
54+
Network string
55+
WantErr bool
56+
Expect string
57+
}{
58+
{"host", true, "conflicting options"},
59+
{"none", true, "can't open '/sys/class/net/eth0/address'"},
60+
{"container:whatever" + tID, true, "conflicting options"},
61+
{"bridge", false, ""},
62+
{networkBridge, false, ""},
63+
{networkMACvlan, false, ""},
64+
{networkIPvlan, true, "not support"},
65+
}
66+
for i, test := range tests {
67+
containerName := fmt.Sprintf("%s_%d", tID, i)
68+
macAddress, err := nettestutil.GenerateMACAddress()
69+
if err != nil {
70+
t.Errorf("failed to generate MAC address: %s", err)
71+
}
72+
if test.Expect == "" && !test.WantErr {
73+
test.Expect = macAddress
74+
}
75+
t.Cleanup(func() {
76+
base.Cmd("rm", "-f", containerName).Run()
77+
})
78+
cmd := base.Cmd("create", "--network", test.Network, "--mac-address", macAddress, "--name", containerName, testutil.CommonImage, "cat", "/sys/class/net/eth0/address")
79+
if !test.WantErr {
80+
cmd.AssertOK()
81+
base.Cmd("start", containerName).AssertOK()
82+
cmd = base.Cmd("logs", containerName)
83+
cmd.AssertOK()
84+
cmd.AssertOutContains(test.Expect)
85+
} else {
86+
if (testutil.GetTarget() == testutil.Docker && test.Network == networkIPvlan) || test.Network == "none" {
87+
// 1. unlike nerdctl
88+
// when using network ipvlan in Docker
89+
// it delays fail on executing start command
90+
// 2. start on network none will success in both
91+
// nerdctl and Docker
92+
cmd.AssertOK()
93+
cmd = base.Cmd("start", containerName)
94+
if test.Network == "none" {
95+
// we check the result on logs command
96+
cmd.AssertOK()
97+
cmd = base.Cmd("logs", containerName)
98+
}
99+
}
100+
cmd.AssertCombinedOutContains(test.Expect)
101+
if test.Network == "none" {
102+
cmd.AssertOK()
103+
} else {
104+
cmd.AssertFail()
105+
}
106+
}
107+
}
108+
}

cmd/nerdctl/run.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ func setCreateFlags(cmd *cobra.Command) {
142142
// FIXME: not support IPV6 yet
143143
cmd.Flags().String("ip", "", "IPv4 address to assign to the container")
144144
cmd.Flags().StringP("hostname", "h", "", "Container host name")
145+
cmd.Flags().String("mac-address", "", "MAC address to assign to the container")
145146
// #endregion
146147

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

576-
netOpts, netSlice, ipAddress, ports, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
577+
netOpts, netSlice, ipAddress, ports, macAddress, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
577578
if err != nil {
578579
return nil, nil, err
579580
}
@@ -656,7 +657,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
656657
return nil, nil, err
657658
}
658659
}
659-
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ipAddress, ports, logURI, anonVolumes, pidFile, platform, mountPoints)
660+
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ipAddress, ports, logURI, anonVolumes, pidFile, platform, mountPoints, macAddress)
660661
if err != nil {
661662
return nil, nil, err
662663
}
@@ -998,7 +999,7 @@ func withStop(stopSignal string, stopTimeout int, ensuredImage *imgutil.EnsuredI
998999
}
9991000
}
10001001

1001-
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) {
1002+
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) {
10021003
m := make(map[string]string)
10031004
m[labels.Namespace] = ns
10041005
if name != "" {
@@ -1056,6 +1057,10 @@ func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts
10561057
m[labels.Mounts] = string(mountPointsJSON)
10571058
}
10581059

1060+
if macAddress != "" {
1061+
m[labels.MACAddress] = macAddress
1062+
}
1063+
10591064
return containerd.WithAdditionalContainerLabels(m), nil
10601065
}
10611066

cmd/nerdctl/run_network.go

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"io/fs"
24+
"net"
2425
"path/filepath"
2526
"runtime"
2627
"strings"
@@ -110,75 +111,88 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C
110111
}
111112
}
112113

113-
func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, error) {
114+
func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, error) {
114115
opts := []oci.SpecOpts{}
115116
portSlice, err := cmd.Flags().GetStringSlice("publish")
116117
if err != nil {
117-
return nil, nil, "", nil, err
118+
return nil, nil, "", nil, "", err
118119
}
119120
ipAddress, err := cmd.Flags().GetString("ip")
120121
if err != nil {
121-
return nil, nil, "", nil, err
122+
return nil, nil, "", nil, "", err
122123
}
123124
netSlice, err := getNetworkSlice(cmd)
124125
if err != nil {
125-
return nil, nil, "", nil, err
126+
return nil, nil, "", nil, "", err
126127
}
127128

128129
if (len(netSlice) == 0) && (ipAddress != "") {
129130
logrus.Warnf("You have assign an IP address %s but no network, So we will use the default network", ipAddress)
130131
}
131132

133+
macAddress, err := getMACAddress(cmd, netSlice)
134+
if err != nil {
135+
return nil, nil, "", nil, "", err
136+
}
137+
132138
ports := make([]gocni.PortMapping, 0)
133139
netType, err := nettype.Detect(netSlice)
134140
if err != nil {
135-
return nil, nil, "", nil, err
141+
return nil, nil, "", nil, "", err
136142
}
137143

138144
switch netType {
139145
case nettype.None:
140146
// NOP
147+
// Docker compatible: if macAddress is specified, set MAC address shall
148+
// not work but run command will success
141149
case nettype.Host:
150+
if macAddress != "" {
151+
return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode")
152+
}
142153
opts = append(opts, oci.WithHostNamespace(specs.NetworkNamespace), oci.WithHostHostsFile, oci.WithHostResolvconf)
143154
case nettype.CNI:
144155
// We only verify flags and generate resolv.conf here.
145156
// The actual network is configured in the oci hook.
146-
if err := verifyCNINetwork(cmd, netSlice); err != nil {
147-
return nil, nil, "", nil, err
157+
if err := verifyCNINetwork(cmd, netSlice, macAddress); err != nil {
158+
return nil, nil, "", nil, "", err
148159
}
149160

150161
if runtime.GOOS == "linux" {
151162
resolvConfPath := filepath.Join(stateDir, "resolv.conf")
152163
if err := buildResolvConf(cmd, resolvConfPath); err != nil {
153-
return nil, nil, "", nil, err
164+
return nil, nil, "", nil, "", err
154165
}
155166

156167
// the content of /etc/hosts is created in OCI Hook
157168
etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id)
158169
if err != nil {
159-
return nil, nil, "", nil, err
170+
return nil, nil, "", nil, "", err
160171
}
161172
opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath))
162173
for _, p := range portSlice {
163174
pm, err := portutil.ParseFlagP(p)
164175
if err != nil {
165-
return nil, nil, "", pm, err
176+
return nil, nil, "", pm, "", err
166177
}
167178
ports = append(ports, pm...)
168179
}
169180
}
170181
case nettype.Container:
182+
if macAddress != "" {
183+
return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode")
184+
}
171185
if err := verifyContainerNetwork(cmd, netSlice); err != nil {
172-
return nil, nil, "", nil, err
186+
return nil, nil, "", nil, "", err
173187
}
174188
network := strings.Split(netSlice[0], ":")
175189
if len(network) != 2 {
176-
return nil, nil, "", nil, fmt.Errorf("invalid network: %s, should be \"container:<id|name>\"", netSlice[0])
190+
return nil, nil, "", nil, "", fmt.Errorf("invalid network: %s, should be \"container:<id|name>\"", netSlice[0])
177191
}
178192
containerName := network[1]
179193
client, ctx, cancel, err := newClient(cmd)
180194
if err != nil {
181-
return nil, nil, "", nil, err
195+
return nil, nil, "", nil, "", err
182196
}
183197
defer cancel()
184198

@@ -224,15 +238,15 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]
224238
}
225239
n, err := walker.Walk(ctx, containerName)
226240
if err != nil {
227-
return nil, nil, "", nil, err
241+
return nil, nil, "", nil, "", err
228242
}
229243
if n == 0 {
230-
return nil, nil, "", nil, fmt.Errorf("no such container: %s", containerName)
244+
return nil, nil, "", nil, "", fmt.Errorf("no such container: %s", containerName)
231245
}
232246
default:
233-
return nil, nil, "", nil, fmt.Errorf("unexpected network type %v", netType)
247+
return nil, nil, "", nil, "", fmt.Errorf("unexpected network type %v", netType)
234248
}
235-
return opts, netSlice, ipAddress, ports, nil
249+
return opts, netSlice, ipAddress, ports, macAddress, nil
236250
}
237251

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

253-
func verifyCNINetwork(cmd *cobra.Command, netSlice []string) error {
267+
func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string) error {
254268
cniPath, err := cmd.Flags().GetString("cni-path")
255269
if err != nil {
256270
return err
@@ -263,12 +277,19 @@ func verifyCNINetwork(cmd *cobra.Command, netSlice []string) error {
263277
if err != nil {
264278
return err
265279
}
280+
macValidNetworks := []string{"bridge", "macvlan"}
266281
netMap := e.NetworkMap()
267282
for _, netstr := range netSlice {
268-
_, ok := netMap[netstr]
283+
netConfig, ok := netMap[netstr]
269284
if !ok {
270285
return fmt.Errorf("network %s not found", netstr)
271286
}
287+
// if MAC address is specified, the type of the network
288+
// must be one of macValidNetworks
289+
netType := netConfig.Plugins[0].Network.Type
290+
if macAddress != "" && !strutil.InStringSlice(macValidNetworks, netType) {
291+
return fmt.Errorf("%s interfaces on network %s do not support --mac-address", netType, netstr)
292+
}
272293
}
273294
return nil
274295
}
@@ -360,3 +381,17 @@ func buildResolvConf(cmd *cobra.Command, resolvConfPath string) error {
360381
}
361382
return nil
362383
}
384+
385+
func getMACAddress(cmd *cobra.Command, netSlice []string) (string, error) {
386+
macAddress, err := cmd.Flags().GetString("mac-address")
387+
if err != nil {
388+
return "", err
389+
}
390+
if macAddress == "" {
391+
return "", nil
392+
}
393+
if _, err := net.ParseMAC(macAddress); err != nil {
394+
return "", err
395+
}
396+
return macAddress, nil
397+
}

cmd/nerdctl/run_network_linux_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,49 @@ func TestSharedNetworkStack(t *testing.T) {
529529
base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80").
530530
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
531531
}
532+
533+
func TestRunContainerWithMACAddress(t *testing.T) {
534+
base := testutil.NewBase(t)
535+
tID := testutil.Identifier(t)
536+
networkBridge := "testNetworkBridge" + tID
537+
networkMACvlan := "testNetworkMACvlan" + tID
538+
networkIPvlan := "testNetworkIPvlan" + tID
539+
base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
540+
base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
541+
base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
542+
t.Cleanup(func() {
543+
base.Cmd("network", "rm", networkBridge).Run()
544+
base.Cmd("network", "rm", networkMACvlan).Run()
545+
base.Cmd("network", "rm", networkIPvlan).Run()
546+
})
547+
tests := []struct {
548+
Network string
549+
WantErr bool
550+
Expect string
551+
}{
552+
{"host", true, "conflicting options"},
553+
{"none", true, "can't open '/sys/class/net/eth0/address'"},
554+
{"container:whatever" + tID, true, "conflicting options"},
555+
{"bridge", false, ""},
556+
{networkBridge, false, ""},
557+
{networkMACvlan, false, ""},
558+
{networkIPvlan, true, "not support"},
559+
}
560+
for _, test := range tests {
561+
macAddress, err := nettestutil.GenerateMACAddress()
562+
if err != nil {
563+
t.Errorf("failed to generate MAC address: %s", err)
564+
}
565+
if test.Expect == "" && !test.WantErr {
566+
test.Expect = macAddress
567+
}
568+
cmd := base.Cmd("run", "--rm", "--network", test.Network, "--mac-address", macAddress, testutil.CommonImage, "cat", "/sys/class/net/eth0/address")
569+
if test.WantErr {
570+
cmd.AssertFail()
571+
cmd.AssertCombinedOutContains(test.Expect)
572+
} else {
573+
cmd.AssertOK()
574+
cmd.AssertOutContains(test.Expect)
575+
}
576+
}
577+
}

pkg/labels/labels.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ const (
8484

8585
// StopTimeout is seconds to wait for stop a container.
8686
StopTimout = Prefix + "stop-timeout"
87+
88+
MACAddress = Prefix + "mac-address"
8789
)
8890

8991
var ShellCompletions = []string{

0 commit comments

Comments
 (0)