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