diff --git a/cmd/dockerd/config.go b/cmd/dockerd/config.go index be332626ae..a90c1bbee6 100644 --- a/cmd/dockerd/config.go +++ b/cmd/dockerd/config.go @@ -27,6 +27,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error { flags.StringVar(&conf.ContainerdAddr, "containerd", "", "containerd grpc address") flags.BoolVar(&conf.CriContainerd, "cri-containerd", false, "start containerd with cri") + flags.Var(opts.NewNamedMapMapOpts("default-network-opts", conf.DefaultNetworkOpts, nil), "default-network-opt", "Default network options") flags.IntVar(&conf.Mtu, "mtu", conf.Mtu, "Set the containers network MTU") flags.IntVar(&conf.NetworkControlPlaneMTU, "network-control-plane-mtu", conf.NetworkControlPlaneMTU, "Network Control plane MTU") flags.IntVar(&conf.NetworkDiagnosticPort, "network-diagnostic-port", 0, "TCP port number of the network diagnostic server") diff --git a/daemon/config/config.go b/daemon/config/config.go index 42f7add76d..60b215227a 100644 --- a/daemon/config/config.go +++ b/daemon/config/config.go @@ -126,6 +126,8 @@ type NetworkConfig struct { DefaultAddressPools opts.PoolsOpt `json:"default-address-pools,omitempty"` // NetworkControlPlaneMTU allows to specify the control plane MTU, this will allow to optimize the network use in some components NetworkControlPlaneMTU int `json:"network-control-plane-mtu,omitempty"` + // Default options for newly created networks + DefaultNetworkOpts map[string]map[string]string `json:"default-network-opts,omitempty"` } // TLSOptions defines TLS configuration for the daemon server. @@ -289,6 +291,7 @@ func New() (*Config, error) { Mtu: DefaultNetworkMtu, NetworkConfig: NetworkConfig{ NetworkControlPlaneMTU: DefaultNetworkMtu, + DefaultNetworkOpts: make(map[string]map[string]string), }, ContainerdNamespace: DefaultContainersNamespace, ContainerdPluginNamespace: DefaultPluginNamespace, diff --git a/daemon/network.go b/daemon/network.go index 5b54d5d296..3bf8852d4f 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -315,9 +315,22 @@ func (daemon *Daemon) createNetwork(create types.NetworkCreateRequest, id string driver = c.Config().DefaultDriver } + networkOptions := make(map[string]string) + for k, v := range create.Options { + networkOptions[k] = v + } + if defaultOpts, ok := daemon.configStore.DefaultNetworkOpts[driver]; create.ConfigFrom == nil && ok { + for k, v := range defaultOpts { + if _, ok := networkOptions[k]; !ok { + logrus.WithFields(logrus.Fields{"driver": driver, "network": id, k: v}).Debug("Applying network default option") + networkOptions[k] = v + } + } + } + nwOptions := []libnetwork.NetworkOption{ libnetwork.NetworkOptionEnableIPv6(create.EnableIPv6), - libnetwork.NetworkOptionDriverOpts(create.Options), + libnetwork.NetworkOptionDriverOpts(networkOptions), libnetwork.NetworkOptionLabels(create.Labels), libnetwork.NetworkOptionAttachable(create.Attachable), libnetwork.NetworkOptionIngress(create.Ingress), diff --git a/integration/network/network_test.go b/integration/network/network_test.go index 00603094a2..bfc6e2998e 100644 --- a/integration/network/network_test.go +++ b/integration/network/network_test.go @@ -4,12 +4,14 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "os/exec" "strings" "testing" "github.com/docker/docker/api/types" + ntypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/integration/internal/network" "github.com/docker/docker/testutil/daemon" @@ -175,3 +177,75 @@ func TestHostIPv4BridgeLabel(t *testing.T) { // Make sure the SNAT rule exists icmd.RunCommand("iptables", "-t", "nat", "-C", "POSTROUTING", "-s", out.IPAM.Config[0].Subnet, "!", "-o", bridgeName, "-j", "SNAT", "--to-source", ipv4SNATAddr).Assert(t, icmd.Success) } + +func TestDefaultNetworkOpts(t *testing.T) { + skip.If(t, testEnv.OSType == "windows") + skip.If(t, testEnv.IsRemoteDaemon) + skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") + + tests := []struct { + name string + mtu int + configFrom bool + args []string + }{ + { + name: "default value", + mtu: 1500, + args: []string{}, + }, + { + name: "cmdline value", + mtu: 1234, + args: []string{"--default-network-opt", "bridge=com.docker.network.driver.mtu=1234"}, + }, + { + name: "config-from value", + configFrom: true, + mtu: 1233, + args: []string{"--default-network-opt", "bridge=com.docker.network.driver.mtu=1234"}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + d := daemon.New(t) + d.StartWithBusybox(t, tc.args...) + defer d.Stop(t) + c := d.NewClientT(t) + defer c.Close() + ctx := context.Background() + + if tc.configFrom { + // Create a new network config + network.CreateNoError(ctx, t, c, "from-net", func(create *types.NetworkCreate) { + create.ConfigOnly = true + create.Options = map[string]string{ + "com.docker.network.driver.mtu": fmt.Sprint(tc.mtu), + } + }) + defer c.NetworkRemove(ctx, "from-net") + } + + // Create a new network + networkName := "testnet" + network.CreateNoError(ctx, t, c, networkName, func(create *types.NetworkCreate) { + if tc.configFrom { + create.ConfigFrom = &ntypes.ConfigReference{ + Network: "from-net", + } + } + }) + defer c.NetworkRemove(ctx, networkName) + + // Start a container to inspect the MTU of its network interface + id1 := container.Run(ctx, t, c, container.WithNetworkMode(networkName)) + defer c.ContainerRemove(ctx, id1, types.ContainerRemoveOptions{Force: true}) + + result, err := container.Exec(ctx, c, id1, []string{"ip", "l", "show", "eth0"}) + assert.NilError(t, err) + assert.Check(t, is.Contains(result.Combined(), fmt.Sprintf(" mtu %d ", tc.mtu)), "Network MTU should have been set to %d", tc.mtu) + }) + } +} diff --git a/opts/opts.go b/opts/opts.go index aacd30af08..dbcd028525 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -146,6 +146,83 @@ func (o *NamedListOpts) Name() string { return o.name } +// NamedMapMapOpts is a MapMapOpts with a configuration name. +// This struct is useful to keep reference to the assigned +// field name in the internal configuration struct. +type NamedMapMapOpts struct { + name string + MapMapOpts +} + +// NewNamedMapMapOpts creates a reference to a new NamedMapOpts struct. +func NewNamedMapMapOpts(name string, values map[string]map[string]string, validator ValidatorFctType) *NamedMapMapOpts { + return &NamedMapMapOpts{ + name: name, + MapMapOpts: *NewMapMapOpts(values, validator), + } +} + +// Name returns the name of the NamedListOpts in the configuration. +func (o *NamedMapMapOpts) Name() string { + return o.name +} + +// MapMapOpts holds a map of maps of values and a validation function. +type MapMapOpts struct { + values map[string]map[string]string + validator ValidatorFctType +} + +// Set validates if needed the input value and add it to the +// internal map, by splitting on '='. +func (opts *MapMapOpts) Set(value string) error { + if opts.validator != nil { + v, err := opts.validator(value) + if err != nil { + return err + } + value = v + } + rk, rv, found := strings.Cut(value, "=") + if !found { + return fmt.Errorf("invalid value %q for map option, should be root-key=key=value", value) + } + k, v, found := strings.Cut(rv, "=") + if !found { + return fmt.Errorf("invalid value %q for map option, should be root-key=key=value", value) + } + if _, ok := opts.values[rk]; !ok { + opts.values[rk] = make(map[string]string) + } + opts.values[rk][k] = v + return nil +} + +// GetAll returns the values of MapOpts as a map. +func (opts *MapMapOpts) GetAll() map[string]map[string]string { + return opts.values +} + +func (opts *MapMapOpts) String() string { + return fmt.Sprintf("%v", opts.values) +} + +// Type returns a string name for this Option type +func (opts *MapMapOpts) Type() string { + return "mapmap" +} + +// NewMapMapOpts creates a new MapMapOpts with the specified map of values and a validator. +func NewMapMapOpts(values map[string]map[string]string, validator ValidatorFctType) *MapMapOpts { + if values == nil { + values = make(map[string]map[string]string) + } + return &MapMapOpts{ + values: values, + validator: validator, + } +} + // MapOpts holds a map of values and a validation function. type MapOpts struct { values map[string]string diff --git a/opts/opts_test.go b/opts/opts_test.go index 850618e320..fe4b7f5ce5 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -334,3 +334,34 @@ func TestParseLink(t *testing.T) { assert.Check(t, is.Equal(alias, "bar")) }) } + +func TestMapMapOpts(t *testing.T) { + tmpMap := make(map[string]map[string]string) + validator := func(val string) (string, error) { + if strings.HasPrefix(val, "invalid-key=") { + return "", fmt.Errorf("invalid key %s", val) + } + return val, nil + } + o := NewMapMapOpts(tmpMap, validator) + o.Set("r1=k11=v11") + assert.Check(t, is.DeepEqual(tmpMap, map[string]map[string]string{"r1": {"k11": "v11"}})) + + o.Set("r2=k21=v21") + assert.Check(t, is.Len(tmpMap, 2)) + + if err := o.Set("invalid-syntax"); err == nil { + t.Error("invalid mapping syntax is not being caught") + } + + if err := o.Set("k=invalid-syntax"); err == nil { + t.Error("invalid value syntax is not being caught") + } + + o.Set("r1=k12=v12") + assert.Check(t, is.DeepEqual(tmpMap["r1"], map[string]string{"k11": "v11", "k12": "v12"})) + + if o.Set("invalid-key={\"k\":\"v\"}") == nil { + t.Error("validator is not being called") + } +}