github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/cli/command/container/opts_test.go (about) 1 package container 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "runtime" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/docker/docker/api/types/container" 15 networktypes "github.com/docker/docker/api/types/network" 16 "github.com/docker/docker/runconfig" 17 "github.com/docker/go-connections/nat" 18 "github.com/spf13/pflag" 19 ) 20 21 func TestValidateAttach(t *testing.T) { 22 valid := []string{ 23 "stdin", 24 "stdout", 25 "stderr", 26 "STDIN", 27 "STDOUT", 28 "STDERR", 29 } 30 if _, err := validateAttach("invalid"); err == nil { 31 t.Fatalf("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") 32 } 33 34 for _, attach := range valid { 35 value, err := validateAttach(attach) 36 if err != nil { 37 t.Fatal(err) 38 } 39 if value != strings.ToLower(attach) { 40 t.Fatalf("Expected [%v], got [%v]", attach, value) 41 } 42 } 43 } 44 45 func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { 46 flags := pflag.NewFlagSet("run", pflag.ContinueOnError) 47 flags.SetOutput(ioutil.Discard) 48 flags.Usage = nil 49 copts := addFlags(flags) 50 if err := flags.Parse(args); err != nil { 51 return nil, nil, nil, err 52 } 53 return parse(flags, copts) 54 } 55 56 func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { 57 config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) 58 return config, hostConfig, err 59 } 60 61 func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { 62 config, hostConfig, err := parsetest(t, args) 63 if err != nil { 64 t.Fatal(err) 65 } 66 return config, hostConfig 67 } 68 69 func TestParseRunLinks(t *testing.T) { 70 if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { 71 t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) 72 } 73 if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { 74 t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) 75 } 76 if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { 77 t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) 78 } 79 } 80 81 func TestParseRunAttach(t *testing.T) { 82 if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr { 83 t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) 84 } 85 if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr { 86 t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) 87 } 88 if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { 89 t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) 90 } 91 if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr { 92 t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) 93 } 94 if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { 95 t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) 96 } 97 98 if _, _, err := parsetest(t, "-a"); err == nil { 99 t.Fatalf("Error parsing attach flags, `-a` should be an error but is not") 100 } 101 if _, _, err := parsetest(t, "-a invalid"); err == nil { 102 t.Fatalf("Error parsing attach flags, `-a invalid` should be an error but is not") 103 } 104 if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil { 105 t.Fatalf("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") 106 } 107 if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil { 108 t.Fatalf("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") 109 } 110 if _, _, err := parsetest(t, "-a stdin -d"); err == nil { 111 t.Fatalf("Error parsing attach flags, `-a stdin -d` should be an error but is not") 112 } 113 if _, _, err := parsetest(t, "-a stdout -d"); err == nil { 114 t.Fatalf("Error parsing attach flags, `-a stdout -d` should be an error but is not") 115 } 116 if _, _, err := parsetest(t, "-a stderr -d"); err == nil { 117 t.Fatalf("Error parsing attach flags, `-a stderr -d` should be an error but is not") 118 } 119 if _, _, err := parsetest(t, "-d --rm"); err == nil { 120 t.Fatalf("Error parsing attach flags, `-d --rm` should be an error but is not") 121 } 122 } 123 124 func TestParseRunVolumes(t *testing.T) { 125 126 // A single volume 127 arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) 128 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { 129 t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) 130 } else if _, exists := config.Volumes[arr[0]]; !exists { 131 t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) 132 } 133 134 // Two volumes 135 arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) 136 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { 137 t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) 138 } else if _, exists := config.Volumes[arr[0]]; !exists { 139 t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) 140 } else if _, exists := config.Volumes[arr[1]]; !exists { 141 t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) 142 } 143 144 // A single bind-mount 145 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) 146 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { 147 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) 148 } 149 150 // Two bind-mounts. 151 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) 152 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 153 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 154 } 155 156 // Two bind-mounts, first read-only, second read-write. 157 // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 158 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) 159 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 160 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 161 } 162 163 // Similar to previous test but with alternate modes which are only supported by Linux 164 if runtime.GOOS != "windows" { 165 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) 166 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 167 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 168 } 169 170 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) 171 if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { 172 t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) 173 } 174 } 175 176 // One bind mount and one volume 177 arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) 178 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { 179 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) 180 } else if _, exists := config.Volumes[arr[1]]; !exists { 181 t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) 182 } 183 184 // Root to non-c: drive letter (Windows specific) 185 if runtime.GOOS == "windows" { 186 arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) 187 if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { 188 t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) 189 } 190 } 191 192 } 193 194 // setupPlatformVolume takes two arrays of volume specs - a Unix style 195 // spec and a Windows style spec. Depending on the platform being unit tested, 196 // it returns one of them, along with a volume string that would be passed 197 // on the docker CLI (e.g. -v /bar -v /foo). 198 func setupPlatformVolume(u []string, w []string) ([]string, string) { 199 var a []string 200 if runtime.GOOS == "windows" { 201 a = w 202 } else { 203 a = u 204 } 205 s := "" 206 for _, v := range a { 207 s = s + "-v " + v + " " 208 } 209 return a, s 210 } 211 212 // check if (a == c && b == d) || (a == d && b == c) 213 // because maps are randomized 214 func compareRandomizedStrings(a, b, c, d string) error { 215 if a == c && b == d { 216 return nil 217 } 218 if a == d && b == c { 219 return nil 220 } 221 return fmt.Errorf("strings don't match") 222 } 223 224 // Simple parse with MacAddress validation 225 func TestParseWithMacAddress(t *testing.T) { 226 invalidMacAddress := "--mac-address=invalidMacAddress" 227 validMacAddress := "--mac-address=92:d0:c6:0a:29:33" 228 if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { 229 t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) 230 } 231 if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { 232 t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) 233 } 234 } 235 236 func TestParseWithMemory(t *testing.T) { 237 invalidMemory := "--memory=invalid" 238 validMemory := "--memory=1G" 239 if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { 240 t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) 241 } 242 if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { 243 t.Fatalf("Expected the config to have '1G' as Memory, got '%v'", hostconfig.Memory) 244 } 245 } 246 247 func TestParseWithMemorySwap(t *testing.T) { 248 invalidMemory := "--memory-swap=invalid" 249 validMemory := "--memory-swap=1G" 250 anotherValidMemory := "--memory-swap=-1" 251 if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { 252 t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) 253 } 254 if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { 255 t.Fatalf("Expected the config to have '1073741824' as MemorySwap, got '%v'", hostconfig.MemorySwap) 256 } 257 if _, hostconfig := mustParse(t, anotherValidMemory); hostconfig.MemorySwap != -1 { 258 t.Fatalf("Expected the config to have '-1' as MemorySwap, got '%v'", hostconfig.MemorySwap) 259 } 260 } 261 262 func TestParseHostname(t *testing.T) { 263 validHostnames := map[string]string{ 264 "hostname": "hostname", 265 "host-name": "host-name", 266 "hostname123": "hostname123", 267 "123hostname": "123hostname", 268 "hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error", 269 } 270 hostnameWithDomain := "--hostname=hostname.domainname" 271 hostnameWithDomainTld := "--hostname=hostname.domainname.tld" 272 for hostname, expectedHostname := range validHostnames { 273 if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { 274 t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname) 275 } 276 } 277 if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" { 278 t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname) 279 } 280 if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" { 281 t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname) 282 } 283 } 284 285 func TestParseWithExpose(t *testing.T) { 286 invalids := map[string]string{ 287 ":": "invalid port format for --expose: :", 288 "8080:9090": "invalid port format for --expose: 8080:9090", 289 "/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", 290 "/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", 291 "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 292 "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 293 "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, 294 "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, 295 } 296 valids := map[string][]nat.Port{ 297 "8080/tcp": {"8080/tcp"}, 298 "8080/udp": {"8080/udp"}, 299 "8080/ncp": {"8080/ncp"}, 300 "8080-8080/udp": {"8080/udp"}, 301 "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, 302 } 303 for expose, expectedError := range invalids { 304 if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { 305 t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) 306 } 307 } 308 for expose, exposedPorts := range valids { 309 config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) 310 if err != nil { 311 t.Fatal(err) 312 } 313 if len(config.ExposedPorts) != len(exposedPorts) { 314 t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) 315 } 316 for _, port := range exposedPorts { 317 if _, ok := config.ExposedPorts[port]; !ok { 318 t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) 319 } 320 } 321 } 322 // Merge with actual published port 323 config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) 324 if err != nil { 325 t.Fatal(err) 326 } 327 if len(config.ExposedPorts) != 2 { 328 t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) 329 } 330 ports := []nat.Port{"80/tcp", "81/tcp"} 331 for _, port := range ports { 332 if _, ok := config.ExposedPorts[port]; !ok { 333 t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) 334 } 335 } 336 } 337 338 func TestParseDevice(t *testing.T) { 339 valids := map[string]container.DeviceMapping{ 340 "/dev/snd": { 341 PathOnHost: "/dev/snd", 342 PathInContainer: "/dev/snd", 343 CgroupPermissions: "rwm", 344 }, 345 "/dev/snd:rw": { 346 PathOnHost: "/dev/snd", 347 PathInContainer: "/dev/snd", 348 CgroupPermissions: "rw", 349 }, 350 "/dev/snd:/something": { 351 PathOnHost: "/dev/snd", 352 PathInContainer: "/something", 353 CgroupPermissions: "rwm", 354 }, 355 "/dev/snd:/something:rw": { 356 PathOnHost: "/dev/snd", 357 PathInContainer: "/something", 358 CgroupPermissions: "rw", 359 }, 360 } 361 for device, deviceMapping := range valids { 362 _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) 363 if err != nil { 364 t.Fatal(err) 365 } 366 if len(hostconfig.Devices) != 1 { 367 t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices) 368 } 369 if hostconfig.Devices[0] != deviceMapping { 370 t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) 371 } 372 } 373 374 } 375 376 func TestParseModes(t *testing.T) { 377 // ipc ko 378 if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { 379 t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) 380 } 381 // ipc ok 382 _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) 383 if err != nil { 384 t.Fatal(err) 385 } 386 if !hostconfig.IpcMode.Valid() { 387 t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) 388 } 389 // pid ko 390 if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { 391 t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) 392 } 393 // pid ok 394 _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) 395 if err != nil { 396 t.Fatal(err) 397 } 398 if !hostconfig.PidMode.Valid() { 399 t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) 400 } 401 // uts ko 402 if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { 403 t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) 404 } 405 // uts ok 406 _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) 407 if err != nil { 408 t.Fatal(err) 409 } 410 if !hostconfig.UTSMode.Valid() { 411 t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) 412 } 413 // shm-size ko 414 expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'` 415 if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr { 416 t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err) 417 } 418 // shm-size ok 419 _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) 420 if err != nil { 421 t.Fatal(err) 422 } 423 if hostconfig.ShmSize != 134217728 { 424 t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) 425 } 426 } 427 428 func TestParseRestartPolicy(t *testing.T) { 429 invalids := map[string]string{ 430 "always:2:3": "invalid restart policy format", 431 "on-failure:invalid": "maximum retry count must be an integer", 432 } 433 valids := map[string]container.RestartPolicy{ 434 "": {}, 435 "always": { 436 Name: "always", 437 MaximumRetryCount: 0, 438 }, 439 "on-failure:1": { 440 Name: "on-failure", 441 MaximumRetryCount: 1, 442 }, 443 } 444 for restart, expectedError := range invalids { 445 if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { 446 t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) 447 } 448 } 449 for restart, expected := range valids { 450 _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) 451 if err != nil { 452 t.Fatal(err) 453 } 454 if hostconfig.RestartPolicy != expected { 455 t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) 456 } 457 } 458 } 459 460 func TestParseRestartPolicyAutoRemove(t *testing.T) { 461 expected := "Conflicting options: --restart and --rm" 462 _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) 463 if err == nil || err.Error() != expected { 464 t.Fatalf("Expected error %v, but got none", expected) 465 } 466 } 467 468 func TestParseHealth(t *testing.T) { 469 checkOk := func(args ...string) *container.HealthConfig { 470 config, _, _, err := parseRun(args) 471 if err != nil { 472 t.Fatalf("%#v: %v", args, err) 473 } 474 return config.Healthcheck 475 } 476 checkError := func(expected string, args ...string) { 477 config, _, _, err := parseRun(args) 478 if err == nil { 479 t.Fatalf("Expected error, but got %#v", config) 480 } 481 if err.Error() != expected { 482 t.Fatalf("Expected %#v, got %#v", expected, err) 483 } 484 } 485 health := checkOk("--no-healthcheck", "img", "cmd") 486 if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { 487 t.Fatalf("--no-healthcheck failed: %#v", health) 488 } 489 490 health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") 491 if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { 492 t.Fatalf("--health-cmd: got %#v", health.Test) 493 } 494 if health.Timeout != 0 { 495 t.Fatalf("--health-cmd: timeout = %s", health.Timeout) 496 } 497 498 checkError("--no-healthcheck conflicts with --health-* options", 499 "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") 500 501 health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "img", "cmd") 502 if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond { 503 t.Fatalf("--health-*: got %#v", health) 504 } 505 } 506 507 func TestParseLoggingOpts(t *testing.T) { 508 // logging opts ko 509 if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { 510 t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) 511 } 512 // logging opts ok 513 _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) 514 if err != nil { 515 t.Fatal(err) 516 } 517 if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { 518 t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) 519 } 520 } 521 522 func TestParseEnvfileVariables(t *testing.T) { 523 e := "open nonexistent: no such file or directory" 524 if runtime.GOOS == "windows" { 525 e = "open nonexistent: The system cannot find the file specified." 526 } 527 // env ko 528 if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 529 t.Fatalf("Expected an error with message '%s', got %v", e, err) 530 } 531 // env ok 532 config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) 533 if err != nil { 534 t.Fatal(err) 535 } 536 if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { 537 t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) 538 } 539 config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) 540 if err != nil { 541 t.Fatal(err) 542 } 543 if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { 544 t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) 545 } 546 } 547 548 func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { 549 // UTF8 with BOM 550 config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) 551 if err != nil { 552 t.Fatal(err) 553 } 554 env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} 555 if len(config.Env) != len(env) { 556 t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) 557 } 558 for i, v := range env { 559 if config.Env[i] != v { 560 t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) 561 } 562 } 563 564 // UTF16 with BOM 565 e := "contains invalid utf8 bytes at line" 566 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 567 t.Fatalf("Expected an error with message '%s', got %v", e, err) 568 } 569 // UTF16BE with BOM 570 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 571 t.Fatalf("Expected an error with message '%s', got %v", e, err) 572 } 573 } 574 575 func TestParseLabelfileVariables(t *testing.T) { 576 e := "open nonexistent: no such file or directory" 577 if runtime.GOOS == "windows" { 578 e = "open nonexistent: The system cannot find the file specified." 579 } 580 // label ko 581 if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 582 t.Fatalf("Expected an error with message '%s', got %v", e, err) 583 } 584 // label ok 585 config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) 586 if err != nil { 587 t.Fatal(err) 588 } 589 if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { 590 t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) 591 } 592 config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) 593 if err != nil { 594 t.Fatal(err) 595 } 596 if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { 597 t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) 598 } 599 } 600 601 func TestParseEntryPoint(t *testing.T) { 602 config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) 603 if err != nil { 604 t.Fatal(err) 605 } 606 if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { 607 t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) 608 } 609 } 610 611 // This tests the cases for binds which are generated through 612 // DecodeContainerConfig rather than Parse() 613 func TestDecodeContainerConfigVolumes(t *testing.T) { 614 615 // Root to root 616 bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) 617 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 618 t.Fatalf("binds %v should have failed", bindsOrVols) 619 } 620 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 621 t.Fatalf("volume %v should have failed", bindsOrVols) 622 } 623 624 // No destination path 625 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) 626 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 627 t.Fatalf("binds %v should have failed", bindsOrVols) 628 } 629 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 630 t.Fatalf("volume %v should have failed", bindsOrVols) 631 } 632 633 // // No destination path or mode 634 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) 635 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 636 t.Fatalf("binds %v should have failed", bindsOrVols) 637 } 638 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 639 t.Fatalf("volume %v should have failed", bindsOrVols) 640 } 641 642 // A whole lot of nothing 643 bindsOrVols = []string{`:`} 644 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 645 t.Fatalf("binds %v should have failed", bindsOrVols) 646 } 647 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 648 t.Fatalf("volume %v should have failed", bindsOrVols) 649 } 650 651 // A whole lot of nothing with no mode 652 bindsOrVols = []string{`::`} 653 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 654 t.Fatalf("binds %v should have failed", bindsOrVols) 655 } 656 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 657 t.Fatalf("volume %v should have failed", bindsOrVols) 658 } 659 660 // Too much including an invalid mode 661 wTmp := os.Getenv("TEMP") 662 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) 663 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 664 t.Fatalf("binds %v should have failed", bindsOrVols) 665 } 666 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 667 t.Fatalf("volume %v should have failed", bindsOrVols) 668 } 669 670 // Windows specific error tests 671 if runtime.GOOS == "windows" { 672 // Volume which does not include a drive letter 673 bindsOrVols = []string{`\tmp`} 674 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 675 t.Fatalf("binds %v should have failed", bindsOrVols) 676 } 677 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 678 t.Fatalf("volume %v should have failed", bindsOrVols) 679 } 680 681 // Root to C-Drive 682 bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} 683 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 684 t.Fatalf("binds %v should have failed", bindsOrVols) 685 } 686 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 687 t.Fatalf("volume %v should have failed", bindsOrVols) 688 } 689 690 // Container path that does not include a drive letter 691 bindsOrVols = []string{`c:\windows:\somewhere`} 692 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 693 t.Fatalf("binds %v should have failed", bindsOrVols) 694 } 695 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 696 t.Fatalf("volume %v should have failed", bindsOrVols) 697 } 698 } 699 700 // Linux-specific error tests 701 if runtime.GOOS != "windows" { 702 // Just root 703 bindsOrVols = []string{`/`} 704 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 705 t.Fatalf("binds %v should have failed", bindsOrVols) 706 } 707 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 708 t.Fatalf("volume %v should have failed", bindsOrVols) 709 } 710 711 // A single volume that looks like a bind mount passed in Volumes. 712 // This should be handled as a bind mount, not a volume. 713 vols := []string{`/foo:/bar`} 714 if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { 715 t.Fatal("Volume /foo:/bar should have succeeded as a volume name") 716 } else if hostConfig.Binds != nil { 717 t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) 718 } else if _, exists := config.Volumes[vols[0]]; !exists { 719 t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) 720 } 721 722 } 723 } 724 725 // callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes 726 // to call DecodeContainerConfig. It effectively does what a client would 727 // do when calling the daemon by constructing a JSON stream of a 728 // ContainerConfigWrapper which is populated by the set of volume specs 729 // passed into it. It returns a config and a hostconfig which can be 730 // validated to ensure DecodeContainerConfig has manipulated the structures 731 // correctly. 732 func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) { 733 var ( 734 b []byte 735 err error 736 c *container.Config 737 h *container.HostConfig 738 ) 739 w := runconfig.ContainerConfigWrapper{ 740 Config: &container.Config{ 741 Volumes: map[string]struct{}{}, 742 }, 743 HostConfig: &container.HostConfig{ 744 NetworkMode: "none", 745 Binds: binds, 746 }, 747 } 748 for _, v := range volumes { 749 w.Config.Volumes[v] = struct{}{} 750 } 751 if b, err = json.Marshal(w); err != nil { 752 return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) 753 } 754 c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) 755 if err != nil { 756 return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) 757 } 758 if c == nil || h == nil { 759 return nil, nil, fmt.Errorf("Empty config or hostconfig") 760 } 761 762 return c, h, err 763 } 764 765 func TestVolumeSplitN(t *testing.T) { 766 for _, x := range []struct { 767 input string 768 n int 769 expected []string 770 }{ 771 {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, 772 {`:C:\foo:d:`, -1, nil}, 773 {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, 774 {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, 775 {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, 776 777 {`d:\`, -1, []string{`d:\`}}, 778 {`d:`, -1, []string{`d:`}}, 779 {`d:\path`, -1, []string{`d:\path`}}, 780 {`d:\path with space`, -1, []string{`d:\path with space`}}, 781 {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, 782 {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, 783 {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, 784 {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, 785 {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, 786 {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, 787 {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, 788 {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, 789 {`name:D:`, -1, []string{`name`, `D:`}}, 790 {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, 791 {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, 792 {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, 793 {`c:\Windows`, -1, []string{`c:\Windows`}}, 794 {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, 795 796 {``, -1, nil}, 797 {`.`, -1, []string{`.`}}, 798 {`..\`, -1, []string{`..\`}}, 799 {`c:\:..\`, -1, []string{`c:\`, `..\`}}, 800 {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, 801 802 // Cover directories with one-character name 803 {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, 804 } { 805 res := volumeSplitN(x.input, x.n) 806 if len(res) < len(x.expected) { 807 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 808 } 809 for i, e := range res { 810 if e != x.expected[i] { 811 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 812 } 813 } 814 } 815 } 816 817 func TestValidateDevice(t *testing.T) { 818 valid := []string{ 819 "/home", 820 "/home:/home", 821 "/home:/something/else", 822 "/with space", 823 "/home:/with space", 824 "relative:/absolute-path", 825 "hostPath:/containerPath:r", 826 "/hostPath:/containerPath:rw", 827 "/hostPath:/containerPath:mrw", 828 } 829 invalid := map[string]string{ 830 "": "bad format for path: ", 831 "./": "./ is not an absolute path", 832 "../": "../ is not an absolute path", 833 "/:../": "../ is not an absolute path", 834 "/:path": "path is not an absolute path", 835 ":": "bad format for path: :", 836 "/tmp:": " is not an absolute path", 837 ":test": "bad format for path: :test", 838 ":/test": "bad format for path: :/test", 839 "tmp:": " is not an absolute path", 840 ":test:": "bad format for path: :test:", 841 "::": "bad format for path: ::", 842 ":::": "bad format for path: :::", 843 "/tmp:::": "bad format for path: /tmp:::", 844 ":/tmp::": "bad format for path: :/tmp::", 845 "path:ro": "ro is not an absolute path", 846 "path:rr": "rr is not an absolute path", 847 "a:/b:ro": "bad mode specified: ro", 848 "a:/b:rr": "bad mode specified: rr", 849 } 850 851 for _, path := range valid { 852 if _, err := validateDevice(path); err != nil { 853 t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) 854 } 855 } 856 857 for path, expectedError := range invalid { 858 if _, err := validateDevice(path); err == nil { 859 t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) 860 } else { 861 if err.Error() != expectedError { 862 t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) 863 } 864 } 865 } 866 }