github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/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 if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { 415 t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err) 416 } 417 // shm-size ok 418 _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) 419 if err != nil { 420 t.Fatal(err) 421 } 422 if hostconfig.ShmSize != 134217728 { 423 t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) 424 } 425 } 426 427 func TestParseRestartPolicy(t *testing.T) { 428 invalids := map[string]string{ 429 "always:2:3": "invalid restart policy format", 430 "on-failure:invalid": "maximum retry count must be an integer", 431 } 432 valids := map[string]container.RestartPolicy{ 433 "": {}, 434 "always": { 435 Name: "always", 436 MaximumRetryCount: 0, 437 }, 438 "on-failure:1": { 439 Name: "on-failure", 440 MaximumRetryCount: 1, 441 }, 442 } 443 for restart, expectedError := range invalids { 444 if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { 445 t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) 446 } 447 } 448 for restart, expected := range valids { 449 _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) 450 if err != nil { 451 t.Fatal(err) 452 } 453 if hostconfig.RestartPolicy != expected { 454 t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) 455 } 456 } 457 } 458 459 func TestParseRestartPolicyAutoRemove(t *testing.T) { 460 expected := "Conflicting options: --restart and --rm" 461 _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) 462 if err == nil || err.Error() != expected { 463 t.Fatalf("Expected error %v, but got none", expected) 464 } 465 } 466 467 func TestParseHealth(t *testing.T) { 468 checkOk := func(args ...string) *container.HealthConfig { 469 config, _, _, err := parseRun(args) 470 if err != nil { 471 t.Fatalf("%#v: %v", args, err) 472 } 473 return config.Healthcheck 474 } 475 checkError := func(expected string, args ...string) { 476 config, _, _, err := parseRun(args) 477 if err == nil { 478 t.Fatalf("Expected error, but got %#v", config) 479 } 480 if err.Error() != expected { 481 t.Fatalf("Expected %#v, got %#v", expected, err) 482 } 483 } 484 health := checkOk("--no-healthcheck", "img", "cmd") 485 if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { 486 t.Fatalf("--no-healthcheck failed: %#v", health) 487 } 488 489 health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") 490 if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { 491 t.Fatalf("--health-cmd: got %#v", health.Test) 492 } 493 if health.Timeout != 0 { 494 t.Fatalf("--health-cmd: timeout = %f", health.Timeout) 495 } 496 497 checkError("--no-healthcheck conflicts with --health-* options", 498 "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") 499 500 health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "img", "cmd") 501 if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond { 502 t.Fatalf("--health-*: got %#v", health) 503 } 504 } 505 506 func TestParseLoggingOpts(t *testing.T) { 507 // logging opts ko 508 if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { 509 t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) 510 } 511 // logging opts ok 512 _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) 513 if err != nil { 514 t.Fatal(err) 515 } 516 if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { 517 t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) 518 } 519 } 520 521 func TestParseEnvfileVariables(t *testing.T) { 522 e := "open nonexistent: no such file or directory" 523 if runtime.GOOS == "windows" { 524 e = "open nonexistent: The system cannot find the file specified." 525 } 526 // env ko 527 if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 528 t.Fatalf("Expected an error with message '%s', got %v", e, err) 529 } 530 // env ok 531 config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) 532 if err != nil { 533 t.Fatal(err) 534 } 535 if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { 536 t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) 537 } 538 config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) 539 if err != nil { 540 t.Fatal(err) 541 } 542 if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { 543 t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) 544 } 545 } 546 547 func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { 548 // UTF8 with BOM 549 config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) 550 if err != nil { 551 t.Fatal(err) 552 } 553 env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} 554 if len(config.Env) != len(env) { 555 t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) 556 } 557 for i, v := range env { 558 if config.Env[i] != v { 559 t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) 560 } 561 } 562 563 // UTF16 with BOM 564 e := "contains invalid utf8 bytes at line" 565 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 566 t.Fatalf("Expected an error with message '%s', got %v", e, err) 567 } 568 // UTF16BE with BOM 569 if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 570 t.Fatalf("Expected an error with message '%s', got %v", e, err) 571 } 572 } 573 574 func TestParseLabelfileVariables(t *testing.T) { 575 e := "open nonexistent: no such file or directory" 576 if runtime.GOOS == "windows" { 577 e = "open nonexistent: The system cannot find the file specified." 578 } 579 // label ko 580 if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 581 t.Fatalf("Expected an error with message '%s', got %v", e, err) 582 } 583 // label ok 584 config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) 585 if err != nil { 586 t.Fatal(err) 587 } 588 if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { 589 t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) 590 } 591 config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) 592 if err != nil { 593 t.Fatal(err) 594 } 595 if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { 596 t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) 597 } 598 } 599 600 func TestParseEntryPoint(t *testing.T) { 601 config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) 602 if err != nil { 603 t.Fatal(err) 604 } 605 if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { 606 t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) 607 } 608 } 609 610 // This tests the cases for binds which are generated through 611 // DecodeContainerConfig rather than Parse() 612 func TestDecodeContainerConfigVolumes(t *testing.T) { 613 614 // Root to root 615 bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) 616 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 617 t.Fatalf("binds %v should have failed", bindsOrVols) 618 } 619 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 620 t.Fatalf("volume %v should have failed", bindsOrVols) 621 } 622 623 // No destination path 624 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) 625 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 626 t.Fatalf("binds %v should have failed", bindsOrVols) 627 } 628 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 629 t.Fatalf("volume %v should have failed", bindsOrVols) 630 } 631 632 // // No destination path or mode 633 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) 634 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 635 t.Fatalf("binds %v should have failed", bindsOrVols) 636 } 637 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 638 t.Fatalf("volume %v should have failed", bindsOrVols) 639 } 640 641 // A whole lot of nothing 642 bindsOrVols = []string{`:`} 643 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 644 t.Fatalf("binds %v should have failed", bindsOrVols) 645 } 646 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 647 t.Fatalf("volume %v should have failed", bindsOrVols) 648 } 649 650 // A whole lot of nothing with no mode 651 bindsOrVols = []string{`::`} 652 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 653 t.Fatalf("binds %v should have failed", bindsOrVols) 654 } 655 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 656 t.Fatalf("volume %v should have failed", bindsOrVols) 657 } 658 659 // Too much including an invalid mode 660 wTmp := os.Getenv("TEMP") 661 bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) 662 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 663 t.Fatalf("binds %v should have failed", bindsOrVols) 664 } 665 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 666 t.Fatalf("volume %v should have failed", bindsOrVols) 667 } 668 669 // Windows specific error tests 670 if runtime.GOOS == "windows" { 671 // Volume which does not include a drive letter 672 bindsOrVols = []string{`\tmp`} 673 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 674 t.Fatalf("binds %v should have failed", bindsOrVols) 675 } 676 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 677 t.Fatalf("volume %v should have failed", bindsOrVols) 678 } 679 680 // Root to C-Drive 681 bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} 682 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 683 t.Fatalf("binds %v should have failed", bindsOrVols) 684 } 685 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 686 t.Fatalf("volume %v should have failed", bindsOrVols) 687 } 688 689 // Container path that does not include a drive letter 690 bindsOrVols = []string{`c:\windows:\somewhere`} 691 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 692 t.Fatalf("binds %v should have failed", bindsOrVols) 693 } 694 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 695 t.Fatalf("volume %v should have failed", bindsOrVols) 696 } 697 } 698 699 // Linux-specific error tests 700 if runtime.GOOS != "windows" { 701 // Just root 702 bindsOrVols = []string{`/`} 703 if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { 704 t.Fatalf("binds %v should have failed", bindsOrVols) 705 } 706 if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { 707 t.Fatalf("volume %v should have failed", bindsOrVols) 708 } 709 710 // A single volume that looks like a bind mount passed in Volumes. 711 // This should be handled as a bind mount, not a volume. 712 vols := []string{`/foo:/bar`} 713 if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { 714 t.Fatal("Volume /foo:/bar should have succeeded as a volume name") 715 } else if hostConfig.Binds != nil { 716 t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) 717 } else if _, exists := config.Volumes[vols[0]]; !exists { 718 t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) 719 } 720 721 } 722 } 723 724 // callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes 725 // to call DecodeContainerConfig. It effectively does what a client would 726 // do when calling the daemon by constructing a JSON stream of a 727 // ContainerConfigWrapper which is populated by the set of volume specs 728 // passed into it. It returns a config and a hostconfig which can be 729 // validated to ensure DecodeContainerConfig has manipulated the structures 730 // correctly. 731 func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) { 732 var ( 733 b []byte 734 err error 735 c *container.Config 736 h *container.HostConfig 737 ) 738 w := runconfig.ContainerConfigWrapper{ 739 Config: &container.Config{ 740 Volumes: map[string]struct{}{}, 741 }, 742 HostConfig: &container.HostConfig{ 743 NetworkMode: "none", 744 Binds: binds, 745 }, 746 } 747 for _, v := range volumes { 748 w.Config.Volumes[v] = struct{}{} 749 } 750 if b, err = json.Marshal(w); err != nil { 751 return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) 752 } 753 c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) 754 if err != nil { 755 return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) 756 } 757 if c == nil || h == nil { 758 return nil, nil, fmt.Errorf("Empty config or hostconfig") 759 } 760 761 return c, h, err 762 } 763 764 func TestVolumeSplitN(t *testing.T) { 765 for _, x := range []struct { 766 input string 767 n int 768 expected []string 769 }{ 770 {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, 771 {`:C:\foo:d:`, -1, nil}, 772 {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, 773 {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, 774 {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, 775 776 {`d:\`, -1, []string{`d:\`}}, 777 {`d:`, -1, []string{`d:`}}, 778 {`d:\path`, -1, []string{`d:\path`}}, 779 {`d:\path with space`, -1, []string{`d:\path with space`}}, 780 {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, 781 {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, 782 {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, 783 {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, 784 {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, 785 {`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`}}, 786 {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, 787 {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, 788 {`name:D:`, -1, []string{`name`, `D:`}}, 789 {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, 790 {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, 791 {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, 792 {`c:\Windows`, -1, []string{`c:\Windows`}}, 793 {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, 794 795 {``, -1, nil}, 796 {`.`, -1, []string{`.`}}, 797 {`..\`, -1, []string{`..\`}}, 798 {`c:\:..\`, -1, []string{`c:\`, `..\`}}, 799 {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, 800 801 // Cover directories with one-character name 802 {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, 803 } { 804 res := volumeSplitN(x.input, x.n) 805 if len(res) < len(x.expected) { 806 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 807 } 808 for i, e := range res { 809 if e != x.expected[i] { 810 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 811 } 812 } 813 } 814 } 815 816 func TestValidateDevice(t *testing.T) { 817 valid := []string{ 818 "/home", 819 "/home:/home", 820 "/home:/something/else", 821 "/with space", 822 "/home:/with space", 823 "relative:/absolute-path", 824 "hostPath:/containerPath:r", 825 "/hostPath:/containerPath:rw", 826 "/hostPath:/containerPath:mrw", 827 } 828 invalid := map[string]string{ 829 "": "bad format for path: ", 830 "./": "./ is not an absolute path", 831 "../": "../ is not an absolute path", 832 "/:../": "../ is not an absolute path", 833 "/:path": "path is not an absolute path", 834 ":": "bad format for path: :", 835 "/tmp:": " is not an absolute path", 836 ":test": "bad format for path: :test", 837 ":/test": "bad format for path: :/test", 838 "tmp:": " is not an absolute path", 839 ":test:": "bad format for path: :test:", 840 "::": "bad format for path: ::", 841 ":::": "bad format for path: :::", 842 "/tmp:::": "bad format for path: /tmp:::", 843 ":/tmp::": "bad format for path: :/tmp::", 844 "path:ro": "ro is not an absolute path", 845 "path:rr": "rr is not an absolute path", 846 "a:/b:ro": "bad mode specified: ro", 847 "a:/b:rr": "bad mode specified: rr", 848 } 849 850 for _, path := range valid { 851 if _, err := validateDevice(path); err != nil { 852 t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) 853 } 854 } 855 856 for path, expectedError := range invalid { 857 if _, err := validateDevice(path); err == nil { 858 t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) 859 } else { 860 if err.Error() != expectedError { 861 t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) 862 } 863 } 864 } 865 }