mirror of https://github.com/docker/cli.git
Permit '=' separator and '[ipv6]' in --add-host
Fixes #4648 Make it easier to specify IPv6 addresses in the '--add-host' option by permitting 'host=ip' in addition to 'host:ip', and allowing square brackets around the address. For example: --add-host=my-hostname:127.0.0.1 --add-host=my-hostname:::1 --add-host=my-hostname=::1 --add-host=my-hostname:[::1] To avoid compatibility problems, the CLI will replace an '=' separator with ':', and strip brackets, before sending the request to the API. Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
parent
af23916995
commit
a682b8e655
|
@ -454,12 +454,12 @@ Specifying the `--isolation` flag without a value is the same as setting `--isol
|
||||||
|
|
||||||
### <a name="add-host"></a> Add entries to container hosts file (--add-host)
|
### <a name="add-host"></a> Add entries to container hosts file (--add-host)
|
||||||
|
|
||||||
You can add other hosts into a container's `/etc/hosts` file by using one or
|
You can add other hosts into a build container's `/etc/hosts` file by using one
|
||||||
more `--add-host` flags. This example adds a static address for a host named
|
or more `--add-host` flags. This example adds static addresses for hosts named
|
||||||
`docker`:
|
`my-hostname` and `my_hostname_v6`:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker build --add-host docker:10.180.0.1 .
|
$ docker build --add-host my_hostname=8.8.8.8 --add-host my_hostname_v6=2001:4860:4860::8888 .
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need your build to connect to services running on the host, you can use
|
If you need your build to connect to services running on the host, you can use
|
||||||
|
@ -467,7 +467,15 @@ the special `host-gateway` value for `--add-host`. In the following example,
|
||||||
build containers resolve `host.docker.internal` to the host's gateway IP.
|
build containers resolve `host.docker.internal` to the host's gateway IP.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker build --add-host host.docker.internal:host-gateway .
|
$ docker build --add-host host.docker.internal=host-gateway .
|
||||||
|
```
|
||||||
|
|
||||||
|
You can wrap an IPv6 address in square brackets.
|
||||||
|
`=` and `:` are both valid separators.
|
||||||
|
Both formats in the following example are valid:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker build --add-host my-hostname:10.180.0.1 --add-host my-hostname_v6=[2001:4860:4860::8888] .
|
||||||
```
|
```
|
||||||
|
|
||||||
### <a name="target"></a> Specifying target build stage (--target)
|
### <a name="target"></a> Specifying target build stage (--target)
|
||||||
|
|
|
@ -746,22 +746,28 @@ section of the Docker run reference page.
|
||||||
|
|
||||||
You can add other hosts into a container's `/etc/hosts` file by using one or
|
You can add other hosts into a container's `/etc/hosts` file by using one or
|
||||||
more `--add-host` flags. This example adds a static address for a host named
|
more `--add-host` flags. This example adds a static address for a host named
|
||||||
`docker`:
|
`my-hostname`:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker run --add-host=docker:93.184.216.34 --rm -it alpine
|
$ docker run --add-host=my-hostname=8.8.8.8 --rm -it alpine
|
||||||
|
|
||||||
/ # ping docker
|
/ # ping my-hostname
|
||||||
PING docker (93.184.216.34): 56 data bytes
|
PING my-hostname (8.8.8.8): 56 data bytes
|
||||||
64 bytes from 93.184.216.34: seq=0 ttl=37 time=93.052 ms
|
64 bytes from 8.8.8.8: seq=0 ttl=37 time=93.052 ms
|
||||||
64 bytes from 93.184.216.34: seq=1 ttl=37 time=92.467 ms
|
64 bytes from 8.8.8.8: seq=1 ttl=37 time=92.467 ms
|
||||||
64 bytes from 93.184.216.34: seq=2 ttl=37 time=92.252 ms
|
64 bytes from 8.8.8.8: seq=2 ttl=37 time=92.252 ms
|
||||||
^C
|
^C
|
||||||
--- docker ping statistics ---
|
--- my-hostname ping statistics ---
|
||||||
4 packets transmitted, 4 packets received, 0% packet loss
|
4 packets transmitted, 4 packets received, 0% packet loss
|
||||||
round-trip min/avg/max = 92.209/92.495/93.052 ms
|
round-trip min/avg/max = 92.209/92.495/93.052 ms
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can wrap an IPv6 address in square brackets:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker run --add-host my-hostname=[2001:db8::33] --rm -it alpine
|
||||||
|
```
|
||||||
|
|
||||||
The `--add-host` flag supports a special `host-gateway` value that resolves to
|
The `--add-host` flag supports a special `host-gateway` value that resolves to
|
||||||
the internal IP address of the host. This is useful when you want containers to
|
the internal IP address of the host. This is useful when you want containers to
|
||||||
connect to services running on the host machine.
|
connect to services running on the host machine.
|
||||||
|
@ -779,11 +785,17 @@ $ echo "hello from host!" > ./hello
|
||||||
$ python3 -m http.server 8000
|
$ python3 -m http.server 8000
|
||||||
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
|
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
|
||||||
$ docker run \
|
$ docker run \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal=host-gateway \
|
||||||
curlimages/curl -s host.docker.internal:8000/hello
|
curlimages/curl -s host.docker.internal:8000/hello
|
||||||
hello from host!
|
hello from host!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `--add-host` flag also accepts a `:` separator, for example:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker run --add-host=my-hostname:8.8.8.8 --rm -it alpine
|
||||||
|
```
|
||||||
|
|
||||||
### <a name="ulimit"></a> Set ulimits in container (--ulimit)
|
### <a name="ulimit"></a> Set ulimits in container (--ulimit)
|
||||||
|
|
||||||
Since setting `ulimit` settings in a container requires extra privileges not
|
Since setting `ulimit` settings in a container requires extra privileges not
|
||||||
|
|
|
@ -78,10 +78,10 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
|
||||||
layers in tact, and one for the squashed version.
|
layers in tact, and one for the squashed version.
|
||||||
|
|
||||||
**--add-host** []
|
**--add-host** []
|
||||||
Add a custom host-to-IP mapping (host:ip)
|
Add a custom host-to-IP mapping (host=ip, or host:ip)
|
||||||
|
|
||||||
Add a line to /etc/hosts. The format is hostname:ip. The **--add-host**
|
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
|
||||||
option can be set multiple times.
|
The **--add-host** option can be set multiple times.
|
||||||
|
|
||||||
**--build-arg** *variable*
|
**--build-arg** *variable*
|
||||||
name and value of a **buildarg**.
|
name and value of a **buildarg**.
|
||||||
|
|
|
@ -121,10 +121,10 @@ executables expect) and pass along signals. The **-a** option can be set for
|
||||||
each of stdin, stdout, and stderr.
|
each of stdin, stdout, and stderr.
|
||||||
|
|
||||||
**--add-host**=[]
|
**--add-host**=[]
|
||||||
Add a custom host-to-IP mapping (host:ip)
|
Add a custom host-to-IP mapping (host=ip, or host:ip)
|
||||||
|
|
||||||
Add a line to /etc/hosts. The format is hostname:ip. The **--add-host**
|
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
|
||||||
option can be set multiple times.
|
The **--add-host** option can be set multiple times.
|
||||||
|
|
||||||
**--annotation**=[]
|
**--annotation**=[]
|
||||||
Add an annotation to the container (passed through to the OCI runtime).
|
Add an annotation to the container (passed through to the OCI runtime).
|
||||||
|
|
|
@ -161,21 +161,53 @@ func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
|
||||||
return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil
|
return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateExtraHost validates that the specified string is a valid extrahost and returns it.
|
// ValidateExtraHost validates that the specified string is a valid extrahost and
|
||||||
// ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6).
|
// returns it. ExtraHost is in the form of name:ip or name=ip, where the ip has
|
||||||
|
// to be a valid ip (IPv4 or IPv6). The address may be enclosed in square
|
||||||
|
// brackets.
|
||||||
//
|
//
|
||||||
// TODO(thaJeztah): remove client-side validation, and delegate to the API server.
|
// For example:
|
||||||
|
//
|
||||||
|
// my-hostname:127.0.0.1
|
||||||
|
// my-hostname:::1
|
||||||
|
// my-hostname=::1
|
||||||
|
// my-hostname:[::1]
|
||||||
|
//
|
||||||
|
// For compatibility with the API server, this function normalises the given
|
||||||
|
// argument to use the ':' separator and strip square brackets enclosing the
|
||||||
|
// address.
|
||||||
func ValidateExtraHost(val string) (string, error) {
|
func ValidateExtraHost(val string) (string, error) {
|
||||||
|
k, v, ok := strings.Cut(val, "=")
|
||||||
|
if !ok {
|
||||||
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
|
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
|
||||||
k, v, ok := strings.Cut(val, ":")
|
k, v, ok = strings.Cut(val, ":")
|
||||||
if !ok || k == "" {
|
}
|
||||||
|
// Check that a hostname was given, and that it doesn't contain a ":". (Colon
|
||||||
|
// isn't allowed in a hostname, along with many other characters. It's
|
||||||
|
// special-cased here because the API server doesn't know about '=' separators in
|
||||||
|
// '--add-host'. So, it'll split at the first colon and generate a strange error
|
||||||
|
// message.)
|
||||||
|
if !ok || k == "" || strings.Contains(k, ":") {
|
||||||
return "", fmt.Errorf("bad format for add-host: %q", val)
|
return "", fmt.Errorf("bad format for add-host: %q", val)
|
||||||
}
|
}
|
||||||
// Skip IPaddr validation for "host-gateway" string
|
// Skip IPaddr validation for "host-gateway" string
|
||||||
if v != hostGatewayName {
|
if v != hostGatewayName {
|
||||||
|
// If the address is enclosed in square brackets, extract it (for IPv6, but
|
||||||
|
// permit it for IPv4 as well; we don't know the address family here, but it's
|
||||||
|
// unambiguous).
|
||||||
|
if len(v) > 2 && v[0] == '[' && v[len(v)-1] == ']' {
|
||||||
|
v = v[1 : len(v)-1]
|
||||||
|
}
|
||||||
|
// ValidateIPAddress returns the address in canonical form (for example,
|
||||||
|
// 0:0:0:0:0:0:0:1 -> ::1). But, stick with the original form, to avoid
|
||||||
|
// surprising a user who's expecting to see the address they supplied in the
|
||||||
|
// output of 'docker inspect' or '/etc/hosts'.
|
||||||
if _, err := ValidateIPAddress(v); err != nil {
|
if _, err := ValidateIPAddress(v); err != nil {
|
||||||
return "", fmt.Errorf("invalid IP address in add-host: %q", v)
|
return "", fmt.Errorf("invalid IP address in add-host: %q", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return val, nil
|
// This result is passed directly to the API, the daemon doesn't accept the '='
|
||||||
|
// separator or an address enclosed in brackets. So, construct something it can
|
||||||
|
// understand.
|
||||||
|
return k + ":" + v, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ package opts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseHost(t *testing.T) {
|
func TestParseHost(t *testing.T) {
|
||||||
|
@ -146,32 +148,152 @@ func TestParseInvalidUnixAddrInvalid(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateExtraHosts(t *testing.T) {
|
func TestValidateExtraHosts(t *testing.T) {
|
||||||
valid := []string{
|
tests := []struct {
|
||||||
`myhost:192.168.0.1`,
|
doc string
|
||||||
`thathost:10.0.2.1`,
|
input string
|
||||||
`anipv6host:2003:ab34:e::1`,
|
expectedOut string // Expect output==input if not set.
|
||||||
`ipv6local:::1`,
|
expectedErr string // Expect success if not set.
|
||||||
`host.docker.internal:host-gateway`,
|
}{
|
||||||
|
{
|
||||||
|
doc: "IPv4, colon sep",
|
||||||
|
input: `myhost:192.168.0.1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv4, eq sep",
|
||||||
|
input: `myhost=192.168.0.1`,
|
||||||
|
expectedOut: `myhost:192.168.0.1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Weird but permitted, IPv4 with brackets",
|
||||||
|
input: `myhost=[192.168.0.1]`,
|
||||||
|
expectedOut: `myhost:192.168.0.1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Host and domain",
|
||||||
|
input: `host.and.domain.invalid:10.0.2.1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6, colon sep",
|
||||||
|
input: `anipv6host:2003:ab34:e::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6, colon sep, brackets",
|
||||||
|
input: `anipv6host:[2003:ab34:e::1]`,
|
||||||
|
expectedOut: `anipv6host:2003:ab34:e::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6, eq sep, brackets",
|
||||||
|
input: `anipv6host=[2003:ab34:e::1]`,
|
||||||
|
expectedOut: `anipv6host:2003:ab34:e::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, colon sep",
|
||||||
|
input: `ipv6local:::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, eq sep",
|
||||||
|
input: `ipv6local=::1`,
|
||||||
|
expectedOut: `ipv6local:::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, eq sep, brackets",
|
||||||
|
input: `ipv6local=[::1]`,
|
||||||
|
expectedOut: `ipv6local:::1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, non-canonical, colon sep",
|
||||||
|
input: `ipv6local:0:0:0:0:0:0:0:1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, non-canonical, eq sep",
|
||||||
|
input: `ipv6local=0:0:0:0:0:0:0:1`,
|
||||||
|
expectedOut: `ipv6local:0:0:0:0:0:0:0:1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, non-canonical, eq sep, brackets",
|
||||||
|
input: `ipv6local=[0:0:0:0:0:0:0:1]`,
|
||||||
|
expectedOut: `ipv6local:0:0:0:0:0:0:0:1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "host-gateway special case, colon sep",
|
||||||
|
input: `host.docker.internal:host-gateway`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "host-gateway special case, eq sep",
|
||||||
|
input: `host.docker.internal=host-gateway`,
|
||||||
|
expectedOut: `host.docker.internal:host-gateway`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad address, colon sep",
|
||||||
|
input: `myhost:192.notanipaddress.1`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad address, eq sep",
|
||||||
|
input: `myhost=192.notanipaddress.1`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "No sep",
|
||||||
|
input: `thathost-nosemicolon10.0.0.1`,
|
||||||
|
expectedErr: `bad format for add-host: "thathost-nosemicolon10.0.0.1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad IPv6",
|
||||||
|
input: `anipv6host:::::1`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "::::1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad IPv6, trailing colons",
|
||||||
|
input: `ipv6local:::0::`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "::0::"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad IPv6, missing close bracket",
|
||||||
|
input: `ipv6addr=[::1`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "[::1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Bad IPv6, missing open bracket",
|
||||||
|
input: `ipv6addr=::1]`,
|
||||||
|
expectedErr: `invalid IP address in add-host: "::1]"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Missing address, colon sep",
|
||||||
|
input: `myhost.invalid:`,
|
||||||
|
expectedErr: `invalid IP address in add-host: ""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "Missing address, eq sep",
|
||||||
|
input: `myhost.invalid=`,
|
||||||
|
expectedErr: `invalid IP address in add-host: ""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "IPv6 localhost, bad name",
|
||||||
|
input: `:=::1`,
|
||||||
|
expectedErr: `bad format for add-host: ":=::1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "No input",
|
||||||
|
input: ``,
|
||||||
|
expectedErr: `bad format for add-host: ""`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid := map[string]string{
|
for _, tc := range tests {
|
||||||
`myhost:192.notanipaddress.1`: `invalid IP`,
|
tc := tc
|
||||||
`thathost-nosemicolon10.0.0.1`: `bad format`,
|
if tc.expectedOut == "" {
|
||||||
`anipv6host:::::1`: `invalid IP`,
|
tc.expectedOut = tc.input
|
||||||
`ipv6local:::0::`: `invalid IP`,
|
|
||||||
}
|
}
|
||||||
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
for _, extrahost := range valid {
|
actualOut, actualErr := ValidateExtraHost(tc.input)
|
||||||
if _, err := ValidateExtraHost(extrahost); err != nil {
|
if tc.expectedErr == "" {
|
||||||
t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err)
|
assert.Check(t, is.Equal(tc.expectedOut, actualOut))
|
||||||
}
|
assert.NilError(t, actualErr)
|
||||||
}
|
} else {
|
||||||
|
assert.Check(t, actualOut == "")
|
||||||
for extraHost, expectedError := range invalid {
|
assert.Check(t, is.Error(actualErr, tc.expectedErr))
|
||||||
if _, err := ValidateExtraHost(extraHost); err == nil {
|
|
||||||
t.Fatalf("ValidateExtraHost(`%q`) should have failed validation", extraHost)
|
|
||||||
} else if !strings.Contains(err.Error(), expectedError) {
|
|
||||||
t.Fatalf("ValidateExtraHost(`%q`) error should contain %q", extraHost, expectedError)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue