github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/e2e/commands_test.go (about) 1 package e2e 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "os/user" 8 "path/filepath" 9 "regexp" 10 "strings" 11 "testing" 12 13 "github.com/deislabs/cnab-go/credentials" 14 "github.com/docker/app/internal" 15 "github.com/docker/app/internal/image" 16 "github.com/docker/app/internal/yaml" 17 "gotest.tools/assert" 18 is "gotest.tools/assert/cmp" 19 "gotest.tools/fs" 20 "gotest.tools/golden" 21 "gotest.tools/icmd" 22 ) 23 24 func TestExitErrorCode(t *testing.T) { 25 cmd, cleanup := dockerCli.createTestCmd() 26 defer cleanup() 27 28 cmd.Command = dockerCli.Command("app", "unknown_command") 29 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 30 ExitCode: 1, 31 Err: "\"unknown_command\" is not a docker app command\nSee 'docker app --help'", 32 }) 33 } 34 35 func TestRender(t *testing.T) { 36 appsPath := filepath.Join("testdata", "render") 37 apps, err := ioutil.ReadDir(appsPath) 38 assert.NilError(t, err, "unable to get apps") 39 for _, app := range apps { 40 appPath := filepath.Join(appsPath, app.Name()) 41 t.Run(app.Name(), testRenderApp(appPath)) 42 } 43 } 44 45 func testRenderApp(appPath string, env ...string) func(*testing.T) { 46 return func(t *testing.T) { 47 cmd, cleanup := dockerCli.createTestCmd() 48 defer cleanup() 49 dir := fs.NewDir(t, "") 50 defer dir.Remove() 51 52 // Build the App 53 cmd.Command = dockerCli.Command("app", "build", ".", "--file", filepath.Join(appPath, "my.dockerapp"), "--tag", "a-simple-tag", "--no-resolve-image") 54 icmd.RunCmd(cmd).Assert(t, icmd.Success) 55 56 // Render the App 57 envParameters := map[string]string{} 58 data, err := ioutil.ReadFile(filepath.Join(appPath, "env.yml")) 59 assert.NilError(t, err) 60 assert.NilError(t, yaml.Unmarshal(data, &envParameters)) 61 args := dockerCli.Command("app", "image", "render", "a-simple-tag", "--parameters-file", filepath.Join(appPath, "parameters-0.yml")) 62 for k, v := range envParameters { 63 args = append(args, "--set", fmt.Sprintf("%s=%s", k, v)) 64 } 65 cmd.Command = args 66 cmd.Env = append(cmd.Env, env...) 67 t.Run("stdout", func(t *testing.T) { 68 result := icmd.RunCmd(cmd).Assert(t, icmd.Success) 69 expected := readFile(t, filepath.Join(appPath, "expected.txt")) 70 actual := result.Stdout() 71 assert.Assert(t, is.Equal(expected, actual), "rendering mismatch") 72 }) 73 t.Run("file", func(t *testing.T) { 74 cmd.Command = append(cmd.Command, "--output="+dir.Join("actual.yaml")) 75 icmd.RunCmd(cmd).Assert(t, icmd.Success) 76 expected := readFile(t, filepath.Join(appPath, "expected.txt")) 77 actual := readFile(t, dir.Join("actual.yaml")) 78 assert.Assert(t, is.Equal(expected, actual), "rendering mismatch") 79 }) 80 } 81 } 82 83 func TestRenderAppNotFound(t *testing.T) { 84 cmd, cleanup := dockerCli.createTestCmd() 85 defer cleanup() 86 87 appName := "non_existing_app:some_tag" 88 cmd.Command = dockerCli.Command("app", "image", "render", appName) 89 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1}).Combined(), 90 []string{fmt.Sprintf("could not render %q: no such App image", appName)}) 91 } 92 93 func TestRenderFormatters(t *testing.T) { 94 runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { 95 cmd := info.configuredCmd 96 97 contextPath := filepath.Join("testdata", "simple") 98 cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-tag", "--no-resolve-image", contextPath) 99 icmd.RunCmd(cmd).Assert(t, icmd.Success) 100 101 cmd.Command = dockerCli.Command("app", "image", "render", "--formatter", "json", "a-simple-tag") 102 result := icmd.RunCmd(cmd).Assert(t, icmd.Success) 103 golden.Assert(t, result.Stdout(), "expected-json-render.golden") 104 105 cmd.Command = dockerCli.Command("app", "image", "render", "--formatter", "yaml", "a-simple-tag") 106 result = icmd.RunCmd(cmd).Assert(t, icmd.Success) 107 golden.Assert(t, result.Stdout(), "expected-yaml-render.golden") 108 }) 109 } 110 111 func checkFileWarning(t *testing.T, goldenFile, composeData string) { 112 cmd, cleanup := dockerCli.createTestCmd() 113 defer cleanup() 114 115 tmpDir := fs.NewDir(t, "app_input", 116 fs.WithFile(internal.ComposeFileName, composeData), 117 ) 118 defer tmpDir.Remove() 119 120 cmd.Dir = tmpDir.Path() 121 cmd.Command = dockerCli.Command("app", "init", "app-test", 122 "--compose-file", tmpDir.Join(internal.ComposeFileName)) 123 stdErr := icmd.RunCmd(cmd).Assert(t, icmd.Success).Stderr() 124 golden.Assert(t, stdErr, goldenFile) 125 } 126 127 func TestInitWarningEnvFiles(t *testing.T) { 128 testCases := []struct { 129 name string 130 golden string 131 compose string 132 }{ 133 { 134 name: "initWarningSingleEnvFileTest", 135 golden: "init-output-warning-single-envfile.golden", 136 compose: `version: "3.2" 137 services: 138 nginx: 139 image: nginx:latest 140 env_file: myenv1.env`, 141 }, 142 { 143 name: "initWarningMultipleEnvFilesTest", 144 golden: "init-output-warning-multiple-envfiles.golden", 145 compose: `version: "3.2" 146 services: 147 nginx: 148 image: nginx:latest 149 env_file: 150 - myenv1.env 151 - myenv2.env`, 152 }, 153 { 154 name: "initNoEnvFilesTest", 155 golden: "init-output-no-envfile.golden", 156 compose: `version: "3.2" 157 services: 158 nginx: 159 image: nginx:latest`, 160 }, 161 } 162 for _, test := range testCases { 163 t.Run(test.name, func(t *testing.T) { 164 checkFileWarning(t, test.golden, test.compose) 165 }) 166 } 167 } 168 169 func TestInit(t *testing.T) { 170 cmd, cleanup := dockerCli.createTestCmd() 171 defer cleanup() 172 173 userData, _ := user.Current() 174 currentUser := "" 175 if userData != nil { 176 currentUser = userData.Username 177 } 178 179 composeData := `version: "3.2" 180 services: 181 nginx: 182 image: nginx:latest 183 command: nginx $NGINX_ARGS ${NGINX_DRY_RUN} 184 ` 185 meta := fmt.Sprintf(`# Version of the application 186 version: 0.1.0 187 # Name of the application 188 name: app-test 189 # A short description of the application 190 description: 191 # List of application maintainers with name and email for each 192 maintainers: 193 - name: %s 194 email: 195 `, currentUser) 196 197 envData := "# some comment\nNGINX_DRY_RUN=-t" 198 tmpDir := fs.NewDir(t, "app_input", 199 fs.WithFile(internal.ComposeFileName, composeData), 200 fs.WithFile(".env", envData), 201 ) 202 defer tmpDir.Remove() 203 204 testAppName := "app-test" 205 dirName := internal.DirNameFromAppName(testAppName) 206 207 cmd.Dir = tmpDir.Path() 208 cmd.Command = dockerCli.Command("app", 209 "init", testAppName, 210 "--compose-file", tmpDir.Join(internal.ComposeFileName)) 211 stdOut := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() 212 golden.Assert(t, stdOut, "init-output.golden") 213 214 manifest := fs.Expected( 215 t, 216 fs.WithMode(0755), 217 fs.WithFile(internal.MetadataFileName, meta, fs.WithMode(0644)), // too many variables, cheating 218 fs.WithFile(internal.ComposeFileName, composeData, fs.WithMode(0644)), 219 fs.WithFile(internal.ParametersFileName, "NGINX_ARGS: FILL ME\nNGINX_DRY_RUN: -t\n", fs.WithMode(0644)), 220 ) 221 assert.Assert(t, fs.Equal(tmpDir.Join(dirName), manifest)) 222 223 // validate metadata with JSON Schema 224 cmd.Command = dockerCli.Command("app", "validate", testAppName) 225 stdOut = icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() 226 golden.Assert(t, stdOut, "validate-output.golden") 227 } 228 229 func TestInitWithInvalidCompose(t *testing.T) { 230 cmd, cleanup := dockerCli.createTestCmd() 231 defer cleanup() 232 composePath := filepath.Join("testdata", "invalid-compose", "docker-compose.yml") 233 234 cmd.Command = dockerCli.Command("app", "init", "invalid", "--compose-file", composePath) 235 stdOut := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 236 ExitCode: 1, 237 }).Combined() 238 golden.Assert(t, stdOut, "init-invalid-output.golden") 239 } 240 241 func TestInspectApp(t *testing.T) { 242 runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { 243 cmd := info.configuredCmd 244 245 // cwd = e2e 246 dir := fs.NewDir(t, "detect-app-binary", 247 fs.WithDir("attachments.dockerapp", fs.FromDir("testdata/attachments.dockerapp"))) 248 defer dir.Remove() 249 250 cmd.Command = dockerCli.Command("app", "image", "inspect") 251 cmd.Dir = dir.Path() 252 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 253 ExitCode: 1, 254 Err: `"docker app image inspect" requires exactly 1 argument.`, 255 }) 256 257 contextPath := filepath.Join("testdata", "simple") 258 cmd.Command = dockerCli.Command("app", "build", "--tag", "simple-app:1.0.0", "--no-resolve-image", contextPath) 259 cmd.Dir = "" 260 icmd.RunCmd(cmd).Assert(t, icmd.Success) 261 262 cmd.Command = dockerCli.Command("app", "image", "inspect", "simple-app:1.0.0") 263 cmd.Dir = dir.Path() 264 output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() 265 golden.Assert(t, output, "app-inspect.golden") 266 }) 267 } 268 269 func TestRunOnlyOne(t *testing.T) { 270 cmd, cleanup := dockerCli.createTestCmd() 271 defer cleanup() 272 273 cmd.Command = dockerCli.Command("app", "run") 274 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 275 ExitCode: 1, 276 Err: `"docker app run" requires exactly 1 argument.`, 277 }) 278 279 cmd.Command = dockerCli.Command("app", "run", "--cnab-bundle-json", image.BundleFilename, "myapp") 280 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 281 ExitCode: 1, 282 Err: `"docker app run" cannot run a bundle and an App image`, 283 }) 284 } 285 286 func TestRunWithLabels(t *testing.T) { 287 runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { 288 cmd := info.configuredCmd 289 290 contextPath := filepath.Join("testdata", "simple") 291 cmd.Command = dockerCli.Command("app", "build", "--tag", "myapp", contextPath) 292 icmd.RunCmd(cmd).Assert(t, icmd.Success) 293 294 cmd.Command = dockerCli.Command("app", "run", "myapp", "--name", "myapp", "--label", "label.key=labelValue") 295 icmd.RunCmd(cmd).Assert(t, icmd.Success) 296 297 services := []string{ 298 "myapp_db", "myapp_web", "myapp_api", 299 } 300 for _, service := range services { 301 cmd.Command = dockerCli.Command("inspect", service) 302 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 303 ExitCode: 0, 304 Out: `"label.key": "labelValue"`, 305 }) 306 } 307 }) 308 } 309 310 func TestDockerAppLifecycle(t *testing.T) { 311 runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { 312 cmd := info.configuredCmd 313 appName := strings.ToLower(strings.Replace(t.Name(), "/", "_", 1)) 314 tmpDir := fs.NewDir(t, appName) 315 defer tmpDir.Remove() 316 317 cmd.Command = dockerCli.Command("app", "build", "--tag", appName, "testdata/simple") 318 icmd.RunCmd(cmd).Assert(t, icmd.Success) 319 320 // Install an illformed Docker Application Package 321 cmd.Command = dockerCli.Command("app", "run", appName, "--set", "web_port=-1", "--name", appName) 322 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 323 ExitCode: 1, 324 Err: "error decoding 'Ports': Invalid hostPort: -1", 325 }) 326 327 // List the installation and check the failed status 328 cmd.Command = dockerCli.Command("app", "ls") 329 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), 330 []string{ 331 `RUNNING APP\s+APP NAME\s+SERVICES\s+LAST ACTION\s+RESULT\s+CREATED\s+MODIFIED\s+REFERENCE`, 332 fmt.Sprintf(`%s\s+simple \(1.1.0-beta1\)\s+0/3\s+install\s+failure\s+.+second[s]?\sago\s+.+second[s]?\sago\s+`, appName), 333 }) 334 335 // Upgrading a failed installation is not allowed 336 cmd.Command = dockerCli.Command("app", "update", appName) 337 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 338 ExitCode: 1, 339 Err: fmt.Sprintf("Running App %q cannot be updated, please use 'docker app run' instead", appName), 340 }) 341 342 // Install a Docker Application Package with an existing failed installation is fine 343 cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName) 344 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), expectedAppRunOutput(appName, true)) 345 assertAppDbLabels(t, &cmd, appName) 346 347 // List the installed application 348 cmd.Command = dockerCli.Command("app", "ls") 349 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), 350 []string{ 351 `RUNNING APP\s+APP NAME\s+SERVICES\s+LAST ACTION\s+RESULT\s+CREATED\s+MODIFIED\s+REFERENCE`, 352 fmt.Sprintf(`%s\s+simple \(1.1.0-beta1\)\s+\d/3\s+install\s+success\s+.+second[s]?\sago\s+.+second[s]?\sago\s+`, appName), 353 }) 354 355 // Installing again the same application is forbidden 356 cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName) 357 icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 358 ExitCode: 1, 359 Err: fmt.Sprintf("Installation %q already exists, use 'docker app update' instead", appName), 360 }) 361 362 // Update the application, changing the port 363 cmd.Command = dockerCli.Command("app", "update", appName, "--set", "web_port=8081") 364 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), 365 []string{ 366 fmt.Sprintf("Updating service %s_db", appName), 367 fmt.Sprintf("Updating service %s_api", appName), 368 fmt.Sprintf("Updating service %s_web", appName), 369 }) 370 assertAppDbLabels(t, &cmd, appName) 371 372 // Uninstall the application 373 cmd.Command = dockerCli.Command("app", "rm", appName) 374 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), expectedAppRmOutput(appName)) 375 }) 376 } 377 378 func TestDockerAppLifecycleMultiRm(t *testing.T) { 379 runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { 380 cmd := info.configuredCmd 381 appName := strings.ToLower(strings.Replace(t.Name(), "/", "_", 1)) 382 tmpDir := fs.NewDir(t, appName) 383 defer tmpDir.Remove() 384 385 cmd.Command = dockerCli.Command("app", "build", "--tag", appName, "testdata/simple") 386 icmd.RunCmd(cmd).Assert(t, icmd.Success) 387 388 // Install multiple applications 389 cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName) 390 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), expectedAppRunOutput(appName, false)) 391 assertAppDbLabels(t, &cmd, appName) 392 appName2 := appName + "2" 393 cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName2, "--set", "web_port=8083") 394 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), expectedAppRunOutput(appName2, false)) 395 assertAppDbLabels(t, &cmd, appName2) 396 397 // Uninstall multiple applications 398 cmd.Command = dockerCli.Command("app", "rm", appName, appName2) 399 checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), append(expectedAppRmOutput(appName), expectedAppRmOutput(appName2)...)) 400 }) 401 } 402 403 func expectedAppRunOutput(appName string, prevFailed bool) []string { 404 expected := []string{} 405 if prevFailed { 406 expected = append(expected, fmt.Sprintf("WARNING: installing over previously failed installation %q", appName)) 407 } 408 expected = append(expected, 409 fmt.Sprintf("Creating network %s_back", appName), 410 fmt.Sprintf("Creating network %s_front", appName), 411 fmt.Sprintf("Creating service %s_db", appName), 412 fmt.Sprintf("Creating service %s_api", appName), 413 fmt.Sprintf("Creating service %s_web", appName), 414 ) 415 return expected 416 } 417 418 func expectedAppRmOutput(appName string) []string { 419 return []string{ 420 fmt.Sprintf("Removing service %s_api", appName), 421 fmt.Sprintf("Removing service %s_db", appName), 422 fmt.Sprintf("Removing service %s_web", appName), 423 fmt.Sprintf("Removing network %s_front", appName), 424 fmt.Sprintf("Removing network %s_back", appName), 425 } 426 } 427 428 func TestCredentials(t *testing.T) { 429 credSet := &credentials.CredentialSet{ 430 Name: "test-creds", 431 Credentials: []credentials.CredentialStrategy{ 432 { 433 Name: "secret1", 434 Source: credentials.Source{ 435 Value: "secret1value", 436 }, 437 }, 438 { 439 Name: "secret2", 440 Source: credentials.Source{ 441 Value: "secret2value", 442 }, 443 }, 444 }, 445 } 446 // Create a tmp dir with a credential store 447 cmd, cleanup := dockerCli.createTestCmd( 448 withCredentialSet(t, "default", credSet), 449 ) 450 defer cleanup() 451 // Create a local credentialSet 452 453 buf, err := json.Marshal(credSet) 454 assert.NilError(t, err) 455 bundleJSON := golden.Get(t, "credential-install-bundle.json") 456 tmpDir := fs.NewDir(t, t.Name(), 457 fs.WithFile(image.BundleFilename, "", fs.WithBytes(bundleJSON)), 458 fs.WithDir("local", 459 fs.WithFile("test-creds.yaml", "", fs.WithBytes(buf)), 460 ), 461 ) 462 defer tmpDir.Remove() 463 464 bundle := tmpDir.Join(image.BundleFilename) 465 466 t.Run("missing", func(t *testing.T) { 467 cmd.Command = dockerCli.Command( 468 "app", "run", 469 "--credential", "secret1=foo", 470 // secret2 deliberately omitted. 471 "--credential", "secret3=baz", 472 "--name", "missing", 473 "--cnab-bundle-json", bundle, 474 ) 475 result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 476 ExitCode: 1, 477 Out: icmd.None, 478 }) 479 golden.Assert(t, result.Stderr(), "credential-install-missing.golden") 480 }) 481 482 t.Run("full", func(t *testing.T) { 483 cmd.Command = dockerCli.Command( 484 "app", "run", 485 "--credential", "secret1=foo", 486 "--credential", "secret2=bar", 487 "--credential", "secret3=baz", 488 "--name", "full", 489 "--cnab-bundle-json", bundle, 490 ) 491 result := icmd.RunCmd(cmd).Assert(t, icmd.Success) 492 golden.Assert(t, result.Stdout(), "credential-install-full.golden") 493 }) 494 495 t.Run("mixed-credstore", func(t *testing.T) { 496 cmd.Command = dockerCli.Command( 497 "app", "run", 498 "--credential-set", "test-creds", 499 "--credential", "secret3=xyzzy", 500 "--name", "mixed-credstore", 501 "--cnab-bundle-json", bundle, 502 ) 503 result := icmd.RunCmd(cmd).Assert(t, icmd.Success) 504 golden.Assert(t, result.Stdout(), "credential-install-mixed-credstore.golden") 505 }) 506 507 t.Run("mixed-local-cred", func(t *testing.T) { 508 cmd.Command = dockerCli.Command( 509 "app", "run", 510 "--credential-set", tmpDir.Join("local", "test-creds.yaml"), 511 "--credential", "secret3=xyzzy", 512 "--name", "mixed-local-cred", 513 "--cnab-bundle-json", bundle, 514 ) 515 result := icmd.RunCmd(cmd).Assert(t, icmd.Success) 516 stdout := result.Stdout() 517 golden.Assert(t, stdout, "credential-install-mixed-local-cred.golden") 518 }) 519 520 t.Run("overload", func(t *testing.T) { 521 cmd.Command = dockerCli.Command( 522 "app", "run", 523 "--credential-set", "test-creds", 524 "--credential", "secret1=overload", 525 "--credential", "secret3=xyzzy", 526 "--name", "overload", 527 "--cnab-bundle-json", bundle, 528 ) 529 result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ 530 ExitCode: 1, 531 Out: icmd.None, 532 }) 533 golden.Assert(t, result.Stderr(), "credential-install-overload.golden") 534 }) 535 } 536 537 func assertAppDbLabels(t *testing.T, cmd *icmd.Cmd, appName string) { 538 cmd.Command = dockerCli.Command("inspect", fmt.Sprintf("%s_db", appName)) 539 checkContains(t, icmd.RunCmd(*cmd).Assert(t, icmd.Success).Combined(), 540 []string{ 541 fmt.Sprintf(`"%s": "%s"`, internal.LabelAppNamespace, appName), 542 fmt.Sprintf(`"%s": ".+"`, internal.LabelAppVersion), 543 }) 544 } 545 546 func checkContains(t *testing.T, combined string, expectedLines []string) { 547 for _, expected := range expectedLines { 548 exp := regexp.MustCompile(expected) 549 assert.Assert(t, exp.MatchString(combined), "expected %q != actual %q", expected, combined) 550 } 551 }