Skip to content
Draft
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
32 changes: 19 additions & 13 deletions cmd/nerdctl/network/network_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func createCommand() *cobra.Command {
cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet")
cmd.Flags().StringArray("ip-range", nil, `Allocate container ip from a sub-range`)
cmd.Flags().StringArray("aux-address", nil, "Auxiliary IPv4 or IPv6 addresses used by Network driver, as name=IP pairs. The IPs are reserved and never assigned to containers")
cmd.Flags().StringArray("label", nil, "Set metadata for a network")
cmd.Flags().Bool("ipv4", true, "Enable IPv4 networking (set to false together with --ipv6 for an IPv6-only network)")
cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking")
Expand Down Expand Up @@ -93,6 +94,10 @@ func createAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
auxAddresses, err := cmd.Flags().GetStringArray("aux-address")
if err != nil {
return err
}
labels, err := cmd.Flags().GetStringArray("label")
if err != nil {
return err
Expand All @@ -112,18 +117,19 @@ func createAction(cmd *cobra.Command, args []string) error {
}

return network.Create(types.NetworkCreateOptions{
GOptions: globalOptions,
Name: name,
Driver: driver,
Options: strutil.ConvertKVStringsToMap(opts),
IPAMDriver: ipamDriver,
IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
Subnets: subnets,
Gateway: gateways,
IPRange: ipRanges,
Labels: labels,
IPv6: ipv6,
IPv4: &ipv4,
Internal: internal,
GOptions: globalOptions,
Name: name,
Driver: driver,
Options: strutil.ConvertKVStringsToMap(opts),
IPAMDriver: ipamDriver,
IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
Subnets: subnets,
Gateway: gateways,
IPRange: ipRanges,
AuxAddresses: auxAddresses,
Labels: labels,
IPv6: ipv6,
IPv4: &ipv4,
Internal: internal,
}, cmd.OutOrStdout())
}
70 changes: 70 additions & 0 deletions cmd/nerdctl/network/network_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,76 @@ func TestNetworkCreate(t *testing.T) {
}
},
},
{
Description: "with aux-address",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.0.0/24",
"--gateway", "10.6.0.1",
"--aux-address", "router=10.6.0.5",
"--aux-address", "dns=10.6.0.6",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("network", "inspect", data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: expect.ExitCodeSuccess,
Output: func(stdout string, t tig.T) {
netw := nerdtest.InspectNetwork(helpers, data.Identifier())
var aux map[string]string
for _, c := range netw.IPAM.Config {
if c.Subnet == "10.6.0.0/24" {
aux = c.AuxiliaryAddresses
}
}
assert.Equal(t, aux["router"], "10.6.0.5")
assert.Equal(t, aux["dns"], "10.6.0.6")
},
}
},
},
{
Description: "aux-address is reserved",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.1.0/24",
"--aux-address", "reserved=10.6.1.5",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
// The reserved address is carved out of the range, so requesting
// it explicitly must fail just as it does on Docker.
return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.1.5", testutil.CommonImage, "true")
},
Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil),
},
{
Description: "an un-reserved address is allocatable",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.2.0/24",
"--aux-address", "reserved=10.6.2.5",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
// Positive control: an address in the same subnet that is not
// reserved allocates fine, so the failure above is specific to the
// reserved IP rather than an unrelated --ip problem.
return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.2.7", testutil.CommonImage, "true")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
}

testCase.Run(t)
Expand Down
3 changes: 2 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1279,12 +1279,13 @@ Flags:
- :whale: `--subnet`: Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"
- :whale: `--gateway`: IPv4 or IPv6 Gateway for the master subnet
- :whale: `--ip-range`: Allocate container ip from a sub-range
- :whale: `--aux-address`: Auxiliary IPv4 or IPv6 addresses, as `name=IP` pairs. Each IP is reserved and never assigned to a container. Repeatable, and matched to the subnet that contains it.
- :whale: `--label`: Set metadata on a network
- :whale: `--ipv4`: Enable IPv4. Enabled by default; set to false with `--ipv6` and an IPv6 subnet for an IPv6-only network. `--ipv4=false` is not supported on Windows.
- :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet.
- :whale: `--internal`: Restrict external access to the network.

Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--scope`
Unimplemented `docker network create` flags: `--attachable`, `--config-from`, `--config-only`, `--ingress`, `--scope`

### :whale: nerdctl network ls

Expand Down
7 changes: 5 additions & 2 deletions pkg/api/types/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ type NetworkCreateOptions struct {
Subnets []string
Gateway []string
IPRange []string
Labels []string
IPv6 bool
// AuxAddresses holds "name=IP" auxiliary addresses (docker --aux-address).
// Each IP is reserved so IPAM never hands it out to a container.
AuxAddresses []string
Labels []string
IPv6 bool
// IPv4 enables IPv4 on the network. A nil value defaults to enabled, so a
// directly-constructed NetworkCreateOptions keeps IPv4 on; setting it to
// false together with IPv6 yields an IPv6-only network.
Expand Down
10 changes: 10 additions & 0 deletions pkg/cmd/network/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func Create(options types.NetworkCreateOptions, stdout io.Writer) error {
return fmt.Errorf("IPv6-only network requires an IPv6 subnet, specify --subnet manually")
}
if len(options.Subnets) == 0 {
// Docker matches each aux-address to a subnet that contains it, so
// without any subnet there is nothing to match. Surface the same
// "no matching subnet for aux-address <ip>" error Docker returns.
aux, err := netutil.ParseAuxAddresses(options.AuxAddresses)
if err != nil {
return err
}
for _, ip := range aux {
return fmt.Errorf("no matching subnet for aux-address %s", ip)
}
if len(options.Gateway) > 0 || len(options.IPRange) > 0 {
return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually")
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/cmd/network/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
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 network

import (
"io"
"testing"

"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/v2/pkg/api/types"
)

// TestCreateAuxAddressWithoutSubnet verifies that an aux-address given without
// any subnet is rejected the same way Docker rejects it, before any CNI setup.
func TestCreateAuxAddressWithoutSubnet(t *testing.T) {
err := Create(types.NetworkCreateOptions{
AuxAddresses: []string{"host=10.9.0.5"},
}, io.Discard)
assert.ErrorContains(t, err, "no matching subnet for aux-address 10.9.0.5")
}
43 changes: 38 additions & 5 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,9 +1052,10 @@ func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) {
}

type IPAMConfig struct {
Subnet string `json:"Subnet,omitempty"`
Gateway string `json:"Gateway,omitempty"`
IPRange string `json:"IPRange,omitempty"`
Subnet string `json:"Subnet,omitempty"`
Gateway string `json:"Gateway,omitempty"`
IPRange string `json:"IPRange,omitempty"`
AuxiliaryAddresses map[string]string `json:"AuxiliaryAddresses,omitempty"`
}

type IPAM struct {
Expand Down Expand Up @@ -1196,7 +1197,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
res.Name = sCNI.Name
for _, plugin := range sCNI.Plugins {
for _, ranges := range plugin.Ipam.Ranges {
res.IPAM.Config = append(res.IPAM.Config, ranges...)
// A range-set normally describes one subnet; an aux-address
// reservation splits it into several sub-ranges that all share the
// subnet and gateway. Report the first entry per distinct subnet so a
// split subnet collapses to one IPAM.Config like Docker, without
// dropping entries for different subnets in the same set. The
// aux-addresses themselves are attached later from a nerdctl label.
seen := make(map[string]struct{}, len(ranges))
for _, r := range ranges {
if _, ok := seen[r.Subnet]; ok {
continue
}
seen[r.Subnet] = struct{}{}
res.IPAM.Config = append(res.IPAM.Config, r)
}
}
}

Expand All @@ -1205,7 +1219,26 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
}

if n.NerdctlLabels != nil {
res.Labels = *n.NerdctlLabels
// Reserved aux-addresses are stored in a nerdctl label (host-local has no
// field for them). Decode it, attach each subnet's pairs to its config so
// inspect reports AuxiliaryAddresses like Docker, and keep the internal
// label out of the user-visible label set.
res.Labels = make(map[string]string, len(*n.NerdctlLabels))
for k, v := range *n.NerdctlLabels {
if k == labels.NetworkAuxAddresses {
var auxBySubnet map[string]map[string]string
if err := json.Unmarshal([]byte(v), &auxBySubnet); err != nil {
return nil, fmt.Errorf("failed to parse %s label: %w", labels.NetworkAuxAddresses, err)
}
for i := range res.IPAM.Config {
if aux, ok := auxBySubnet[res.IPAM.Config[i].Subnet]; ok {
res.IPAM.Config[i].AuxiliaryAddresses = aux
}
}
continue
}
res.Labels[k] = v
}
}

// Parse network subnets for interface matching
Expand Down
26 changes: 26 additions & 0 deletions pkg/inspecttypes/dockercompat/dockercompat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,32 @@ func TestGetUlimitsFromNative(t *testing.T) {
}
}

func TestNetworkFromNative(t *testing.T) {
// The first range-set is one subnet split into sub-ranges by an aux-address
// reservation and must collapse to a single IPAM.Config; the second set holds
// two distinct subnets that must both be kept; the empty set contributes
// nothing. Aux-addresses live in a nerdctl label (not the CNI config) and must
// be attached to the matching subnet while staying out of the user labels.
cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` +
`[{"Subnet":"10.6.0.0/24","Gateway":"10.6.0.1"},{"Subnet":"10.6.0.0/24"}],` +
`[{"Subnet":"10.7.0.0/24"},{"Subnet":"10.8.0.0/24"}],` +
`[]` +
`]}}]}`
lbls := map[string]string{
labels.NetworkAuxAddresses: `{"10.6.0.0/24":{"router":"10.6.0.5"}}`,
"user": "keep",
}
got, err := NetworkFromNative(&native.Network{CNI: []byte(cni), NerdctlLabels: &lbls})
assert.NilError(t, err)
assert.DeepEqual(t, []IPAMConfig{
{Subnet: "10.6.0.0/24", Gateway: "10.6.0.1", AuxiliaryAddresses: map[string]string{"router": "10.6.0.5"}},
{Subnet: "10.7.0.0/24"},
{Subnet: "10.8.0.0/24"},
}, got.IPAM.Config)
// The internal aux label is hidden; genuine user labels are preserved.
assert.DeepEqual(t, map[string]string{"user": "keep"}, got.Labels)
}

func TestNetworkSettingsFromNative(t *testing.T) {
tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw")
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ const (
// (like "nerdctl/default-network=true" or "nerdctl/default-network=false")
NerdctlDefaultNetwork = Prefix + "default-network"

// NetworkAuxAddresses stores a network's reserved --aux-address name=IP
// pairs, grouped per subnet, as a JSON object (map[subnetCIDR]map[name]IP).
// host-local has no field for auxiliary addresses, so they are kept here
// instead of in the CNI config and read back by `network inspect` to report
// AuxiliaryAddresses like Docker.
NetworkAuxAddresses = Prefix + "network-aux-addresses"

// ContainerAutoRemove is to check whether the --rm option is specified.
ContainerAutoRemove = Prefix + "auto-remove"

Expand Down
Loading
Loading