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