Skip to content

Commit

Permalink
Merge pull request moby#48807 from robmry/v6only/host_gateway_ip
Browse files Browse the repository at this point in the history
IPv6 only: Allow IPv4 and IPv6 host-gateway-ip addresses
  • Loading branch information
thaJeztah authored Nov 27, 2024
2 parents d58e56e + c9a1e4d commit c565f74
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 214 deletions.
20 changes: 10 additions & 10 deletions builder/builder-next/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -354,7 +354,7 @@ func (b *Builder) Build(ctx context.Context, opt backend.BuildConfig) (*builder.
return nil, errors.Errorf("network mode %q not supported by buildkit", opt.Options.NetworkMode)
}

extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts, b.dnsconfig.HostGatewayIP)
extraHosts, err := toBuildkitExtraHosts(opt.Options.ExtraHosts, b.dnsconfig.HostGatewayIPs)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -600,7 +600,7 @@ func (j *buildJob) SetUpload(ctx context.Context, rc io.ReadCloser) error {
}

// toBuildkitExtraHosts converts hosts from docker key:value format to buildkit's csv format
func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) {
func toBuildkitExtraHosts(inp []string, hostGatewayIPs []netip.Addr) (string, error) {
if len(inp) == 0 {
return "", nil
}
Expand All @@ -611,17 +611,17 @@ func toBuildkitExtraHosts(inp []string, hostGatewayIP net.IP) (string, error) {
return "", errors.Errorf("invalid host %s", h)
}
// If the IP Address is a "host-gateway", replace this value with the
// IP address stored in the daemon level HostGatewayIP config variable.
// IP address(es) stored in the daemon level HostGatewayIPs config variable.
if ip == opts.HostGatewayName {
gateway := hostGatewayIP.String()
if gateway == "" {
if len(hostGatewayIPs) == 0 {
return "", fmt.Errorf("unable to derive the IP value for host-gateway")
}
ip = gateway
} else if net.ParseIP(ip) == nil {
return "", fmt.Errorf("invalid host %s", h)
for _, gip := range hostGatewayIPs {
hosts = append(hosts, host+"="+gip.String())
}
} else {
hosts = append(hosts, host+"="+ip)
}
hosts = append(hosts, host+"="+ip)
}
return strings.Join(hosts, ","), nil
}
Expand Down
12 changes: 11 additions & 1 deletion builder/builder-next/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,16 @@ func getLabels(opt Opt, labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
labels[wlabel.HostGatewayIP] = opt.DNSConfig.HostGatewayIP.String()
if len(opt.DNSConfig.HostGatewayIPs) > 0 {
// TODO(robmry) - buildx has its own version of toBuildkitExtraHosts(), which
// needs to be updated to understand >1 address. For now, take the IPv4 address
// if there is one, else IPv6.
for _, gip := range opt.DNSConfig.HostGatewayIPs {
labels[wlabel.HostGatewayIP] = gip.String()
if gip.Is4() {
break
}
}
}
return labels
}
2 changes: 1 addition & 1 deletion cmd/dockerd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
flags.IPSliceVar(&conf.DNS, "dns", conf.DNS, "DNS server to use")
flags.Var(opts.NewNamedListOptsRef("dns-opts", &conf.DNSOptions, nil), "dns-opt", "DNS options to use")
flags.Var(opts.NewListOptsRef(&conf.DNSSearch, opts.ValidateDNSSearch), "dns-search", "DNS search domains to use")
flags.IPVar(&conf.HostGatewayIP, "host-gateway-ip", nil, "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the default bridge")
flags.Var(dopts.NewNamedIPListOptsRef("host-gateway-ips", &conf.HostGatewayIPs), "host-gateway-ip", "IP addresses that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP addresses of the default bridge")
flags.Var(opts.NewNamedListOptsRef("labels", &conf.Labels, opts.ValidateLabel), "label", "Set key=value labels to the daemon")
flags.StringVar(&conf.LogConfig.Type, "log-driver", "json-file", "Default driver for container logs")
flags.Var(opts.NewNamedMapOpts("log-opts", conf.LogConfig.Config, nil), "log-opt", "Default log driver options for containers")
Expand Down
78 changes: 67 additions & 11 deletions daemon/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"bytes"
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net"
"net/netip"
"net/url"
"os"
"strings"
Expand All @@ -14,6 +16,7 @@ import (
"github.com/containerd/log"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types/versions"
dopts "github.com/docker/docker/internal/opts"
"github.com/docker/docker/opts"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
Expand Down Expand Up @@ -103,6 +106,24 @@ var skipDuplicates = map[string]bool{
"runtimes": true,
}

// migratedNamedConfig describes legacy configuration file keys that have been migrated
// from simple entries equivalent to command line flags, to a named option.
//
// For example, "host-gateway-ip" allowed for a single IP address. "host-gateway-ips"
// allows for an IPv4 and an IPv6 address, and is implemented as a NamedOption for
// command line flag "--host-gateway-ip".
//
// Each legacy name is mapped to its new name and a function that can be called to
// migrate config from one to the other. The migration function is only called after
// confirming that the option is only specified in one of the new, old or command
// line options.
var migratedNamedConfig = map[string]struct {
newName string
migrate func(*Config)
}{
"host-gateway-ip": {newName: "host-gateway-ips", migrate: migrateHostGatewayIP},
}

// LogConfig represents the default log configuration.
// It includes json tags to deserialize configuration from a file
// using the same names that the flags in the command line use.
Expand Down Expand Up @@ -139,10 +160,11 @@ type TLSOptions struct {

// DNSConfig defines the DNS configurations.
type DNSConfig struct {
DNS []net.IP `json:"dns,omitempty"`
DNSOptions []string `json:"dns-opts,omitempty"`
DNSSearch []string `json:"dns-search,omitempty"`
HostGatewayIP net.IP `json:"host-gateway-ip,omitempty"`
DNS []net.IP `json:"dns,omitempty"`
DNSOptions []string `json:"dns-opts,omitempty"`
DNSSearch []string `json:"dns-search,omitempty"`
HostGatewayIP net.IP `json:"host-gateway-ip,omitempty"` // Deprecated: this single-IP is migrated to HostGatewayIPs
HostGatewayIPs []netip.Addr `json:"host-gateway-ips,omitempty"`
}

// CommonConfig defines the configuration of a docker daemon which is
Expand Down Expand Up @@ -518,6 +540,10 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con
return nil, err
}

for _, mc := range migratedNamedConfig {
mc.migrate(&config)
}

return &config, nil
}

Expand Down Expand Up @@ -568,7 +594,7 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag
return errors.Errorf("the following directives don't match any configuration option: %s", strings.Join(unknown, ", "))
}

var conflicts []string
// 3. Search keys that are present as a flag and as a file option.
printConflict := func(name string, flagValue, fileValue interface{}) string {
switch name {
case "http-proxy", "https-proxy":
Expand All @@ -578,8 +604,8 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag
return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
}

// 3. Search keys that are present as a flag and as a file option.
duplicatedConflicts := func(f *pflag.Flag) {
var conflicts []string
flags.Visit(func(f *pflag.Flag) {
// search option name in the json configuration payload if the value is a named option
if namedOption, ok := f.Value.(opts.NamedOption); ok {
if optsValue, ok := config[namedOption.Name()]; ok && !skipDuplicates[namedOption.Name()] {
Expand All @@ -594,14 +620,30 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag
}
}
}
})

// 4. Search for options that have been migrated to a NamedOption. These must not
// be specified using both old and new config file names, or using the original
// config file name and on the command line. (Or using the new config file name
// and the command line, but those have already been found by the search above.)
var errs []error
for oldName, migration := range migratedNamedConfig {
oldNameVal, haveOld := config[oldName]
_, haveNew := config[migration.newName]
if haveOld {
if haveNew {
errs = append(errs, fmt.Errorf("%s and %s must not both be specified in the config file", oldName, migration.newName))
}
if f := flags.Lookup(oldName); f != nil && f.Changed {
conflicts = append(conflicts, printConflict(oldName, f.Value.String(), oldNameVal))
}
}
}

flags.Visit(duplicatedConflicts)

if len(conflicts) > 0 {
return errors.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))
errs = append(errs, errors.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", ")))
}
return nil
return stderrors.Join(errs...)
}

// ValidateMinAPIVersion verifies if the given API version is within the
Expand Down Expand Up @@ -656,6 +698,11 @@ func Validate(config *Config) error {
}
}

// validate HostGatewayIPs
if err := dopts.ValidateHostGatewayIPs(config.HostGatewayIPs); err != nil {
return err
}

// validate Labels
for _, label := range config.Labels {
if _, err := opts.ValidateLabel(label); err != nil {
Expand Down Expand Up @@ -705,3 +752,12 @@ func MaskCredentials(rawURL string) string {
parsedURL.User = url.UserPassword("xxxxx", "xxxxx")
return parsedURL.String()
}

func migrateHostGatewayIP(config *Config) {
hgip := config.HostGatewayIP //nolint:staticcheck // ignore SA1019: migrating to HostGatewayIPs.
if hgip != nil {
addr, _ := netip.AddrFromSlice(hgip)
config.HostGatewayIPs = []netip.Addr{addr}
config.HostGatewayIP = nil //nolint:staticcheck // ignore SA1019: clearing old value.
}
}
146 changes: 146 additions & 0 deletions daemon/config/config_linux_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package config // import "github.com/docker/docker/daemon/config"

import (
"net/netip"
"testing"

"github.com/docker/docker/api/types/container"
dopts "github.com/docker/docker/internal/opts"
"github.com/docker/docker/opts"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
Expand Down Expand Up @@ -219,3 +221,147 @@ func TestUnixGetInitPath(t *testing.T) {
assert.Equal(t, tc.config.GetInitPath(), tc.expectedInitPath)
}
}

func TestDaemonConfigurationHostGatewayIP(t *testing.T) {
tests := []struct {
name string
config string
flags []string
expVal []string
expSetErr string
expErr string
}{
{
name: "flag IPv4 only",
config: `{}`,
flags: []string{"192.0.2.1"},
expVal: []string{"192.0.2.1"},
},
{
name: "flag IPv6 only",
config: `{}`,
flags: []string{"2001:db8::1234"},
expVal: []string{"2001:db8::1234"},
},
{
name: "flag IPv4 and IPv6",
config: `{}`,
flags: []string{"2001:db8::1234", "192.0.2.1"},
expVal: []string{"2001:db8::1234", "192.0.2.1"},
},
{
name: "flag two IPv4",
config: `{}`,
flags: []string{"192.0.2.1", "192.0.2.2"},
expErr: "merged configuration validation from file and command line flags failed: only one IPv4 host gateway IP address can be specified",
},
{
name: "flag two IPv6",
config: `{}`,
flags: []string{"2001:db8::1234", "2001:db8::5678"},
expErr: "merged configuration validation from file and command line flags failed: only one IPv6 host gateway IP address can be specified",
},
{
name: "legacy config",
config: `{"host-gateway-ip": "2001:db8::1234"}`,
expVal: []string{"2001:db8::1234"},
},
{
name: "config ipv4",
config: `{"host-gateway-ips": ["192.0.2.1"]}`,
expVal: []string{"192.0.2.1"},
},
{
name: "config ipv6",
config: `{"host-gateway-ips": ["2001:db8::1234"]}`,
expVal: []string{"2001:db8::1234"},
},
{
name: "config ipv4 and ipv6",
config: `{"host-gateway-ips": ["2001:db8::1234", "192.0.2.1"]}`,
expVal: []string{"2001:db8::1234", "192.0.2.1"},
},
{
name: "config two ipv4",
config: `{"host-gateway-ips": ["192.0.2.1", "192.0.2.2"]}`,
expErr: "merged configuration validation from file and command line flags failed: only one IPv4 host gateway IP address can be specified",
},
{
name: "config two ipv6",
config: `{"host-gateway-ips": ["2001:db8::1234", "2001:db8::5678"]}`,
expErr: "merged configuration validation from file and command line flags failed: only one IPv6 host gateway IP address can be specified",
},
{
name: "flag bad address",
flags: []string{"hello"},
expSetErr: `invalid argument "hello" for "--host-gateway-ip" flag: ParseAddr("hello"): unable to parse IP`,
},
{
name: "config bad address",
config: `{"host-gateway-ips": ["hello"]}`,
expErr: `ParseAddr("hello"): unable to parse IP`,
},
{
name: "config not array",
config: `{"host-gateway-ips": "192.0.2.1"}`,
expErr: `json: cannot unmarshal string into Go struct field Config.host-gateway-ips of type []netip.Addr`,
},
{
name: "config old and new",
config: `{"host-gateway-ip": "192.0.2.1", "host-gateway-ips": ["192.0.2.1"]}`,
expErr: "host-gateway-ip and host-gateway-ips must not both be specified in the config file",
},
{
name: "config old and flag",
flags: []string{"192.0.2.1"},
config: `{"host-gateway-ip": "192.0.2.2"}`,
expErr: "the following directives are specified both as a flag and in the configuration file: host-gateway-ip: (from flag: [192.0.2.1], from file: 192.0.2.2)",
},
{
name: "config new and flag",
flags: []string{"192.0.2.1"},
config: `{"host-gateway-ips": ["192.0.2.2", "2001:db8::1234"]}`,
expErr: "the following directives are specified both as a flag and in the configuration file: host-gateway-ips: (from flag: [192.0.2.1], from file: [192.0.2.2 2001:db8::1234])",
},
{
name: "config new and old and flag",
flags: []string{"192.0.2.1"},
config: `{"host-gateway-ip": "192.0.2.2", "host-gateway-ips": ["192.0.2.3"]}`,
expErr: "host-gateway-ip and host-gateway-ips must not both be specified in the config file\n" +
"the following directives are specified both as a flag and in the configuration file: host-gateway-ips: (from flag: [192.0.2.1], from file: [192.0.2.3]), host-gateway-ip: (from flag: [192.0.2.1], from file: 192.0.2.2)",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
c, err := New()
assert.NilError(t, err)

configFile := makeConfigFile(t, tc.config)
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags.Var(dopts.NewNamedIPListOptsRef("host-gateway-ips", &c.HostGatewayIPs),
"host-gateway-ip", "a usage message")
for _, flagVal := range tc.flags {
err := flags.Set("host-gateway-ip", flagVal)
if tc.expSetErr != "" {
assert.Check(t, is.Error(err, tc.expSetErr))
return
}
assert.NilError(t, err)
}
cc, err := MergeDaemonConfigurations(c, flags, configFile)
if tc.expErr != "" {
assert.Check(t, is.Error(err, tc.expErr))
assert.Check(t, is.Nil(cc))
} else {
assert.NilError(t, err)
var expVal []netip.Addr
for _, ev := range tc.expVal {
expVal = append(expVal, netip.MustParseAddr(ev))
}
assert.Check(t, is.DeepEqual(cc.HostGatewayIPs, expVal, cmpopts.EquateComparable(netip.Addr{})))
assert.Check(t, is.Nil(cc.HostGatewayIP)) //nolint:staticcheck // ignore SA1019: deprecated field should be nil
}
})
}
}
Loading

0 comments on commit c565f74

Please sign in to comment.