diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index f9c35c845aa..511d24bc4dc 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -156,6 +156,126 @@ func TestNetworkCreate(t *testing.T) { } }, }, + { + Description: "with static IPv4 address", + Setup: func(data test.Data, helpers test.Helpers) { + networkName := data.Identifier() + staticIP := "172.19.0.100" + data.Labels().Set("networkName", networkName) + data.Labels().Set("staticIP", staticIP) + helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--subnet", "172.19.0.0/24") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get("networkName")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIP"), testutil.CommonImage, "ip", "addr", "show", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet %s/24", data.Labels().Get("staticIP")))) + }, + } + }, + }, + { + Description: "with static IPv6 address", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + networkName := data.Identifier() + staticIPv6 := "2001:db8:1::100" + data.Labels().Set("networkName", networkName) + data.Labels().Set("staticIPv6", staticIPv6) + helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--ipv6", "--subnet", "2001:db8:1::/64") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get("networkName")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIPv6"), testutil.CommonImage, "ip", "addr", "show", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet6 %s/64", data.Labels().Get("staticIPv6")))) + }, + } + }, + }, + { + Description: "with dual-stack static IP addresses", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + networkName := data.Identifier() + staticIPv4 := "172.20.0.100" + staticIPv6 := "2001:db8:2::100" + data.Labels().Set("networkName", networkName) + data.Labels().Set("staticIPv4", staticIPv4) + data.Labels().Set("staticIPv6", staticIPv6) + helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--subnet", "172.20.0.0/24", "--ipv6", "--subnet", "2001:db8:2::/64") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get("networkName")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIPv4"), "--ip", data.Labels().Get("staticIPv6"), testutil.CommonImage, "ip", "addr", "show", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet %s/24", data.Labels().Get("staticIPv4")))) + assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet6 %s/64", data.Labels().Get("staticIPv6")))) + }, + } + }, + }, + { + Description: "with static IPv6 address on macvlan", + Require: nerdtest.OnlyIPv6, + Setup: func(data test.Data, helpers test.Helpers) { + dummyLinkName := "dummy-" + data.Identifier() + networkName := data.Identifier() + staticIPv6 := "2001:db8:3::100" + subnet := "2001:db8:3::/64" + + data.Labels().Set("dummyLinkName", dummyLinkName) + data.Labels().Set("networkName", networkName) + data.Labels().Set("staticIPv6", staticIPv6) + + // Create a dummy interface to be the parent of the macvlan network + helpers.Custom("ip", "link", "add", dummyLinkName, "type", "dummy").Run(&test.Expected{ExitCode: 0}) + helpers.Custom("ip", "link", "set", dummyLinkName, "up").Run(&test.Expected{ExitCode: 0}) + + // Create the macvlan network + helpers.Ensure("network", "create", networkName, + "--driver", "macvlan", + "--parent", dummyLinkName, + "--ipv6", + "--subnet", subnet) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get("networkName")) + helpers.Anyhow("ip", "link", "del", data.Labels().Get("dummyLinkName")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--net", data.Labels().Get("networkName"), + "--ip6", data.Labels().Get("staticIPv6"), + testutil.CommonImage, "ip", "addr", "show", "eth0") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet6 %s/64", data.Labels().Get("staticIPv6")))) + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 804250f80ec..68dd63bdf3a 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -597,6 +597,9 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e if value != nil && value.Ipv4Address != "" { c.RunArgs = append(c.RunArgs, "--ip="+value.Ipv4Address) } + if value != nil && value.Ipv6Address != "" { + c.RunArgs = append(c.RunArgs, "--ip6="+value.Ipv6Address) + } if value != nil && value.MacAddress != "" { c.RunArgs = append(c.RunArgs, "--mac-address="+value.MacAddress) } diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go index ee7a704897d..baceccceef8 100644 --- a/pkg/composer/serviceparser/serviceparser_test.go +++ b/pkg/composer/serviceparser/serviceparser_test.go @@ -482,6 +482,44 @@ services: } +func TestParseDualStackAddress(t *testing.T) { + t.Parallel() + const dockerComposeYAML = ` +services: + foo: + image: nginx:alpine + networks: + default: + ipv4_address: "172.30.0.100" + ipv6_address: "2001:db8:abc:123::42" +networks: + default: + driver: bridge + ipam: + driver: default + config: + - subnet: "172.30.0.0/24" + - subnet: "2001:db8:abc:123::/64" +` + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil) + assert.NilError(t, err) + + fooSvc, err := project.GetService("foo") + assert.NilError(t, err) + + foo, err := Parse(project, fooSvc) + assert.NilError(t, err) + + t.Logf("foo: %+v", foo) + for _, c := range foo.Containers { + assert.Assert(t, in(c.RunArgs, "--ip=172.30.0.100")) + assert.Assert(t, in(c.RunArgs, "--ip6=2001:db8:abc:123::42")) + } +} + func TestParseConfigs(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { diff --git a/pkg/netutil/cni_plugin_unix.go b/pkg/netutil/cni_plugin_unix.go index 2851c7b5a3a..b2f91c7ebec 100644 --- a/pkg/netutil/cni_plugin_unix.go +++ b/pkg/netutil/cni_plugin_unix.go @@ -136,11 +136,12 @@ func (*tuningConfig) GetPluginType() string { // https://github.com/containernetworking/plugins/blob/v1.0.1/plugins/ipam/host-local/backend/allocator/config.go#L47-L56 type hostLocalIPAMConfig struct { - Type string `json:"type"` - Routes []IPAMRoute `json:"routes,omitempty"` - ResolveConf string `json:"resolveConf,omitempty"` - DataDir string `json:"dataDir,omitempty"` - Ranges [][]IPAMRange `json:"ranges,omitempty"` + Type string `json:"type"` + Routes []IPAMRoute `json:"routes,omitempty"` + ResolveConf string `json:"resolveConf,omitempty"` + DataDir string `json:"dataDir,omitempty"` + Ranges [][]IPAMRange `json:"ranges,omitempty"` + Capabilities map[string]bool `json:"capabilities,omitempty"` } func newHostLocalIPAMConfig() *hostLocalIPAMConfig { diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index 046c173d122..ef2b0adb8ee 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -138,6 +138,10 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] bridge.HairpinMode = true if ipv6 { bridge.Capabilities["ips"] = true + // Explicitly declare capabilities that are implicitly lost when + // the bridge.Capabilities map becomes non-empty. + bridge.Capabilities["dns"] = true + bridge.Capabilities["portMappings"] = true } // Determine the appropriate firewall ingress policy based on icc setting @@ -207,6 +211,10 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] vlan.IPAM = ipam if ipv6 { vlan.Capabilities["ips"] = true + // Explicitly declare capabilities that are implicitly lost when + // the vlan.Capabilities map becomes non-empty. + vlan.Capabilities["dns"] = true + vlan.Capabilities["portMappings"] = true } plugins = []CNIPlugin{vlan} default: @@ -225,6 +233,11 @@ func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRan {Dst: "0.0.0.0/0"}, } } + // Explicitly declare "ips" capability for host-local IPAM + if ipamConf.Capabilities == nil { + ipamConf.Capabilities = make(map[string]bool) + } + ipamConf.Capabilities["ips"] = true ranges, findIPv4, err := e.parseIPAMRanges(subnets, gatewayStr, ipRangeStr, ipv6) if err != nil { return nil, err