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