mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2026-10-24 16:59:19 +03:00
Update the Docker client libraries from v28 to v29 because v28 is no longer maintained. The minimum requirement for Docker has been raised to Docker 25.0. Users of Debian 13 (Trixie) or 12 (Bookworm) that use IPv6 [are affected by a bug](https://github.com/moby/moby/pull/49520) in the Docker version shipped by Debian that results in error messages like: ``` 2026/07/01 14:58:59 Could not get list of networks: ParseAddr("fd05:d0ca:2::1/112"): unexpected character, want colon (at "/112") ``` Affected Debian users should either not upgrade Forgejo Runner or switch to the Docker package provided by Docker until the problem is either resolved by the Docker or Debian developers. <!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - other - [PR](https://code.forgejo.org/forgejo/runner/pulls/1547): <!--number 1547 --><!--line 0 --><!--description Y2hvcmU6IHVwZ3JhZGUgZ2l0aHViLmNvbS9kb2NrZXIvY2xpIHRvIHYyOQ==-->chore: upgrade github.com/docker/cli to v29<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1547
1200 lines
42 KiB
Go
1200 lines
42 KiB
Go
// This file is a copy of https://raw.githubusercontent.com/docker/cli/refs/tags/v29.5.2/cli/command/container/opts_test.go
|
|
// with adjusted imports and support for `--platform` patched in. It has to be updated from time to time.
|
|
//
|
|
// docker/cli is licensed under the Apache License, Version 2.0.
|
|
// See DOCKER_LICENSE for the full license text.
|
|
//
|
|
|
|
//nolint:unparam
|
|
package docker
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/moby/moby/api/types/container"
|
|
networktypes "github.com/moby/moby/api/types/network"
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/require"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
func mustParseMAC(s string) networktypes.HardwareAddr {
|
|
mac, err := net.ParseMAC(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return networktypes.HardwareAddr(mac)
|
|
}
|
|
|
|
func TestValidateAttach(t *testing.T) {
|
|
valid := []string{
|
|
"stdin",
|
|
"stdout",
|
|
"stderr",
|
|
"STDIN",
|
|
"STDOUT",
|
|
"STDERR",
|
|
}
|
|
if _, err := validateAttach("invalid"); err == nil {
|
|
t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing")
|
|
}
|
|
|
|
for _, attach := range valid {
|
|
value, err := validateAttach(attach)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value != strings.ToLower(attach) {
|
|
t.Fatalf("Expected [%v], got [%v]", attach, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
|
|
flags, copts := setupRunFlags()
|
|
if err := flags.Parse(args); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
// TODO(dnephin): fix tests to accept ContainerConfig; see https://github.com/moby/moby/pull/31621
|
|
containerCfg, err := parse(flags, copts, runtime.GOOS)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
return containerCfg.Config, containerCfg.HostConfig, containerCfg.NetworkingConfig, err
|
|
}
|
|
|
|
func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
|
|
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
|
|
flags.SetOutput(io.Discard)
|
|
flags.Usage = nil
|
|
copts := addFlags(flags)
|
|
return flags, copts
|
|
}
|
|
|
|
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) {
|
|
t.Helper()
|
|
config, hostConfig, nwConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
|
|
assert.NilError(t, err)
|
|
return config, hostConfig, nwConfig
|
|
}
|
|
|
|
func TestParseRunLinks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expHostConfigLinks []string
|
|
expNetConfigLinks map[string][]string
|
|
}{
|
|
// Default bridge - legacy links ...
|
|
{
|
|
name: "default/onelink",
|
|
input: "--link a:b",
|
|
expHostConfigLinks: []string{"a:b"},
|
|
expNetConfigLinks: map[string][]string{"default": nil},
|
|
},
|
|
{
|
|
name: "default/twolinks",
|
|
input: "--link a:b --link c:d",
|
|
expHostConfigLinks: []string{"a:b", "c:d"},
|
|
expNetConfigLinks: map[string][]string{"default": nil},
|
|
},
|
|
{
|
|
name: "bridge/onelink",
|
|
input: "--network bridge --link a:b",
|
|
expHostConfigLinks: []string{"a:b"},
|
|
// expNetConfigLinks - no EndpointsConfig is created for a single named network with no options set.
|
|
// See the "For backward compatibility" comment in parseNetworkOpts().
|
|
},
|
|
{
|
|
name: "default/nolinks",
|
|
expNetConfigLinks: map[string][]string{"default": nil},
|
|
},
|
|
|
|
// User-defined bridge - links become DNS aliases ...
|
|
{
|
|
name: "userdefnet/onelink",
|
|
input: "--network userdefnet --link a:b",
|
|
expHostConfigLinks: []string{"a:b"},
|
|
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b"}},
|
|
},
|
|
{
|
|
name: "userdefnet/twolinks",
|
|
input: "--network userdefnet --link a:b --link c:d",
|
|
expHostConfigLinks: []string{"a:b", "c:d"},
|
|
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b", "c:d"}},
|
|
},
|
|
{
|
|
name: "userdefnet/nolinks",
|
|
input: "--network userdefnet",
|
|
},
|
|
{
|
|
// Link options are applied to the first network (and there's no "advanced syntax"
|
|
// link key, like "--network name=userdefnet,link=a:b").
|
|
name: "links apply to the first network",
|
|
input: "--network userdefnet --link a:b --network bar --link c:d",
|
|
expHostConfigLinks: []string{"a:b", "c:d"},
|
|
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b", "c:d"}, "bar": nil},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, hostConfig, netConfig := mustParse(t, tc.input)
|
|
assert.Check(t, is.DeepEqual(hostConfig.Links, tc.expHostConfigLinks))
|
|
assert.Check(t, is.Len(netConfig.EndpointsConfig, len(tc.expNetConfigLinks)))
|
|
for netName, expLinks := range tc.expNetConfigLinks {
|
|
nc, ok := netConfig.EndpointsConfig[netName]
|
|
assert.Assert(t, ok)
|
|
assert.Check(t, is.DeepEqual(nc.Links, expLinks))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRunAttach(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected container.Config
|
|
}{
|
|
{
|
|
input: "",
|
|
expected: container.Config{
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
},
|
|
},
|
|
{
|
|
input: "-i",
|
|
expected: container.Config{
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
},
|
|
},
|
|
{
|
|
input: "-a stdin",
|
|
expected: container.Config{
|
|
AttachStdin: true,
|
|
},
|
|
},
|
|
{
|
|
input: "-a stdin -a stdout",
|
|
expected: container.Config{
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
},
|
|
},
|
|
{
|
|
input: "-a stdin -a stdout -a stderr",
|
|
expected: container.Config{
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
config, _, _ := mustParse(t, tc.input)
|
|
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
|
assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
|
|
assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRunWithInvalidArgs(t *testing.T) {
|
|
tests := []struct {
|
|
args []string
|
|
error string
|
|
}{
|
|
{
|
|
args: []string{"-a", "ubuntu", "bash"},
|
|
error: `invalid argument "ubuntu" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
|
},
|
|
{
|
|
args: []string{"-a", "invalid", "ubuntu", "bash"},
|
|
error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
|
},
|
|
{
|
|
args: []string{"-a", "invalid", "-a", "stdout", "ubuntu", "bash"},
|
|
error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
|
},
|
|
{
|
|
args: []string{"-a", "stdout", "-a", "stderr", "-z", "ubuntu", "bash"},
|
|
error: `unknown shorthand flag: 'z' in -z`,
|
|
},
|
|
{
|
|
args: []string{"-a", "stdin", "-z", "ubuntu", "bash"},
|
|
error: `unknown shorthand flag: 'z' in -z`,
|
|
},
|
|
{
|
|
args: []string{"-a", "stdout", "-z", "ubuntu", "bash"},
|
|
error: `unknown shorthand flag: 'z' in -z`,
|
|
},
|
|
{
|
|
args: []string{"-a", "stderr", "-z", "ubuntu", "bash"},
|
|
error: `unknown shorthand flag: 'z' in -z`,
|
|
},
|
|
{
|
|
args: []string{"-z", "--rm", "ubuntu", "bash"},
|
|
error: `unknown shorthand flag: 'z' in -z`,
|
|
},
|
|
}
|
|
flags, _ := setupRunFlags()
|
|
for _, tc := range tests {
|
|
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
|
assert.Error(t, flags.Parse(tc.args), tc.error)
|
|
})
|
|
}
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func TestParseWithVolumes(t *testing.T) {
|
|
// A single volume
|
|
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
|
|
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
|
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
|
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
|
|
}
|
|
|
|
// Two volumes
|
|
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
|
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
|
|
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
|
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
|
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
|
|
} else if _, exists := config.Volumes[arr[1]]; !exists { // ignore shadow-check
|
|
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
|
|
}
|
|
|
|
// A single bind mount
|
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
|
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
|
|
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
|
|
}
|
|
|
|
// Two bind mounts.
|
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
|
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
}
|
|
|
|
// Two bind mounts, first read-only, second read-write.
|
|
// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
|
|
arr, tryit = setupPlatformVolume(
|
|
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
|
|
[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`},
|
|
)
|
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
}
|
|
|
|
// Similar to previous test but with alternate modes which are only supported by Linux
|
|
if runtime.GOOS != "windows" {
|
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
|
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
}
|
|
|
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
|
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
}
|
|
}
|
|
|
|
// One bind mount and one volume
|
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
|
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
|
|
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
|
|
} else if _, exists := config.Volumes[arr[1]]; !exists {
|
|
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
|
|
}
|
|
|
|
// Root to non-c: drive letter (Windows specific)
|
|
if runtime.GOOS == "windows" {
|
|
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
|
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
|
|
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
// setupPlatformVolume takes two arrays of volume specs - a Unix style
|
|
// spec and a Windows style spec. Depending on the platform being unit tested,
|
|
// it returns one of them, along with a volume string that would be passed
|
|
// on the docker CLI (e.g. -v /bar -v /foo).
|
|
func setupPlatformVolume(u, w []string) ([]string, string) {
|
|
var a []string
|
|
if runtime.GOOS == "windows" {
|
|
a = w
|
|
} else {
|
|
a = u
|
|
}
|
|
s := ""
|
|
for _, v := range a {
|
|
s = s + "-v " + v + " "
|
|
}
|
|
return a, s
|
|
}
|
|
|
|
// check if (a == c && b == d) || (a == d && b == c)
|
|
// because maps are randomized
|
|
func compareRandomizedStrings(a, b, c, d string) error {
|
|
if a == c && b == d {
|
|
return nil
|
|
}
|
|
if a == d && b == c {
|
|
return nil
|
|
}
|
|
return errors.New("strings don't match")
|
|
}
|
|
|
|
// Simple parse with MacAddress validation
|
|
func TestParseWithMacAddress(t *testing.T) {
|
|
invalidMacAddress := "--mac-address=invalidMacAddress"
|
|
validMacAddress := "--mac-address=92:d0:c6:0a:29:33"
|
|
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
|
|
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
|
}
|
|
_, hostConfig, nwConfig := mustParse(t, validMacAddress)
|
|
defaultNw := hostConfig.NetworkMode.NetworkName()
|
|
if nwConfig.EndpointsConfig[defaultNw].MacAddress.String() != "92:d0:c6:0a:29:33" {
|
|
t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
|
|
}
|
|
}
|
|
|
|
func TestRunFlagsParseWithMemory(t *testing.T) {
|
|
flags, _ := setupRunFlags()
|
|
args := []string{"--memory=invalid", "img", "cmd"}
|
|
err := flags.Parse(args)
|
|
assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
|
|
|
|
_, hostconfig, _ := mustParse(t, "--memory=1G")
|
|
assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
|
|
}
|
|
|
|
func TestParseWithMemorySwap(t *testing.T) {
|
|
flags, _ := setupRunFlags()
|
|
args := []string{"--memory-swap=invalid", "img", "cmd"}
|
|
err := flags.Parse(args)
|
|
assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
|
|
|
|
_, hostconfig, _ := mustParse(t, "--memory-swap=1G")
|
|
assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
|
|
|
|
_, hostconfig, _ = mustParse(t, "--memory-swap=-1")
|
|
assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
|
|
}
|
|
|
|
func TestParseHostname(t *testing.T) {
|
|
validHostnames := map[string]string{
|
|
"hostname": "hostname",
|
|
"host-name": "host-name",
|
|
"hostname123": "hostname123",
|
|
"123hostname": "123hostname",
|
|
"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
|
|
}
|
|
hostnameWithDomain := "--hostname=hostname.domainname"
|
|
hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
|
|
for hostname, expectedHostname := range validHostnames {
|
|
if config, _, _ := mustParse(t, "--hostname="+hostname); config.Hostname != expectedHostname {
|
|
t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
|
|
}
|
|
}
|
|
if config, _, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
|
|
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
|
|
}
|
|
if config, _, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
|
|
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
|
|
}
|
|
}
|
|
|
|
func TestParseHostnameDomainname(t *testing.T) {
|
|
validDomainnames := map[string]string{
|
|
"domainname": "domainname",
|
|
"domain-name": "domain-name",
|
|
"domainname123": "domainname123",
|
|
"123domainname": "123domainname",
|
|
"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
|
|
}
|
|
for domainname, expectedDomainname := range validDomainnames {
|
|
if config, _, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
|
|
t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
|
|
}
|
|
}
|
|
if config, _, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
|
|
t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
|
|
}
|
|
if config, _, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
|
|
t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
|
|
}
|
|
}
|
|
|
|
func TestParseWithExpose(t *testing.T) {
|
|
t.Run("invalid", func(t *testing.T) {
|
|
tests := map[string]string{
|
|
":": `invalid range format for --expose: invalid start port ':': invalid syntax`,
|
|
"8080:9090": `invalid range format for --expose: invalid start port '8080:9090': invalid syntax`,
|
|
"/tcp": `invalid range format for --expose: invalid start port '': value is empty`,
|
|
"/udp": `invalid range format for --expose: invalid start port '': value is empty`,
|
|
"NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
|
|
"NaN-NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
|
|
"8080-NaN/tcp": `invalid range format for --expose: invalid end port 'NaN': invalid syntax`,
|
|
"1234567890-8080/tcp": `invalid range format for --expose: invalid start port '1234567890': value out of range`,
|
|
}
|
|
for expose, expectedError := range tests {
|
|
t.Run(expose, func(t *testing.T) {
|
|
_, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
|
assert.Error(t, err, expectedError)
|
|
})
|
|
}
|
|
})
|
|
t.Run("valid", func(t *testing.T) {
|
|
tests := map[string][]networktypes.Port{
|
|
"8080/tcp": {networktypes.MustParsePort("8080/tcp")},
|
|
"8080/udp": {networktypes.MustParsePort("8080/udp")},
|
|
"8080/ncp": {networktypes.MustParsePort("8080/ncp")},
|
|
"8080-8080/udp": {networktypes.MustParsePort("8080/udp")},
|
|
"8080-8082/tcp": {networktypes.MustParsePort("8080/tcp"), networktypes.MustParsePort("8081/tcp"), networktypes.MustParsePort("8082/tcp")},
|
|
}
|
|
for expose, exposedPorts := range tests {
|
|
t.Run(expose, func(t *testing.T) {
|
|
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
|
assert.NilError(t, err)
|
|
for _, port := range exposedPorts {
|
|
_, ok := config.ExposedPorts[port]
|
|
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("merge with published", func(t *testing.T) {
|
|
// Merge with actual published port
|
|
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Len(config.ExposedPorts, 2))
|
|
ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")}
|
|
for _, port := range ports {
|
|
_, ok := config.ExposedPorts[port]
|
|
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseDevice(t *testing.T) {
|
|
skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side
|
|
testCases := []struct {
|
|
devices []string
|
|
deviceMapping *container.DeviceMapping
|
|
deviceRequests []container.DeviceRequest
|
|
}{
|
|
{
|
|
devices: []string{"/dev/snd"},
|
|
deviceMapping: &container.DeviceMapping{
|
|
PathOnHost: "/dev/snd",
|
|
PathInContainer: "/dev/snd",
|
|
CgroupPermissions: "rwm",
|
|
},
|
|
},
|
|
{
|
|
devices: []string{"/dev/snd:rw"},
|
|
deviceMapping: &container.DeviceMapping{
|
|
PathOnHost: "/dev/snd",
|
|
PathInContainer: "/dev/snd",
|
|
CgroupPermissions: "rw",
|
|
},
|
|
},
|
|
{
|
|
devices: []string{"/dev/snd:/something"},
|
|
deviceMapping: &container.DeviceMapping{
|
|
PathOnHost: "/dev/snd",
|
|
PathInContainer: "/something",
|
|
CgroupPermissions: "rwm",
|
|
},
|
|
},
|
|
{
|
|
devices: []string{"/dev/snd:/something:rw"},
|
|
deviceMapping: &container.DeviceMapping{
|
|
PathOnHost: "/dev/snd",
|
|
PathInContainer: "/something",
|
|
CgroupPermissions: "rw",
|
|
},
|
|
},
|
|
{
|
|
devices: []string{"vendor.com/class=name"},
|
|
deviceMapping: nil,
|
|
deviceRequests: []container.DeviceRequest{
|
|
{
|
|
Driver: "cdi",
|
|
DeviceIDs: []string{"vendor.com/class=name"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
devices: []string{"vendor.com/class=name", "/dev/snd:/something:rw"},
|
|
deviceMapping: &container.DeviceMapping{
|
|
PathOnHost: "/dev/snd",
|
|
PathInContainer: "/something",
|
|
CgroupPermissions: "rw",
|
|
},
|
|
deviceRequests: []container.DeviceRequest{
|
|
{
|
|
Driver: "cdi",
|
|
DeviceIDs: []string{"vendor.com/class=name"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(fmt.Sprintf("%s", tc.devices), func(t *testing.T) {
|
|
var args []string
|
|
for _, d := range tc.devices {
|
|
args = append(args, fmt.Sprintf("--device=%v", d))
|
|
}
|
|
args = append(args, "img", "cmd")
|
|
|
|
_, hostconfig, _, err := parseRun(args)
|
|
|
|
assert.NilError(t, err)
|
|
|
|
if tc.deviceMapping != nil {
|
|
if assert.Check(t, is.Len(hostconfig.Devices, 1)) {
|
|
assert.Check(t, is.DeepEqual(*tc.deviceMapping, hostconfig.Devices[0]))
|
|
}
|
|
} else {
|
|
assert.Check(t, is.Len(hostconfig.Devices, 0))
|
|
}
|
|
|
|
assert.Check(t, is.DeepEqual(tc.deviceRequests, hostconfig.DeviceRequests))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseNetworkConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
flags []string
|
|
expected map[string]*networktypes.EndpointSettings
|
|
expectedHostCfg container.HostConfig
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "single-network-legacy",
|
|
flags: []string{"--network", "net1"},
|
|
expected: map[string]*networktypes.EndpointSettings{},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "single-network-advanced",
|
|
flags: []string{"--network", "name=net1"},
|
|
expected: map[string]*networktypes.EndpointSettings{},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "single-network-legacy-with-options",
|
|
flags: []string{
|
|
"--ip", "172.20.88.22",
|
|
"--ip6", "2001:db8::8822",
|
|
"--link", "foo:bar",
|
|
"--link", "bar:baz",
|
|
"--link-local-ip", "169.254.2.2",
|
|
"--link-local-ip", "fe80::169:254:2:2",
|
|
"--network", "name=net1",
|
|
"--network-alias", "web1",
|
|
"--network-alias", "web2",
|
|
},
|
|
expected: map[string]*networktypes.EndpointSettings{
|
|
"net1": {
|
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
|
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
|
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
|
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
|
|
},
|
|
Links: []string{"foo:bar", "bar:baz"},
|
|
Aliases: []string{"web1", "web2"},
|
|
},
|
|
},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "multiple-network-advanced-mixed",
|
|
flags: []string{
|
|
"--ip", "172.20.88.22",
|
|
"--ip6", "2001:db8::8822",
|
|
"--link", "foo:bar",
|
|
"--link", "bar:baz",
|
|
"--link-local-ip", "169.254.2.2",
|
|
"--link-local-ip", "fe80::169:254:2:2",
|
|
"--network", "name=net1,driver-opt=field1=value1",
|
|
"--network-alias", "web1",
|
|
"--network-alias", "web2",
|
|
"--network", "net2",
|
|
"--network", "name=net3,alias=web3,driver-opt=field3=value3,ip=172.20.88.22,ip6=2001:db8::8822",
|
|
"--network", "name=net4,mac-address=02:32:1c:23:00:04,link-local-ip=169.254.169.254",
|
|
},
|
|
expected: map[string]*networktypes.EndpointSettings{
|
|
"net1": {
|
|
DriverOpts: map[string]string{"field1": "value1"},
|
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
|
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
|
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
|
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
|
|
},
|
|
Links: []string{"foo:bar", "bar:baz"},
|
|
Aliases: []string{"web1", "web2"},
|
|
},
|
|
"net2": {},
|
|
"net3": {
|
|
DriverOpts: map[string]string{"field3": "value3"},
|
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
|
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
|
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
|
},
|
|
Aliases: []string{"web3"},
|
|
},
|
|
"net4": {
|
|
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
|
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")},
|
|
},
|
|
},
|
|
},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "single-network-advanced-with-options",
|
|
flags: []string{"--network", "name=net1,alias=web1,alias=web2,driver-opt=field1=value1,driver-opt=field2=value2,ip=172.20.88.22,ip6=2001:db8::8822,mac-address=02:32:1c:23:00:04"},
|
|
expected: map[string]*networktypes.EndpointSettings{
|
|
"net1": {
|
|
DriverOpts: map[string]string{
|
|
"field1": "value1",
|
|
"field2": "value2",
|
|
},
|
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
|
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
|
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
|
},
|
|
Aliases: []string{"web1", "web2"},
|
|
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
|
},
|
|
},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "multiple-networks",
|
|
flags: []string{"--network", "net1", "--network", "name=net2"},
|
|
expected: map[string]*networktypes.EndpointSettings{"net1": {}, "net2": {}},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "advanced-options-with-standalone-mac-address-flag",
|
|
flags: []string{"--network=name=net1,alias=foobar", "--mac-address", "52:0f:f3:dc:50:10"},
|
|
expected: map[string]*networktypes.EndpointSettings{
|
|
"net1": {
|
|
Aliases: []string{"foobar"},
|
|
MacAddress: mustParseMAC("52:0f:f3:dc:50:10"),
|
|
},
|
|
},
|
|
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
|
},
|
|
{
|
|
name: "conflict-network",
|
|
flags: []string{"--network", "duplicate", "--network", "name=duplicate"},
|
|
expectedErr: `network "duplicate" is specified multiple times`,
|
|
},
|
|
{
|
|
name: "conflict-options-alias",
|
|
flags: []string{"--network", "name=net1,alias=web1", "--network-alias", "web1"},
|
|
expectedErr: `conflicting options: cannot specify both --network-alias and per-network alias`,
|
|
},
|
|
{
|
|
name: "conflict-options-ip",
|
|
flags: []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip", "172.20.88.22"},
|
|
expectedErr: `conflicting options: cannot specify both --ip and per-network IPv4 address`,
|
|
},
|
|
{
|
|
name: "conflict-options-ip6",
|
|
flags: []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip6", "2001:db8::8822"},
|
|
expectedErr: `conflicting options: cannot specify both --ip6 and per-network IPv6 address`,
|
|
},
|
|
{
|
|
name: "invalid-mixed-network-types",
|
|
flags: []string{"--network", "name=host", "--network", "net1"},
|
|
expectedErr: `conflicting options: cannot attach both user-defined and non-user-defined network-modes`,
|
|
},
|
|
{
|
|
name: "conflict-options-link-local-ip",
|
|
flags: []string{"--network", "name=net1,link-local-ip=169.254.169.254", "--link-local-ip", "169.254.10.8"},
|
|
expectedErr: `conflicting options: cannot specify both --link-local-ip and per-network link-local IP addresses`,
|
|
},
|
|
{
|
|
name: "conflict-options-mac-address",
|
|
flags: []string{"--network", "name=net1,mac-address=02:32:1c:23:00:04", "--mac-address", "02:32:1c:23:00:04"},
|
|
expectedErr: `conflicting options: cannot specify both --mac-address and per-network MAC address`,
|
|
},
|
|
{
|
|
name: "invalid-mac-address",
|
|
flags: []string{"--network", "name=net1,mac-address=foobar"},
|
|
expectedErr: "foobar is not a valid mac address",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, hConfig, nwConfig, err := parseRun(tc.flags)
|
|
|
|
if tc.expectedErr != "" {
|
|
assert.Error(t, err, tc.expectedErr)
|
|
return
|
|
}
|
|
|
|
assert.NilError(t, err)
|
|
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
|
|
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{}))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseModes(t *testing.T) {
|
|
// pid ko
|
|
flags, copts := setupRunFlags()
|
|
args := []string{"--pid=container:", "img", "cmd"}
|
|
assert.NilError(t, flags.Parse(args))
|
|
_, err := parse(flags, copts, runtime.GOOS)
|
|
assert.ErrorContains(t, err, "--pid: invalid PID mode")
|
|
|
|
// pid ok
|
|
_, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"})
|
|
assert.NilError(t, err)
|
|
if !hostconfig.PidMode.Valid() {
|
|
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
|
|
}
|
|
|
|
// uts ko
|
|
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled
|
|
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
|
|
|
// uts ok
|
|
_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
|
|
assert.NilError(t, err)
|
|
if !hostconfig.UTSMode.Valid() {
|
|
t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
|
|
}
|
|
}
|
|
|
|
func TestRunFlagsParseShmSize(t *testing.T) {
|
|
// shm-size ko
|
|
flags, _ := setupRunFlags()
|
|
args := []string{"--shm-size=a128m", "img", "cmd"}
|
|
expectedErr := `invalid argument "a128m" for "--shm-size" flag:`
|
|
err := flags.Parse(args)
|
|
assert.ErrorContains(t, err, expectedErr)
|
|
|
|
// shm-size ok
|
|
_, hostconfig, _, err := parseRun([]string{"--shm-size=128m", "img", "cmd"})
|
|
assert.NilError(t, err)
|
|
if hostconfig.ShmSize != 134217728 {
|
|
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
|
|
}
|
|
}
|
|
|
|
func TestParseRestartPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected container.RestartPolicy
|
|
expectedErr string
|
|
}{
|
|
{
|
|
input: "",
|
|
},
|
|
{
|
|
input: "no",
|
|
expected: container.RestartPolicy{
|
|
Name: container.RestartPolicyDisabled,
|
|
},
|
|
},
|
|
{
|
|
input: ":1",
|
|
expectedErr: "invalid restart policy format: no policy provided before colon",
|
|
},
|
|
{
|
|
input: "always",
|
|
expected: container.RestartPolicy{
|
|
Name: container.RestartPolicyAlways,
|
|
},
|
|
},
|
|
{
|
|
input: "always:2:3",
|
|
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
|
|
},
|
|
{
|
|
input: "on-failure:1",
|
|
expected: container.RestartPolicy{
|
|
Name: container.RestartPolicyOnFailure,
|
|
MaximumRetryCount: 1,
|
|
},
|
|
},
|
|
{
|
|
input: "on-failure:invalid",
|
|
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
|
|
},
|
|
{
|
|
input: "unless-stopped",
|
|
expected: container.RestartPolicy{
|
|
Name: container.RestartPolicyUnlessStopped,
|
|
},
|
|
},
|
|
{
|
|
input: "unless-stopped:invalid",
|
|
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
|
|
},
|
|
|
|
// Unknown / invalid combinations: validation is handled by the daemon>
|
|
{
|
|
input: "anything:123",
|
|
expected: container.RestartPolicy{Name: "anything", MaximumRetryCount: 123},
|
|
},
|
|
{
|
|
input: "negative:-123",
|
|
expected: container.RestartPolicy{Name: "negative", MaximumRetryCount: -123},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
|
|
if tc.expectedErr != "" {
|
|
assert.Check(t, is.Error(err, tc.expectedErr))
|
|
assert.Check(t, is.Nil(hostConfig))
|
|
} else {
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual(hostConfig.RestartPolicy, tc.expected))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
|
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled
|
|
const expected = "conflicting options: cannot specify both --restart and --rm"
|
|
assert.Check(t, is.Error(err, expected))
|
|
}
|
|
|
|
func TestParseHealth(t *testing.T) {
|
|
checkOk := func(args ...string) *container.HealthConfig {
|
|
config, _, _, err := parseRun(args)
|
|
if err != nil {
|
|
t.Fatalf("%#v: %v", args, err)
|
|
}
|
|
return config.Healthcheck
|
|
}
|
|
checkError := func(expected string, args ...string) {
|
|
config, _, _, err := parseRun(args)
|
|
if err == nil {
|
|
t.Fatalf("Expected error, but got %#v", config)
|
|
}
|
|
if err.Error() != expected {
|
|
t.Fatalf("Expected %#v, got %#v", expected, err)
|
|
}
|
|
}
|
|
health := checkOk("--no-healthcheck", "img", "cmd")
|
|
if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" {
|
|
t.Fatalf("--no-healthcheck failed: %#v", health)
|
|
}
|
|
|
|
health = checkOk("--health-cmd=/check.sh -q", "img", "cmd")
|
|
if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" {
|
|
t.Fatalf("--health-cmd: got %#v", health.Test)
|
|
}
|
|
if health.Timeout != 0 {
|
|
t.Fatalf("--health-cmd: timeout = %s", health.Timeout)
|
|
}
|
|
|
|
checkError("--no-healthcheck conflicts with --health-* options",
|
|
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
|
|
|
|
health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "--health-start-interval=1s", "img", "cmd")
|
|
if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second || health.StartInterval != 1*time.Second {
|
|
t.Fatalf("--health-*: got %#v", health)
|
|
}
|
|
}
|
|
|
|
func TestParseLoggingOpts(t *testing.T) {
|
|
// logging opts ko
|
|
if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" {
|
|
t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err)
|
|
}
|
|
// logging opts ok
|
|
_, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 {
|
|
t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy)
|
|
}
|
|
}
|
|
|
|
func TestParseEnvfileVariables(t *testing.T) {
|
|
expErr := "--env-file: open nonexistent: no such file or directory"
|
|
if runtime.GOOS == "windows" {
|
|
expErr = "--env-file: open nonexistent: The system cannot find the file specified."
|
|
}
|
|
// env ko
|
|
if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != expErr {
|
|
t.Fatalf("Expected an error with message '%s', got %v", expErr, err)
|
|
}
|
|
// env ok
|
|
config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" {
|
|
t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env)
|
|
}
|
|
config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" {
|
|
t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env)
|
|
}
|
|
}
|
|
|
|
func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
|
// UTF8 with BOM
|
|
config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"}
|
|
if len(config.Env) != len(env) {
|
|
t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env)
|
|
}
|
|
for i, v := range env {
|
|
if config.Env[i] != v {
|
|
t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i]))
|
|
}
|
|
}
|
|
|
|
// UTF16 with BOM
|
|
e := "invalid utf8 bytes at line"
|
|
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
|
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
|
}
|
|
// UTF16BE with BOM
|
|
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
|
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
|
}
|
|
}
|
|
|
|
func TestParseLabelfileVariables(t *testing.T) {
|
|
expErr := "--label-file: open nonexistent: no such file or directory"
|
|
if runtime.GOOS == "windows" {
|
|
expErr = "--label-file: open nonexistent: The system cannot find the file specified."
|
|
}
|
|
// label ko
|
|
if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != expErr {
|
|
t.Fatalf("Expected an error with message '%s', got %v", expErr, err)
|
|
}
|
|
// label ok
|
|
config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" {
|
|
t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels)
|
|
}
|
|
config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" {
|
|
t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels)
|
|
}
|
|
}
|
|
|
|
func TestParseEntryPoint(t *testing.T) {
|
|
config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual(config.Entrypoint, []string{"anything"}))
|
|
}
|
|
|
|
func TestValidateDevice(t *testing.T) {
|
|
skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side
|
|
valid := []string{
|
|
"/home",
|
|
"/home:/home",
|
|
"/home:/something/else",
|
|
"/with space",
|
|
"/home:/with space",
|
|
"relative:/absolute-path",
|
|
"hostPath:/containerPath:r",
|
|
"/hostPath:/containerPath:rw",
|
|
"/hostPath:/containerPath:mrw",
|
|
}
|
|
invalid := map[string]string{
|
|
"": "bad format for path: ",
|
|
"./": "./ is not an absolute path",
|
|
"../": "../ is not an absolute path",
|
|
"/:../": "../ is not an absolute path",
|
|
"/:path": "path is not an absolute path",
|
|
":": "bad format for path: :",
|
|
"/tmp:": " is not an absolute path",
|
|
":test": "bad format for path: :test",
|
|
":/test": "bad format for path: :/test",
|
|
"tmp:": " is not an absolute path",
|
|
":test:": "bad format for path: :test:",
|
|
"::": "bad format for path: ::",
|
|
":::": "bad format for path: :::",
|
|
"/tmp:::": "bad format for path: /tmp:::",
|
|
":/tmp::": "bad format for path: :/tmp::",
|
|
"path:ro": "ro is not an absolute path",
|
|
"path:rr": "rr is not an absolute path",
|
|
"a:/b:ro": "bad mode specified: ro",
|
|
"a:/b:rr": "bad mode specified: rr",
|
|
}
|
|
|
|
for _, path := range valid {
|
|
if _, err := validateDevice(path, runtime.GOOS); err != nil {
|
|
t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err)
|
|
}
|
|
}
|
|
|
|
for path, expectedError := range invalid {
|
|
if _, err := validateDevice(path, runtime.GOOS); err == nil {
|
|
t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
|
|
} else if err.Error() != expectedError {
|
|
t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseSystemPaths(t *testing.T) {
|
|
tests := []struct {
|
|
doc string
|
|
in, out, masked, readonly []string
|
|
}{
|
|
{
|
|
doc: "not set",
|
|
in: []string{},
|
|
out: []string{},
|
|
},
|
|
{
|
|
doc: "not set, preserve other options",
|
|
in: []string{
|
|
"seccomp=unconfined",
|
|
"apparmor=unconfined",
|
|
"label=user:USER",
|
|
"foo=bar",
|
|
},
|
|
out: []string{
|
|
"seccomp=unconfined",
|
|
"apparmor=unconfined",
|
|
"label=user:USER",
|
|
"foo=bar",
|
|
},
|
|
},
|
|
{
|
|
doc: "unconfined",
|
|
in: []string{"systempaths=unconfined"},
|
|
out: []string{},
|
|
masked: []string{},
|
|
readonly: []string{},
|
|
},
|
|
{
|
|
doc: "unconfined and other options",
|
|
in: []string{"foo=bar", "bar=baz", "systempaths=unconfined"},
|
|
out: []string{"foo=bar", "bar=baz"},
|
|
masked: []string{},
|
|
readonly: []string{},
|
|
},
|
|
{
|
|
doc: "unknown option",
|
|
in: []string{"foo=bar", "systempaths=unknown", "bar=baz"},
|
|
out: []string{"foo=bar", "systempaths=unknown", "bar=baz"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(tc.in)
|
|
assert.DeepEqual(t, securityOpts, tc.out)
|
|
assert.DeepEqual(t, maskedPaths, tc.masked)
|
|
assert.DeepEqual(t, readonlyPaths, tc.readonly)
|
|
}
|
|
}
|
|
|
|
func TestConvertToStandardNotation(t *testing.T) {
|
|
valid := map[string][]string{
|
|
"20:10/tcp": {"target=10,published=20"},
|
|
"40:30": {"40:30"},
|
|
"20:20 80:4444": {"20:20", "80:4444"},
|
|
"1500:2500/tcp 1400:1300": {"target=2500,published=1500", "1400:1300"},
|
|
"1500:200/tcp 90:80/tcp": {"published=1500,target=200", "target=80,published=90"},
|
|
}
|
|
|
|
invalid := [][]string{
|
|
{"published=1500,target:444"},
|
|
{"published=1500,444"},
|
|
{"published=1500,target,444"},
|
|
}
|
|
|
|
for key, ports := range valid {
|
|
convertedPorts, err := convertToStandardNotation(ports)
|
|
if err != nil {
|
|
assert.NilError(t, err)
|
|
}
|
|
assert.DeepEqual(t, strings.Split(key, " "), convertedPorts)
|
|
}
|
|
|
|
for _, ports := range invalid {
|
|
if _, err := convertToStandardNotation(ports); err == nil {
|
|
t.Fatalf("ConvertToStandardNotation(`%q`) should have failed conversion", ports)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePlatformFlag(t *testing.T) {
|
|
tests := []struct {
|
|
input []string
|
|
output string
|
|
}{
|
|
{
|
|
input: []string{"--platform", "linux/amd64"},
|
|
output: "linux/amd64",
|
|
},
|
|
{
|
|
input: []string{"--platform=linux/arm64"},
|
|
output: "linux/arm64",
|
|
},
|
|
{
|
|
input: []string{},
|
|
output: "",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
flags, copts := setupRunFlags()
|
|
err := flags.Parse(tc.input)
|
|
require.NoError(t, err)
|
|
|
|
containerConfig, err := parse(flags, copts, runtime.GOOS)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.output, containerConfig.Platform)
|
|
}
|
|
}
|