Skip to content

Commit 87aa769

Browse files
committed
Support --ip argument when run the container
Signed-off-by: Zheao.Li <me@manjusaka.me>
1 parent 5a6d99a commit 87aa769

File tree

6 files changed

+152
-22
lines changed

6 files changed

+152
-22
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ Network flags:
368368
- :whale: `--dns`: Set custom DNS servers
369369
- :whale: `-h, --hostname`: Container host name
370370
- :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip)
371+
- :whale: `--ip`: Specific static IP address(es) to use
371372

372373
Cgroup flags:
373374
- :whale: `--cpus`: Number of CPUs

cmd/nerdctl/run.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ func setCreateFlags(cmd *cobra.Command) {
121121
cmd.Flags().StringSlice("dns", nil, "Set custom DNS servers")
122122
// publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman)
123123
cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host")
124+
// FIXME: not support IPV6 yet
125+
cmd.Flags().String("ip", "", "IPv4 address to assign to the container")
124126
cmd.Flags().StringP("hostname", "h", "", "Container host name")
125127
// #endregion
126128

@@ -469,7 +471,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
469471
}
470472
cOpts = append(cOpts, restartOpts...)
471473

472-
netOpts, netSlice, ports, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
474+
netOpts, netSlice, ipAddress, ports, err := generateNetOpts(cmd, dataStore, stateDir, ns, id)
473475
if err != nil {
474476
return nil, "", nil, err
475477
}
@@ -553,7 +555,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
553555
return nil, "", nil, err
554556
}
555557
extraHosts = strutil.DedupeStrSlice(extraHosts)
556-
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ports, logURI, anonVolumes, pidFile, platform)
558+
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, extraHosts, netSlice, ipAddress, ports, logURI, anonVolumes, pidFile, platform)
557559
if err != nil {
558560
return nil, "", nil, err
559561
}
@@ -794,7 +796,7 @@ func readKVStringsMapfFromLabel(cmd *cobra.Command) (map[string]string, error) {
794796
return strutil.ConvertKVStringsToMap(labels), nil
795797
}
796798

797-
func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts, networks []string, ports []gocni.PortMapping, logURI string, anonVolumes []string, pidFile, platform string) (containerd.NewContainerOpts, error) {
799+
func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts, networks []string, ipAddress string, ports []gocni.PortMapping, logURI string, anonVolumes []string, pidFile, platform string) (containerd.NewContainerOpts, error) {
798800
m := make(map[string]string)
799801
m[labels.Namespace] = ns
800802
if name != "" {
@@ -834,6 +836,10 @@ func withInternalLabels(ns, name, hostname, containerStateDir string, extraHosts
834836
m[labels.PIDFile] = pidFile
835837
}
836838

839+
if ipAddress != "" {
840+
m[labels.IPAddress] = ipAddress
841+
}
842+
837843
m[labels.Platform], err = platformutil.NormalizeString(platform)
838844
if err != nil {
839845
return nil, err

cmd/nerdctl/run_network.go

+26-17
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/containerd/nerdctl/pkg/rootlessutil"
3636
"github.com/containerd/nerdctl/pkg/strutil"
3737
"github.com/opencontainers/runtime-spec/specs-go"
38+
"github.com/sirupsen/logrus"
3839
"github.com/spf13/cobra"
3940
)
4041

@@ -104,21 +105,29 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C
104105
}
105106
}
106107

107-
func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, []gocni.PortMapping, error) {
108+
func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, error) {
108109
opts := []oci.SpecOpts{}
109110
portSlice, err := cmd.Flags().GetStringSlice("publish")
110111
if err != nil {
111-
return nil, nil, nil, err
112+
return nil, nil, "", nil, err
113+
}
114+
ipAddress, err := cmd.Flags().GetString("ip")
115+
if err != nil {
116+
return nil, nil, "", nil, err
112117
}
113118
netSlice, err := getNetworkSlice(cmd)
114119
if err != nil {
115-
return nil, nil, nil, err
120+
return nil, nil, "", nil, err
121+
}
122+
123+
if (netSlice == nil || len(netSlice) == 0) && (ipAddress != "") {
124+
logrus.Warnf("You have assign an IP address %s but no network, So we will use the default network", ipAddress)
116125
}
117126

118127
ports := make([]gocni.PortMapping, 0)
119128
netType, err := nettype.Detect(netSlice)
120129
if err != nil {
121-
return nil, nil, nil, err
130+
return nil, nil, "", nil, err
122131
}
123132

124133
switch netType {
@@ -131,44 +140,44 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]
131140
// The actual network is configured in the oci hook.
132141
cniPath, err := cmd.Flags().GetString("cni-path")
133142
if err != nil {
134-
return nil, nil, nil, err
143+
return nil, nil, "", nil, err
135144
}
136145
cniNetconfpath, err := cmd.Flags().GetString("cni-netconfpath")
137146
if err != nil {
138-
return nil, nil, nil, err
147+
return nil, nil, "", nil, err
139148
}
140149
e, err := netutil.NewCNIEnv(cniPath, cniNetconfpath)
141150
if err != nil {
142-
return nil, nil, nil, err
151+
return nil, nil, "", nil, err
143152
}
144153
netMap := e.NetworkMap()
145154
for _, netstr := range netSlice {
146155
_, ok := netMap[netstr]
147156
if !ok {
148-
return nil, nil, nil, fmt.Errorf("network %s not found", netstr)
157+
return nil, nil, "", nil, fmt.Errorf("network %s not found", netstr)
149158
}
150159
}
151160

152161
resolvConfPath := filepath.Join(stateDir, "resolv.conf")
153162
dnsValue, err := cmd.Flags().GetStringSlice("dns")
154163
if err != nil {
155-
return nil, nil, nil, err
164+
return nil, nil, "", nil, err
156165
}
157166
if runtime.GOOS == "linux" {
158167
conf, err := resolvconf.Get()
159168
if err != nil {
160-
return nil, nil, nil, err
169+
return nil, nil, "", nil, err
161170
}
162171
slirp4Dns := []string{}
163172
if rootlessutil.IsRootlessChild() {
164173
slirp4Dns, err = dnsutil.GetSlirp4netnsDns()
165174
if err != nil {
166-
return nil, nil, nil, err
175+
return nil, nil, "", nil, err
167176
}
168177
}
169178
conf, err = resolvconf.FilterResolvDNS(conf.Content, true)
170179
if err != nil {
171-
return nil, nil, nil, err
180+
return nil, nil, "", nil, err
172181
}
173182
searchDomains := resolvconf.GetSearchDomains(conf.Content)
174183
dnsOptions := resolvconf.GetOptions(conf.Content)
@@ -177,25 +186,25 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]
177186
nameServers = resolvconf.GetNameservers(conf.Content, resolvconf.IPv4)
178187
}
179188
if _, err := resolvconf.Build(resolvConfPath, append(slirp4Dns, nameServers...), searchDomains, dnsOptions); err != nil {
180-
return nil, nil, nil, err
189+
return nil, nil, "", nil, err
181190
}
182191

183192
// the content of /etc/hosts is created in OCI Hook
184193
etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id)
185194
if err != nil {
186-
return nil, nil, nil, err
195+
return nil, nil, "", nil, err
187196
}
188197
opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath))
189198
for _, p := range portSlice {
190199
pm, err := portutil.ParseFlagP(p)
191200
if err != nil {
192-
return nil, nil, pm, err
201+
return nil, nil, "", pm, err
193202
}
194203
ports = append(ports, pm...)
195204
}
196205
}
197206
default:
198-
return nil, nil, nil, fmt.Errorf("unexpected network type %v", netType)
207+
return nil, nil, "", nil, fmt.Errorf("unexpected network type %v", netType)
199208
}
200-
return opts, netSlice, ports, nil
209+
return opts, netSlice, ipAddress, ports, nil
201210
}

cmd/nerdctl/run_network_linux_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,75 @@ func TestRunPort(t *testing.T) {
395395
}
396396

397397
}
398+
399+
func TestRunContainerWithStaticIP(t *testing.T) {
400+
if rootlessutil.IsRootless() {
401+
t.Skip("Static IP assignment is not supported rootless mode yet.")
402+
}
403+
networkName := "test-network"
404+
networkSubnet := "172.0.0.0/16"
405+
base := testutil.NewBase(t)
406+
cmd := base.Cmd("network", "create", networkName, "--subnet", networkSubnet)
407+
cmd.AssertOK()
408+
defer base.Cmd("network", "rm", networkName).Run()
409+
testCases := []struct {
410+
ip string
411+
shouldSuccess bool
412+
useNetwork bool
413+
checkTheIpAddress bool
414+
}{
415+
{
416+
ip: "172.0.0.2",
417+
shouldSuccess: true,
418+
useNetwork: true,
419+
checkTheIpAddress: true,
420+
},
421+
{
422+
ip: "192.0.0.2",
423+
shouldSuccess: false,
424+
useNetwork: true,
425+
checkTheIpAddress: false,
426+
},
427+
{
428+
ip: "10.4.0.2",
429+
shouldSuccess: true,
430+
useNetwork: false,
431+
checkTheIpAddress: false,
432+
},
433+
}
434+
tID := testutil.Identifier(t)
435+
for i, tc := range testCases {
436+
i := i
437+
tc := tc
438+
tcName := fmt.Sprintf("%+v", tc)
439+
t.Run(tcName, func(t *testing.T) {
440+
testContainerName := fmt.Sprintf("%s-%d", tID, i)
441+
base := testutil.NewBase(t)
442+
defer base.Cmd("rm", "-f", testContainerName).Run()
443+
args := []string{
444+
"run", "-d", "--name", testContainerName,
445+
}
446+
if tc.useNetwork {
447+
args = append(args, []string{"--network", networkName}...)
448+
}
449+
args = append(args, []string{"--ip", tc.ip, testutil.NginxAlpineImage}...)
450+
cmd := base.Cmd(args...)
451+
if !tc.shouldSuccess {
452+
cmd.AssertFail()
453+
return
454+
} else {
455+
cmd.AssertOK()
456+
}
457+
if tc.checkTheIpAddress {
458+
inspectCmd := base.Cmd("inspect", testContainerName, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"")
459+
result := inspectCmd.Run()
460+
stdoutContent := result.Stdout() + result.Stderr()
461+
assert.Assert(cmd.Base.T, result.ExitCode == 0, stdoutContent)
462+
if !strings.Contains(stdoutContent, tc.ip) {
463+
t.Fail()
464+
return
465+
}
466+
}
467+
})
468+
}
469+
}

pkg/labels/labels.go

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const (
5757
// Ports is a JSON-marshalled string of []gocni.PortMapping .
5858
Ports = Prefix + "ports"
5959

60+
// IPAddress is the static IP address of the container assigned by the user
61+
62+
IPAddress = Prefix + "ip"
63+
6064
// LogURI is the log URI
6165
LogURI = Prefix + "log-uri"
6266

pkg/ocihook/ocihook.go

+40-2
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin
195195
}
196196
}
197197

198+
if ipAddress, ok := o.state.Annotations[labels.IPAddress]; ok {
199+
o.containerIP = ipAddress
200+
}
201+
198202
if rootlessutil.IsRootlessChild() {
199203
o.rootlessKitClient, err = rootlessutil.NewRootlessKitClient()
200204
if err != nil {
@@ -229,6 +233,7 @@ type handlerOpts struct {
229233
rootlessKitClient rlkclient.Client
230234
bypassClient b4nndclient.Client
231235
extraHosts map[string]string // ip:host
236+
containerIP string
232237
}
233238

234239
// hookSpec is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/containerd/command/oci-hook.go#L59-L64
@@ -321,6 +326,25 @@ func getPortMapOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) {
321326
return nil, nil
322327
}
323328

329+
func getIPAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) {
330+
if opts.containerIP != "" {
331+
if rootlessutil.IsRootlessChild() {
332+
return nil, fmt.Errorf("containerIP assignment is not supported in rootless mode")
333+
}
334+
335+
return []gocni.NamespaceOpts{
336+
gocni.WithLabels(map[string]string{
337+
// Special tick for go-cni. Because go-cni marks all labels and args as same
338+
// So, we need add a special label to pass the containerIP to the host-local plugin.
339+
// FYI: https://github.com/containerd/go-cni/blob/v1.1.3/README.md?plain=1#L57-L64
340+
"IgnoreUnknown": "1",
341+
}),
342+
gocni.WithArgs("IP", opts.containerIP),
343+
}, nil
344+
}
345+
return nil, nil
346+
}
347+
324348
func onCreateRuntime(opts *handlerOpts) error {
325349
loadAppArmor()
326350

@@ -338,6 +362,13 @@ func onCreateRuntime(opts *handlerOpts) error {
338362
if err != nil {
339363
return err
340364
}
365+
ipAddressOpts, err := getIPAddressOpts(opts)
366+
if err != nil {
367+
return err
368+
}
369+
var namespaceOpts []gocni.NamespaceOpts
370+
namespaceOpts = append(namespaceOpts, portMapOpts...)
371+
namespaceOpts = append(namespaceOpts, ipAddressOpts...)
341372
hsMeta := hostsstore.Meta{
342373
Namespace: opts.state.Annotations[labels.Namespace],
343374
ID: opts.state.ID,
@@ -346,7 +377,7 @@ func onCreateRuntime(opts *handlerOpts) error {
346377
ExtraHosts: opts.extraHosts,
347378
Name: opts.state.Annotations[labels.Name],
348379
}
349-
cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, portMapOpts...)
380+
cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, namespaceOpts...)
350381
if err != nil {
351382
return fmt.Errorf("failed to call cni.Setup: %w", err)
352383
}
@@ -424,7 +455,14 @@ func onPostStop(opts *handlerOpts) error {
424455
if err != nil {
425456
return err
426457
}
427-
if err := opts.cni.Remove(ctx, opts.fullID, "", portMapOpts...); err != nil {
458+
ipAddressOpts, err := getIPAddressOpts(opts)
459+
if err != nil {
460+
return err
461+
}
462+
var namespaceOpts []gocni.NamespaceOpts
463+
namespaceOpts = append(namespaceOpts, portMapOpts...)
464+
namespaceOpts = append(namespaceOpts, ipAddressOpts...)
465+
if err := opts.cni.Remove(ctx, opts.fullID, "", namespaceOpts...); err != nil {
428466
logrus.WithError(err).Errorf("failed to call cni.Remove")
429467
return err
430468
}

0 commit comments

Comments
 (0)