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