github.com/hms58/moby@v1.13.1/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 TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { 670 // UTF8 with BOM 671 config, _, _, err := parseRun([]string{"--env-file=fixtures/utf8.env", "img", "cmd"}) 672 if err != nil { 673 t.Fatal(err) 674 } 675 env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} 676 if len(config.Env) != len(env) { 677 t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) 678 } 679 for i, v := range env { 680 if config.Env[i] != v { 681 t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) 682 } 683 } 684 685 // UTF16 with BOM 686 e := "contains invalid utf8 bytes at line" 687 if _, _, _, err := parseRun([]string{"--env-file=fixtures/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 688 t.Fatalf("Expected an error with message '%s', got %v", e, err) 689 } 690 // UTF16BE with BOM 691 if _, _, _, err := parseRun([]string{"--env-file=fixtures/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { 692 t.Fatalf("Expected an error with message '%s', got %v", e, err) 693 } 694 } 695 696 func TestParseLabelfileVariables(t *testing.T) { 697 e := "open nonexistent: no such file or directory" 698 if runtime.GOOS == "windows" { 699 e = "open nonexistent: The system cannot find the file specified." 700 } 701 // label ko 702 if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { 703 t.Fatalf("Expected an error with message '%s', got %v", e, err) 704 } 705 // label ok 706 config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) 707 if err != nil { 708 t.Fatal(err) 709 } 710 if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { 711 t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) 712 } 713 config, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"}) 714 if err != nil { 715 t.Fatal(err) 716 } 717 if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { 718 t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) 719 } 720 } 721 722 func TestParseEntryPoint(t *testing.T) { 723 config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) 724 if err != nil { 725 t.Fatal(err) 726 } 727 if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { 728 t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) 729 } 730 } 731 732 func TestValidateLink(t *testing.T) { 733 valid := []string{ 734 "name", 735 "dcdfbe62ecd0:alias", 736 "7a67485460b7642516a4ad82ecefe7f57d0c4916f530561b71a50a3f9c4e33da", 737 "angry_torvalds:linus", 738 } 739 invalid := map[string]string{ 740 "": "empty string specified for links", 741 "too:much:of:it": "bad format for links: too:much:of:it", 742 } 743 744 for _, link := range valid { 745 if _, err := ValidateLink(link); err != nil { 746 t.Fatalf("ValidateLink(`%q`) should succeed: error %q", link, err) 747 } 748 } 749 750 for link, expectedError := range invalid { 751 if _, err := ValidateLink(link); err == nil { 752 t.Fatalf("ValidateLink(`%q`) should have failed validation", link) 753 } else { 754 if !strings.Contains(err.Error(), expectedError) { 755 t.Fatalf("ValidateLink(`%q`) error should contain %q", link, expectedError) 756 } 757 } 758 } 759 } 760 761 func TestParseLink(t *testing.T) { 762 name, alias, err := ParseLink("name:alias") 763 if err != nil { 764 t.Fatalf("Expected not to error out on a valid name:alias format but got: %v", err) 765 } 766 if name != "name" { 767 t.Fatalf("Link name should have been name, got %s instead", name) 768 } 769 if alias != "alias" { 770 t.Fatalf("Link alias should have been alias, got %s instead", alias) 771 } 772 // short format definition 773 name, alias, err = ParseLink("name") 774 if err != nil { 775 t.Fatalf("Expected not to error out on a valid name only format but got: %v", err) 776 } 777 if name != "name" { 778 t.Fatalf("Link name should have been name, got %s instead", name) 779 } 780 if alias != "name" { 781 t.Fatalf("Link alias should have been name, got %s instead", alias) 782 } 783 // empty string link definition is not allowed 784 if _, _, err := ParseLink(""); err == nil || !strings.Contains(err.Error(), "empty string specified for links") { 785 t.Fatalf("Expected error 'empty string specified for links' but got: %v", err) 786 } 787 // more than two colons are not allowed 788 if _, _, err := ParseLink("link:alias:wrong"); err == nil || !strings.Contains(err.Error(), "bad format for links: link:alias:wrong") { 789 t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err) 790 } 791 } 792 793 func TestValidateDevice(t *testing.T) { 794 valid := []string{ 795 "/home", 796 "/home:/home", 797 "/home:/something/else", 798 "/with space", 799 "/home:/with space", 800 "relative:/absolute-path", 801 "hostPath:/containerPath:r", 802 "/hostPath:/containerPath:rw", 803 "/hostPath:/containerPath:mrw", 804 } 805 invalid := map[string]string{ 806 "": "bad format for path: ", 807 "./": "./ is not an absolute path", 808 "../": "../ is not an absolute path", 809 "/:../": "../ is not an absolute path", 810 "/:path": "path is not an absolute path", 811 ":": "bad format for path: :", 812 "/tmp:": " is not an absolute path", 813 ":test": "bad format for path: :test", 814 ":/test": "bad format for path: :/test", 815 "tmp:": " is not an absolute path", 816 ":test:": "bad format for path: :test:", 817 "::": "bad format for path: ::", 818 ":::": "bad format for path: :::", 819 "/tmp:::": "bad format for path: /tmp:::", 820 ":/tmp::": "bad format for path: :/tmp::", 821 "path:ro": "ro is not an absolute path", 822 "path:rr": "rr is not an absolute path", 823 "a:/b:ro": "bad mode specified: ro", 824 "a:/b:rr": "bad mode specified: rr", 825 } 826 827 for _, path := range valid { 828 if _, err := ValidateDevice(path); err != nil { 829 t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) 830 } 831 } 832 833 for path, expectedError := range invalid { 834 if _, err := ValidateDevice(path); err == nil { 835 t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) 836 } else { 837 if err.Error() != expectedError { 838 t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) 839 } 840 } 841 } 842 } 843 844 func TestVolumeSplitN(t *testing.T) { 845 for _, x := range []struct { 846 input string 847 n int 848 expected []string 849 }{ 850 {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, 851 {`:C:\foo:d:`, -1, nil}, 852 {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, 853 {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, 854 {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, 855 856 {`d:\`, -1, []string{`d:\`}}, 857 {`d:`, -1, []string{`d:`}}, 858 {`d:\path`, -1, []string{`d:\path`}}, 859 {`d:\path with space`, -1, []string{`d:\path with space`}}, 860 {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, 861 {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, 862 {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, 863 {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, 864 {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, 865 {`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`}}, 866 {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, 867 {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, 868 {`name:D:`, -1, []string{`name`, `D:`}}, 869 {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, 870 {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, 871 {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, 872 {`c:\Windows`, -1, []string{`c:\Windows`}}, 873 {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, 874 875 {``, -1, nil}, 876 {`.`, -1, []string{`.`}}, 877 {`..\`, -1, []string{`..\`}}, 878 {`c:\:..\`, -1, []string{`c:\`, `..\`}}, 879 {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, 880 881 // Cover directories with one-character name 882 {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, 883 } { 884 res := volumeSplitN(x.input, x.n) 885 if len(res) < len(x.expected) { 886 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 887 } 888 for i, e := range res { 889 if e != x.expected[i] { 890 t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) 891 } 892 } 893 } 894 }