github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/container/opts_test.go (about) 1 package container 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "runtime" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/docker/docker/api/types/container" 13 networktypes "github.com/docker/docker/api/types/network" 14 "github.com/docker/go-connections/nat" 15 "github.com/pkg/errors" 16 "github.com/spf13/pflag" 17 "gotest.tools/v3/assert" 18 is "gotest.tools/v3/assert/cmp" 19 "gotest.tools/v3/skip" 20 ) 21 22 func TestValidateAttach(t *testing.T) { 23 valid := []string{ 24 "stdin", 25 "stdout", 26 "stderr", 27 "STDIN", 28 "STDOUT", 29 "STDERR", 30 } 31 if _, err := validateAttach("invalid"); err == nil { 32 t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") 33 } 34 35 for _, attach := range valid { 36 value, err := validateAttach(attach) 37 if err != nil { 38 t.Fatal(err) 39 } 40 if value != strings.ToLower(attach) { 41 t.Fatalf("Expected [%v], got [%v]", attach, value) 42 } 43 } 44 } 45 46 func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { 47 flags, copts := setupRunFlags() 48 if err := flags.Parse(args); err != nil { 49 return nil, nil, nil, err 50 } 51 // TODO: fix tests to accept ContainerConfig 52 containerConfig, err := parse(flags, copts, runtime.GOOS) 53 if err != nil { 54 return nil, nil, nil, err 55 } 56 return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err 57 } 58 59 func setupRunFlags() (*pflag.FlagSet, *containerOptions) { 60 flags := pflag.NewFlagSet("run", pflag.ContinueOnError) 61 flags.SetOutput(ioutil.Discard) 62 flags.Usage = nil 63 copts := addFlags(flags) 64 return flags, copts 65 } 66 67 func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { 68 t.Helper() 69 config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash")) 70 assert.NilError(t, err) 71 return config, hostConfig 72 } 73 74 func TestParseRunLinks(t *testing.T) { 75 if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { 76 t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) 77 } 78 if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { 79 t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) 80 } 81 if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { 82 t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) 83 } 84 } 85 86 func TestParseRunAttach(t *testing.T) { 87 tests := []struct { 88 input string 89 expected container.Config 90 }{ 91 { 92 input: "", 93 expected: container.Config{ 94 AttachStdout: true, 95 AttachStderr: true, 96 }, 97 }, 98 { 99 input: "-i", 100 expected: container.Config{ 101 AttachStdin: true, 102 AttachStdout: true, 103 AttachStderr: true, 104 }, 105 }, 106 { 107 input: "-a stdin", 108 expected: container.Config{ 109 AttachStdin: true, 110 }, 111 }, 112 { 113 input: "-a stdin -a stdout", 114 expected: container.Config{ 115 AttachStdin: true, 116 AttachStdout: true, 117 }, 118 }, 119 { 120 input: "-a stdin -a stdout -a stderr", 121 expected: container.Config{ 122 AttachStdin: true, 123 AttachStdout: true, 124 AttachStderr: true, 125 }, 126 }, 127 } 128 for _, tc := range tests { 129 tc := tc 130 t.Run(tc.input, func(t *testing.T) { 131 config, _ := mustParse(t, tc.input) 132 assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin) 133 assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout) 134 assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr) 135 }) 136 } 137 } 138 139 func TestParseRunWithInvalidArgs(t *testing.T) { 140 tests := []struct { 141 args []string 142 error string 143 }{ 144 { 145 args: []string{"-a", "ubuntu", "bash"}, 146 error: `invalid argument "ubuntu" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`, 147 }, 148 { 149 args: []string{"-a", "invalid", "ubuntu", "bash"}, 150 error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`, 151 }, 152 { 153 args: []string{"-a", "invalid", "-a", "stdout", "ubuntu", "bash"}, 154 error: `invalid argument "invalid" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`, 155 }, 156 { 157 args: []string{"-a", "stdout", "-a", "stderr", "-z", "ubuntu", "bash"}, 158 error: `unknown shorthand flag: 'z' in -z`, 159 }, 160 { 161 args: []string{"-a", "stdin", "-z", "ubuntu", "bash"}, 162 error: `unknown shorthand flag: 'z' in -z`, 163 }, 164 { 165 args: []string{"-a", "stdout", "-z", "ubuntu", "bash"}, 166 error: `unknown shorthand flag: 'z' in -z`, 167 }, 168 { 169 args: []string{"-a", "stderr", "-z", "ubuntu", "bash"}, 170 error: `unknown shorthand flag: 'z' in -z`, 171 }, 172 { 173 args: []string{"-z", "--rm", "ubuntu", "bash"}, 174 error: `unknown shorthand flag: 'z' in -z`, 175 }, 176 } 177 flags, _ := setupRunFlags() 178 for _, tc := range tests { 179 t.Run(strings.Join(tc.args, " "), func(t *testing.T) { 180 assert.Error(t, flags.Parse(tc.args), tc.error) 181 }) 182 } 183 } 184 185 // nolint: gocyclo 186 func TestParseWithVolumes(t *testing.T) { 187 188 // A single volume 189 arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) 190 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { 191 t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) 192 } else if _, exists := config.Volumes[arr[0]]; !exists { 193 t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) 194 } 195 196 // Two volumes 197 arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) 198 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { 199 t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) 200 } else if _, exists := config.Volumes[arr[0]]; !exists { 201 t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) 202 } else if _, exists := config.Volumes[arr[1]]; !exists { 203 t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) 204 } 205 206 // A single bind mount 207 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) 208 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { 209 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) 210 } 211 212 // Two bind mounts. 213 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) 214 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 215 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 216 } 217 218 // Two bind mounts, first read-only, second read-write. 219 // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 220 arr, tryit = setupPlatformVolume( 221 []string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, 222 []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) 223 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 224 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 225 } 226 227 // Similar to previous test but with alternate modes which are only supported by Linux 228 if runtime.GOOS != "windows" { 229 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) 230 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 231 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 232 } 233 234 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) 235 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 236 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 237 } 238 } 239 240 // One bind mount and one volume 241 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) 242 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { 243 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) 244 } else if _, exists := config.Volumes[arr[1]]; !exists { 245 t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) 246 } 247 248 // Root to non-c: drive letter (Windows specific) 249 if runtime.GOOS == "windows" { 250 arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) 251 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { 252 t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) 253 } 254 } 255 256 } 257 258 // setupPlatformVolume takes two arrays of volume specs - a Unix style 259 // spec and a Windows style spec. Depending on the platform being unit tested, 260 // it returns one of them, along with a volume string that would be passed 261 // on the docker CLI (e.g. -v /bar -v /foo). 262 func setupPlatformVolume(u []string, w []string) ([]string, string) { 263 var a []string 264 if runtime.GOOS == "windows" { 265 a = w 266 } else { 267 a = u 268 } 269 s := "" 270 for _, v := range a { 271 s = s + "-v " + v + " " 272 } 273 return a, s 274 } 275 276 // check if (a == c && b == d) || (a == d && b == c) 277 // because maps are randomized 278 func compareRandomizedStrings(a, b, c, d string) error { 279 if a == c && b == d { 280 return nil 281 } 282 if a == d && b == c { 283 return nil 284 } 285 return errors.Errorf("strings don't match") 286 } 287 288 // Simple parse with MacAddress validation 289 func TestParseWithMacAddress(t *testing.T) { 290 invalidMacAddress := "--mac-address=invalidMacAddress" 291 validMacAddress := "--mac-address=92:d0:c6:0a:29:33" 292 if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { 293 t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) 294 } 295 if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { 296 t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) 297 } 298 } 299 300 func TestRunFlagsParseWithMemory(t *testing.T) { 301 flags, _ := setupRunFlags() 302 args := []string{"--memory=invalid", "img", "cmd"} 303 err := flags.Parse(args) 304 assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`) 305 306 _, hostconfig := mustParse(t, "--memory=1G") 307 assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory)) 308 } 309 310 func TestParseWithMemorySwap(t *testing.T) { 311 flags, _ := setupRunFlags() 312 args := []string{"--memory-swap=invalid", "img", "cmd"} 313 err := flags.Parse(args) 314 assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`) 315 316 _, hostconfig := mustParse(t, "--memory-swap=1G") 317 assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap)) 318 319 _, hostconfig = mustParse(t, "--memory-swap=-1") 320 assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap)) 321 } 322 323 func TestParseHostname(t *testing.T) { 324 validHostnames := map[string]string{ 325 "hostname": "hostname", 326 "host-name": "host-name", 327 "hostname123": "hostname123", 328 "123hostname": "123hostname", 329 "hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error", 330 } 331 hostnameWithDomain := "--hostname=hostname.domainname" 332 hostnameWithDomainTld := "--hostname=hostname.domainname.tld" 333 for hostname, expectedHostname := range validHostnames { 334 if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { 335 t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname) 336 } 337 } 338 if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" { 339 t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname) 340 } 341 if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" { 342 t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname) 343 } 344 } 345 346 func TestParseHostnameDomainname(t *testing.T) { 347 validDomainnames := map[string]string{ 348 "domainname": "domainname", 349 "domain-name": "domain-name", 350 "domainname123": "domainname123", 351 "123domainname": "123domainname", 352 "domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors", 353 } 354 for domainname, expectedDomainname := range validDomainnames { 355 if config, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname { 356 t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname) 357 } 358 } 359 if config, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" { 360 t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname) 361 } 362 if config, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" { 363 t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname) 364 } 365 } 366 367 func TestParseWithExpose(t *testing.T) { 368 invalids := map[string]string{ 369 ":": "invalid port format for --expose: :", 370 "8080:9090": "invalid port format for --expose: 8080:9090", 371 "/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", 372 "/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", 373 "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 374 "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 375 "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 376 "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, 377 } 378 valids := map[string][]nat.Port{ 379 "8080/tcp": {"8080/tcp"}, 380 "8080/udp": {"8080/udp"}, 381 "8080/ncp": {"8080/ncp"}, 382 "8080-8080/udp": {"8080/udp"}, 383 "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, 384 } 385 for expose, expectedError := range invalids { 386 if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { 387 t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) 388 } 389 } 390 for expose, exposedPorts := range valids { 391 config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) 392 if err != nil { 393 t.Fatal(err) 394 } 395 if len(config.ExposedPorts) != len(exposedPorts) { 396 t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) 397 } 398 for _, port := range exposedPorts { 399 if _, ok := config.ExposedPorts[port]; !ok { 400 t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) 401 } 402 } 403 } 404 // Merge with actual published port 405 config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) 406 if err != nil { 407 t.Fatal(err) 408 } 409 if len(config.ExposedPorts) != 2 { 410 t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) 411 } 412 ports := []nat.Port{"80/tcp", "81/tcp"} 413 for _, port := range ports { 414 if _, ok := config.ExposedPorts[port]; !ok { 415 t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) 416 } 417 } 418 } 419 420 func TestParseDevice(t *testing.T) { 421 skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side 422 valids := map[string]container.DeviceMapping{ 423 "/dev/snd": { 424 PathOnHost: "/dev/snd", 425 PathInContainer: "/dev/snd", 426 CgroupPermissions: "rwm", 427 }, 428 "/dev/snd:rw": { 429 PathOnHost: "/dev/snd", 430 PathInContainer: "/dev/snd", 431 CgroupPermissions: "rw", 432 }, 433 "/dev/snd:/something": { 434 PathOnHost: "/dev/snd", 435 PathInContainer: "/something", 436 CgroupPermissions: "rwm", 437 }, 438 "/dev/snd:/something:rw": { 439 PathOnHost: "/dev/snd", 440 PathInContainer: "/something", 441 CgroupPermissions: "rw", 442 }, 443 } 444 for device, deviceMapping := range valids { 445 _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) 446 if err != nil { 447 t.Fatal(err) 448 } 449 if len(hostconfig.Devices) != 1 { 450 t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices) 451 } 452 if hostconfig.Devices[0] != deviceMapping { 453 t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) 454 } 455 } 456 457 } 458 459 func TestParseNetworkConfig(t *testing.T) { 460 tests := []struct { 461 name string 462 flags []string 463 expected map[string]*networktypes.EndpointSettings 464 expectedCfg container.HostConfig 465 expectedErr string 466 }{ 467 { 468 name: "single-network-legacy", 469 flags: []string{"--network", "net1"}, 470 expected: map[string]*networktypes.EndpointSettings{}, 471 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 472 }, 473 { 474 name: "single-network-advanced", 475 flags: []string{"--network", "name=net1"}, 476 expected: map[string]*networktypes.EndpointSettings{}, 477 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 478 }, 479 { 480 name: "single-network-legacy-with-options", 481 flags: []string{ 482 "--ip", "172.20.88.22", 483 "--ip6", "2001:db8::8822", 484 "--link", "foo:bar", 485 "--link", "bar:baz", 486 "--link-local-ip", "169.254.2.2", 487 "--link-local-ip", "fe80::169:254:2:2", 488 "--network", "name=net1", 489 "--network-alias", "web1", 490 "--network-alias", "web2", 491 }, 492 expected: map[string]*networktypes.EndpointSettings{ 493 "net1": { 494 IPAMConfig: &networktypes.EndpointIPAMConfig{ 495 IPv4Address: "172.20.88.22", 496 IPv6Address: "2001:db8::8822", 497 LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, 498 }, 499 Links: []string{"foo:bar", "bar:baz"}, 500 Aliases: []string{"web1", "web2"}, 501 }, 502 }, 503 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 504 }, 505 { 506 name: "multiple-network-advanced-mixed", 507 flags: []string{ 508 "--ip", "172.20.88.22", 509 "--ip6", "2001:db8::8822", 510 "--link", "foo:bar", 511 "--link", "bar:baz", 512 "--link-local-ip", "169.254.2.2", 513 "--link-local-ip", "fe80::169:254:2:2", 514 "--network", "name=net1,driver-opt=field1=value1", 515 "--network-alias", "web1", 516 "--network-alias", "web2", 517 "--network", "net2", 518 "--network", "name=net3,alias=web3,driver-opt=field3=value3,ip=172.20.88.22,ip6=2001:db8::8822", 519 }, 520 expected: map[string]*networktypes.EndpointSettings{ 521 "net1": { 522 DriverOpts: map[string]string{"field1": "value1"}, 523 IPAMConfig: &networktypes.EndpointIPAMConfig{ 524 IPv4Address: "172.20.88.22", 525 IPv6Address: "2001:db8::8822", 526 LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, 527 }, 528 Links: []string{"foo:bar", "bar:baz"}, 529 Aliases: []string{"web1", "web2"}, 530 }, 531 "net2": {}, 532 "net3": { 533 DriverOpts: map[string]string{"field3": "value3"}, 534 IPAMConfig: &networktypes.EndpointIPAMConfig{ 535 IPv4Address: "172.20.88.22", 536 IPv6Address: "2001:db8::8822", 537 }, 538 Aliases: []string{"web3"}, 539 }, 540 }, 541 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 542 }, 543 { 544 name: "single-network-advanced-with-options", 545 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"}, 546 expected: map[string]*networktypes.EndpointSettings{ 547 "net1": { 548 DriverOpts: map[string]string{ 549 "field1": "value1", 550 "field2": "value2", 551 }, 552 IPAMConfig: &networktypes.EndpointIPAMConfig{ 553 IPv4Address: "172.20.88.22", 554 IPv6Address: "2001:db8::8822", 555 }, 556 Aliases: []string{"web1", "web2"}, 557 }, 558 }, 559 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 560 }, 561 { 562 name: "multiple-networks", 563 flags: []string{"--network", "net1", "--network", "name=net2"}, 564 expected: map[string]*networktypes.EndpointSettings{"net1": {}, "net2": {}}, 565 expectedCfg: container.HostConfig{NetworkMode: "net1"}, 566 }, 567 { 568 name: "conflict-network", 569 flags: []string{"--network", "duplicate", "--network", "name=duplicate"}, 570 expectedErr: `network "duplicate" is specified multiple times`, 571 }, 572 { 573 name: "conflict-options-alias", 574 flags: []string{"--network", "name=net1,alias=web1", "--network-alias", "web1"}, 575 expectedErr: `conflicting options: cannot specify both --network-alias and per-network alias`, 576 }, 577 { 578 name: "conflict-options-ip", 579 flags: []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip", "172.20.88.22"}, 580 expectedErr: `conflicting options: cannot specify both --ip and per-network IPv4 address`, 581 }, 582 { 583 name: "conflict-options-ip6", 584 flags: []string{"--network", "name=net1,ip=172.20.88.22,ip6=2001:db8::8822", "--ip6", "2001:db8::8822"}, 585 expectedErr: `conflicting options: cannot specify both --ip6 and per-network IPv6 address`, 586 }, 587 { 588 name: "invalid-mixed-network-types", 589 flags: []string{"--network", "name=host", "--network", "net1"}, 590 expectedErr: `conflicting options: cannot attach both user-defined and non-user-defined network-modes`, 591 }, 592 } 593 594 for _, tc := range tests { 595 t.Run(tc.name, func(t *testing.T) { 596 _, hConfig, nwConfig, err := parseRun(tc.flags) 597 598 if tc.expectedErr != "" { 599 assert.Error(t, err, tc.expectedErr) 600 return 601 } 602 603 assert.NilError(t, err) 604 assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode) 605 assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected) 606 }) 607 } 608 } 609 610 func TestParseModes(t *testing.T) { 611 // pid ko 612 flags, copts := setupRunFlags() 613 args := []string{"--pid=container:", "img", "cmd"} 614 assert.NilError(t, flags.Parse(args)) 615 _, err := parse(flags, copts, runtime.GOOS) 616 assert.ErrorContains(t, err, "--pid: invalid PID mode") 617 618 // pid ok 619 _, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"}) 620 assert.NilError(t, err) 621 if !hostconfig.PidMode.Valid() { 622 t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) 623 } 624 625 // uts ko 626 _, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled 627 assert.ErrorContains(t, err, "--uts: invalid UTS mode") 628 629 // uts ok 630 _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) 631 assert.NilError(t, err) 632 if !hostconfig.UTSMode.Valid() { 633 t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) 634 } 635 } 636 637 func TestRunFlagsParseShmSize(t *testing.T) { 638 // shm-size ko 639 flags, _ := setupRunFlags() 640 args := []string{"--shm-size=a128m", "img", "cmd"} 641 expectedErr := `invalid argument "a128m" for "--shm-size" flag: invalid size: 'a128m'` 642 err := flags.Parse(args) 643 assert.ErrorContains(t, err, expectedErr) 644 645 // shm-size ok 646 _, hostconfig, _, err := parseRun([]string{"--shm-size=128m", "img", "cmd"}) 647 assert.NilError(t, err) 648 if hostconfig.ShmSize != 134217728 { 649 t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) 650 } 651 } 652 653 func TestParseRestartPolicy(t *testing.T) { 654 invalids := map[string]string{ 655 "always:2:3": "invalid restart policy format", 656 "on-failure:invalid": "maximum retry count must be an integer", 657 } 658 valids := map[string]container.RestartPolicy{ 659 "": {}, 660 "always": { 661 Name: "always", 662 MaximumRetryCount: 0, 663 }, 664 "on-failure:1": { 665 Name: "on-failure", 666 MaximumRetryCount: 1, 667 }, 668 } 669 for restart, expectedError := range invalids { 670 if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { 671 t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) 672 } 673 } 674 for restart, expected := range valids { 675 _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) 676 if err != nil { 677 t.Fatal(err) 678 } 679 if hostconfig.RestartPolicy != expected { 680 t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) 681 } 682 } 683 } 684 685 func TestParseRestartPolicyAutoRemove(t *testing.T) { 686 expected := "Conflicting options: --restart and --rm" 687 _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled 688 if err == nil || err.Error() != expected { 689 t.Fatalf("Expected error %v, but got none", expected) 690 } 691 } 692 693 func TestParseHealth(t *testing.T) { 694 checkOk := func(args ...string) *container.HealthConfig { 695 config, _, _, err := parseRun(args) 696 if err != nil { 697 t.Fatalf("%#v: %v", args, err) 698 } 699 return config.Healthcheck 700 } 701 checkError := func(expected string, args ...string) { 702 config, _, _, err := parseRun(args) 703 if err == nil { 704 t.Fatalf("Expected error, but got %#v", config) 705 } 706 if err.Error() != expected { 707 t.Fatalf("Expected %#v, got %#v", expected, err) 708 } 709 } 710 health := checkOk("--no-healthcheck", "img", "cmd") 711 if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { 712 t.Fatalf("--no-healthcheck failed: %#v", health) 713 } 714 715 health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") 716 if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { 717 t.Fatalf("--health-cmd: got %#v", health.Test) 718 } 719 if health.Timeout != 0 { 720 t.Fatalf("--health-cmd: timeout = %s", health.Timeout) 721 } 722 723 checkError("--no-healthcheck conflicts with --health-* options", 724 "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") 725 726 health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd") 727 if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second { 728 t.Fatalf("--health-*: got %#v", health) 729 } 730 } 731 732 func TestParseLoggingOpts(t *testing.T) { 733 // logging opts ko 734 if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { 735 t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) 736 } 737 // logging opts ok 738 _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) 739 if err != nil { 740 t.Fatal(err) 741 } 742 if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { 743 t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) 744 } 745 } 746 747 func TestParseEnvfileVariables(t *testing.T) { 748 e := "open nonexistent: no such file or directory" 749 if runtime.GOOS == "windows" { 750 e = "open nonexistent: The system cannot find the file specified." 751 } 752 // env ko 753 if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 754 t.Fatalf("Expected an error with message '%s', got %v", e, err) 755 } 756 // env ok 757 config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) 758 if err != nil { 759 t.Fatal(err) 760 } 761 if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { 762 t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) 763 } 764 config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) 765 if err != nil { 766 t.Fatal(err) 767 } 768 if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { 769 t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) 770 } 771 } 772 773 func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { 774 // UTF8 with BOM 775 config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) 776 if err != nil { 777 t.Fatal(err) 778 } 779 env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} 780 if len(config.Env) != len(env) { 781 t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) 782 } 783 for i, v := range env { 784 if config.Env[i] != v { 785 t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) 786 } 787 } 788 789 // UTF16 with BOM 790 e := "contains invalid utf8 bytes at line" 791 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 792 t.Fatalf("Expected an error with message '%s', got %v", e, err) 793 } 794 // UTF16BE with BOM 795 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 796 t.Fatalf("Expected an error with message '%s', got %v", e, err) 797 } 798 } 799 800 func TestParseLabelfileVariables(t *testing.T) { 801 e := "open nonexistent: no such file or directory" 802 if runtime.GOOS == "windows" { 803 e = "open nonexistent: The system cannot find the file specified." 804 } 805 // label ko 806 if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 807 t.Fatalf("Expected an error with message '%s', got %v", e, err) 808 } 809 // label ok 810 config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) 811 if err != nil { 812 t.Fatal(err) 813 } 814 if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { 815 t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) 816 } 817 config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) 818 if err != nil { 819 t.Fatal(err) 820 } 821 if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { 822 t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) 823 } 824 } 825 826 func TestParseEntryPoint(t *testing.T) { 827 config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) 828 if err != nil { 829 t.Fatal(err) 830 } 831 if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { 832 t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) 833 } 834 } 835 836 func TestValidateDevice(t *testing.T) { 837 skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side 838 valid := []string{ 839 "/home", 840 "/home:/home", 841 "/home:/something/else", 842 "/with space", 843 "/home:/with space", 844 "relative:/absolute-path", 845 "hostPath:/containerPath:r", 846 "/hostPath:/containerPath:rw", 847 "/hostPath:/containerPath:mrw", 848 } 849 invalid := map[string]string{ 850 "": "bad format for path: ", 851 "./": "./ is not an absolute path", 852 "../": "../ is not an absolute path", 853 "/:../": "../ is not an absolute path", 854 "/:path": "path is not an absolute path", 855 ":": "bad format for path: :", 856 "/tmp:": " is not an absolute path", 857 ":test": "bad format for path: :test", 858 ":/test": "bad format for path: :/test", 859 "tmp:": " is not an absolute path", 860 ":test:": "bad format for path: :test:", 861 "::": "bad format for path: ::", 862 ":::": "bad format for path: :::", 863 "/tmp:::": "bad format for path: /tmp:::", 864 ":/tmp::": "bad format for path: :/tmp::", 865 "path:ro": "ro is not an absolute path", 866 "path:rr": "rr is not an absolute path", 867 "a:/b:ro": "bad mode specified: ro", 868 "a:/b:rr": "bad mode specified: rr", 869 } 870 871 for _, path := range valid { 872 if _, err := validateDevice(path, runtime.GOOS); err != nil { 873 t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) 874 } 875 } 876 877 for path, expectedError := range invalid { 878 if _, err := validateDevice(path, runtime.GOOS); err == nil { 879 t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) 880 } else { 881 if err.Error() != expectedError { 882 t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) 883 } 884 } 885 } 886 } 887 888 func TestParseSystemPaths(t *testing.T) { 889 tests := []struct { 890 doc string 891 in, out, masked, readonly []string 892 }{ 893 { 894 doc: "not set", 895 in: []string{}, 896 out: []string{}, 897 }, 898 { 899 doc: "not set, preserve other options", 900 in: []string{ 901 "seccomp=unconfined", 902 "apparmor=unconfined", 903 "label=user:USER", 904 "foo=bar", 905 }, 906 out: []string{ 907 "seccomp=unconfined", 908 "apparmor=unconfined", 909 "label=user:USER", 910 "foo=bar", 911 }, 912 }, 913 { 914 doc: "unconfined", 915 in: []string{"systempaths=unconfined"}, 916 out: []string{}, 917 masked: []string{}, 918 readonly: []string{}, 919 }, 920 { 921 doc: "unconfined and other options", 922 in: []string{"foo=bar", "bar=baz", "systempaths=unconfined"}, 923 out: []string{"foo=bar", "bar=baz"}, 924 masked: []string{}, 925 readonly: []string{}, 926 }, 927 { 928 doc: "unknown option", 929 in: []string{"foo=bar", "systempaths=unknown", "bar=baz"}, 930 out: []string{"foo=bar", "systempaths=unknown", "bar=baz"}, 931 }, 932 } 933 934 for _, tc := range tests { 935 securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(tc.in) 936 assert.DeepEqual(t, securityOpts, tc.out) 937 assert.DeepEqual(t, maskedPaths, tc.masked) 938 assert.DeepEqual(t, readonlyPaths, tc.readonly) 939 } 940 } 941 942 func TestConvertToStandardNotation(t *testing.T) { 943 valid := map[string][]string{ 944 "20:10/tcp": {"target=10,published=20"}, 945 "40:30": {"40:30"}, 946 "20:20 80:4444": {"20:20", "80:4444"}, 947 "1500:2500/tcp 1400:1300": {"target=2500,published=1500", "1400:1300"}, 948 "1500:200/tcp 90:80/tcp": {"published=1500,target=200", "target=80,published=90"}, 949 } 950 951 invalid := [][]string{ 952 {"published=1500,target:444"}, 953 {"published=1500,444"}, 954 {"published=1500,target,444"}, 955 } 956 957 for key, ports := range valid { 958 convertedPorts, err := convertToStandardNotation(ports) 959 960 if err != nil { 961 assert.NilError(t, err) 962 } 963 assert.DeepEqual(t, strings.Split(key, " "), convertedPorts) 964 } 965 966 for _, ports := range invalid { 967 if _, err := convertToStandardNotation(ports); err == nil { 968 t.Fatalf("ConvertToStandardNotation(`%q`) should have failed conversion", ports) 969 } 970 } 971 }