github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/tiltfile_test.go (about) 1 package tiltfile 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "path/filepath" 9 "runtime" 10 "sort" 11 "strconv" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 appsv1 "k8s.io/api/apps/v1" 19 v1 "k8s.io/api/core/v1" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/apimachinery/pkg/labels" 22 23 "github.com/tilt-dev/clusterid" 24 tiltanalytics "github.com/tilt-dev/tilt/internal/analytics" 25 "github.com/tilt-dev/tilt/internal/container" 26 "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" 27 ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/apis/tiltfile" 28 "github.com/tilt-dev/tilt/internal/docker" 29 "github.com/tilt-dev/tilt/internal/dockercompose" 30 "github.com/tilt-dev/tilt/internal/feature" 31 "github.com/tilt-dev/tilt/internal/ignore" 32 "github.com/tilt-dev/tilt/internal/k8s" 33 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 34 "github.com/tilt-dev/tilt/internal/localexec" 35 "github.com/tilt-dev/tilt/internal/ospath" 36 "github.com/tilt-dev/tilt/internal/sliceutils" 37 "github.com/tilt-dev/tilt/internal/testutils" 38 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 39 "github.com/tilt-dev/tilt/internal/tiltfile/cisettings" 40 "github.com/tilt-dev/tilt/internal/tiltfile/config" 41 "github.com/tilt-dev/tilt/internal/tiltfile/hasher" 42 tiltfile_k8s "github.com/tilt-dev/tilt/internal/tiltfile/k8s" 43 "github.com/tilt-dev/tilt/internal/tiltfile/k8scontext" 44 "github.com/tilt-dev/tilt/internal/tiltfile/testdata" 45 "github.com/tilt-dev/tilt/internal/tiltfile/tiltextension" 46 "github.com/tilt-dev/tilt/internal/tiltfile/version" 47 "github.com/tilt-dev/tilt/internal/yaml" 48 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 49 "github.com/tilt-dev/tilt/pkg/logger" 50 "github.com/tilt-dev/tilt/pkg/model" 51 "github.com/tilt-dev/wmclient/pkg/analytics" 52 ) 53 54 type localResourceLinks []model.Link 55 type k8sResourceLinks []model.Link 56 type dcResourceLinks []model.Link 57 58 const simpleDockerfile = "FROM golang:1.10" 59 60 const simpleDockerignore = "build/" 61 62 func TestNoTiltfile(t *testing.T) { 63 f := newFixture(t) 64 65 f.loadErrString("No Tiltfile found at") 66 f.assertConfigFiles("Tiltfile") 67 } 68 69 func TestEmpty(t *testing.T) { 70 f := newFixture(t) 71 72 f.file("Tiltfile", "") 73 f.load() 74 } 75 76 func TestMissingDockerfile(t *testing.T) { 77 f := newFixture(t) 78 79 f.file("Tiltfile", ` 80 docker_build('gcr.io/foo', 'foo') 81 k8s_resource('foo', 'foo.yaml') 82 `) 83 84 f.loadErrString(filepath.Join("foo", "Dockerfile"), testutils.IsNotExistMessage(), "error reading dockerfile") 85 } 86 87 func TestCustomBuildBadMethodCall(t *testing.T) { 88 f := newFixture(t) 89 f.setupFoo() 90 f.file("Tiltfile", ` 91 hfb = custom_build( 92 'gcr.io/foo', 93 'docker build -t $TAG foo', 94 ['foo'] 95 ).asdf() 96 `) 97 98 f.loadErrString("Error: custom_build has no .asdf field or method") 99 } 100 101 func TestSimple(t *testing.T) { 102 f := newFixture(t) 103 104 f.setupFoo() 105 106 f.file("Tiltfile", ` 107 docker_build('gcr.io/foo', 'foo') 108 k8s_yaml('foo.yaml') 109 `) 110 111 f.load("foo") 112 113 m := f.assertNextManifest("foo", 114 db(image("gcr.io/foo")), 115 deployment("foo")) 116 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 117 118 iTarget := m.ImageTargetAt(0) 119 120 // Make sure there's no live update in the default case. 121 assert.True(t, iTarget.IsDockerBuild()) 122 assert.True(t, liveupdate.IsEmptySpec(iTarget.LiveUpdateSpec)) 123 } 124 125 // I.e. make sure that we handle de/normalization between `fooimage` <--> `docker.io/library/fooimage` 126 func TestLocalImageRef(t *testing.T) { 127 f := newFixture(t) 128 129 f.dockerfile("foo/Dockerfile") 130 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 131 132 f.file("Tiltfile", ` 133 134 docker_build('fooimage', 'foo') 135 k8s_yaml('foo.yaml') 136 `) 137 138 f.load() 139 140 f.assertNextManifest("foo", 141 db(image("fooimage")), 142 deployment("foo")) 143 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 144 } 145 146 func TestExplicitDockerfileIsConfigFile(t *testing.T) { 147 f := newFixture(t) 148 f.setupFoo() 149 f.dockerfile("other/Dockerfile") 150 f.file("Tiltfile", ` 151 docker_build('gcr.io/foo', 'foo', dockerfile='other/Dockerfile') 152 k8s_yaml('foo.yaml') 153 `) 154 f.load() 155 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "other/Dockerfile", "foo/.dockerignore") 156 } 157 158 func TestDockerfileNone(t *testing.T) { 159 f := newFixture(t) 160 f.setupFoo() 161 f.file("Tiltfile", ` 162 docker_build('gcr.io/foo', 'foo', dockerfile=None) 163 k8s_yaml('foo.yaml') 164 `) 165 f.load() 166 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/Dockerfile", "foo/.dockerignore") 167 } 168 169 func TestExplicitDockerfileAsLocalPath(t *testing.T) { 170 f := newFixture(t) 171 f.setupFoo() 172 f.dockerfile("other/Dockerfile") 173 f.file("Tiltfile", ` 174 r = local_git_repo('.') 175 docker_build('gcr.io/foo', 'foo', dockerfile=r.paths('other/Dockerfile')) 176 k8s_yaml('foo.yaml') 177 `) 178 f.load() 179 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "other/Dockerfile", "foo/.dockerignore") 180 } 181 182 func TestExplicitDockerfileContents(t *testing.T) { 183 f := newFixture(t) 184 f.setupFoo() 185 f.file("Tiltfile", ` 186 docker_build('gcr.io/foo', 'foo', dockerfile_contents='FROM alpine') 187 k8s_yaml('foo.yaml') 188 `) 189 f.load() 190 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/.dockerignore") 191 f.assertNextManifest("foo", db(image("gcr.io/foo"))) 192 } 193 194 func TestExplicitDockerfileContentsAsBlob(t *testing.T) { 195 f := newFixture(t) 196 f.setupFoo() 197 f.dockerfile("other/Dockerfile") 198 f.file("Tiltfile", ` 199 df = read_file('other/Dockerfile') 200 docker_build('gcr.io/foo', 'foo', dockerfile_contents=df) 201 k8s_yaml('foo.yaml') 202 `) 203 f.load() 204 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "other/Dockerfile", "foo/.dockerignore") 205 f.assertNextManifest("foo", db(image("gcr.io/foo"))) 206 } 207 208 func TestCantSpecifyDFPathAndContents(t *testing.T) { 209 f := newFixture(t) 210 f.setupFoo() 211 f.dockerfile("other/Dockerfile") 212 f.file("Tiltfile", ` 213 docker_build('gcr.io/foo', 'foo', dockerfile_contents='FROM alpine', dockerfile='foo/Dockerfile') 214 k8s_yaml('foo.yaml') 215 `) 216 217 f.loadErrString("Cannot specify both dockerfile and dockerfile_contents") 218 } 219 220 func TestVerifiesGitRepo(t *testing.T) { 221 f := newFixture(t) 222 f.file("Tiltfile", "local_git_repo('.')") 223 f.loadErrString("isn't a valid git repo") 224 } 225 226 func TestLocal(t *testing.T) { 227 f := newFixture(t) 228 229 f.setupFoo() 230 231 f.file("Tiltfile", ` 232 docker_build('gcr.io/foo', 'foo') 233 cmd = 'cat foo.yaml' 234 if os.name == 'nt': 235 cmd = 'type foo.yaml' 236 yaml = local(cmd) 237 k8s_yaml(yaml) 238 `) 239 240 f.load() 241 242 f.assertNextManifest("foo", 243 db(image("gcr.io/foo")), 244 deployment("foo")) 245 246 cmdStr := "cat foo.yaml" 247 if runtime.GOOS == "windows" { 248 cmdStr = "type foo.yaml" 249 } 250 assert.Contains(t, f.out.String(), "local: "+cmdStr) 251 assert.Contains(t, f.out.String(), " → kind: Deployment") 252 } 253 254 func TestLocalBat(t *testing.T) { 255 f := newFixture(t) 256 257 f.setupFoo() 258 259 f.file("Tiltfile", ` 260 docker_build('gcr.io/foo', 'foo') 261 yaml = local(command='cat foo.yaml', command_bat='type foo.yaml') 262 k8s_yaml(yaml) 263 `) 264 265 f.load() 266 267 f.assertNextManifest("foo", 268 db(image("gcr.io/foo")), 269 deployment("foo")) 270 271 cmdStr := "cat foo.yaml" 272 if runtime.GOOS == "windows" { 273 cmdStr = "type foo.yaml" 274 } 275 assert.Contains(t, f.out.String(), "local: "+cmdStr) 276 assert.Contains(t, f.out.String(), " → kind: Deployment") 277 } 278 279 func TestLocalEnv(t *testing.T) { 280 f := newFixture(t) 281 282 // contrived example to ensure that the environment is correctly passed to local -- an env var is echoed back out 283 // which then gets passed as an ignore so that it's visible in the load result for assertion 284 f.file("Tiltfile", ` 285 ignore = str(local('echo $FOO', command_bat='echo %FOO%', env={'FOO': 'bar'})).rstrip('\r\n') 286 watch_settings(ignore=ignore) 287 `) 288 289 f.load() 290 291 assert.Equal(t, []string{"bar"}, f.loadResult.WatchSettings.Ignores[0].Patterns) 292 } 293 294 func TestLocalEmptyArray(t *testing.T) { 295 f := newFixture(t) 296 297 f.file("Tiltfile", ` 298 local([]) 299 `) 300 301 f.loadErrString("empty cmd") 302 } 303 304 func TestLocalEmptyString(t *testing.T) { 305 f := newFixture(t) 306 307 f.file("Tiltfile", ` 308 local('') 309 `) 310 311 f.loadErrString("empty cmd") 312 } 313 314 func TestLocalStdin(t *testing.T) { 315 f := newFixture(t) 316 317 f.file("Tiltfile", ` 318 local('head -4 | tail -2', stdin='''foo 319 bar 320 baz 321 quu 322 qux 323 ''') 324 `) 325 326 f.load() 327 require.Contains(t, f.out.String(), `head -4 | tail -2 328 → baz 329 → quu`) 330 } 331 332 func TestLocalStdinChain(t *testing.T) { 333 if runtime.GOOS == "windows" { 334 t.Skip() 335 } 336 337 f := newFixture(t) 338 339 f.file("Tiltfile", ` 340 local('cat', stdin=local('echo hi')) 341 `) 342 343 f.load() 344 require.Contains(t, f.out.String(), "local: echo hi\n → hi\nlocal: cat\n → hi") 345 } 346 347 func TestCustomBuildBat(t *testing.T) { 348 f := newFixture(t) 349 350 f.setupFoo() 351 352 f.file("Tiltfile", ` 353 custom_build('gcr.io/foo', command='unix build', command_bat='windows build', deps=[]) 354 k8s_yaml('foo.yaml') 355 `) 356 357 f.load() 358 359 args := "unix build" 360 if runtime.GOOS == "windows" { 361 args = "windows build" 362 } 363 f.assertNextManifest("foo", 364 cb( 365 image("gcr.io/foo"), 366 cmd(args, f.Path()), 367 ), 368 deployment("foo")) 369 } 370 371 func TestLocalQuiet(t *testing.T) { 372 f := newFixture(t) 373 374 f.setupFoo() 375 376 f.file("Tiltfile", ` 377 local('echo foobar', quiet=True) 378 `) 379 380 f.load() 381 382 assert.Contains(t, f.out.String(), "local: echo foobar") 383 assert.NotContains(t, f.out.String(), " → foobar") 384 } 385 386 func TestLocalEchoOff(t *testing.T) { 387 f := newFixture(t) 388 389 f.setupFoo() 390 391 f.file("Tiltfile", ` 392 local('echo foobar', echo_off=True) 393 `) 394 395 f.load() 396 397 assert.NotContains(t, f.out.String(), "local: echo foobar") 398 } 399 400 func TestLocalNoOutput(t *testing.T) { 401 type tc struct { 402 echoOff bool 403 quiet bool 404 shouldDisplayNoOutput bool 405 } 406 407 // only if BOTH quiet=False + echo_off=False should the [no output] show up 408 // * if quiet=True, we don't care about output, so doesn't make sense to log that there was NO output 409 // * if echo_off=True, we don't know what command it's coming from, so it's more confusing than helpful 410 tcs := []tc{ 411 {echoOff: true, quiet: true, shouldDisplayNoOutput: false}, 412 {echoOff: false, quiet: true, shouldDisplayNoOutput: false}, 413 {echoOff: true, quiet: true, shouldDisplayNoOutput: false}, 414 {echoOff: false, quiet: false, shouldDisplayNoOutput: true}, 415 } 416 417 goBoolToStarlark := func(v bool) string { 418 if v { 419 return "True" 420 } 421 return "False" 422 } 423 424 for _, tc := range tcs { 425 name := fmt.Sprintf("EchoOff%s_Quiet%s", goBoolToStarlark(tc.echoOff), goBoolToStarlark(tc.quiet)) 426 t.Run(name, func(t *testing.T) { 427 f := newFixture(t) 428 429 f.setupFoo() 430 431 f.file( 432 "Tiltfile", fmt.Sprintf(` 433 local('exit 0', echo_off=%s, quiet=%s) 434 `, goBoolToStarlark(tc.echoOff), goBoolToStarlark(tc.quiet))) 435 436 f.load() 437 438 out := f.out.String() 439 if !tc.echoOff { 440 assert.Contains(t, out, "local: exit 0") 441 } else { 442 assert.NotContains(t, out, "exit") 443 } 444 445 if tc.shouldDisplayNoOutput { 446 assert.Contains(t, out, "[no output]") 447 } else { 448 assert.NotContains(t, out, "no output") 449 } 450 }) 451 } 452 } 453 454 func TestLocalArgvCmd(t *testing.T) { 455 if runtime.GOOS == "windows" { 456 t.Skip("windows doesn't support argv commands. Go converts it to a single string") 457 } 458 f := newFixture(t) 459 460 // this would generate a syntax error if evaluated by a shell 461 f.file("Tiltfile", `local(['echo', 'a"b'])`) 462 f.load() 463 464 assert.Contains(t, f.out.String(), `a"b`) 465 } 466 467 func TestLocalTiltEnvPropagation(t *testing.T) { 468 f := newFixture(t) 469 470 doTest := func(t testing.TB, expectedHost string, expectedPort int) { 471 t.Helper() 472 473 f.file("Tiltfile", ` 474 local(command='echo Tilt host is $TILT_HOST', command_bat='echo Tilt host is %TILT_HOST%', echo_off=True) 475 local(command='echo Tilt port is $TILT_PORT', command_bat='echo Tilt port is %TILT_PORT%', echo_off=True) 476 `) 477 f.load() 478 479 assert.Contains(t, f.out.String(), fmt.Sprintf(`Tilt host is %s`, expectedHost)) 480 assert.Contains(t, f.out.String(), fmt.Sprintf(`Tilt port is %d`, expectedPort)) 481 } 482 483 t.Run("Implicit", func(t *testing.T) { 484 os.Unsetenv("TILT_HOST") 485 os.Unsetenv("TILT_PORT") 486 // $TILT_HOST + $TILT_PORT are not explicitly defined anywhere in the test fixture but should be 487 // auto-populated (hardcoded to 1.2.3.4/12345 for tests - no real apiserver is actually loaded) 488 f.webHost = "1.2.3.4" 489 doTest(t, "1.2.3.4", 12345) 490 }) 491 492 t.Run("Explicit", func(t *testing.T) { 493 t.Setenv("TILT_HOST", "7.8.9.0") 494 t.Setenv("TILT_PORT", "7890") 495 496 // if values were explicitly passed (e.g. `local('...', env={"TILT_PORT": 7890})`, they should be respected 497 doTest(t, "7.8.9.0", 7890) 498 }) 499 } 500 501 func TestReadFile(t *testing.T) { 502 f := newFixture(t) 503 504 f.setupFoo() 505 506 f.file("Tiltfile", ` 507 docker_build('gcr.io/foo', 'foo') 508 yaml = read_file('foo.yaml') 509 k8s_yaml(yaml) 510 `) 511 512 f.load() 513 514 f.assertNextManifest("foo", 515 db(image("gcr.io/foo")), 516 deployment("foo")) 517 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 518 } 519 520 func TestKustomize(t *testing.T) { 521 f := newFixture(t) 522 523 f.setupFoo() 524 f.file("kustomization.yaml", kustomizeFileText) 525 f.file("configMap.yaml", kustomizeConfigMapText) 526 f.file("deployment.yaml", kustomizeDeploymentText) 527 f.file("service.yaml", kustomizeServiceText) 528 f.file("Tiltfile", ` 529 530 docker_build("gcr.io/foo", "foo") 531 k8s_yaml(kustomize(".")) 532 k8s_resource("the-deployment", "foo") 533 `) 534 f.load() 535 f.assertNextManifest("foo", deployment("the-deployment"), numEntities(2)) 536 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "configMap.yaml", "deployment.yaml", "kustomization.yaml", "service.yaml") 537 } 538 539 func TestKustomizeFlags(t *testing.T) { 540 f := newFixture(t) 541 542 f.setupFoo() 543 f.file("kustomization.yaml", kustomizeFileText) 544 f.file("configMap.yaml", kustomizeConfigMapText) 545 f.file("deployment.yaml", kustomizeDeploymentText) 546 f.file("service.yaml", kustomizeServiceText) 547 f.file("Tiltfile", ` 548 549 docker_build("gcr.io/foo", "foo") 550 k8s_yaml(kustomize(".", flags=['--enable-helm'])) 551 k8s_resource("the-deployment", "foo") 552 `) 553 f.load() 554 f.assertNextManifest("foo", deployment("the-deployment"), numEntities(2)) 555 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "configMap.yaml", "deployment.yaml", "kustomization.yaml", "service.yaml") 556 assert.Contains(t, f.out.String(), "kustomize build --enable-helm") 557 } 558 559 func TestKustomizeBin(t *testing.T) { 560 f := newFixture(t) 561 f.file("kustomization.yaml", kustomizeFileText) 562 f.file("configMap.yaml", kustomizeConfigMapText) 563 f.file("deployment.yaml", kustomizeDeploymentText) 564 f.file("service.yaml", kustomizeServiceText) 565 sentinel := f.WriteFile("kustomize.txt", "") 566 var wrapper string 567 if runtime.GOOS == "windows" { 568 wrapper = f.WriteFile("kustomize.bat", fmt.Sprintf(`@echo off 569 echo %%* > %s 570 kustomize.exe %%* 571 `, sentinel)) 572 // convert backslashes in path 573 wrapper = strings.ReplaceAll(wrapper, "\\", "/") 574 } else { 575 wrapper = f.WriteFile("kustomize", fmt.Sprintf(`#!/bin/sh 576 echo "$@" > %s 577 exec kustomize "$@" 578 `, sentinel)) 579 _ = os.Chmod(wrapper, 0755) 580 } 581 582 f.file("Tiltfile", fmt.Sprintf(` 583 k8s_yaml(kustomize(".", kustomize_bin="%s")) 584 k8s_resource("the-deployment", "foo") 585 `, wrapper)) 586 f.load() 587 sentinelContents, err := os.ReadFile(sentinel) 588 assert.Nil(t, err) 589 assert.EqualValues(t, "build .", strings.Trim(string(sentinelContents), " \r\n")) 590 } 591 592 func TestKustomizeError(t *testing.T) { 593 f := newFixture(t) 594 595 f.file("Tiltfile", "kustomize('.')") 596 f.loadErrString("unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization'") 597 } 598 599 func TestKustomization(t *testing.T) { 600 f := newFixture(t) 601 602 f.setupFoo() 603 f.file("Kustomization", kustomizeFileText) 604 f.file("configMap.yaml", kustomizeConfigMapText) 605 f.file("deployment.yaml", kustomizeDeploymentText) 606 f.file("service.yaml", kustomizeServiceText) 607 f.file("Tiltfile", ` 608 609 docker_build("gcr.io/foo", "foo") 610 k8s_yaml(kustomize(".")) 611 k8s_resource("the-deployment", "foo") 612 `) 613 f.load() 614 f.assertNextManifest("foo", deployment("the-deployment"), numEntities(2)) 615 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "configMap.yaml", "deployment.yaml", "Kustomization", "service.yaml") 616 } 617 618 func TestDockerBuildTarget(t *testing.T) { 619 f := newFixture(t) 620 621 f.setupFoo() 622 f.file("Tiltfile", ` 623 k8s_yaml('foo.yaml') 624 docker_build("gcr.io/foo", "foo", target='stage') 625 `) 626 f.load() 627 m := f.assertNextManifest("foo") 628 assert.Equal(t, "stage", m.ImageTargets[0].BuildDetails.(model.DockerBuild).Target) 629 } 630 631 func TestDockerBuildSSH(t *testing.T) { 632 f := newFixture(t) 633 634 f.setupFoo() 635 f.file("Tiltfile", ` 636 k8s_yaml('foo.yaml') 637 docker_build("gcr.io/foo", "foo", ssh='default') 638 `) 639 f.load() 640 m := f.assertNextManifest("foo") 641 assert.Equal(t, []string{"default"}, m.ImageTargets[0].BuildDetails.(model.DockerBuild).SSHAgentConfigs) 642 } 643 644 func TestDockerBuildSecret(t *testing.T) { 645 f := newFixture(t) 646 647 f.setupFoo() 648 f.file("Tiltfile", ` 649 k8s_yaml('foo.yaml') 650 docker_build("gcr.io/foo", "foo", secret='id=shibboleth') 651 `) 652 f.load() 653 m := f.assertNextManifest("foo") 654 assert.Equal(t, []string{"id=shibboleth"}, m.ImageTargets[0].BuildDetails.(model.DockerBuild).Secrets) 655 } 656 657 func TestDockerBuildNetwork(t *testing.T) { 658 f := newFixture(t) 659 660 f.setupFoo() 661 f.file("Tiltfile", ` 662 k8s_yaml('foo.yaml') 663 docker_build("gcr.io/foo", "foo", network='default') 664 `) 665 f.load() 666 m := f.assertNextManifest("foo") 667 assert.Equal(t, "default", m.ImageTargets[0].BuildDetails.(model.DockerBuild).Network) 668 } 669 670 func TestDockerBuildPull(t *testing.T) { 671 f := newFixture(t) 672 673 f.setupFoo() 674 f.file("Tiltfile", ` 675 k8s_yaml('foo.yaml') 676 docker_build("gcr.io/foo", "foo", pull=True) 677 `) 678 f.load() 679 m := f.assertNextManifest("foo") 680 assert.True(t, m.ImageTargets[0].BuildDetails.(model.DockerBuild).Pull) 681 } 682 683 func TestDockerBuildCacheFrom(t *testing.T) { 684 f := newFixture(t) 685 686 f.setupFoo() 687 f.file("Tiltfile", ` 688 k8s_yaml('foo.yaml') 689 docker_build("gcr.io/foo", "foo", cache_from='gcr.io/foo') 690 `) 691 f.load() 692 m := f.assertNextManifest("foo") 693 assert.Equal(t, []string{"gcr.io/foo"}, m.ImageTargets[0].BuildDetails.(model.DockerBuild).CacheFrom) 694 } 695 696 func TestDockerBuildExtraTagString(t *testing.T) { 697 f := newFixture(t) 698 699 f.setupFoo() 700 f.file("Tiltfile", ` 701 k8s_yaml('foo.yaml') 702 docker_build("gcr.io/foo", "foo", extra_tag='foo:latest') 703 `) 704 f.load() 705 m := f.assertNextManifest("foo") 706 assert.Equal(t, []string{"foo:latest"}, 707 m.ImageTargets[0].BuildDetails.(model.DockerBuild).ExtraTags) 708 } 709 710 func TestDockerBuildExtraTagList(t *testing.T) { 711 f := newFixture(t) 712 713 f.setupFoo() 714 f.file("Tiltfile", ` 715 k8s_yaml('foo.yaml') 716 docker_build("gcr.io/foo", "foo", extra_tag=['foo:latest', 'foo:jenkins-1234']) 717 `) 718 f.load() 719 m := f.assertNextManifest("foo") 720 assert.Equal(t, []string{"foo:latest", "foo:jenkins-1234"}, 721 m.ImageTargets[0].BuildDetails.(model.DockerBuild).ExtraTags) 722 } 723 724 func TestDockerBuildExtraTagListInvalid(t *testing.T) { 725 f := newFixture(t) 726 727 f.setupFoo() 728 f.file("Tiltfile", ` 729 k8s_yaml('foo.yaml') 730 docker_build("gcr.io/foo", "foo", extra_tag='cherry bomb') 731 `) 732 f.loadErrString("Argument extra_tag=\"cherry bomb\" not a valid image reference: invalid reference format") 733 } 734 735 func TestDockerBuildCache(t *testing.T) { 736 f := newFixture(t) 737 738 f.setupFoo() 739 f.file("Tiltfile", ` 740 k8s_yaml('foo.yaml') 741 docker_build("gcr.io/foo", "foo", cache='/paths/to/cache') 742 `) 743 f.loadAssertWarnings(cacheObsoleteWarning) 744 } 745 746 func TestK8sResourceAdditiveLinks(t *testing.T) { 747 f := newFixture(t) 748 749 f.setupExpand() 750 f.file("Tiltfile", ` 751 752 k8s_yaml('all.yaml') 753 k8s_resource('a', links=['http://demo-a.localhost/']) 754 k8s_resource('a') 755 k8s_resource('b', links=['http://demo-b.localhost/']) 756 k8s_resource('b', links=['http://demo-b.localhost/api']) 757 `) 758 f.load() 759 f.assertNextManifest("a", 760 k8sResourceLinks{model.MustNewLink("http://demo-a.localhost/", "")}) 761 f.assertNextManifest("b", 762 k8sResourceLinks{ 763 model.MustNewLink("http://demo-b.localhost/", ""), 764 model.MustNewLink("http://demo-b.localhost/api", ""), 765 }) 766 } 767 768 func TestDuplicateImageNames(t *testing.T) { 769 f := newFixture(t) 770 771 f.setupExpand() 772 f.file("Tiltfile", ` 773 k8s_yaml('all.yaml') 774 docker_build('gcr.io/a', 'a') 775 docker_build('gcr.io/a', 'a') 776 `) 777 778 f.loadErrString("Image for ref \"gcr.io/a\" has already been defined") 779 } 780 781 func TestInvalidImageNameInDockerBuild(t *testing.T) { 782 f := newFixture(t) 783 784 f.setupExpand() 785 f.file("Tiltfile", ` 786 k8s_yaml('all.yaml') 787 docker_build("ceci n'est pas une valid image ref", 'a') 788 `) 789 790 f.loadErrString("invalid reference format") 791 } 792 793 func TestInvalidImageNameInK8SYAML(t *testing.T) { 794 f := newFixture(t) 795 796 f.file("Tiltfile", ` 797 yaml_str = """ 798 kind: Pod 799 apiVersion: v1 800 metadata: 801 name: test-pod 802 spec: 803 containers: 804 - image: IMAGE_URL 805 """ 806 807 k8s_yaml([blob(yaml_str)])`) 808 809 f.loadErrString("invalid reference format", "test-pod", "IMAGE_URL") 810 } 811 812 type portForwardCase struct { 813 name string 814 expr string 815 expected []model.PortForward 816 errorMsg string 817 webHost model.WebHost 818 } 819 820 func newPortForwardSuccessCase(name, expr string, expected []model.PortForward) portForwardCase { 821 return portForwardCase{name: name, expr: expr, expected: expected} 822 } 823 824 func newPortForwardErrorCase(name, expr, errorMsg string) portForwardCase { 825 return portForwardCase{name: name, expr: expr, errorMsg: errorMsg} 826 } 827 828 type resourceLinkCase struct { 829 name string 830 expr string 831 expected []model.Link 832 errorMsg string 833 } 834 835 func newResourceLinkSuccessCase(name, expr string, expected []model.Link) resourceLinkCase { 836 return resourceLinkCase{name: name, expr: expr, expected: expected} 837 } 838 839 func newResourceLinkErrorCase(name, expr, errorMsg string) resourceLinkCase { 840 return resourceLinkCase{name: name, expr: expr, errorMsg: errorMsg} 841 } 842 843 func TestPortForward(t *testing.T) { 844 portForwardCases := []portForwardCase{ 845 // int values 846 newPortForwardSuccessCase("value_int_local", "8000", []model.PortForward{{LocalPort: 8000}}), 847 newPortForwardErrorCase("value_int_local_negative", "-1", "not in the valid range"), 848 newPortForwardErrorCase("value_int_local_large", "8000000", "not in the valid range"), 849 850 // string values 851 newPortForwardSuccessCase("value_string_local", "'10000'", []model.PortForward{{LocalPort: 10000}}), 852 newPortForwardSuccessCase("value_string_both", "'10000:8000'", []model.PortForward{{LocalPort: 10000, ContainerPort: 8000}}), 853 newPortForwardErrorCase("value_string_garbage", "'garbage'", "not in the valid range"), 854 newPortForwardErrorCase("value_string_empty", "''", "not in the valid range"), 855 856 // PortForward values (via constructor) 857 newPortForwardSuccessCase("value_constructor_local", "port_forward(8001)", []model.PortForward{{LocalPort: 8001}}), 858 newPortForwardSuccessCase("value_constructor_local_named", "port_forward(8001, name='foo')", []model.PortForward{{LocalPort: 8001, Name: "foo"}}), 859 newPortForwardSuccessCase("value_constructor_local_path", "port_forward(8001, link_path='v1/ui')", 860 []model.PortForward{model.MustPortForward(8001, 0, "", "", "v1/ui")}), 861 newPortForwardSuccessCase("value_constructor_both", "port_forward(8001, 443)", []model.PortForward{{LocalPort: 8001, ContainerPort: 443}}), 862 newPortForwardSuccessCase("value_constructor_both_named", "port_forward(8001, 443, name='foo')", []model.PortForward{{LocalPort: 8001, ContainerPort: 443, Name: "foo"}}), 863 newPortForwardSuccessCase("value_constructor_all_positional", "port_forward(8001, 443, 'foo', 'v1/ui', 'elastic.local')", 864 []model.PortForward{model.MustPortForward(8001, 443, "elastic.local", "foo", "v1/ui")}), 865 newPortForwardErrorCase("value_constructor_no_local_port", "port_forward(container_port=443)", "missing argument for local_port"), 866 newPortForwardErrorCase("value_constructor_local_port_wrong_type", "port_forward('8001')", "for parameter local_port: got string, want int"), 867 newPortForwardErrorCase("value_constructor_bad_path", "port_forward(8001, 443, link_path='invalid_escape%')", "invalid URL escape"), 868 newPortForwardErrorCase("value_constructor_name_wrong_type", "port_forward(8001, 443, 54321)", "for parameter name: got int, want string"), 869 newPortForwardSuccessCase("value_constructor_host", "port_forward(8001, 443, host='elastic.local')", 870 []model.PortForward{{LocalPort: 8001, ContainerPort: 443, Host: "elastic.local"}}), 871 newPortForwardErrorCase("value_constructor_host_wrong_type", "port_forward(8001, 443, host=54321)", "for parameter \"host\": got int, want string"), 872 873 // list values 874 newPortForwardSuccessCase("list_mixed", "[8000, port_forward(8001, 443), '8002', '8003:444'],", []model.PortForward{{LocalPort: 8000}, {LocalPort: 8001, ContainerPort: 443}, {LocalPort: 8002}, {LocalPort: 8003, ContainerPort: 444}}), 875 876 // parsing host 877 newPortForwardErrorCase("value_host_bad", "'bad+host:10000:8000'", "not a valid hostname or IP address"), 878 newPortForwardSuccessCase("value_host_good_ip", "'0.0.0.0:10000:8000'", []model.PortForward{{LocalPort: 10000, ContainerPort: 8000, Host: "0.0.0.0"}}), 879 newPortForwardSuccessCase("value_host_good_domain", "'tilt.dev:10000:8000'", []model.PortForward{{LocalPort: 10000, ContainerPort: 8000, Host: "tilt.dev"}}), 880 portForwardCase{name: "default_web_host", expr: "8000", webHost: "0.0.0.0", 881 expected: []model.PortForward{{LocalPort: 8000, Host: "0.0.0.0"}}}, 882 portForwardCase{name: "override_web_host", expr: "'tilt.dev:10000:8000'", webHost: "0.0.0.0", 883 expected: []model.PortForward{{LocalPort: 10000, ContainerPort: 8000, Host: "tilt.dev"}}}, 884 885 // None 886 newPortForwardSuccessCase("none", "None", []model.PortForward{}), 887 newPortForwardSuccessCase("empty_array", "[]", []model.PortForward{}), 888 } 889 890 for _, c := range portForwardCases { 891 t.Run(c.name, func(t *testing.T) { 892 f := newFixture(t) 893 894 f.webHost = c.webHost 895 f.setupFoo() 896 s := ` 897 docker_build('gcr.io/foo', 'foo') 898 k8s_yaml('foo.yaml') 899 k8s_resource('foo', port_forwards=EXPR) 900 ` 901 s = strings.ReplaceAll(s, "EXPR", c.expr) 902 f.file("Tiltfile", s) 903 904 if c.errorMsg != "" { 905 f.loadErrString(c.errorMsg) 906 return 907 } 908 909 f.load() 910 f.assertNextManifest("foo", 911 c.expected, 912 db(image("gcr.io/foo")), 913 deployment("foo")) 914 }) 915 } 916 } 917 918 func TestResourceLinks(t *testing.T) { 919 cases := []resourceLinkCase{ 920 newResourceLinkErrorCase("invalid_type", "123", 921 "Want a string, a link, or a sequence of these; found 123"), 922 923 newResourceLinkSuccessCase("value_string", "'http://www.zombo.com'", 924 []model.Link{model.MustNewLink("http://www.zombo.com", "")}), 925 newResourceLinkSuccessCase("value_string_adds_scheme", "'www.zombo.com'", 926 []model.Link{model.MustNewLink("http://www.zombo.com", "")}), 927 newResourceLinkSuccessCase("value_string_preserves_nonhttp_scheme", "'ws://www.zombo.com'", 928 []model.Link{model.MustNewLink("ws://www.zombo.com", "")}), 929 newResourceLinkErrorCase("value_string_empty_url", "''", "url empty"), 930 931 newResourceLinkSuccessCase("value_link_named", "link('https://www.zombo.com', name='zombo')", 932 []model.Link{model.MustNewLink("https://www.zombo.com", "zombo")}), 933 newResourceLinkSuccessCase("value_link_unnamed", "link('https://www.zombo.com')", 934 []model.Link{model.MustNewLink("https://www.zombo.com", "")}), 935 newResourceLinkSuccessCase("value_link_positional_args", "link('https://www.zombo.com', 'zombo')", 936 []model.Link{model.MustNewLink("https://www.zombo.com", "zombo")}), 937 newResourceLinkSuccessCase("link_constructor_adds_scheme", "link('www.zombo.com', 'zombo')", 938 []model.Link{model.MustNewLink("http://www.zombo.com", "zombo")}), 939 newResourceLinkErrorCase("link_constructor_requires_URL", "link(name='zombo')", 940 "link: missing argument for url"), 941 newResourceLinkErrorCase("link_constructor_empty_URL", "link('')", 942 "url empty"), 943 944 newResourceLinkSuccessCase("value_list_strings", "['https://www.apple.edu', 'https://www.zombo.com']", 945 []model.Link{model.MustNewLink("https://www.apple.edu", ""), model.MustNewLink("https://www.zombo.com", "")}), 946 newResourceLinkSuccessCase("list_strings_add_scheme", "['www.apple.edu', 'www.zombo.com']", 947 []model.Link{model.MustNewLink("http://www.apple.edu", ""), model.MustNewLink("http://www.zombo.com", "")}), 948 newResourceLinkSuccessCase("value_list_links", 949 "[link('www.apple.edu'), link('www.zombo.com', 'zombo')]", 950 []model.Link{model.MustNewLink("http://www.apple.edu", ""), model.MustNewLink("http://www.zombo.com", "zombo")}), 951 newResourceLinkSuccessCase("value_list_,mixed", 952 "['www.apple.edu', link('www.zombo.com', 'zombo')]", 953 []model.Link{model.MustNewLink("http://www.apple.edu", ""), model.MustNewLink("http://www.zombo.com", "zombo")}), 954 newResourceLinkErrorCase("link_bad_type", "['www.apple.edu', 123]", 955 "Want a string, a link, or a sequence of these; found 123"), 956 } 957 958 for _, c := range cases { 959 t.Run("LocalResource-"+c.name, func(t *testing.T) { 960 f := newFixture(t) 961 962 tiltfile := fmt.Sprintf(` 963 local_resource('foo', 'echo hi', links=%s) 964 `, c.expr) 965 f.file("Tiltfile", tiltfile) 966 967 if c.errorMsg != "" { 968 f.loadErrString(c.errorMsg) 969 return 970 } 971 972 f.load() 973 f.assertNextManifest("foo", 974 localResourceLinks(c.expected), 975 localTarget(updateCmd(f.Path(), "echo hi", nil)), 976 ) 977 }) 978 979 t.Run("K8s-"+c.name, func(t *testing.T) { 980 f := newFixture(t) 981 982 f.setupFoo() 983 s := ` 984 docker_build('gcr.io/foo', 'foo') 985 k8s_yaml('foo.yaml') 986 k8s_resource('foo', links=EXPR) 987 k8s_resource('foo') # test that subsequent calls don't clear the links 988 ` 989 990 s = strings.ReplaceAll(s, "EXPR", c.expr) 991 f.file("Tiltfile", s) 992 993 if c.errorMsg != "" { 994 f.loadErrString(c.errorMsg) 995 return 996 } 997 998 f.load() 999 f.assertNextManifest("foo", 1000 k8sResourceLinks(c.expected), 1001 db(image("gcr.io/foo")), 1002 deployment("foo")) 1003 }) 1004 1005 t.Run("dc-"+c.name, func(t *testing.T) { 1006 f := newFixture(t) 1007 1008 f.file("docker-compose.yml", `version: '3.0' 1009 services: 1010 foo: 1011 image: gcr.io/foo 1012 `) 1013 s := ` 1014 docker_compose('docker-compose.yml') 1015 dc_resource('foo', links=EXPR) 1016 dc_resource('foo') # test that subsequent calls don't clear the links 1017 ` 1018 1019 s = strings.ReplaceAll(s, "EXPR", c.expr) 1020 f.file("Tiltfile", s) 1021 1022 if c.errorMsg != "" { 1023 f.loadErrString(c.errorMsg) 1024 return 1025 } 1026 1027 f.load() 1028 f.assertNextManifest("foo", 1029 dcResourceLinks(c.expected), 1030 ) 1031 }) 1032 } 1033 } 1034 1035 func TestK8sResourceWithLinksAndPortForwards(t *testing.T) { 1036 f := newFixture(t) 1037 1038 f.setupFoo() 1039 f.file("Tiltfile", ` 1040 docker_build('gcr.io/foo', 'foo') 1041 k8s_yaml('foo.yaml') 1042 k8s_resource('foo', port_forwards=[8000, 8001], links=link("www.zombo.com", name="zombo")) 1043 `) 1044 1045 f.load() 1046 f.assertNextManifest("foo", 1047 []model.PortForward{{LocalPort: 8000}, {LocalPort: 8001}}, 1048 k8sResourceLinks{model.MustNewLink("http://www.zombo.com", "zombo")}, 1049 db(image("gcr.io/foo")), 1050 deployment("foo")) 1051 } 1052 1053 func TestExpand(t *testing.T) { 1054 f := newFixture(t) 1055 f.setupExpand() 1056 f.file("Tiltfile", ` 1057 k8s_yaml('all.yaml') 1058 docker_build('gcr.io/a', 'a') 1059 docker_build('gcr.io/b', 'b') 1060 docker_build('gcr.io/c', 'c') 1061 docker_build('gcr.io/d', 'd') 1062 `) 1063 f.load() 1064 f.assertNextManifest("a", db(image("gcr.io/a")), deployment("a")) 1065 f.assertNextManifest("b", db(image("gcr.io/b")), deployment("b")) 1066 f.assertNextManifest("c", db(image("gcr.io/c")), deployment("c")) 1067 f.assertNextManifest("d", db(image("gcr.io/d")), deployment("d")) 1068 f.assertNoMoreManifests() // should be no unresourced yaml remaining 1069 f.assertConfigFiles("Tiltfile", ".tiltignore", "all.yaml", "a/Dockerfile", "a/.dockerignore", "b/Dockerfile", "b/.dockerignore", "c/Dockerfile", "c/.dockerignore", "d/Dockerfile", "d/.dockerignore") 1070 } 1071 1072 func TestExpandUnresourced(t *testing.T) { 1073 f := newFixture(t) 1074 f.dockerfile("a/Dockerfile") 1075 1076 f.yaml("all.yaml", 1077 deployment("a", image("gcr.io/a")), 1078 secret("a-secret"), 1079 ) 1080 1081 f.gitInit("") 1082 f.file("Tiltfile", ` 1083 k8s_yaml('all.yaml') 1084 docker_build('gcr.io/a', 'a') 1085 `) 1086 1087 f.load() 1088 f.assertNextManifest("a", db(image("gcr.io/a")), deployment("a")) 1089 f.assertNextManifestUnresourced("a-secret") 1090 } 1091 1092 func TestUnresourcedPodCreatorYamlAsManifest(t *testing.T) { 1093 f := newFixture(t) 1094 1095 f.yaml("pod_creator.yaml", deployment("pod-creator"), secret("not-pod-creator")) 1096 1097 f.file("Tiltfile", ` 1098 k8s_yaml('pod_creator.yaml') 1099 `) 1100 f.load() 1101 1102 f.assertNextManifest("pod-creator", deployment("pod-creator")) 1103 f.assertNextManifestUnresourced("not-pod-creator") 1104 } 1105 1106 func TestUnresourcedYamlGroupingV1(t *testing.T) { 1107 f := newFixture(t) 1108 1109 labelsA := map[string]string{"keyA": "valueA"} 1110 labelsB := map[string]string{"keyB": "valueB"} 1111 labelsC := map[string]string{"keyC": "valueC"} 1112 f.yaml("all.yaml", 1113 deployment("deployment-a", withLabels(labelsA)), 1114 1115 deployment("deployment-b", withLabels(labelsB)), 1116 service("service-b", withLabels(labelsB)), 1117 1118 deployment("deployment-c", withLabels(labelsC)), 1119 service("service-c1", withLabels(labelsC)), 1120 service("service-c2", withLabels(labelsC)), 1121 1122 secret("someSecret"), 1123 ) 1124 1125 f.file("Tiltfile", `k8s_yaml('all.yaml')`) 1126 f.load() 1127 1128 f.assertNextManifest("deployment-a", deployment("deployment-a")) 1129 f.assertNextManifest("deployment-b", deployment("deployment-b"), service("service-b")) 1130 f.assertNextManifest("deployment-c", deployment("deployment-c"), service("service-c1"), service("service-c2")) 1131 f.assertNextManifestUnresourced("someSecret") 1132 } 1133 1134 func TestUnresourcedYamlGroupingV2(t *testing.T) { 1135 f := newFixture(t) 1136 1137 labelsA := map[string]string{"keyA": "valueA"} 1138 labelsB := map[string]string{"keyB": "valueB"} 1139 labelsC := map[string]string{"keyC": "valueC"} 1140 f.yaml("all.yaml", 1141 deployment("deployment-a", withLabels(labelsA)), 1142 1143 deployment("deployment-b", withLabels(labelsB)), 1144 service("service-b", withLabels(labelsB)), 1145 1146 deployment("deployment-c", withLabels(labelsC)), 1147 service("service-c1", withLabels(labelsC)), 1148 service("service-c2", withLabels(labelsC)), 1149 1150 secret("someSecret"), 1151 ) 1152 1153 f.file("Tiltfile", ` 1154 k8s_yaml('all.yaml')`) 1155 f.load() 1156 1157 f.assertNextManifest("deployment-a", deployment("deployment-a")) 1158 f.assertNextManifest("deployment-b", deployment("deployment-b"), service("service-b")) 1159 f.assertNextManifest("deployment-c", deployment("deployment-c"), service("service-c1"), service("service-c2")) 1160 f.assertNextManifestUnresourced("someSecret") 1161 } 1162 1163 func TestK8sGroupedWhenAddedToResource(t *testing.T) { 1164 f := newFixture(t) 1165 f.setupExpand() 1166 1167 labelsA := map[string]string{"keyA": "valueA"} 1168 labelsB := map[string]string{"keyB": "valueB"} 1169 labelsC := map[string]string{"keyC": "valueC"} 1170 f.yaml("all.yaml", 1171 deployment("deployment-a", image("gcr.io/a"), withLabels(labelsA)), 1172 1173 deployment("deployment-b", image("gcr.io/b"), withLabels(labelsB)), 1174 service("service-b", withLabels(labelsB)), 1175 1176 deployment("deployment-c", image("gcr.io/c"), withLabels(labelsC)), 1177 service("service-c1", withLabels(labelsC)), 1178 service("service-c2", withLabels(labelsC)), 1179 ) 1180 1181 f.file("Tiltfile", ` 1182 1183 k8s_yaml('all.yaml') 1184 docker_build('gcr.io/a', 'a') 1185 docker_build('gcr.io/b', 'b') 1186 docker_build('gcr.io/c', 'c') 1187 `) 1188 f.load() 1189 1190 f.assertNextManifest("deployment-a", deployment("deployment-a")) 1191 f.assertNextManifest("deployment-b", deployment("deployment-b"), service("service-b")) 1192 f.assertNextManifest("deployment-c", deployment("deployment-c"), service("service-c1"), service("service-c2")) 1193 } 1194 1195 func TestImplicitK8sResourceWithoutDockerBuild(t *testing.T) { 1196 f := newFixture(t) 1197 f.setupFoo() 1198 f.file("Tiltfile", ` 1199 1200 k8s_yaml('foo.yaml') 1201 k8s_resource('foo', port_forwards=8000) 1202 `) 1203 f.load() 1204 f.assertNextManifest("foo", []model.PortForward{{LocalPort: 8000}}) 1205 } 1206 1207 func TestExpandTwoDeploymentsWithSameImage(t *testing.T) { 1208 f := newFixture(t) 1209 f.setupExpand() 1210 f.yaml("all.yaml", 1211 deployment("a", image("gcr.io/a")), 1212 deployment("a2", image("gcr.io/a")), 1213 deployment("b", image("gcr.io/b")), 1214 deployment("c", image("gcr.io/c")), 1215 deployment("d", image("gcr.io/d")), 1216 ) 1217 f.file("Tiltfile", ` 1218 1219 k8s_yaml('all.yaml') 1220 docker_build('gcr.io/a', 'a') 1221 docker_build('gcr.io/b', 'b') 1222 docker_build('gcr.io/c', 'c') 1223 docker_build('gcr.io/d', 'd') 1224 `) 1225 f.load() 1226 f.assertNextManifest("a", db(image("gcr.io/a")), deployment("a")) 1227 f.assertNextManifest("a2", db(image("gcr.io/a")), deployment("a2")) 1228 f.assertNextManifest("b", db(image("gcr.io/b")), deployment("b")) 1229 f.assertNextManifest("c", db(image("gcr.io/c")), deployment("c")) 1230 f.assertNextManifest("d", db(image("gcr.io/d")), deployment("d")) 1231 } 1232 1233 func TestMultipleYamlFiles(t *testing.T) { 1234 f := newFixture(t) 1235 1236 f.setupExpand() 1237 f.yaml("a.yaml", deployment("a", image("gcr.io/a"))) 1238 f.yaml("b.yaml", deployment("b", image("gcr.io/b"))) 1239 f.yaml("c.yaml", deployment("c", image("gcr.io/c"))) 1240 f.yaml("d.yaml", deployment("d", image("gcr.io/d"))) 1241 f.file("Tiltfile", ` 1242 k8s_yaml(['a.yaml', 'b.yaml', 'c.yaml', 'd.yaml']) 1243 docker_build('gcr.io/a', 'a') 1244 docker_build('gcr.io/b', 'b') 1245 docker_build('gcr.io/c', 'c') 1246 docker_build('gcr.io/d', 'd') 1247 `) 1248 f.load() 1249 f.assertNextManifest("a", db(image("gcr.io/a")), deployment("a")) 1250 f.assertNextManifest("b", db(image("gcr.io/b")), deployment("b")) 1251 f.assertNextManifest("c", db(image("gcr.io/c")), deployment("c")) 1252 f.assertNextManifest("d", db(image("gcr.io/d")), deployment("d")) 1253 } 1254 1255 func TestLoadOneManifest(t *testing.T) { 1256 f := newFixture(t) 1257 1258 f.setupFooAndBar() 1259 f.file("Tiltfile", ` 1260 docker_build('gcr.io/foo', 'foo') 1261 k8s_yaml('foo.yaml') 1262 1263 docker_build('gcr.io/bar', 'bar') 1264 k8s_yaml('bar.yaml') 1265 `) 1266 1267 f.load("foo") 1268 require.Equal(t, []model.ManifestName{"foo"}, f.loadResult.EnabledManifests) 1269 1270 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml", "bar/Dockerfile", "bar/.dockerignore", "bar.yaml") 1271 } 1272 1273 func TestUncategorizedEnabledEvenIfNotSpecified(t *testing.T) { 1274 f := newFixture(t) 1275 1276 f.setupFooAndBar() 1277 f.yaml("service.yaml", service("some-service")) 1278 1279 f.file("Tiltfile", ` 1280 docker_build('gcr.io/foo', 'foo') 1281 k8s_yaml('foo.yaml') 1282 1283 docker_build('gcr.io/bar', 'bar') 1284 k8s_yaml('bar.yaml') 1285 1286 k8s_yaml('service.yaml') 1287 `) 1288 1289 f.load("foo") 1290 require.Equal(t, []model.ManifestName{"foo", "uncategorized"}, f.loadResult.EnabledManifests) 1291 } 1292 1293 func TestLoadTypoManifest(t *testing.T) { 1294 f := newFixture(t) 1295 1296 f.setupFooAndBar() 1297 f.file("Tiltfile", ` 1298 docker_build('gcr.io/foo', 'foo') 1299 k8s_yaml('foo.yaml') 1300 1301 docker_build('gcr.io/bar', 'bar') 1302 k8s_yaml('bar.yaml') 1303 `) 1304 1305 tlr := f.newTiltfileLoader().Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), []string{"baz"}), nil) 1306 err := tlr.Error 1307 if assert.Error(t, err) { 1308 assert.Equal(t, `You specified some resources that could not be found: "baz" 1309 Is this a typo? Existing resources in Tiltfile: "foo", "bar"`, err.Error()) 1310 } 1311 } 1312 1313 func TestBasicGitPathFilter(t *testing.T) { 1314 f := newFixture(t) 1315 1316 f.gitInit("") 1317 f.file("Dockerfile", "FROM golang:1.10") 1318 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1319 f.file("Tiltfile", ` 1320 docker_build('gcr.io/foo', '.') 1321 k8s_yaml('foo.yaml') 1322 `) 1323 1324 f.load("foo") 1325 f.assertNextManifest("foo", 1326 buildFilters(".git"), 1327 fileChangeFilters(".git"), 1328 buildFilters("Tiltfile"), 1329 fileChangeFilters("Tiltfile"), 1330 buildMatches("foo.yaml"), 1331 fileChangeMatches("foo.yaml"), 1332 ) 1333 } 1334 1335 func TestCustomBuildGitPathFilter(t *testing.T) { 1336 f := newFixture(t) 1337 1338 f.gitInit("") 1339 f.file("Dockerfile", "FROM golang:1.10") 1340 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1341 f.file("Tiltfile", ` 1342 custom_build('gcr.io/foo', 'docker build -t gcr.io/foo .', ['.']) 1343 k8s_yaml('foo.yaml') 1344 `) 1345 1346 f.load("foo") 1347 f.assertNextManifest("foo", 1348 fileChangeFilters(".git"), 1349 ) 1350 } 1351 1352 func TestDockerignorePathFilter(t *testing.T) { 1353 f := newFixture(t) 1354 1355 f.gitInit("") 1356 f.file("Dockerfile", "FROM golang:1.10") 1357 f.file(".dockerignore", "*.txt") 1358 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1359 f.file("Tiltfile", ` 1360 docker_build('gcr.io/foo', '.') 1361 k8s_yaml('foo.yaml') 1362 `) 1363 1364 f.load("foo") 1365 f.assertNextManifest("foo", 1366 buildFilters("a.txt"), 1367 fileChangeFilters("a.txt"), 1368 buildMatches("txt.a"), 1369 fileChangeMatches("txt.a"), 1370 ) 1371 } 1372 1373 // When the custom_build lists one dep, it should pick 1374 // up the dockerignore from that directory. 1375 func TestDockerignoreCustomBuildRelativeDirs(t *testing.T) { 1376 f := newFixture(t) 1377 1378 f.file(".dockerignore", "src/sub/a.txt") 1379 f.file("src/.dockerignore", "sub/b.txt") 1380 f.file("src/sub/.dockerignore", "c.txt") 1381 1382 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1383 f.file("Tiltfile", ` 1384 custom_build('gcr.io/foo', 'build-image', deps=['./src']) 1385 k8s_yaml('foo.yaml') 1386 `) 1387 1388 f.load("foo") 1389 f.assertNextManifest("foo", 1390 fileChangeFilters("src/sub/b.txt"), 1391 fileChangeMatches("src/sub/a.txt"), 1392 fileChangeMatches("src/sub/c.txt"), 1393 ) 1394 } 1395 1396 // When the custom_build lists multiple deps, it should pick 1397 // up the dockerignores from both those directories. 1398 func TestDockerignoreCustomBuildMultipleDeps(t *testing.T) { 1399 f := newFixture(t) 1400 1401 f.file(".dockerignore", "src/sub/a.txt") 1402 f.file("src/.dockerignore", "sub/b.txt") 1403 f.file("src/sub/.dockerignore", "c.txt") 1404 1405 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1406 f.file("Tiltfile", ` 1407 custom_build('gcr.io/foo', 'build-image', deps=['./src', './src/sub']) 1408 k8s_yaml('foo.yaml') 1409 `) 1410 1411 f.load("foo") 1412 f.assertNextManifest("foo", 1413 fileChangeFilters("src/sub/b.txt"), 1414 fileChangeMatches("src/sub/a.txt"), 1415 fileChangeFilters("src/sub/c.txt"), 1416 ) 1417 } 1418 1419 func TestDockerignorePathFilterSubdir(t *testing.T) { 1420 f := newFixture(t) 1421 1422 f.gitInit("") 1423 f.file("foo/Dockerfile", "FROM golang:1.10") 1424 f.file("foo/.dockerignore", "*.txt") 1425 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1426 f.file("Tiltfile", ` 1427 docker_build('gcr.io/foo', 'foo') 1428 k8s_yaml('foo.yaml') 1429 `) 1430 1431 f.load("foo") 1432 f.assertNextManifest("foo", 1433 buildFilters("foo/a.txt"), 1434 fileChangeFilters("foo/a.txt"), 1435 buildMatches("foo/txt.a"), 1436 fileChangeMatches("foo/txt.a"), 1437 ) 1438 } 1439 1440 func TestK8sYAMLInputBareString(t *testing.T) { 1441 f := newFixture(t) 1442 1443 f.setupFoo() 1444 f.WriteFile("bar.yaml", "im not yaml") 1445 f.file("Tiltfile", ` 1446 k8s_yaml('bar.yaml') 1447 docker_build("gcr.io/foo", "foo", cache='/paths/to/cache') 1448 `) 1449 1450 f.loadErrString("bar.yaml is not a valid YAML file") 1451 } 1452 1453 func TestK8sYAMLInputFromReadFile(t *testing.T) { 1454 f := newFixture(t) 1455 1456 f.setupFoo() 1457 f.file("Tiltfile", ` 1458 k8s_yaml(str(read_file('foo.yaml'))) 1459 docker_build("gcr.io/foo", "foo", cache='/paths/to/cache') 1460 `) 1461 1462 if runtime.GOOS == "windows" { 1463 f.loadErrString("The filename, directory name, or volume label syntax is incorrect") 1464 } else { 1465 f.loadErrString("no such file or directory") 1466 } 1467 } 1468 1469 func TestK8sYAMLInvalid(t *testing.T) { 1470 f := newFixture(t) 1471 1472 f.setupFoo() 1473 f.file("Tiltfile", ` 1474 k8s_yaml(blob('''apiVersion: v1 1475 kind: Secret 1476 metadata: 1477 name: mysecret 1478 type: Opaque 1479 data: 1480 stuff: "!"''')) 1481 docker_build("gcr.io/foo", "foo", cache='/paths/to/cache') 1482 `) 1483 1484 f.loadErrString( 1485 `Error reading yaml from Tiltfile blob() call: decoding Secret "mysecret": illegal base64 data at input byte 0`) 1486 } 1487 1488 func TestFilterYamlByLabel(t *testing.T) { 1489 f := newFixture(t) 1490 f.file("k8s.yaml", yaml.ConcatYAML( 1491 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1492 testyaml.SnackYaml, testyaml.SanchoYAML)) 1493 f.file("Tiltfile", ` 1494 labels = {'app': 'doggos'} 1495 doggos, rest = filter_yaml('k8s.yaml', labels=labels) 1496 k8s_yaml(doggos) 1497 `) 1498 1499 f.load() 1500 f.assertNextManifest("doggos", deployment("doggos"), service("doggos")) 1501 f.assertNoMoreManifests() 1502 } 1503 1504 func TestFilterYamlByName(t *testing.T) { 1505 f := newFixture(t) 1506 f.file("k8s.yaml", yaml.ConcatYAML( 1507 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1508 testyaml.SnackYaml, testyaml.SanchoYAML)) 1509 f.file("Tiltfile", ` 1510 doggos, rest = filter_yaml('k8s.yaml', name='doggos') 1511 k8s_yaml(doggos) 1512 `) 1513 1514 f.load() 1515 f.assertNextManifest("doggos", deployment("doggos"), service("doggos")) 1516 f.assertNoMoreManifests() 1517 } 1518 1519 func TestFilterYamlByNameKind(t *testing.T) { 1520 f := newFixture(t) 1521 f.file("k8s.yaml", yaml.ConcatYAML( 1522 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1523 testyaml.SnackYaml, testyaml.SanchoYAML)) 1524 f.file("Tiltfile", ` 1525 doggos, rest = filter_yaml('k8s.yaml', name='doggos', kind='deployment') 1526 k8s_yaml(doggos) 1527 `) 1528 1529 f.load() 1530 f.assertNextManifest("doggos", deployment("doggos")) 1531 f.assertNoMoreManifests() 1532 } 1533 1534 func TestFilterYamlByNamespace(t *testing.T) { 1535 f := newFixture(t) 1536 f.file("k8s.yaml", yaml.ConcatYAML( 1537 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1538 testyaml.SnackYaml, testyaml.SanchoYAML)) 1539 f.file("Tiltfile", ` 1540 doggos, rest = filter_yaml('k8s.yaml', namespace='the-dog-zone') 1541 k8s_yaml(doggos) 1542 `) 1543 1544 f.load() 1545 f.assertNextManifest("doggos", deployment("doggos")) 1546 f.assertNoMoreManifests() 1547 } 1548 1549 func TestFilterYamlByApiVersion(t *testing.T) { 1550 f := newFixture(t) 1551 f.file("k8s.yaml", yaml.ConcatYAML( 1552 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1553 testyaml.SnackYaml, testyaml.SanchoYAML)) 1554 f.file("Tiltfile", ` 1555 doggos, rest = filter_yaml('k8s.yaml', name='doggos', api_version='apps/v1') 1556 k8s_yaml(doggos) 1557 `) 1558 1559 f.load() 1560 f.assertNextManifest("doggos", deployment("doggos")) 1561 f.assertNoMoreManifests() 1562 } 1563 1564 func TestFilterYamlNoMatch(t *testing.T) { 1565 f := newFixture(t) 1566 f.file("k8s.yaml", yaml.ConcatYAML(testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml)) 1567 f.file("Tiltfile", ` 1568 doggos, rest = filter_yaml('k8s.yaml', namespace='dne', kind='deployment') 1569 k8s_yaml(doggos) 1570 `) 1571 f.loadErrString(emptyYAMLError.Error()) 1572 } 1573 1574 func TestYamlNone(t *testing.T) { 1575 f := newFixture(t) 1576 1577 f.setupFoo() 1578 1579 f.file("Tiltfile", ` 1580 k8s_yaml(None) 1581 `) 1582 f.loadErrString(emptyYAMLError.Error()) 1583 } 1584 1585 func TestYamlEmptyBlob(t *testing.T) { 1586 f := newFixture(t) 1587 1588 f.setupFoo() 1589 1590 f.file("Tiltfile", ` 1591 k8s_yaml(blob('')) 1592 `) 1593 f.loadErrString(emptyYAMLError.Error()) 1594 } 1595 1596 func TestDuplicateLocalResources(t *testing.T) { 1597 f := newFixture(t) 1598 1599 f.setupFoo() 1600 1601 f.file("Tiltfile", ` 1602 local_resource('foo', 'echo foo') 1603 local_resource('foo', 'echo foo') 1604 `) 1605 1606 f.loadErrString(`local_resource named "foo" already exists`) 1607 } 1608 1609 // These tests are for behavior that we specifically enabled in Starlark 1610 // in the init() function 1611 func TestTopLevelIfStatement(t *testing.T) { 1612 f := newFixture(t) 1613 1614 f.setupFoo() 1615 1616 f.file("Tiltfile", ` 1617 if True: 1618 docker_build('gcr.io/foo', 'foo') 1619 k8s_yaml('foo.yaml') 1620 `) 1621 1622 f.load() 1623 1624 f.assertNextManifest("foo", 1625 db(image("gcr.io/foo")), 1626 deployment("foo")) 1627 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 1628 } 1629 1630 func TestTopLevelForLoop(t *testing.T) { 1631 f := newFixture(t) 1632 1633 f.setupFoo() 1634 1635 f.file("Tiltfile", ` 1636 for i in range(1, 3): 1637 print(i) 1638 `) 1639 1640 f.load() 1641 } 1642 1643 func TestTopLevelVariableRename(t *testing.T) { 1644 f := newFixture(t) 1645 1646 f.setupFoo() 1647 1648 f.file("Tiltfile", ` 1649 x = 1 1650 x = 2 1651 `) 1652 1653 f.load() 1654 } 1655 1656 func TestEmptyDockerfileDockerBuild(t *testing.T) { 1657 f := newFixture(t) 1658 f.setupFoo() 1659 f.file("foo/Dockerfile", "") 1660 f.file("Tiltfile", ` 1661 docker_build('gcr.io/foo', 'foo') 1662 k8s_yaml('foo.yaml') 1663 `) 1664 f.load() 1665 m := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 1666 assert.True(t, m.ImageTargetAt(0).IsDockerBuild()) 1667 } 1668 1669 func TestSanchoSidecar(t *testing.T) { 1670 f := newFixture(t) 1671 f.setupFoo() 1672 f.file("Dockerfile", "FROM golang:1.10") 1673 f.file("k8s.yaml", testyaml.SanchoSidecarYAML) 1674 f.file("Tiltfile", ` 1675 k8s_yaml('k8s.yaml') 1676 docker_build('gcr.io/some-project-162817/sancho', '.') 1677 docker_build('gcr.io/some-project-162817/sancho-sidecar', '.') 1678 `) 1679 f.load() 1680 1681 assert.Equal(t, 1, len(f.loadResult.Manifests)) 1682 m := f.assertNextManifest("sancho") 1683 assert.Equal(t, 2, len(m.ImageTargets)) 1684 assert.Equal(t, "gcr.io/some-project-162817/sancho", 1685 m.ImageTargetAt(0).ImageMapSpec.Selector) 1686 assert.Equal(t, "gcr.io/some-project-162817/sancho-sidecar", 1687 m.ImageTargetAt(1).ImageMapSpec.Selector) 1688 } 1689 1690 func TestSanchoRedisSidecar(t *testing.T) { 1691 f := newFixture(t) 1692 f.setupFoo() 1693 f.file("Dockerfile", "FROM golang:1.10") 1694 f.file("k8s.yaml", testyaml.SanchoRedisSidecarYAML) 1695 f.file("Tiltfile", ` 1696 k8s_yaml('k8s.yaml') 1697 docker_build('gcr.io/some-project-162817/sancho', '.') 1698 `) 1699 f.load() 1700 1701 assert.Equal(t, 1, len(f.loadResult.Manifests)) 1702 m := f.assertNextManifest("sancho") 1703 assert.Equal(t, 1, len(m.ImageTargets)) 1704 assert.Equal(t, "gcr.io/some-project-162817/sancho", 1705 m.ImageTargetAt(0).ImageMapSpec.Selector) 1706 } 1707 1708 func TestExtraPodSelectors(t *testing.T) { 1709 f := newFixture(t) 1710 1711 f.setupExtraPodSelectors("[{'foo': 'bar', 'baz': 'qux'}, {'quux': 'corge'}]") 1712 f.load() 1713 1714 f.assertNextManifest("foo", 1715 extraPodSelectors(labels.Set{"foo": "bar", "baz": "qux"}, labels.Set{"quux": "corge"}), 1716 podReadiness(model.PodReadinessWait)) 1717 } 1718 1719 func TestExtraPodSelectorsNotList(t *testing.T) { 1720 f := newFixture(t) 1721 1722 f.setupExtraPodSelectors("'hello'") 1723 f.loadErrString("got starlark.String", "dict or a list") 1724 } 1725 1726 func TestExtraPodSelectorsDict(t *testing.T) { 1727 f := newFixture(t) 1728 1729 f.setupExtraPodSelectors("{'foo': 'bar'}") 1730 f.load() 1731 f.assertNextManifest("foo", 1732 extraPodSelectors(labels.Set{"foo": "bar"}), 1733 podReadiness(model.PodReadinessWait)) 1734 } 1735 1736 func TestExtraPodSelectorsElementNotDict(t *testing.T) { 1737 f := newFixture(t) 1738 1739 f.setupExtraPodSelectors("['hello']") 1740 f.loadErrString("must be dicts", "starlark.String") 1741 } 1742 1743 func TestExtraPodSelectorsKeyNotString(t *testing.T) { 1744 f := newFixture(t) 1745 1746 f.setupExtraPodSelectors("[{54321: 'hello'}]") 1747 f.loadErrString("keys must be strings", "54321") 1748 } 1749 1750 func TestExtraPodSelectorsValueNotString(t *testing.T) { 1751 f := newFixture(t) 1752 1753 f.setupExtraPodSelectors("[{'hello': 54321}]") 1754 f.loadErrString("values must be strings", "54321") 1755 } 1756 1757 func TestPodReadinessDefaultDeployment(t *testing.T) { 1758 f := newFixture(t) 1759 1760 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1761 f.file("Tiltfile", ` 1762 k8s_yaml('foo.yaml') 1763 `) 1764 1765 f.load("foo") 1766 f.assertNextManifest("foo", 1767 deployment("foo"), 1768 podReadiness(model.PodReadinessWait), 1769 ) 1770 } 1771 1772 func TestPodReadinessDefaultConfigMap(t *testing.T) { 1773 f := newFixture(t) 1774 1775 f.file("config.yaml", `apiVersion: v1 1776 kind: ConfigMap 1777 metadata: 1778 name: config 1779 data: 1780 foo: bar 1781 `) 1782 f.file("Tiltfile", ` 1783 k8s_yaml('config.yaml') 1784 k8s_resource(new_name='config', objects=['config']) 1785 `) 1786 1787 f.load("config") 1788 f.assertNextManifest("config", 1789 podReadiness(model.PodReadinessIgnore), 1790 ) 1791 } 1792 1793 func TestPodReadinessDefaultJob(t *testing.T) { 1794 f := newFixture(t) 1795 1796 f.file("job.yaml", `apiVersion: batch/v1 1797 kind: Job 1798 metadata: 1799 name: myjob 1800 `) 1801 f.file("Tiltfile", ` 1802 k8s_yaml('job.yaml') 1803 `) 1804 1805 f.load("myjob") 1806 f.assertNextManifest("myjob", 1807 podReadiness(model.PodReadinessSucceeded), 1808 ) 1809 } 1810 1811 func TestK8sDiscoveryStrategy(t *testing.T) { 1812 f := newFixture(t) 1813 1814 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1815 f.file("Tiltfile", ` 1816 k8s_yaml('foo.yaml') 1817 k8s_resource('foo', discovery_strategy='selectors-only') 1818 `) 1819 1820 f.load("foo") 1821 f.assertNextManifest("foo", 1822 deployment("foo"), 1823 v1alpha1.KubernetesDiscoveryStrategySelectorsOnly, 1824 ) 1825 } 1826 1827 func TestK8sDiscoveryStrategyInvalid(t *testing.T) { 1828 f := newFixture(t) 1829 1830 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1831 f.file("Tiltfile", ` 1832 k8s_yaml('foo.yaml') 1833 k8s_resource('foo', discovery_strategy='typo') 1834 `) 1835 1836 f.loadErrString("Invalid. Must be one of: \"default\", \"selectors-only\"") 1837 } 1838 1839 func TestPodReadinessOverrideDeployment(t *testing.T) { 1840 f := newFixture(t) 1841 1842 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1843 f.file("Tiltfile", ` 1844 k8s_yaml('foo.yaml') 1845 k8s_resource('foo', pod_readiness='ignore') 1846 `) 1847 1848 f.load("foo") 1849 f.assertNextManifest("foo", 1850 deployment("foo"), 1851 podReadiness(model.PodReadinessIgnore), 1852 ) 1853 } 1854 1855 func TestPodReadinessOverrideConfigMap(t *testing.T) { 1856 f := newFixture(t) 1857 1858 f.file("config.yaml", `apiVersion: v1 1859 kind: ConfigMap 1860 metadata: 1861 name: config 1862 data: 1863 foo: "bar" 1864 `) 1865 f.file("Tiltfile", ` 1866 k8s_yaml('config.yaml') 1867 k8s_resource(new_name='config', objects=['config'], pod_readiness='wait') 1868 `) 1869 1870 f.load("config") 1871 f.assertNextManifest("config", 1872 podReadiness(model.PodReadinessWait), 1873 ) 1874 } 1875 1876 func TestPodReadinessInvalid(t *testing.T) { 1877 f := newFixture(t) 1878 1879 f.file("config.yaml", `apiVersion: v1 1880 kind: ConfigMap 1881 metadata: 1882 name: config 1883 data: 1884 foo: bar 1885 `) 1886 f.file("Tiltfile", ` 1887 k8s_yaml('config.yaml') 1888 k8s_resource(new_name='config', objects=['config'], pod_readiness='w') 1889 `) 1890 1891 f.loadErrString("Invalid value. Allowed: {ignore, wait}. Got: w") 1892 } 1893 1894 func TestDockerBuildMatchingTag(t *testing.T) { 1895 f := newFixture(t) 1896 1897 f.gitInit("") 1898 f.file("Dockerfile", "FROM golang:1.10") 1899 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1900 f.file("Tiltfile", ` 1901 docker_build('gcr.io/foo:stable', '.') 1902 k8s_yaml('foo.yaml') 1903 `) 1904 1905 f.load("foo") 1906 f.assertNextManifest("foo", 1907 deployment("foo"), 1908 ) 1909 } 1910 1911 func TestDockerBuildButK8sMissing(t *testing.T) { 1912 f := newFixture(t) 1913 1914 f.gitInit("") 1915 f.file("Dockerfile", "FROM golang:1.10") 1916 f.file("Tiltfile", ` 1917 docker_build('gcr.io/foo:stable', '.') 1918 `) 1919 1920 f.loadAssertWarnings(unmatchedImageNoConfigsWarning) 1921 } 1922 1923 func TestDockerBuildButK8sMissingTag(t *testing.T) { 1924 f := newFixture(t) 1925 1926 f.gitInit("") 1927 f.file("Dockerfile", "FROM golang:1.10") 1928 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1929 f.file("Tiltfile", ` 1930 docker_build('gcr.io/foo:stable', '.') 1931 k8s_yaml('foo.yaml') 1932 `) 1933 1934 w := unusedImageWarning("gcr.io/foo:stable", []string{"gcr.io/foo"}, "Kubernetes") 1935 f.loadAssertWarnings(w) 1936 } 1937 1938 func TestDockerBuildUnusedSuppressWarning(t *testing.T) { 1939 f := newFixture(t) 1940 1941 f.gitInit("") 1942 f.file("Dockerfile", "FROM golang:1.10") 1943 f.file("Tiltfile", ` 1944 docker_build('a', '.') 1945 docker_build('b', '.') 1946 update_settings(suppress_unused_image_warnings=['a']) 1947 update_settings(suppress_unused_image_warnings=['b']) 1948 `) 1949 1950 f.load() 1951 } 1952 1953 func TestDockerBuildButK8sNonMatchingTag(t *testing.T) { 1954 f := newFixture(t) 1955 1956 f.gitInit("") 1957 f.file("Dockerfile", "FROM golang:1.10") 1958 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:beta"))) 1959 f.file("Tiltfile", ` 1960 docker_build('gcr.io/foo:stable', '.') 1961 k8s_yaml('foo.yaml') 1962 `) 1963 1964 w := unusedImageWarning("gcr.io/foo:stable", []string{"gcr.io/foo"}, "Kubernetes") 1965 f.loadAssertWarnings(w) 1966 } 1967 1968 func TestFail(t *testing.T) { 1969 f := newFixture(t) 1970 1971 f.file("Tiltfile", ` 1972 fail("this is an error") 1973 print("not this") 1974 fail("or this") 1975 `) 1976 1977 f.loadErrString("this is an error") 1978 } 1979 1980 func TestBlob(t *testing.T) { 1981 f := newFixture(t) 1982 1983 f.file( 1984 "Tiltfile", 1985 fmt.Sprintf(`k8s_yaml(blob('''%s'''))`, testyaml.SnackYaml), 1986 ) 1987 1988 f.load() 1989 1990 f.assertNextManifest("snack", deployment("snack")) 1991 } 1992 1993 func TestBlobErr(t *testing.T) { 1994 f := newFixture(t) 1995 1996 f.file( 1997 "Tiltfile", 1998 `blob(42)`, 1999 ) 2000 2001 f.loadErrString("for parameter input: got int, want string") 2002 } 2003 2004 func TestImageDependency(t *testing.T) { 2005 f := newFixture(t) 2006 2007 f.gitInit("") 2008 f.file("imageA.dockerfile", "FROM golang:1.10") 2009 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2010 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2011 f.file("Tiltfile", ` 2012 2013 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2014 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2015 k8s_yaml('foo.yaml') 2016 `) 2017 2018 f.load() 2019 f.assertNextManifest("foo", deployment("foo", image("gcr.io/image-a"), image("gcr.io/image-b"))) 2020 } 2021 2022 func TestImageDependencyLiveUpdate(t *testing.T) { 2023 f := newFixture(t) 2024 2025 f.gitInit("") 2026 f.file("message.txt", "Hello!") 2027 f.file("imageA.dockerfile", "FROM golang:1.10") 2028 f.file("imageB.dockerfile", `FROM gcr.io/image-a 2029 ADD message.txt /tmp/message.txt`) 2030 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2031 f.file("Tiltfile", ` 2032 2033 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile', 2034 live_update=[sync('message.txt', '/tmp/message.txt')]) 2035 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2036 k8s_yaml('foo.yaml') 2037 `) 2038 2039 f.load() 2040 m := f.assertNextManifest("foo", 2041 deployment("foo", image("gcr.io/image-a"), image("gcr.io/image-b"))) 2042 2043 assert.True(t, liveupdate.IsEmptySpec(m.ImageTargetAt(0).LiveUpdateSpec)) 2044 assert.False(t, liveupdate.IsEmptySpec(m.ImageTargetAt(1).LiveUpdateSpec)) 2045 } 2046 2047 func TestImageDependencyCycle(t *testing.T) { 2048 f := newFixture(t) 2049 2050 f.gitInit("") 2051 f.file("imageA.dockerfile", "FROM gcr.io/image-b") 2052 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2053 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2054 f.file("Tiltfile", ` 2055 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2056 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2057 k8s_yaml('foo.yaml') 2058 `) 2059 2060 f.loadErrString("Image dependency cycle: gcr.io/image-b") 2061 } 2062 2063 func TestImageDependencyDiamond(t *testing.T) { 2064 f := newFixture(t) 2065 2066 f.gitInit("") 2067 f.file("imageA.dockerfile", "FROM golang:1.10") 2068 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2069 f.file("imageC.dockerfile", "FROM gcr.io/image-a") 2070 f.file("imageD.dockerfile", ` 2071 FROM gcr.io/image-b 2072 FROM gcr.io/image-c 2073 `) 2074 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-d"))) 2075 f.file("Tiltfile", ` 2076 2077 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2078 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2079 docker_build('gcr.io/image-c', '.', dockerfile='imageC.dockerfile') 2080 docker_build('gcr.io/image-d', '.', dockerfile='imageD.dockerfile') 2081 k8s_yaml('foo.yaml') 2082 `) 2083 2084 f.load() 2085 2086 m := f.assertNextManifest("foo", deployment("foo")) 2087 assert.Equal(t, []string{ 2088 "gcr.io_image-a", 2089 "gcr.io_image-b", 2090 "gcr.io_image-c", 2091 "gcr.io_image-d", 2092 }, f.imageTargetNames(m)) 2093 } 2094 2095 func TestImageDependencyTwice(t *testing.T) { 2096 f := newFixture(t) 2097 2098 f.gitInit("") 2099 f.file("imageA.dockerfile", "FROM golang:1.10") 2100 f.file("imageB.dockerfile", `FROM golang:1.10 2101 COPY --from=gcr.io/image-a /src/package.json /src/package.json 2102 COPY --from=gcr.io/image-a /src/package.lock /src/package.lock 2103 `) 2104 f.file("snack.yaml", ` 2105 apiVersion: apps/v1 2106 kind: Deployment 2107 metadata: 2108 name: snack 2109 labels: 2110 app: snack 2111 spec: 2112 selector: 2113 matchLabels: 2114 app: snack 2115 template: 2116 metadata: 2117 labels: 2118 app: snack 2119 spec: 2120 containers: 2121 - name: snack1 2122 image: gcr.io/image-b 2123 command: ["/go/bin/snack"] 2124 - name: snack2 2125 image: gcr.io/image-b 2126 command: ["/go/bin/snack"] 2127 `) 2128 f.file("Tiltfile", ` 2129 2130 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2131 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2132 k8s_yaml('snack.yaml') 2133 `) 2134 2135 f.load() 2136 2137 m := f.assertNextManifest("snack") 2138 assert.Equal(t, []string{ 2139 "gcr.io_image-a", 2140 "gcr.io_image-b", 2141 }, f.imageTargetNames(m)) 2142 assert.Equal(t, []string{ 2143 "gcr.io_image-a", 2144 "gcr.io_image-b", 2145 "snack", // the deploy name 2146 }, f.idNames(m.DependencyIDs())) 2147 assert.Equal(t, []string{}, f.idNames(m.ImageTargets[0].DependencyIDs())) 2148 assert.Equal(t, []string{"gcr.io_image-a"}, f.idNames(m.ImageTargets[1].DependencyIDs())) 2149 assert.Equal(t, []string{"gcr.io_image-b"}, f.idNames(m.DeployTarget.DependencyIDs())) 2150 } 2151 2152 func TestImageDependencyNormalization(t *testing.T) { 2153 f := newFixture(t) 2154 2155 f.gitInit("") 2156 f.file("common.dockerfile", "FROM golang:1.10") 2157 f.file("auth.dockerfile", "FROM vandelay/common") 2158 f.yaml("auth.yaml", deployment("auth", image("vandelay/auth"))) 2159 f.file("Tiltfile", ` 2160 docker_build('vandelay/common', '.', dockerfile='common.dockerfile') 2161 docker_build('vandelay/auth', '.', dockerfile='auth.dockerfile') 2162 k8s_yaml('auth.yaml') 2163 `) 2164 2165 f.load() 2166 2167 m := f.assertNextManifest("auth", deployment("auth")) 2168 assert.Equal(t, []string{ 2169 "vandelay_common", 2170 "vandelay_auth", 2171 }, f.imageTargetNames(m)) 2172 } 2173 2174 func TestImagesWithSameNameAssembly(t *testing.T) { 2175 f := newFixture(t) 2176 2177 f.gitInit("") 2178 f.file("app.dockerfile", "FROM golang:1.10") 2179 f.file("app-jessie.dockerfile", "FROM golang:1.10-jessie") 2180 f.yaml("app.yaml", 2181 deployment("app", image("vandelay/app")), 2182 deployment("app-jessie", image("vandelay/app:jessie"))) 2183 f.file("Tiltfile", ` 2184 2185 docker_build('vandelay/app', '.', dockerfile='app.dockerfile') 2186 docker_build('vandelay/app:jessie', '.', dockerfile='app-jessie.dockerfile') 2187 k8s_yaml('app.yaml') 2188 `) 2189 2190 f.load() 2191 2192 f.assertNextManifest("app", deployment("app", image("vandelay/app"))) 2193 f.assertNextManifest("app-jessie", deployment("app-jessie", image("vandelay/app:jessie"))) 2194 } 2195 2196 func TestImagesWithSameNameDifferentManifests(t *testing.T) { 2197 f := newFixture(t) 2198 2199 f.gitInit("") 2200 f.file("app.dockerfile", "FROM golang:1.10") 2201 f.file("app-jessie.dockerfile", "FROM golang:1.10-jessie") 2202 f.yaml("app.yaml", 2203 deployment("app", image("vandelay/app")), 2204 deployment("app-jessie", image("vandelay/app:jessie"))) 2205 f.file("Tiltfile", ` 2206 docker_build('vandelay/app', '.', dockerfile='app.dockerfile') 2207 docker_build('vandelay/app:jessie', '.', dockerfile='app-jessie.dockerfile') 2208 k8s_yaml('app.yaml') 2209 `) 2210 2211 f.load() 2212 2213 m := f.assertNextManifest("app", deployment("app")) 2214 assert.Equal(t, []string{ 2215 "vandelay_app", 2216 }, f.imageTargetNames(m)) 2217 2218 m = f.assertNextManifest("app-jessie", deployment("app-jessie")) 2219 assert.Equal(t, []string{ 2220 "vandelay_app:jessie", 2221 }, f.imageTargetNames(m)) 2222 } 2223 2224 func TestImageRefSuggestion(t *testing.T) { 2225 f := newFixture(t) 2226 2227 f.setupFoo() 2228 f.file("Tiltfile", ` 2229 docker_build('gcr.typo.io/foo', 'foo') 2230 k8s_yaml('foo.yaml') 2231 `) 2232 2233 w := unusedImageWarning("gcr.typo.io/foo", []string{"gcr.io/foo"}, "Kubernetes") 2234 f.loadAssertWarnings(w) 2235 } 2236 2237 func TestDir(t *testing.T) { 2238 f := newFixture(t) 2239 2240 f.gitInit("") 2241 f.yaml("config/foo.yaml", deployment("foo", image("gcr.io/foo"))) 2242 f.yaml("config/bar.yaml", deployment("bar", image("gcr.io/bar"))) 2243 f.file("Tiltfile", `k8s_yaml(listdir('config'))`) 2244 2245 f.load("foo", "bar") 2246 f.assertNumManifests(2) 2247 f.assertConfigFiles("Tiltfile", ".tiltignore", "config/foo.yaml", "config/bar.yaml") 2248 } 2249 2250 func TestDirRecursive(t *testing.T) { 2251 f := newFixture(t) 2252 2253 f.gitInit("") 2254 f.file("foo/bar", "bar") 2255 f.file("foo/baz/qux", "qux") 2256 f.file("Tiltfile", `files = listdir('foo', recursive=True) 2257 2258 for f in files: 2259 read_file(f) 2260 `) 2261 2262 f.load() 2263 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo", "foo/bar", "foo/baz/qux") 2264 } 2265 2266 func TestCallCounts(t *testing.T) { 2267 f := newFixture(t) 2268 2269 f.gitInit("") 2270 f.file("Dockerfile", "FROM golang:1.10") 2271 f.yaml("foo.yaml", 2272 deployment("foo", image("gcr.io/foo")), 2273 deployment("bar", image("gcr.io/bar"))) 2274 f.file("Tiltfile", ` 2275 docker_build('gcr.io/foo', '.') 2276 docker_build('gcr.io/bar', '.') 2277 k8s_yaml('foo.yaml') 2278 `) 2279 2280 f.load() 2281 2282 require.Len(t, f.an.Counts, 1) 2283 expectedCallCounts := map[string]int{ 2284 "docker_build": 2, 2285 "k8s_yaml": 1, 2286 } 2287 tags := f.an.Counts[0].Tags 2288 for arg, expectedCount := range expectedCallCounts { 2289 count, ok := tags[fmt.Sprintf("tiltfile.invoked.%s", arg)] 2290 require.True(t, ok, "arg %s was not counted in %v", arg, tags) 2291 require.Equal(t, strconv.Itoa(expectedCount), count, "arg %s had the wrong count in %v", arg, tags) 2292 } 2293 } 2294 2295 func TestArgCounts(t *testing.T) { 2296 f := newFixture(t) 2297 2298 f.gitInit("") 2299 f.file("Dockerfile", "FROM golang:1.10") 2300 f.yaml("foo.yaml", 2301 deployment("foo", image("gcr.io/foo")), 2302 deployment("bar", image("gcr.io/bar"))) 2303 f.file("Tiltfile", ` 2304 docker_build(ref='gcr.io/foo', context='.', dockerfile='Dockerfile') 2305 docker_build('gcr.io/bar', '.') 2306 k8s_yaml('foo.yaml') 2307 `) 2308 2309 f.load() 2310 2311 require.Len(t, f.an.Counts, 1) 2312 expectedArgCounts := map[string]int{ 2313 "docker_build.arg.context": 2, 2314 "docker_build.arg.dockerfile": 1, 2315 "docker_build.arg.ref": 2, 2316 "k8s_yaml.arg.yaml": 1, 2317 } 2318 tags := f.an.Counts[0].Tags 2319 for arg, expectedCount := range expectedArgCounts { 2320 count, ok := tags[fmt.Sprintf("tiltfile.invoked.%s", arg)] 2321 require.True(t, ok, "tiltfile.invoked.%s was not counted in %v", arg, tags) 2322 require.Equal(t, strconv.Itoa(expectedCount), count, "tiltfile.invoked.%s had the wrong count in %v", arg, tags) 2323 } 2324 } 2325 2326 func TestK8sManifestRefInjectCounts(t *testing.T) { 2327 f := newFixture(t) 2328 2329 f.gitInit("") 2330 f.file("Dockerfile", "FROM golang:1.10") 2331 f.file("sancho_twin.yaml", testyaml.SanchoTwoContainersOneImageYAML) // 1 img x 2 c 2332 f.file("sancho_sidecar.yaml", testyaml.SanchoSidecarYAML) // 2 imgs (1 c each) 2333 f.file("blorg.yaml", testyaml.BlorgJobYAML) 2334 2335 f.file("Tiltfile", ` 2336 docker_build('gcr.io/some-project-162817/sancho', '.') 2337 docker_build('gcr.io/some-project-162817/sancho-sidecar', '.') 2338 docker_build('gcr.io/blorg-dev/blorg-backend:devel-nick', '.') 2339 2340 k8s_yaml(['sancho_twin.yaml', 'sancho_sidecar.yaml', 'blorg.yaml']) 2341 `) 2342 2343 f.load() 2344 2345 sanchoTwin := f.assertNextManifest("sancho-2c1i") 2346 sTwinInjectCounts := sanchoTwin.K8sTarget().RefInjectCounts() 2347 assert.Len(t, sTwinInjectCounts, 1) 2348 assert.Equal(t, sTwinInjectCounts[testyaml.SanchoImage], 2) 2349 2350 sanchoSidecar := f.assertNextManifest("sancho") 2351 ssInjectCounts := sanchoSidecar.K8sTarget().RefInjectCounts() 2352 assert.Len(t, ssInjectCounts, 2) 2353 assert.Equal(t, ssInjectCounts[testyaml.SanchoImage], 1) 2354 assert.Equal(t, ssInjectCounts[testyaml.SanchoSidecarImage], 1) 2355 2356 blorgJob := f.assertNextManifest("blorg-job") 2357 blorgInjectCounts := blorgJob.K8sTarget().RefInjectCounts() 2358 assert.Len(t, blorgInjectCounts, 1) 2359 assert.Equal(t, blorgJob.K8sTarget().RefInjectCounts()["gcr.io/blorg-dev/blorg-backend:devel-nick"], 1) 2360 } 2361 2362 func TestYamlErrorFromLocal(t *testing.T) { 2363 f := newFixture(t) 2364 f.file("Tiltfile", ` 2365 yaml = local('echo hi') 2366 k8s_yaml(yaml) 2367 `) 2368 f.loadErrString("echo hi") 2369 } 2370 2371 func TestYamlErrorFromReadFile(t *testing.T) { 2372 f := newFixture(t) 2373 f.file("foo.yaml", "hi") 2374 f.file("Tiltfile", ` 2375 k8s_yaml(read_file('foo.yaml')) 2376 `) 2377 f.loadErrString(fmt.Sprintf("file: %s", f.JoinPath("foo.yaml"))) 2378 } 2379 2380 func TestYamlErrorFromBlob(t *testing.T) { 2381 f := newFixture(t) 2382 f.file("Tiltfile", ` 2383 k8s_yaml(blob('hi')) 2384 `) 2385 f.loadErrString("from Tiltfile blob() call") 2386 } 2387 2388 func TestCustomBuildWithTag(t *testing.T) { 2389 f := newFixture(t) 2390 2391 tiltfile := `k8s_yaml('foo.yaml') 2392 custom_build( 2393 'gcr.io/foo', 2394 'docker build -t gcr.io/foo:my-great-tag foo', 2395 ['foo'], 2396 tag='my-great-tag' 2397 )` 2398 2399 f.setupFoo() 2400 f.file("Tiltfile", tiltfile) 2401 2402 f.load("foo") 2403 f.assertNumManifests(1) 2404 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/.dockerignore") 2405 m := f.assertNextManifest("foo", 2406 cb( 2407 image("gcr.io/foo"), 2408 deps(f.JoinPath("foo")), 2409 cmd("docker build -t gcr.io/foo:my-great-tag foo", f.Path()), 2410 tag("my-great-tag"), 2411 ), 2412 deployment("foo")) 2413 assert.False(t, m.ImageTargets[0].CustomBuildInfo().SkipsPush()) 2414 } 2415 2416 func TestCustomBuildDisablePush(t *testing.T) { 2417 f := newFixture(t) 2418 2419 tiltfile := `k8s_yaml('foo.yaml') 2420 hfb = custom_build( 2421 'gcr.io/foo', 2422 'docker build -t $TAG foo', 2423 ['foo'], 2424 disable_push=True, 2425 )` 2426 2427 f.setupFoo() 2428 f.file("Tiltfile", tiltfile) 2429 2430 f.load("foo") 2431 f.assertNumManifests(1) 2432 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/.dockerignore") 2433 f.assertNextManifest("foo", 2434 cb( 2435 image("gcr.io/foo"), 2436 deps(f.JoinPath("foo")), 2437 cmd("docker build -t $TAG foo", f.Path()), 2438 disablePush(true), 2439 ), 2440 deployment("foo")) 2441 } 2442 2443 func TestCustomBuildSkipsLocalDocker(t *testing.T) { 2444 f := newFixture(t) 2445 2446 tiltfile := ` 2447 k8s_yaml('foo.yaml') 2448 custom_build( 2449 'gcr.io/foo', 2450 'buildah bud -t $TAG foo && buildah push $TAG $TAG', 2451 ['foo'], 2452 skips_local_docker=True, 2453 )` 2454 2455 f.setupFoo() 2456 f.file("Tiltfile", tiltfile) 2457 2458 f.load("foo") 2459 m := f.assertNextManifest("foo", 2460 cb( 2461 image("gcr.io/foo"), 2462 ), 2463 deployment("foo")) 2464 assert.Equal(t, v1alpha1.CmdImageOutputRemote, m.ImageTargets[0].CustomBuildInfo().OutputMode) 2465 assert.True(t, m.ImageTargets[0].CustomBuildInfo().SkipsPush()) 2466 } 2467 2468 func TestImageObjectJSONPath(t *testing.T) { 2469 f := newFixture(t) 2470 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2471 kind: UselessMachine 2472 metadata: 2473 name: um 2474 spec: 2475 image: 2476 repo: tilt.dev/frontend`) 2477 f.dockerfile("Dockerfile") 2478 f.file("Tiltfile", ` 2479 k8s_yaml('um.yaml') 2480 k8s_kind(kind='UselessMachine', image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2481 docker_build('tilt.dev/frontend', '.') 2482 `) 2483 2484 f.load() 2485 m := f.assertNextManifest("um", 2486 podReadiness(model.PodReadinessWait)) 2487 assert.Equal(t, "tilt.dev/frontend", 2488 m.ImageTargets[0].ImageMapSpec.Selector) 2489 } 2490 2491 func TestImageObjectJSONPathNoMatch(t *testing.T) { 2492 f := newFixture(t) 2493 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2494 kind: UselessMachine 2495 metadata: 2496 name: um 2497 spec: 2498 repo: tilt.dev/frontend`) 2499 f.dockerfile("Dockerfile") 2500 f.file("Tiltfile", ` 2501 k8s_yaml('um.yaml') 2502 k8s_kind(kind='UselessMachine', image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2503 docker_build('tilt.dev/frontend', '.') 2504 `) 2505 2506 f.loadErrString("finding image", "UselessMachine/um", ".spec.image") 2507 } 2508 2509 func TestImageObjectJSONPathPodReadinessIgnore(t *testing.T) { 2510 f := newFixture(t) 2511 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2512 kind: UselessMachine 2513 metadata: 2514 name: um 2515 spec: 2516 image: 2517 repo: tilt.dev/frontend`) 2518 f.dockerfile("Dockerfile") 2519 f.file("Tiltfile", ` 2520 k8s_yaml('um.yaml') 2521 k8s_kind(kind='UselessMachine', pod_readiness='ignore', 2522 image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2523 docker_build('tilt.dev/frontend', '.') 2524 `) 2525 2526 f.load() 2527 m := f.assertNextManifest("um", 2528 podReadiness(model.PodReadinessIgnore)) 2529 assert.Equal(t, "tilt.dev/frontend", 2530 m.ImageTargets[0].ImageMapSpec.Selector) 2531 } 2532 2533 func TestExtraImageLocationOneImage(t *testing.T) { 2534 f := newFixture(t) 2535 f.setupCRD() 2536 f.dockerfile("env/Dockerfile") 2537 f.dockerfile("builder/Dockerfile") 2538 f.file("Tiltfile", ` 2539 2540 k8s_yaml('crd.yaml') 2541 k8s_image_json_path(kind='Environment', paths='{.spec.runtime.image}') 2542 docker_build('test/mycrd-env', 'env') 2543 `) 2544 2545 f.load("mycrd") 2546 f.assertNextManifest("mycrd", 2547 db( 2548 image("test/mycrd-env"), 2549 ), 2550 k8sObject("mycrd", "Environment"), 2551 ) 2552 } 2553 2554 func TestConflictingWorkloadNames(t *testing.T) { 2555 f := newFixture(t) 2556 2557 f.dockerfile("foo1/Dockerfile") 2558 f.dockerfile("foo2/Dockerfile") 2559 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 2560 f.yaml("foo2.yaml", deployment("foo", image("gcr.io/foo2"), namespace("ns2"))) 2561 2562 f.file("Tiltfile", ` 2563 2564 k8s_yaml(['foo1.yaml', 'foo2.yaml']) 2565 docker_build('gcr.io/foo1', 'foo1') 2566 docker_build('gcr.io/foo2', 'foo2') 2567 `) 2568 f.load("foo:deployment:ns1", "foo:deployment:ns2") 2569 2570 f.assertNextManifest("foo:deployment:ns1", db(image("gcr.io/foo1"))) 2571 f.assertNextManifest("foo:deployment:ns2", db(image("gcr.io/foo2"))) 2572 } 2573 2574 type k8sKindTest struct { 2575 name string 2576 k8sKindArgs string 2577 expectWorkload bool 2578 expectImage bool 2579 expectedError string 2580 preamble string 2581 expectedResourceName model.ManifestName 2582 } 2583 2584 func TestK8sKind(t *testing.T) { 2585 tests := []k8sKindTest{ 2586 {name: "match kind", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}'", expectWorkload: true, expectImage: true}, 2587 {name: "don't match kind", k8sKindArgs: "'fdas', image_json_path='{.spec.runtime.image}'", expectWorkload: false}, 2588 {name: "match apiVersion", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}', api_version='fission.io/v1'", expectWorkload: true, expectImage: true}, 2589 {name: "don't match apiVersion", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}', api_version='fission.io/v2'"}, 2590 {name: "invalid kind regexp", k8sKindArgs: "'*', image_json_path='{.spec.runtime.image}'", expectedError: "error parsing kind regexp"}, 2591 {name: "invalid apiVersion regexp", k8sKindArgs: "'Environment', api_version='*', image_json_path='{.spec.runtime.image}'", expectedError: "error parsing apiVersion regexp"}, 2592 {name: "no image", k8sKindArgs: "'Environment'", expectWorkload: true}, 2593 } 2594 2595 for _, test := range tests { 2596 t.Run(test.name, func(t *testing.T) { 2597 f := newFixture(t) 2598 f.setupCRD() 2599 f.dockerfile("env/Dockerfile") 2600 f.dockerfile("builder/Dockerfile") 2601 img := "" 2602 if !test.expectWorkload || test.expectImage { 2603 img = "docker_build('test/mycrd-env', 'env')" 2604 } 2605 f.file("Tiltfile", fmt.Sprintf(` 2606 2607 %s 2608 k8s_yaml('crd.yaml') 2609 k8s_kind(%s) 2610 %s 2611 `, test.preamble, test.k8sKindArgs, img)) 2612 2613 if test.expectWorkload { 2614 if test.expectedError != "" { 2615 t.Fatal("invalid test: cannot expect both workload and error") 2616 } 2617 expectedResourceName := model.ManifestName("mycrd") 2618 if test.expectedResourceName != "" { 2619 expectedResourceName = test.expectedResourceName 2620 } 2621 f.load(string(expectedResourceName)) 2622 var imageOpt interface{} 2623 if test.expectImage { 2624 imageOpt = db(image("test/mycrd-env")) 2625 } else { 2626 imageOpt = funcOpt(func(t *testing.T, m model.Manifest) bool { 2627 return assert.Equal(t, 0, len(m.ImageTargets)) 2628 }) 2629 } 2630 f.assertNextManifest( 2631 expectedResourceName, 2632 k8sObject("mycrd", "Environment"), 2633 imageOpt) 2634 } else { 2635 if test.expectImage { 2636 t.Fatal("invalid test: cannot expect image without expecting workload") 2637 } 2638 if test.expectedError == "" { 2639 f.loadAssertWarnings(unmatchedImageAllUnresourcedWarning) 2640 } else { 2641 f.loadErrString(test.expectedError) 2642 } 2643 } 2644 }) 2645 } 2646 } 2647 2648 func TestK8sKindImageJSONPathPositional(t *testing.T) { 2649 f := newFixture(t) 2650 f.setupCRD() 2651 f.dockerfile("env/Dockerfile") 2652 f.dockerfile("builder/Dockerfile") 2653 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2654 k8s_kind('Environment', '{.spec.runtime.image}') 2655 docker_build('test/mycrd-env', 'env') 2656 `) 2657 2658 f.loadErrString("got 2 arguments, want at most 1") 2659 } 2660 2661 func TestExtraImageLocationTwoImages(t *testing.T) { 2662 f := newFixture(t) 2663 f.setupCRD() 2664 f.dockerfile("env/Dockerfile") 2665 f.dockerfile("builder/Dockerfile") 2666 f.file("Tiltfile", ` 2667 2668 k8s_yaml('crd.yaml') 2669 k8s_image_json_path(['{.spec.runtime.image}', '{.spec.builder.image}'], kind='Environment') 2670 docker_build('test/mycrd-builder', 'builder') 2671 docker_build('test/mycrd-env', 'env') 2672 `) 2673 2674 f.load("mycrd") 2675 f.assertNextManifest("mycrd", 2676 db( 2677 image("test/mycrd-env"), 2678 ), 2679 db( 2680 image("test/mycrd-builder"), 2681 ), 2682 k8sObject("mycrd", "Environment"), 2683 ) 2684 } 2685 2686 func TestExtraImageLocationDeploymentEnvVarByName(t *testing.T) { 2687 f := newFixture(t) 2688 2689 f.dockerfile("foo/Dockerfile") 2690 f.dockerfile("foo-fetcher/Dockerfile") 2691 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2692 f.dockerfile("bar/Dockerfile") 2693 // just throwing bar in here to make sure it doesn't error out because it has no FETCHER_IMAGE 2694 f.yaml("bar.yaml", deployment("bar", image("gcr.io/bar"))) 2695 f.gitInit("") 2696 2697 f.file("Tiltfile", `k8s_yaml(['foo.yaml', 'bar.yaml']) 2698 docker_build('gcr.io/foo', 'foo') 2699 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2700 docker_build('gcr.io/bar', 'bar') 2701 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", name='foo') 2702 `) 2703 f.load("foo", "bar") 2704 f.assertNextManifest("foo", 2705 db( 2706 image("gcr.io/foo"), 2707 ), 2708 db( 2709 image("gcr.io/foo-fetcher"), 2710 ), 2711 ) 2712 f.assertNextManifest("bar", 2713 db( 2714 image("gcr.io/bar"), 2715 ), 2716 ) 2717 } 2718 2719 func TestExtraImageLocationDeploymentEnvVarMatch(t *testing.T) { 2720 f := newFixture(t) 2721 2722 f.dockerfile("foo/Dockerfile") 2723 f.dockerfile("foo-fetcher/Dockerfile") 2724 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2725 f.gitInit("") 2726 2727 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2728 docker_build('gcr.io/foo', 'foo') 2729 docker_build('gcr.io/foo-fetcher', 'foo-fetcher', match_in_env_vars=True) 2730 `) 2731 f.load("foo") 2732 f.assertNextManifest("foo", 2733 db( 2734 image("gcr.io/foo"), 2735 ), 2736 db( 2737 image("gcr.io/foo-fetcher").withMatchInEnvVars(), 2738 ), 2739 ) 2740 } 2741 2742 func TestExtraImageLocationDeploymentEnvVarDoesNotMatchIfNotSpecified(t *testing.T) { 2743 f := newFixture(t) 2744 2745 f.dockerfile("foo/Dockerfile") 2746 f.dockerfile("foo-fetcher/Dockerfile") 2747 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2748 f.gitInit("") 2749 2750 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2751 docker_build('gcr.io/foo', 'foo') 2752 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2753 `) 2754 f.loadAssertWarnings(unusedImageWarning("gcr.io/foo-fetcher", []string{"gcr.io/foo"}, "Kubernetes")) 2755 f.assertNextManifest("foo", 2756 db( 2757 image("gcr.io/foo"), 2758 ), 2759 ) 2760 2761 } 2762 2763 func TestK8sImageJSONPathArgs(t *testing.T) { 2764 tests := []struct { 2765 name string 2766 args string 2767 expectMatch bool 2768 expectedError string 2769 }{ 2770 {"match name", "name='foo'", true, ""}, 2771 {"don't match name", "name='bar'", false, ""}, 2772 {"match name w/ regex", "name='.*o'", true, ""}, 2773 {"match kind", "name='foo', kind='Deployment'", true, ""}, 2774 {"don't match kind", "name='bar', kind='asdf'", false, ""}, 2775 {"match apiVersion", "name='foo', api_version='apps/v1'", true, ""}, 2776 {"match apiVersion+kind w/ regex", "name='foo', kind='Deployment', api_version='apps/.*'", true, ""}, 2777 {"don't match apiVersion", "name='bar', api_version='apps/v2'", false, ""}, 2778 {"match namespace", "name='foo', namespace='default'", true, ""}, 2779 {"don't match namespace", "name='bar', namespace='asdf'", false, ""}, 2780 {"invalid name regex", "name='*'", false, "error parsing name regexp"}, 2781 {"invalid kind regex", "kind='*'", false, "error parsing kind regexp"}, 2782 {"invalid apiVersion regex", "name='foo', api_version='*'", false, "error parsing apiVersion regexp"}, 2783 {"invalid namespace regex", "namespace='*'", false, "error parsing namespace regexp"}, 2784 {"regexes are case-insensitive", "name='FOO'", true, ""}, 2785 {"regexes that specify case insensitivity still work", "name='(?i)FOO'", true, ""}, 2786 } 2787 for _, test := range tests { 2788 t.Run(test.name, func(t *testing.T) { 2789 f := newFixture(t) 2790 2791 f.dockerfile("foo/Dockerfile") 2792 f.dockerfile("foo-fetcher/Dockerfile") 2793 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2794 f.gitInit("") 2795 2796 f.file("Tiltfile", fmt.Sprintf(`k8s_yaml('foo.yaml') 2797 docker_build('gcr.io/foo', 'foo') 2798 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2799 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", %s) 2800 `, test.args)) 2801 if test.expectMatch { 2802 if test.expectedError != "" { 2803 t.Fatal("illegal test definition: cannot expect both match and error") 2804 } 2805 f.load("foo") 2806 f.assertNextManifest("foo", 2807 db( 2808 image("gcr.io/foo"), 2809 ), 2810 db( 2811 image("gcr.io/foo-fetcher"), 2812 ), 2813 ) 2814 } else { 2815 if test.expectedError == "" { 2816 w := unusedImageWarning("gcr.io/foo-fetcher", []string{"gcr.io/foo"}, "Kubernetes") 2817 f.loadAssertWarnings(w) 2818 } else { 2819 f.loadErrString(test.expectedError) 2820 } 2821 } 2822 }) 2823 } 2824 } 2825 2826 func TestExtraImageLocationDeploymentEnvVarByNameAndNamespace(t *testing.T) { 2827 f := newFixture(t) 2828 2829 f.dockerfile("foo/Dockerfile") 2830 f.dockerfile("foo-fetcher/Dockerfile") 2831 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2832 f.gitInit("") 2833 2834 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2835 docker_build('gcr.io/foo', 'foo') 2836 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2837 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", name='foo', namespace='default') 2838 `) 2839 f.load("foo") 2840 f.assertNextManifest("foo", 2841 db( 2842 image("gcr.io/foo"), 2843 ), 2844 db( 2845 image("gcr.io/foo-fetcher"), 2846 ), 2847 ) 2848 } 2849 2850 func TestExtraImageLocationNoMatch(t *testing.T) { 2851 f := newFixture(t) 2852 f.setupCRD() 2853 f.dockerfile("env/Dockerfile") 2854 f.dockerfile("builder/Dockerfile") 2855 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2856 k8s_image_json_path('{.foobar}', kind='Environment') 2857 docker_build('test/mycrd-env', 'env') 2858 `) 2859 2860 f.loadErrString("{.foobar}", "foobar is not found") 2861 } 2862 2863 func TestExtraImageLocationInvalidJsonPath(t *testing.T) { 2864 f := newFixture(t) 2865 f.setupCRD() 2866 f.dockerfile("env/Dockerfile") 2867 f.dockerfile("builder/Dockerfile") 2868 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2869 k8s_image_json_path('{foobar()}', kind='Environment') 2870 docker_build('test/mycrd-env', 'env') 2871 `) 2872 2873 f.loadErrString("{foobar()}", "unrecognized identifier foobar()") 2874 } 2875 2876 func TestExtraImageLocationNoPaths(t *testing.T) { 2877 f := newFixture(t) 2878 f.file("Tiltfile", `k8s_image_json_path(kind='MyType')`) 2879 f.loadErrString("missing argument for paths") 2880 } 2881 2882 func TestExtraImageLocationNotListOrString(t *testing.T) { 2883 f := newFixture(t) 2884 f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=8)`) 2885 f.loadErrString("for parameter \"paths\": Expected string, got: 8") 2886 } 2887 2888 func TestExtraImageLocationListContainsNonString(t *testing.T) { 2889 f := newFixture(t) 2890 f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=["foo", 8])`) 2891 f.loadErrString("for parameter \"paths\": Expected string, got: 8") 2892 } 2893 2894 func TestExtraImageLocationNoSelectorSpecified(t *testing.T) { 2895 f := newFixture(t) 2896 f.file("Tiltfile", `k8s_image_json_path(paths=["foo"])`) 2897 f.loadErrString("at least one of kind, name, or namespace must be specified") 2898 } 2899 2900 func TestDockerBuildEmptyDockerFileArg(t *testing.T) { 2901 f := newFixture(t) 2902 f.file("Tiltfile", ` 2903 docker_build('web/api', '', dockerfile='') 2904 `) 2905 f.loadErrString("error reading dockerfile") 2906 } 2907 2908 func TestK8sYamlEmptyArg(t *testing.T) { 2909 f := newFixture(t) 2910 f.file("Tiltfile", ` 2911 k8s_yaml('') 2912 `) 2913 f.loadErrString("error reading yaml file") 2914 } 2915 2916 func TestTwoDefaultRegistries(t *testing.T) { 2917 f := newFixture(t) 2918 2919 f.file("Tiltfile", ` 2920 default_registry("gcr.io") 2921 default_registry("docker.io")`) 2922 2923 f.loadErrString("default registry already defined") 2924 } 2925 2926 func TestDefaultRegistryInvalid(t *testing.T) { 2927 f := newFixture(t) 2928 2929 f.setupFoo() 2930 f.file("Tiltfile", ` 2931 default_registry("foo") 2932 docker_build('gcr.io/foo', 'foo') 2933 `) 2934 2935 f.loadErrString("Traceback ", "repository name must be canonical") 2936 } 2937 2938 func TestDefaultRegistryHostFromCluster(t *testing.T) { 2939 f := newFixture(t) 2940 2941 f.setupFoo() 2942 f.file("Tiltfile", ` 2943 default_registry("abc.io", host_from_cluster="def.io") 2944 k8s_yaml('foo.yaml') 2945 docker_build('gcr.io/foo', 'foo') 2946 `) 2947 2948 f.load() 2949 2950 f.assertNextManifest("foo", 2951 db(image("gcr.io/foo").withLocalRef("abc.io/gcr.io_foo").withClusterRef("def.io/gcr.io_foo")), 2952 deployment("foo")) 2953 } 2954 2955 func TestDefaultRegistryAtEndOfTiltfile(t *testing.T) { 2956 f := newFixture(t) 2957 2958 f.setupFoo() 2959 // default_registry is the last entry to test that it doesn't only affect subsequently defined images 2960 f.file("Tiltfile", ` 2961 docker_build('gcr.io/foo', 'foo') 2962 k8s_yaml('foo.yaml') 2963 default_registry('bar.com') 2964 `) 2965 2966 f.load() 2967 2968 f.assertNextManifest("foo", 2969 db(image("gcr.io/foo").withLocalRef("bar.com/gcr.io_foo")), 2970 deployment("foo")) 2971 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 2972 } 2973 2974 func TestDefaultRegistryTwoImagesOnlyDifferByTag(t *testing.T) { 2975 f := newFixture(t) 2976 2977 f.dockerfile("bar/Dockerfile") 2978 f.yaml("bar.yaml", deployment("bar", image("gcr.io/foo:bar"))) 2979 2980 f.dockerfile("baz/Dockerfile") 2981 f.yaml("baz.yaml", deployment("baz", image("gcr.io/foo:baz"))) 2982 2983 f.gitInit("") 2984 f.file("Tiltfile", ` 2985 2986 docker_build('gcr.io/foo:bar', 'bar') 2987 docker_build('gcr.io/foo:baz', 'baz') 2988 k8s_yaml('bar.yaml') 2989 k8s_yaml('baz.yaml') 2990 default_registry('example.com') 2991 `) 2992 2993 f.load() 2994 2995 f.assertNextManifest("bar", 2996 db(image("gcr.io/foo:bar").withLocalRef("example.com/gcr.io_foo")), 2997 deployment("bar")) 2998 f.assertNextManifest("baz", 2999 db(image("gcr.io/foo:baz").withLocalRef("example.com/gcr.io_foo")), 3000 deployment("baz")) 3001 f.assertConfigFiles("Tiltfile", ".tiltignore", "bar/Dockerfile", "bar/.dockerignore", "bar.yaml", "baz/Dockerfile", "baz/.dockerignore", "baz.yaml") 3002 } 3003 3004 func TestDefaultRegistrySingleName(t *testing.T) { 3005 f := newFixture(t) 3006 3007 f.dockerfile("fe/Dockerfile") 3008 f.yaml("fe.yaml", deployment("fe", image("fe"))) 3009 3010 f.dockerfile("be/Dockerfile") 3011 f.yaml("be.yaml", deployment("be", image("be"))) 3012 3013 f.gitInit("") 3014 f.file("Tiltfile", ` 3015 3016 docker_build('fe', './fe') 3017 docker_build('be', './be') 3018 k8s_yaml('fe.yaml') 3019 k8s_yaml('be.yaml') 3020 default_registry('123.dkr.ecr.us-east-1.amazonaws.com', single_name='team-a/dev') 3021 `) 3022 3023 f.load() 3024 3025 fe := f.assertNextManifest("fe", 3026 db(image("fe").withLocalRef("123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev")), 3027 deployment("fe")) 3028 3029 feRefs, err := fe.ImageTargets[0].Refs(f.cluster(fe)) 3030 assert.NoError(t, err) 3031 feTaggedRefs, err := feRefs.AddTagSuffix("tilt-build-123") 3032 assert.NoError(t, err) 3033 assert.Equal(t, "123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev:fe-tilt-build-123", 3034 feTaggedRefs.LocalRef.String()) 3035 3036 be := f.assertNextManifest("be", 3037 db(image("be").withLocalRef("123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev")), 3038 deployment("be")) 3039 3040 beRefs, err := be.ImageTargets[0].Refs(f.cluster(be)) 3041 assert.NoError(t, err) 3042 beTaggedRefs, err := beRefs.AddTagSuffix("tilt-build-456") 3043 assert.NoError(t, err) 3044 assert.Equal(t, "123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev:be-tilt-build-456", 3045 beTaggedRefs.LocalRef.String()) 3046 } 3047 3048 func TestDefaultReadFile(t *testing.T) { 3049 f := newFixture(t) 3050 f.setupFooAndBar() 3051 tiltfile := ` 3052 result = read_file("this_file_does_not_exist", default="foo") 3053 docker_build('gcr.io/foo', 'foo') 3054 k8s_yaml(str(result) + '.yaml') 3055 ` 3056 3057 f.file("Tiltfile", tiltfile) 3058 3059 f.load() 3060 3061 f.assertNextManifest("foo", 3062 db(image("gcr.io/foo")), 3063 deployment("foo")) 3064 3065 f.assertConfigFiles("Tiltfile", ".tiltignore", "this_file_does_not_exist", "foo.yaml", "foo/Dockerfile", "foo/.dockerignore") 3066 } 3067 3068 func TestWatchFile(t *testing.T) { 3069 f := newFixture(t) 3070 3071 f.setupFoo() 3072 3073 f.file("hello", "world") 3074 f.file("Tiltfile", ` 3075 docker_build('gcr.io/foo', 'foo') 3076 watch_file('hello') 3077 k8s_yaml('foo.yaml') 3078 `) 3079 3080 f.load() 3081 3082 f.assertNextManifest("foo", 3083 db(image("gcr.io/foo")), 3084 deployment("foo")) 3085 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml", "hello") 3086 } 3087 3088 func TestAssemblyBasic(t *testing.T) { 3089 f := newFixture(t) 3090 3091 f.setupFoo() 3092 3093 f.file("Tiltfile", ` 3094 docker_build('gcr.io/foo', 'foo') 3095 k8s_yaml('foo.yaml') 3096 `) 3097 3098 f.load("foo") 3099 3100 f.assertNextManifest("foo", 3101 db(image("gcr.io/foo")), 3102 deployment("foo")) 3103 3104 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/Dockerfile", "foo/.dockerignore") 3105 } 3106 3107 func TestAssemblyTwoWorkloadsSameImage(t *testing.T) { 3108 f := newFixture(t) 3109 3110 f.setupFoo() 3111 f.yaml("bar.yaml", deployment("bar", image("gcr.io/foo"))) 3112 3113 f.file("Tiltfile", ` 3114 3115 docker_build('gcr.io/foo', 'foo') 3116 k8s_yaml(['foo.yaml', 'bar.yaml']) 3117 `) 3118 3119 f.load("foo", "bar") 3120 3121 f.assertNextManifest("foo", 3122 db(image("gcr.io/foo")), 3123 deployment("foo")) 3124 f.assertNextManifest("bar", 3125 db(image("gcr.io/foo")), 3126 deployment("bar")) 3127 3128 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "bar.yaml", "foo/Dockerfile", "foo/.dockerignore") 3129 } 3130 3131 // Fix a bug where a service with no selectors trivially matched all pods, so Tilt grouped 3132 // it with the first workload (https://github.com/tilt-dev/tilt/issues/4233) 3133 func TestAssemblyServiceWithoutSelectorMatchesNothing(t *testing.T) { 3134 f := newFixture(t) 3135 3136 f.yaml("all.yaml", 3137 deployment("foo", withLabels(map[string]string{"app": "foo"})), 3138 3139 service("service-without-selectors", withLabels(map[string]string{})), 3140 ) 3141 f.file("Tiltfile", ` 3142 k8s_yaml('all.yaml') 3143 `) 3144 3145 f.load() 3146 3147 f.assertNextManifest("foo", deployment("foo")) 3148 3149 f.assertNextManifestUnresourced("service-without-selectors") 3150 } 3151 3152 func TestK8sResourceNoMatch(t *testing.T) { 3153 f := newFixture(t) 3154 3155 f.setupFoo() 3156 f.file("Tiltfile", ` 3157 3158 k8s_yaml('foo.yaml') 3159 k8s_resource('bar', new_name='baz') 3160 `) 3161 3162 f.loadErrString("specified unknown resource \"bar\". known k8s resources: foo") 3163 } 3164 3165 func TestK8sResourceNewName(t *testing.T) { 3166 f := newFixture(t) 3167 3168 f.setupFoo() 3169 f.file("Tiltfile", ` 3170 3171 k8s_yaml('foo.yaml') 3172 k8s_resource('foo', new_name='bar') 3173 `) 3174 3175 f.load() 3176 f.assertNumManifests(1) 3177 f.assertNextManifest("bar", deployment("foo")) 3178 } 3179 3180 func TestK8sResourceRenameTwice(t *testing.T) { 3181 f := newFixture(t) 3182 3183 f.setupFoo() 3184 f.file("Tiltfile", ` 3185 k8s_yaml('foo.yaml') 3186 k8s_resource('foo', new_name='bar') 3187 k8s_resource('bar', new_name='baz') 3188 `) 3189 3190 f.load() 3191 f.assertNumManifests(1) 3192 f.assertNextManifest("baz", deployment("foo")) 3193 } 3194 3195 func TestK8sResourceNewNameConflict(t *testing.T) { 3196 f := newFixture(t) 3197 3198 f.setupFooAndBar() 3199 f.file("Tiltfile", ` 3200 3201 k8s_yaml(['foo.yaml', 'bar.yaml']) 3202 k8s_resource('foo', new_name='bar') 3203 `) 3204 3205 f.loadErrString("\"foo\" to \"bar\"", "already exists") 3206 } 3207 3208 func TestK8sResourceRenameConflictingNames(t *testing.T) { 3209 f := newFixture(t) 3210 3211 f.dockerfile("foo1/Dockerfile") 3212 f.dockerfile("foo2/Dockerfile") 3213 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 3214 f.yaml("foo2.yaml", deployment("foo", image("gcr.io/foo2"), namespace("ns2"))) 3215 3216 f.file("Tiltfile", ` 3217 3218 k8s_yaml(['foo1.yaml', 'foo2.yaml']) 3219 docker_build('gcr.io/foo1', 'foo1') 3220 docker_build('gcr.io/foo2', 'foo2') 3221 k8s_resource('foo:deployment:ns2', new_name='foo') 3222 `) 3223 f.load("foo:deployment:ns1", "foo") 3224 3225 f.assertNextManifest("foo:deployment:ns1", db(image("gcr.io/foo1"))) 3226 f.assertNextManifest("foo", db(image("gcr.io/foo2"))) 3227 } 3228 3229 func TestConflictingNewNames(t *testing.T) { 3230 f := newFixture(t) 3231 3232 f.yaml("ns1.yaml", namespace("ns1")) 3233 f.yaml("ns2.yaml", namespace("ns2")) 3234 f.file("Tiltfile", ` 3235 k8s_yaml(['ns1.yaml', 'ns2.yaml']) 3236 k8s_resource(new_name='foo', objects=['ns1:namespace']) 3237 k8s_resource(new_name='foo', objects=['ns2:namespace']) 3238 `) 3239 3240 f.loadErrString("k8s_resource named \"foo\" already exists") 3241 } 3242 3243 func TestAdditivePortForwards(t *testing.T) { 3244 f := newFixture(t) 3245 3246 f.setupFoo() 3247 3248 f.file("Tiltfile", ` 3249 3250 k8s_yaml('foo.yaml') 3251 k8s_resource('foo', port_forwards=8001) 3252 k8s_resource('foo', port_forwards=8000) 3253 `) 3254 3255 f.load() 3256 f.assertNextManifest("foo", []model.PortForward{{LocalPort: 8001}, {LocalPort: 8000}}) 3257 } 3258 3259 func TestWorkloadToResourceFunction(t *testing.T) { 3260 f := newFixture(t) 3261 3262 f.setupFoo() 3263 3264 f.file("Tiltfile", ` 3265 3266 docker_build('gcr.io/foo', 'foo') 3267 k8s_yaml('foo.yaml') 3268 def wtrf(id): 3269 return 'hello-' + id.name 3270 workload_to_resource_function(wtrf) 3271 k8s_resource('hello-foo', port_forwards=8000) 3272 `) 3273 3274 f.load() 3275 f.assertNumManifests(1) 3276 f.assertNextManifest("hello-foo", db(image("gcr.io/foo")), []model.PortForward{{LocalPort: 8000}}) 3277 } 3278 3279 func TestWorkloadToResourceFunctionConflict(t *testing.T) { 3280 f := newFixture(t) 3281 3282 f.setupFooAndBar() 3283 3284 f.file("Tiltfile", ` 3285 3286 docker_build('gcr.io/foo', 'foo') 3287 docker_build('gcr.io/bar', 'bar') 3288 k8s_yaml(['foo.yaml', 'bar.yaml']) 3289 def wtrf(id): 3290 return 'baz' 3291 workload_to_resource_function(wtrf) 3292 `) 3293 3294 f.loadErrString("workload_to_resource_function", "bar:deployment:default:apps", "foo:deployment:default:apps", "'baz'") 3295 } 3296 3297 func TestWorkloadToResourceFunctionError(t *testing.T) { 3298 f := newFixture(t) 3299 3300 f.setupFoo() 3301 3302 f.file("Tiltfile", ` 3303 3304 docker_build('gcr.io/foo', 'foo') 3305 k8s_yaml('foo.yaml') 3306 def wtrf(id): 3307 return 1 + 'asdf' 3308 workload_to_resource_function(wtrf) 3309 k8s_resource('hello-foo', port_forwards=8000) 3310 `) 3311 3312 f.loadErrString("'foo:deployment:default:apps'", "unknown binary op: int + string", "Tiltfile:5:1", workloadToResourceFunctionN) 3313 } 3314 3315 func TestWorkloadToResourceFunctionReturnsNonString(t *testing.T) { 3316 f := newFixture(t) 3317 3318 f.setupFoo() 3319 3320 f.file("Tiltfile", ` 3321 3322 docker_build('gcr.io/foo', 'foo') 3323 k8s_yaml('foo.yaml') 3324 def wtrf(id): 3325 return 1 3326 workload_to_resource_function(wtrf) 3327 k8s_resource('hello-foo', port_forwards=8000) 3328 `) 3329 3330 f.loadErrString("'foo:deployment:default:apps'", "invalid return value", "wanted: string. got: starlark.Int", "Tiltfile:5:1", workloadToResourceFunctionN) 3331 } 3332 3333 func TestWorkloadToResourceFunctionTakesNoArgs(t *testing.T) { 3334 f := newFixture(t) 3335 3336 f.setupFoo() 3337 3338 f.file("Tiltfile", ` 3339 3340 docker_build('gcr.io/foo', 'foo') 3341 k8s_yaml('foo.yaml') 3342 def wtrf(): 3343 return "hello" 3344 workload_to_resource_function(wtrf) 3345 k8s_resource('hello-foo', port_forwards=8000) 3346 `) 3347 3348 f.loadErrString("workload_to_resource_function arg must take 1 argument. wtrf takes 0") 3349 } 3350 3351 func TestWorkloadToResourceFunctionTakesTwoArgs(t *testing.T) { 3352 f := newFixture(t) 3353 3354 f.setupFoo() 3355 3356 f.file("Tiltfile", ` 3357 3358 docker_build('gcr.io/foo', 'foo') 3359 k8s_yaml('foo.yaml') 3360 def wtrf(a, b): 3361 return "hello" 3362 workload_to_resource_function(wtrf) 3363 k8s_resource('hello-foo', port_forwards=8000) 3364 `) 3365 3366 f.loadErrString("workload_to_resource_function arg must take 1 argument. wtrf takes 2") 3367 } 3368 3369 func TestMultipleLiveUpdatesOnManifest(t *testing.T) { 3370 f := newFixture(t) 3371 3372 f.gitInit("") 3373 f.file("sancho/Dockerfile", "FROM golang:1.10") 3374 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3375 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3376 f.file("Tiltfile", ` 3377 k8s_yaml('sancho.yaml') 3378 docker_build('gcr.io/some-project-162817/sancho', './sancho', 3379 live_update=[sync('./sancho/foo', '/bar')] 3380 ) 3381 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar', 3382 live_update=[sync('./sidecar/baz', '/quux')] 3383 ) 3384 `) 3385 3386 sync1 := v1alpha1.LiveUpdateSync{LocalPath: filepath.Join("sancho", "foo"), ContainerPath: "/bar"} 3387 expectedLU1 := v1alpha1.LiveUpdateSpec{ 3388 BasePath: f.Path(), 3389 Syncs: []v1alpha1.LiveUpdateSync{sync1}, 3390 } 3391 3392 sync2 := v1alpha1.LiveUpdateSync{LocalPath: filepath.Join("sidecar", "baz"), ContainerPath: "/quux"} 3393 expectedLU2 := v1alpha1.LiveUpdateSpec{ 3394 BasePath: f.Path(), 3395 Syncs: []v1alpha1.LiveUpdateSync{sync2}, 3396 } 3397 3398 f.load() 3399 f.assertNextManifest("sancho", 3400 db(image("gcr.io/some-project-162817/sancho"), expectedLU1), 3401 db(image("gcr.io/some-project-162817/sancho-sidecar"), expectedLU2), 3402 ) 3403 } 3404 3405 func TestImpossibleLiveUpdatesOKNoLiveUpdate(t *testing.T) { 3406 f := newFixture(t) 3407 3408 f.gitInit("") 3409 f.file("sancho/Dockerfile", "FROM golang:1.10") 3410 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3411 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3412 f.file("Tiltfile", ` 3413 k8s_yaml('sancho.yaml') 3414 docker_build('gcr.io/some-project-162817/sancho', './sancho') 3415 3416 # no LiveUpdate on this so nothing meriting a warning 3417 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar') 3418 `) 3419 3420 // Expect no warnings! 3421 f.load() 3422 } 3423 3424 func TestImpossibleLiveUpdatesOKSecondContainerLiveUpdate(t *testing.T) { 3425 f := newFixture(t) 3426 3427 f.gitInit("") 3428 f.file("sancho/Dockerfile", "FROM golang:1.10") 3429 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3430 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3431 f.file("Tiltfile", ` 3432 k8s_yaml('sancho.yaml') 3433 3434 # this is the second k8s container, but only the first image target, so should be OK 3435 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar') 3436 `) 3437 3438 // Expect no warnings! 3439 f.load() 3440 } 3441 3442 func TestTriggerModeK8S(t *testing.T) { 3443 for _, testCase := range []struct { 3444 name string 3445 globalSetting triggerMode 3446 k8sResourceSetting triggerMode 3447 specifyAutoInit bool 3448 autoInit bool 3449 expectedTriggerMode model.TriggerMode 3450 }{ 3451 {"default", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3452 {"explicit global auto", TriggerModeAuto, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3453 {"explicit global manual", TriggerModeManual, TriggerModeUnset, false, false, model.TriggerModeManualWithAutoInit}, 3454 {"kr auto", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3455 {"kr manual", TriggerModeUnset, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 3456 {"kr manual, auto_init=False", TriggerModeUnset, TriggerModeManual, true, false, model.TriggerModeManual}, 3457 {"kr manual, auto_init=True", TriggerModeUnset, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3458 {"kr override auto", TriggerModeManual, TriggerModeAuto, false, false, model.TriggerModeAuto}, 3459 {"kr override manual", TriggerModeAuto, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 3460 {"kr override manual, auto_init=False", TriggerModeAuto, TriggerModeManual, true, false, model.TriggerModeManual}, 3461 {"kr override manual, auto_init=True", TriggerModeAuto, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3462 } { 3463 t.Run(testCase.name, func(t *testing.T) { 3464 f := newFixture(t) 3465 3466 f.setupFoo() 3467 3468 var globalTriggerModeDirective string 3469 switch testCase.globalSetting { 3470 case TriggerModeUnset: 3471 globalTriggerModeDirective = "" 3472 default: 3473 globalTriggerModeDirective = fmt.Sprintf("trigger_mode(%s)", testCase.globalSetting.String()) 3474 } 3475 3476 var k8sResourceDirective string 3477 switch testCase.k8sResourceSetting { 3478 case TriggerModeUnset: 3479 k8sResourceDirective = "" 3480 default: 3481 autoInitOption := "" 3482 if testCase.specifyAutoInit { 3483 autoInitOption = ", auto_init=" 3484 if testCase.autoInit { 3485 autoInitOption += "True" 3486 } else { 3487 autoInitOption += "False" 3488 } 3489 } 3490 k8sResourceDirective = fmt.Sprintf("k8s_resource('foo', trigger_mode=%s%s)", testCase.k8sResourceSetting.String(), autoInitOption) 3491 } 3492 3493 f.file("Tiltfile", fmt.Sprintf(` 3494 %s 3495 docker_build('gcr.io/foo', 'foo') 3496 k8s_yaml('foo.yaml') 3497 %s 3498 `, globalTriggerModeDirective, k8sResourceDirective)) 3499 3500 f.load() 3501 3502 f.assertNumManifests(1) 3503 f.assertNextManifest("foo", testCase.expectedTriggerMode) 3504 }) 3505 } 3506 } 3507 3508 func TestTriggerModeLocal(t *testing.T) { 3509 for _, testCase := range []struct { 3510 name string 3511 globalSetting triggerMode 3512 localResourceSetting triggerMode 3513 specifyAutoInit bool 3514 autoInit bool 3515 expectedTriggerMode model.TriggerMode 3516 }{ 3517 {"default", TriggerModeUnset, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3518 {"explicit global auto", TriggerModeAuto, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3519 {"explicit global manual", TriggerModeManual, TriggerModeUnset, false, true, model.TriggerModeManualWithAutoInit}, 3520 {"explicit global auto, autoInit=True", TriggerModeAuto, TriggerModeUnset, true, true, model.TriggerModeAuto}, 3521 {"explicit global auto, autoInit=False", TriggerModeAuto, TriggerModeUnset, true, false, model.TriggerModeAutoWithManualInit}, 3522 {"explicit global manual, autoInit=True", TriggerModeManual, TriggerModeUnset, true, true, model.TriggerModeManualWithAutoInit}, 3523 {"explicit global manual, autoInit=False", TriggerModeManual, TriggerModeUnset, true, false, model.TriggerModeManual}, 3524 {"local_resource auto", TriggerModeUnset, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3525 {"local_resource manual", TriggerModeUnset, TriggerModeManual, false, true, model.TriggerModeManualWithAutoInit}, 3526 {"local_resource auto, autoInit=True", TriggerModeUnset, TriggerModeAuto, true, true, model.TriggerModeAuto}, 3527 {"local_resource auto, autoInit=False", TriggerModeUnset, TriggerModeAuto, true, false, model.TriggerModeAutoWithManualInit}, 3528 {"local_resource manual, autoInit=True", TriggerModeUnset, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3529 {"local_resource manual, autoInit=False", TriggerModeUnset, TriggerModeManual, true, false, model.TriggerModeManual}, 3530 {"local_resource override auto", TriggerModeManual, TriggerModeAuto, false, true, model.TriggerModeAuto}, 3531 {"local_resource override manual", TriggerModeAuto, TriggerModeManual, false, true, model.TriggerModeManualWithAutoInit}, 3532 } { 3533 t.Run(testCase.name, func(t *testing.T) { 3534 f := newFixture(t) 3535 3536 var globalTriggerModeDirective string 3537 switch testCase.globalSetting { 3538 case TriggerModeUnset: 3539 globalTriggerModeDirective = "" 3540 case TriggerModeManual: 3541 globalTriggerModeDirective = "trigger_mode(TRIGGER_MODE_MANUAL)" 3542 case TriggerModeAuto: 3543 globalTriggerModeDirective = "trigger_mode(TRIGGER_MODE_AUTO)" 3544 } 3545 3546 resourceTriggerModeArg := "" 3547 switch testCase.localResourceSetting { 3548 case TriggerModeManual: 3549 resourceTriggerModeArg = ", trigger_mode=TRIGGER_MODE_MANUAL" 3550 case TriggerModeAuto: 3551 resourceTriggerModeArg = ", trigger_mode=TRIGGER_MODE_AUTO" 3552 } 3553 3554 autoInitArg := "" 3555 if testCase.specifyAutoInit { 3556 if testCase.autoInit { 3557 autoInitArg = ", auto_init=True" 3558 } else { 3559 autoInitArg = ", auto_init=False" 3560 } 3561 } 3562 3563 localResourceDirective := fmt.Sprintf("local_resource('foo', 'echo hi'%s%s)", resourceTriggerModeArg, autoInitArg) 3564 3565 f.file("Tiltfile", fmt.Sprintf(` 3566 %s 3567 %s 3568 `, globalTriggerModeDirective, localResourceDirective)) 3569 3570 f.load() 3571 3572 f.assertNumManifests(1) 3573 f.assertNextManifest("foo", testCase.expectedTriggerMode) 3574 }) 3575 } 3576 } 3577 3578 func TestTriggerModeInt(t *testing.T) { 3579 f := newFixture(t) 3580 3581 f.file("Tiltfile", ` 3582 trigger_mode(1) 3583 `) 3584 f.loadErrString("got int, want TriggerMode") 3585 } 3586 3587 func TestMultipleTriggerMode(t *testing.T) { 3588 f := newFixture(t) 3589 3590 f.file("Tiltfile", ` 3591 trigger_mode(TRIGGER_MODE_MANUAL) 3592 trigger_mode(TRIGGER_MODE_MANUAL) 3593 `) 3594 f.loadErrString("trigger_mode can only be called once") 3595 } 3596 3597 func TestK8sContext(t *testing.T) { 3598 f := newFixture(t) 3599 3600 f.setupFoo() 3601 3602 f.file("Tiltfile", ` 3603 if k8s_context() != 'fake-context': 3604 fail('bad context') 3605 if k8s_namespace() != 'fake-namespace': 3606 fail('bad namespace') 3607 k8s_yaml('foo.yaml') 3608 docker_build('gcr.io/foo', 'foo') 3609 `) 3610 3611 f.load() 3612 f.assertNextManifest("foo", 3613 db(image("gcr.io/foo")), 3614 deployment("foo")) 3615 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 3616 3617 } 3618 3619 func TestDockerbuildIgnoreAsString(t *testing.T) { 3620 f := newFixture(t) 3621 3622 f.dockerfile("Dockerfile") 3623 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3624 f.file("Tiltfile", ` 3625 3626 docker_build('gcr.io/foo', '.', ignore="*.txt") 3627 k8s_yaml('foo.yaml') 3628 `) 3629 3630 f.load() 3631 f.assertNextManifest("foo", 3632 buildFilters("a.txt"), 3633 fileChangeFilters("a.txt"), 3634 buildMatches("txt.a"), 3635 fileChangeMatches("txt.a"), 3636 ) 3637 } 3638 3639 func TestDockerbuildIgnoreAsArray(t *testing.T) { 3640 f := newFixture(t) 3641 3642 f.dockerfile("Dockerfile") 3643 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3644 f.file("Tiltfile", ` 3645 3646 docker_build('gcr.io/foo', '.', ignore=["*.txt", "*.md"]) 3647 k8s_yaml('foo.yaml') 3648 `) 3649 3650 f.load() 3651 f.assertNextManifest("foo", 3652 buildFilters("a.txt"), 3653 buildFilters("a.md"), 3654 fileChangeFilters("a.txt"), 3655 fileChangeFilters("a.md"), 3656 buildMatches("txt.a"), 3657 fileChangeMatches("txt.a"), 3658 ) 3659 } 3660 3661 func TestDockerbuildInvalidIgnore(t *testing.T) { 3662 f := newFixture(t) 3663 3664 f.dockerfile("foo/Dockerfile") 3665 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3666 3667 f.file("Tiltfile", ` 3668 3669 docker_build('fooimage', 'foo', ignore=[127]) 3670 k8s_yaml('foo.yaml') 3671 `) 3672 3673 f.loadErrString("ignore must be a string or a sequence of strings; found a starlark.Int") 3674 } 3675 3676 func TestDockerbuildOnly(t *testing.T) { 3677 f := newFixture(t) 3678 3679 f.dockerfile("Dockerfile") 3680 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3681 f.file("Tiltfile", ` 3682 docker_build('gcr.io/foo', '.', only="myservice") 3683 k8s_yaml('foo.yaml') 3684 `) 3685 3686 f.load() 3687 f.assertNextManifest("foo", 3688 buildFilters("otherservice/bar"), 3689 fileChangeFilters("otherservice/bar"), 3690 buildMatches("myservice/bar"), 3691 fileChangeMatches("myservice/bar"), 3692 ) 3693 } 3694 3695 func TestDockerbuildOnlyAsArray(t *testing.T) { 3696 f := newFixture(t) 3697 3698 f.dockerfile("Dockerfile") 3699 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3700 f.file("Tiltfile", ` 3701 docker_build('gcr.io/foo', '.', only=["common", "myservice"]) 3702 k8s_yaml('foo.yaml') 3703 `) 3704 3705 f.load() 3706 f.assertNextManifest("foo", 3707 buildFilters("otherservice/bar"), 3708 fileChangeFilters("otherservice/bar"), 3709 buildMatches("myservice/bar"), 3710 fileChangeMatches("myservice/bar"), 3711 buildMatches("common/bar"), 3712 fileChangeMatches("common/bar"), 3713 ) 3714 } 3715 3716 func TestDockerbuildInvalidOnly(t *testing.T) { 3717 f := newFixture(t) 3718 3719 f.dockerfile("foo/Dockerfile") 3720 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3721 3722 f.file("Tiltfile", ` 3723 3724 docker_build('fooimage', 'foo', only=[127]) 3725 k8s_yaml('foo.yaml') 3726 `) 3727 3728 f.loadErrString("only must be a string or a sequence of strings; found a starlark.Int") 3729 } 3730 3731 func TestDockerbuildInvalidOnlyGlob(t *testing.T) { 3732 f := newFixture(t) 3733 3734 f.dockerfile("foo/Dockerfile") 3735 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3736 3737 f.file("Tiltfile", ` 3738 3739 docker_build('fooimage', 'foo', only=["**/common"]) 3740 k8s_yaml('foo.yaml') 3741 `) 3742 3743 f.loadErrString("'only' does not support '*' file globs") 3744 } 3745 3746 func TestDockerbuildOnlyAndIgnore(t *testing.T) { 3747 f := newFixture(t) 3748 3749 f.dockerfile("Dockerfile") 3750 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3751 f.file("Tiltfile", ` 3752 docker_build('gcr.io/foo', '.', ignore="**/*.md", only=["common", "myservice"]) 3753 k8s_yaml('foo.yaml') 3754 `) 3755 3756 f.load() 3757 f.assertNextManifest("foo", 3758 buildFilters("otherservice/bar"), 3759 fileChangeFilters("otherservice/bar"), 3760 buildFilters("myservice/README.md"), 3761 fileChangeFilters("myservice/README.md"), 3762 buildMatches("myservice/bar"), 3763 fileChangeMatches("myservice/bar"), 3764 buildMatches("common/bar"), 3765 fileChangeMatches("common/bar"), 3766 ) 3767 } 3768 3769 // if the same file is ignored and included, the ignore takes precedence 3770 func TestDockerbuildOnlyAndIgnoreSameFile(t *testing.T) { 3771 f := newFixture(t) 3772 3773 f.dockerfile("Dockerfile") 3774 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3775 f.file("Tiltfile", ` 3776 docker_build('gcr.io/foo', '.', ignore="common/README.md", only="common/README.md") 3777 k8s_yaml('foo.yaml') 3778 `) 3779 3780 f.load() 3781 f.assertNextManifest("foo", 3782 buildFilters("common/README.md"), 3783 fileChangeFilters("common/README.md"), 3784 ) 3785 } 3786 3787 // If an only rule starts with a !, we assume that paths starts with a ! 3788 // We don't do a double negative 3789 func TestDockerbuildOnlyHasException(t *testing.T) { 3790 f := newFixture(t) 3791 3792 f.dockerfile("Dockerfile") 3793 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3794 f.file("Tiltfile", ` 3795 docker_build('gcr.io/foo', '.', only="!myservice") 3796 k8s_yaml('foo.yaml') 3797 `) 3798 3799 f.load() 3800 f.assertNextManifest("foo", 3801 buildFilters("otherservice/bar"), 3802 fileChangeFilters("otherservice/bar"), 3803 buildMatches("!myservice/bar"), 3804 fileChangeMatches("!myservice/bar"), 3805 ) 3806 } 3807 3808 // What if you have \n in strings? 3809 // That's hard to make work easily, so let's just throw an error 3810 func TestDockerbuildIgnoreWithNewline(t *testing.T) { 3811 f := newFixture(t) 3812 3813 f.dockerfile("Dockerfile") 3814 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3815 f.file("Tiltfile", ` 3816 docker_build('gcr.io/foo', '.', ignore="\nweirdfile.txt") 3817 k8s_yaml('foo.yaml') 3818 `) 3819 3820 f.loadErrString(`ignore cannot contain newlines; found ignore: "\nweirdfile.txt"`) 3821 } 3822 func TestDockerbuildOnlyWithNewline(t *testing.T) { 3823 f := newFixture(t) 3824 3825 f.dockerfile("Dockerfile") 3826 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3827 f.file("Tiltfile", ` 3828 docker_build('gcr.io/foo', '.', only="\nweirdfile.txt") 3829 k8s_yaml('foo.yaml') 3830 `) 3831 3832 f.loadErrString(`only cannot contain newlines; found only: "\nweirdfile.txt`) 3833 } 3834 3835 // Custom Build Ignores(Single file) 3836 func TestCustomBuildIgnoresSingular(t *testing.T) { 3837 f := newFixture(t) 3838 f.setupFoo() 3839 3840 f.file("Tiltfile", ` 3841 3842 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 3843 ['foo'], ignore="a.txt") 3844 k8s_yaml('foo.yaml') 3845 `) // custom build doesnt support globs for dependencies 3846 f.load() 3847 f.assertNextManifest("foo", 3848 fileChangeFilters("foo/a.txt"), 3849 fileChangeMatches("foo/txt.a"), 3850 ) 3851 } 3852 3853 // Custom Build Ignores(Multiple files) 3854 func TestCustomBuildIgnoresMultiple(t *testing.T) { 3855 f := newFixture(t) 3856 f.setupFoo() 3857 3858 f.file("Tiltfile", ` 3859 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 3860 ['foo'], ignore=["a.md","a.txt"]) 3861 k8s_yaml('foo.yaml') 3862 `) 3863 f.load() 3864 f.assertNextManifest("foo", 3865 fileChangeFilters("foo/a.txt"), 3866 fileChangeFilters("foo/a.md"), 3867 fileChangeMatches("foo/txt.a"), 3868 fileChangeMatches("foo/md.a"), 3869 ) 3870 } 3871 3872 func TestEnableFeature(t *testing.T) { 3873 f := newFixture(t) 3874 f.features["testflag_disabled"] = feature.Value{Enabled: false} 3875 f.setupFoo() 3876 3877 f.file("Tiltfile", `enable_feature('testflag_disabled')`) 3878 f.load() 3879 3880 f.assertFeature("testflag_disabled", true) 3881 } 3882 3883 func TestEnableFeatureWithError(t *testing.T) { 3884 f := newFixture(t) 3885 f.features["testflag_disabled"] = feature.Value{Enabled: false} 3886 f.setupFoo() 3887 3888 f.file("Tiltfile", ` 3889 enable_feature('testflag_disabled') 3890 fail('goodnight moon') 3891 `) 3892 f.loadErrString("goodnight moon") 3893 3894 f.assertFeature("testflag_disabled", true) 3895 } 3896 3897 func TestDisableFeature(t *testing.T) { 3898 f := newFixture(t) 3899 f.features["testflag_enabled"] = feature.Value{Enabled: true} 3900 f.setupFoo() 3901 3902 f.file("Tiltfile", `disable_feature('testflag_enabled')`) 3903 f.load() 3904 3905 f.assertFeature("testflag_enabled", false) 3906 } 3907 3908 func TestEnableFeatureThatDoesNotExist(t *testing.T) { 3909 f := newFixture(t) 3910 f.setupFoo() 3911 3912 f.file("Tiltfile", `enable_feature('testflag')`) 3913 3914 f.loadErrString("Unknown feature flag: testflag") 3915 } 3916 3917 func TestDisableFeatureThatDoesNotExist(t *testing.T) { 3918 f := newFixture(t) 3919 f.setupFoo() 3920 3921 f.file("Tiltfile", `disable_feature('testflag')`) 3922 3923 f.loadErrString("Unknown feature flag: testflag") 3924 } 3925 3926 func TestDisableObsoleteFeature(t *testing.T) { 3927 f := newFixture(t) 3928 f.features["obsoleteflag"] = feature.Value{Status: feature.Obsolete, Enabled: true} 3929 f.setupFoo() 3930 3931 f.file("Tiltfile", `disable_feature('obsoleteflag')`) 3932 f.loadAssertWarnings("Obsolete feature flag: obsoleteflag") 3933 } 3934 3935 func TestDisableSnapshots(t *testing.T) { 3936 f := newFixture(t) 3937 f.setupFoo() 3938 3939 f.file("Tiltfile", `disable_snapshots()`) 3940 f.load() 3941 3942 f.assertFeature(feature.Snapshots, false) 3943 } 3944 3945 func TestDockerBuildEntrypointString(t *testing.T) { 3946 f := newFixture(t) 3947 3948 f.dockerfile("Dockerfile") 3949 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3950 f.file("Tiltfile", ` 3951 docker_build('gcr.io/foo', '.', entrypoint="/bin/the_app") 3952 k8s_yaml('foo.yaml') 3953 `) 3954 3955 f.load() 3956 f.assertNextManifest("foo", db(image("gcr.io/foo"), entrypoint(model.ToUnixCmdInDir("/bin/the_app", f.Path())))) 3957 } 3958 3959 func TestDockerBuildContainerArgs(t *testing.T) { 3960 f := newFixture(t) 3961 3962 f.dockerfile("Dockerfile") 3963 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3964 f.file("Tiltfile", ` 3965 docker_build('gcr.io/foo', '.', container_args=["bar"]) 3966 k8s_yaml('foo.yaml') 3967 `) 3968 3969 f.load() 3970 3971 m := f.assertNextManifest("foo") 3972 assert.Equal(t, 3973 &v1alpha1.ImageMapOverrideArgs{Args: []string{"bar"}}, 3974 m.ImageTargets[0].OverrideArgs) 3975 } 3976 3977 func TestDockerBuildEntrypointArray(t *testing.T) { 3978 f := newFixture(t) 3979 3980 f.dockerfile("Dockerfile") 3981 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3982 f.file("Tiltfile", ` 3983 docker_build('gcr.io/foo', '.', entrypoint=["/bin/the_app"]) 3984 k8s_yaml('foo.yaml') 3985 `) 3986 3987 f.load() 3988 f.assertNextManifest("foo", db(image("gcr.io/foo"), entrypoint(model.Cmd{Argv: []string{"/bin/the_app"}}))) 3989 } 3990 3991 func TestDockerBuild_buildArgs(t *testing.T) { 3992 f := newFixture(t) 3993 3994 f.setupFoo() 3995 3996 f.file("rev.txt", "hello") 3997 f.file("Tiltfile", ` 3998 cmd = 'cat rev.txt' 3999 if os.name == 'nt': 4000 cmd = 'type rev.txt' 4001 docker_build('gcr.io/foo', 'foo', build_args={'GIT_REV': local(cmd)}) 4002 k8s_yaml('foo.yaml') 4003 `) 4004 4005 f.load("foo") 4006 4007 m := f.assertNextManifest("foo") 4008 assert.Equal(t, 4009 []string{"GIT_REV=hello"}, 4010 m.ImageTargets[0].DockerBuildInfo().Args) 4011 } 4012 4013 func TestCustomBuildEntrypoint(t *testing.T) { 4014 f := newFixture(t) 4015 4016 f.dockerfile("Dockerfile") 4017 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4018 f.file("Tiltfile", ` 4019 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 4020 ['foo'], entrypoint="/bin/the_app") 4021 k8s_yaml('foo.yaml') 4022 `) 4023 4024 f.load() 4025 f.assertNextManifest("foo", cb( 4026 image("gcr.io/foo"), 4027 deps(f.JoinPath("foo")), 4028 cmd("docker build -t $EXPECTED_REF foo", f.Path()), 4029 entrypoint(model.ToUnixCmdInDir("/bin/the_app", f.Path()))), 4030 ) 4031 } 4032 4033 func TestCustomBuildContainerArgs(t *testing.T) { 4034 f := newFixture(t) 4035 4036 f.dockerfile("Dockerfile") 4037 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4038 f.file("Tiltfile", ` 4039 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 4040 ['foo'], container_args=['bar']) 4041 k8s_yaml('foo.yaml') 4042 `) 4043 4044 f.load() 4045 assert.Equal(t, 4046 &v1alpha1.ImageMapOverrideArgs{Args: []string{"bar"}}, 4047 f.assertNextManifest("foo").ImageTargets[0].OverrideArgs) 4048 } 4049 4050 // See comments on ImageTarget#MaybeIgnoreRegistry() 4051 func TestCustomBuildSkipsLocalDockerAndTagPassedIgnoresLocalRegistry(t *testing.T) { 4052 f := newFixture(t) 4053 4054 f.dockerfile("Dockerfile") 4055 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4056 f.file("Tiltfile", ` 4057 default_registry('localhost:5000') 4058 custom_build('gcr.io/foo', ':', ["."], tag='gcr.io/foo:latest', skips_local_docker=True) 4059 k8s_yaml('foo.yaml') 4060 `) 4061 4062 f.load() 4063 m := f.assertNextManifest("foo") 4064 refs, err := m.ImageTargets[0].Refs(f.cluster(m)) 4065 require.NoError(t, err) 4066 assert.Equal(t, "gcr.io/foo", refs.ClusterRef().String()) 4067 } 4068 4069 func TestDuplicateYAMLEntityWithinSingleResource(t *testing.T) { 4070 f := newFixture(t) 4071 4072 f.gitInit("") 4073 f.yaml("resource.yaml", 4074 service("doggos"), 4075 service("doggos")) 4076 f.file("Tiltfile", ` 4077 k8s_yaml('resource.yaml') 4078 `) 4079 stack := fmt.Sprintf(`Traceback (most recent call last): 4080 %s:2:9: in <toplevel> 4081 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4082 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service doggos", stack).Error()) 4083 } 4084 4085 func TestDuplicateYAMLEntityWithinSingleResourceAllowed(t *testing.T) { 4086 f := newFixture(t) 4087 4088 f.gitInit("") 4089 f.yaml("resource.yaml", 4090 service("doggos"), 4091 service("doggos")) 4092 f.file("Tiltfile", ` 4093 k8s_yaml('resource.yaml', allow_duplicates=True) 4094 `) 4095 f.load() 4096 } 4097 4098 func TestDuplicateYAMLEntityAcrossResources(t *testing.T) { 4099 f := newFixture(t) 4100 4101 f.dockerfile("foo1/Dockerfile") 4102 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 4103 f.file("Tiltfile", ` 4104 4105 k8s_yaml(['foo1.yaml', 'foo1.yaml']) 4106 k8s_resource('foo:deployment:ns1', new_name='foo') 4107 `) 4108 4109 stack := fmt.Sprintf(`Traceback (most recent call last): 4110 %s:3:9: in <toplevel> 4111 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4112 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Deployment foo (Namespace: ns1)", stack).Error()) 4113 } 4114 4115 func TestDuplicateYAMLEntityInSingleWorkload(t *testing.T) { 4116 //Services corresponding to a deployment get pulled into the same resource. 4117 f := newFixture(t) 4118 4119 labelsFoo := map[string]string{"foo": "bar"} 4120 f.yaml("all.yaml", 4121 deployment("foo-deployment", image("gcr.io/foo1"), namespace("ns1"), withLabels(labelsFoo)), 4122 service("foo-service", withLabels(labelsFoo)), 4123 service("foo-service", withLabels(labelsFoo))) 4124 f.file("Tiltfile", ` 4125 k8s_yaml('all.yaml') 4126 `) 4127 4128 stack := fmt.Sprintf(`Traceback (most recent call last): 4129 %s:2:9: in <toplevel> 4130 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4131 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service foo-service", stack).Error()) 4132 } 4133 4134 func TestDuplicateYAMLEntityInUserAssembledNonWorkloadResource(t *testing.T) { 4135 f := newFixture(t) 4136 f.gitInit("") 4137 f.yaml("all.yaml", 4138 service("foo-service"), 4139 service("foo-service")) 4140 f.file("Tiltfile", ` 4141 k8s_yaml('all.yaml') 4142 k8s_resource(objects=['foo-service:Service:default'], new_name='my-services') 4143 `) 4144 4145 stack := fmt.Sprintf(`Traceback (most recent call last): 4146 %s:2:9: in <toplevel> 4147 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4148 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service foo-service", stack).Error()) 4149 } 4150 4151 func TestSetTeamID(t *testing.T) { 4152 f := newFixture(t) 4153 4154 f.file("Tiltfile", "set_team('sharks')") 4155 f.load() 4156 4157 assert.Equal(t, "sharks", f.loadResult.TeamID) 4158 } 4159 4160 func TestSetTeamIDEmpty(t *testing.T) { 4161 f := newFixture(t) 4162 4163 f.file("Tiltfile", "set_team('')") 4164 f.loadErrString("team_id cannot be empty") 4165 } 4166 4167 func TestSetTeamIDMultiple(t *testing.T) { 4168 f := newFixture(t) 4169 4170 f.file("Tiltfile", ` 4171 set_team('sharks') 4172 set_team('jets') 4173 `) 4174 f.loadErrString("team_id set multiple times", "'sharks'", "'jets'") 4175 } 4176 4177 func TestK8SContextAcceptance(t *testing.T) { 4178 for _, test := range []struct { 4179 name string 4180 contextName k8s.KubeContext 4181 env clusterid.Product 4182 expectError bool 4183 expectedErrorSubstrings []string 4184 }{ 4185 {"minikube", "minikube", clusterid.ProductMinikube, false, nil}, 4186 {"docker-for-desktop", "docker-for-desktop", clusterid.ProductDockerDesktop, false, nil}, 4187 {"kind", "KIND", clusterid.ProductKIND, false, nil}, 4188 {"gke", "gke", clusterid.ProductGKE, true, []string{"'gke'", "If you're sure", "switch k8s contexts", "allow_k8s_contexts"}}, 4189 {"allowed", "allowed-context", clusterid.ProductGKE, false, nil}, 4190 } { 4191 t.Run(test.name, func(t *testing.T) { 4192 f := newFixture(t) 4193 4194 f.file("Tiltfile", ` 4195 k8s_yaml("foo.yaml") 4196 allow_k8s_contexts("allowed-context") 4197 `) 4198 f.setupFoo() 4199 4200 f.k8sContext = test.contextName 4201 f.k8sEnv = test.env 4202 if !test.expectError { 4203 f.load() 4204 } else { 4205 f.loadErrString(test.expectedErrorSubstrings...) 4206 } 4207 }) 4208 } 4209 } 4210 4211 // Test for fix to https://github.com/tilt-dev/tilt/issues/4234 4212 func TestCheckK8SContextWhenOnlyUncategorizedK8s(t *testing.T) { 4213 f := newFixture(t) 4214 4215 // We'll only have Uncategorized k8s entities, no K8s resources-- 4216 // make sure we still check K8sContext and throw an error if need be 4217 f.yaml("service.yaml", service("some-service")) 4218 4219 f.file("Tiltfile", ` 4220 k8s_yaml("service.yaml") 4221 allow_k8s_contexts("allowed-context") 4222 `) 4223 f.setupFoo() 4224 4225 f.k8sContext = "gke" 4226 f.k8sEnv = clusterid.ProductGKE 4227 4228 f.loadErrString("If you're sure", "switch k8s contexts", "allow_k8s_contexts") 4229 } 4230 4231 func TestLocalObeysAllowedK8sContexts(t *testing.T) { 4232 for _, test := range []struct { 4233 name string 4234 contextName k8s.KubeContext 4235 env clusterid.Product 4236 expectError bool 4237 expectedErrorSubstrings []string 4238 }{ 4239 {"gke", "gke", clusterid.ProductGKE, true, []string{"'gke'", "If you're sure", "switch k8s contexts", "allow_k8s_contexts"}}, 4240 {"allowed", "allowed-context", clusterid.ProductGKE, false, nil}, 4241 {"docker-compose", "unknown", k8s.ProductNone, false, nil}, 4242 } { 4243 t.Run(test.name, func(t *testing.T) { 4244 f := newFixture(t) 4245 4246 f.file("Tiltfile", ` 4247 allow_k8s_contexts("allowed-context") 4248 local('echo hi') 4249 `) 4250 f.setupFoo() 4251 4252 f.k8sContext = test.contextName 4253 f.k8sEnv = test.env 4254 if !test.expectError { 4255 f.load() 4256 } else { 4257 f.loadErrString(test.expectedErrorSubstrings...) 4258 } 4259 }) 4260 } 4261 } 4262 4263 func TestLocalResourceOnlyUpdateCmd(t *testing.T) { 4264 f := newFixture(t) 4265 4266 f.file("Tiltfile", ` 4267 local_resource("test", "echo hi", deps=["foo/bar", "foo/a.txt"]) 4268 `) 4269 4270 f.setupFoo() 4271 f.file(".gitignore", "*.txt") 4272 f.load() 4273 4274 f.assertNumManifests(1) 4275 path1 := "foo/bar" 4276 path2 := "foo/a.txt" 4277 m := f.assertNextManifest("test", localTarget(updateCmd(f.Path(), "echo hi", nil), deps(path1, path2)), fileChangeMatches("foo/a.txt")) 4278 4279 lt := m.LocalTarget() 4280 assert.Equal(t, []v1alpha1.IgnoreDef{ 4281 {BasePath: f.JoinPath(".git")}, 4282 }, lt.GetFileWatchIgnores()) 4283 4284 f.assertConfigFiles("Tiltfile", ".tiltignore") 4285 } 4286 4287 func TestLocalResourceOnlyServeCmd(t *testing.T) { 4288 f := newFixture(t) 4289 4290 f.file("Tiltfile", ` 4291 local_resource("test", serve_cmd="sleep 1000") 4292 `) 4293 4294 f.load() 4295 4296 f.assertNumManifests(1) 4297 f.assertNextManifest("test", localTarget(serveCmd(f.Path(), "sleep 1000", nil))) 4298 4299 f.assertConfigFiles("Tiltfile", ".tiltignore") 4300 } 4301 4302 func TestLocalResourceUpdateAndServeCmd(t *testing.T) { 4303 f := newFixture(t) 4304 4305 f.file("Tiltfile", ` 4306 local_resource("test", cmd="echo hi", serve_cmd="sleep 1000") 4307 `) 4308 4309 f.load() 4310 4311 f.assertNumManifests(1) 4312 f.assertNextManifest("test", localTarget( 4313 updateCmd(f.Path(), "echo hi", nil), 4314 serveCmd(f.Path(), "sleep 1000", nil), 4315 )) 4316 4317 f.assertConfigFiles("Tiltfile", ".tiltignore") 4318 } 4319 4320 func TestLocalResourceNeitherUpdateOrServeCmd(t *testing.T) { 4321 f := newFixture(t) 4322 4323 f.file("Tiltfile", ` 4324 local_resource("test") 4325 `) 4326 4327 f.loadErrString("local_resource must have a cmd and/or a serve_cmd, but both were empty") 4328 } 4329 4330 func TestLocalResourceUpdateCmdArray(t *testing.T) { 4331 f := newFixture(t) 4332 4333 f.file("Tiltfile", ` 4334 local_resource("test", ["echo", "hi"]) 4335 `) 4336 4337 f.load() 4338 f.assertNumManifests(1) 4339 f.assertNextManifest("test", localTarget(updateCmdArray(f.Path(), []string{"echo", "hi"}, nil))) 4340 } 4341 4342 func TestLocalResourceServeCmdArray(t *testing.T) { 4343 f := newFixture(t) 4344 4345 f.file("Tiltfile", ` 4346 local_resource("test", serve_cmd=["echo", "hi"]) 4347 `) 4348 4349 f.load() 4350 f.assertNumManifests(1) 4351 f.assertNextManifest("test", localTarget(serveCmdArray(f.Path(), []string{"echo", "hi"}, nil))) 4352 } 4353 4354 func TestLocalResourceWorkdir(t *testing.T) { 4355 f := newFixture(t) 4356 4357 f.file("nested/Tiltfile", ` 4358 local_resource("nested-local", "echo nested", deps=["foo/bar", "more_nested/repo"]) 4359 `) 4360 f.file("Tiltfile", ` 4361 include('nested/Tiltfile') 4362 local_resource("toplvl-local", "echo hello world", deps=["foo/baz", "foo/a.txt"]) 4363 `) 4364 4365 f.setupFoo() 4366 f.MkdirAll("nested/.git") 4367 f.MkdirAll("nested/more_nested/repo/.git") 4368 f.MkdirAll("foo/baz/.git") 4369 f.MkdirAll("foo/.git") // no Tiltfile lives here, nor is it a LocalResource dep; won't be pulled in as a repo 4370 f.load() 4371 4372 f.assertNumManifests(2) 4373 mNested := f.assertNextManifest("nested-local", 4374 localTarget(updateCmd(f.JoinPath("nested"), "echo nested", nil), 4375 deps("nested/foo/bar", "nested/more_nested/repo"))) 4376 4377 ltNested := mNested.LocalTarget() 4378 assert.Equal(t, []v1alpha1.IgnoreDef{ 4379 {BasePath: f.JoinPath("nested/more_nested/repo", ".git")}, 4380 {BasePath: f.JoinPath("nested", ".git")}, 4381 }, ltNested.GetFileWatchIgnores()) 4382 4383 mTop := f.assertNextManifest("toplvl-local", localTarget(updateCmd(f.Path(), "echo hello world", nil), deps("foo/baz", "foo/a.txt"))) 4384 ltTop := mTop.LocalTarget() 4385 assert.Equal(t, []v1alpha1.IgnoreDef{ 4386 {BasePath: f.JoinPath("foo/baz", ".git")}, 4387 {BasePath: f.JoinPath(".git")}, 4388 }, ltTop.GetFileWatchIgnores()) 4389 } 4390 4391 func TestLocalResourceIgnore(t *testing.T) { 4392 f := newFixture(t) 4393 4394 f.file(".dockerignore", "**/**.c") 4395 f.file("Tiltfile", "include('proj/Tiltfile')") 4396 f.file("proj/Tiltfile", ` 4397 local_resource("test", "echo hi", deps=["foo"], ignore=["**/*.a", "foo/bar.d"]) 4398 `) 4399 4400 f.setupFoo() 4401 f.file(".gitignore", "*.txt") 4402 f.load() 4403 4404 m := f.assertNextManifest("test") 4405 4406 // TODO(dmiller): I can't figure out how to translate these in to (file\build)(Matches\Filters) assert functions 4407 filter := ignore.CreateFileChangeFilter(m.LocalTarget().GetFileWatchIgnores()) 4408 4409 for _, tc := range []struct { 4410 path string 4411 expectMatch bool 4412 }{ 4413 {"proj/foo/bar.a", true}, 4414 {"proj/foo/bar.b", false}, 4415 {"proj/foo/baz/bar.a", true}, 4416 {"proj/foo/bar.d", true}, 4417 } { 4418 matches, err := filter.Matches(f.JoinPath(tc.path)) 4419 require.NoError(t, err) 4420 require.Equal(t, tc.expectMatch, matches, tc.path) 4421 } 4422 } 4423 4424 func TestLocalResourceUpdateCmdEnv(t *testing.T) { 4425 f := newFixture(t) 4426 4427 f.file("Tiltfile", ` 4428 local_resource("test", "echo hi", env={"KEY1": "value1", "KEY2": "value2"}, serve_cmd="sleep 1000") 4429 `) 4430 4431 f.load() 4432 f.assertNumManifests(1) 4433 f.assertNextManifest("test", localTarget( 4434 updateCmd(f.Path(), "echo hi", []string{"KEY1=value1", "KEY2=value2"}), 4435 serveCmd(f.Path(), "sleep 1000", nil), 4436 )) 4437 } 4438 4439 func TestLocalResourceServeCmdEnv(t *testing.T) { 4440 f := newFixture(t) 4441 4442 f.file("Tiltfile", ` 4443 local_resource("test", "echo hi", serve_cmd="sleep 1000", serve_env={"KEY1": "value1", "KEY2": "value2"}) 4444 `) 4445 4446 f.load() 4447 f.assertNumManifests(1) 4448 f.assertNextManifest("test", localTarget( 4449 updateCmd(f.Path(), "echo hi", nil), 4450 serveCmd(f.Path(), "sleep 1000", []string{"KEY1=value1", "KEY2=value2"}), 4451 )) 4452 } 4453 4454 func TestLocalResourceUpdateCmdDir(t *testing.T) { 4455 f := newFixture(t) 4456 4457 f.file("nested/inside.txt", "inside the nested directory") 4458 f.file("Tiltfile", ` 4459 local_resource("test", cmd="cat inside.txt", dir="nested") 4460 `) 4461 4462 f.load() 4463 f.assertNumManifests(1) 4464 f.assertNextManifest("test", localTarget( 4465 updateCmd(f.JoinPath(f.Path(), "nested"), "cat inside.txt", nil), 4466 )) 4467 } 4468 4469 func TestLocalResourceUpdateCmdDirNone(t *testing.T) { 4470 f := newFixture(t) 4471 4472 f.file("here.txt", "same level") 4473 f.file("Tiltfile", ` 4474 local_resource("test", cmd="cat here.txt", dir=None) 4475 `) 4476 4477 f.load() 4478 f.assertNumManifests(1) 4479 f.assertNextManifest("test", localTarget( 4480 updateCmd(f.Path(), "cat here.txt", nil), 4481 )) 4482 } 4483 4484 func TestLocalResourceServeCmdDir(t *testing.T) { 4485 f := newFixture(t) 4486 4487 f.file("nested/inside.txt", "inside the nested directory") 4488 f.file("Tiltfile", ` 4489 local_resource("test", serve_cmd="cat inside.txt", serve_dir="nested") 4490 `) 4491 4492 f.load() 4493 f.assertNumManifests(1) 4494 f.assertNextManifest("test", localTarget( 4495 serveCmd(f.JoinPath(f.Path(), "nested"), "cat inside.txt", nil), 4496 )) 4497 } 4498 4499 func TestLocalResourceServeCmdDirNone(t *testing.T) { 4500 f := newFixture(t) 4501 4502 f.file("here.txt", "same level") 4503 f.file("Tiltfile", ` 4504 local_resource("test", serve_cmd="cat here.txt", serve_dir=None) 4505 `) 4506 4507 f.load() 4508 f.assertNumManifests(1) 4509 f.assertNextManifest("test", localTarget( 4510 serveCmd(f.Path(), "cat here.txt", nil), 4511 )) 4512 } 4513 4514 func TestCustomBuildStoresTiltfilePath(t *testing.T) { 4515 f := newFixture(t) 4516 4517 f.file("Tiltfile", `include('proj/Tiltfile') 4518 k8s_yaml("foo.yaml")`) 4519 f.file("proj/Tiltfile", ` 4520 custom_build( 4521 'gcr.io/foo', 4522 'build.sh', 4523 ['foo'] 4524 ) 4525 `) 4526 f.file("proj/build.sh", "docker build -t $EXPECTED_REF gcr.io/foo") 4527 f.file("proj/Dockerfile", "FROM alpine") 4528 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4529 4530 f.load() 4531 f.assertNumManifests(1) 4532 f.assertNextManifest("foo", cb( 4533 image("gcr.io/foo"), 4534 deps(f.JoinPath("proj/foo")), 4535 cmd("build.sh", f.JoinPath("proj")), 4536 )) 4537 } 4538 4539 func TestSecretString(t *testing.T) { 4540 f := newFixture(t) 4541 4542 f.file("secret.yaml", ` 4543 apiVersion: v1 4544 kind: Secret 4545 metadata: 4546 name: my-secret 4547 stringData: 4548 client-id: hello 4549 client-secret: world 4550 `) 4551 f.file("Tiltfile", ` 4552 k8s_yaml('secret.yaml') 4553 `) 4554 4555 f.load() 4556 4557 secrets := f.loadResult.Secrets 4558 assert.Equal(t, 2, len(secrets)) 4559 assert.Equal(t, "client-id", secrets["hello"].Key) 4560 assert.Equal(t, "hello", string(secrets["hello"].Value)) 4561 assert.Equal(t, "aGVsbG8=", string(secrets["hello"].ValueEncoded)) 4562 assert.Equal(t, "client-secret", secrets["world"].Key) 4563 assert.Equal(t, "world", string(secrets["world"].Value)) 4564 assert.Equal(t, "d29ybGQ=", string(secrets["world"].ValueEncoded)) 4565 } 4566 4567 func TestSecretBytes(t *testing.T) { 4568 f := newFixture(t) 4569 4570 f.file("secret.yaml", ` 4571 apiVersion: v1 4572 kind: Secret 4573 metadata: 4574 name: my-secret 4575 data: 4576 client-id: aGVsbG8= 4577 client-secret: d29ybGQ= 4578 `) 4579 f.file("Tiltfile", ` 4580 k8s_yaml('secret.yaml') 4581 `) 4582 4583 f.load() 4584 4585 secrets := f.loadResult.Secrets 4586 assert.Equal(t, 2, len(secrets)) 4587 assert.Equal(t, "client-id", secrets["hello"].Key) 4588 assert.Equal(t, "hello", string(secrets["hello"].Value)) 4589 assert.Equal(t, "aGVsbG8=", string(secrets["hello"].ValueEncoded)) 4590 assert.Equal(t, "client-secret", secrets["world"].Key) 4591 assert.Equal(t, "world", string(secrets["world"].Value)) 4592 assert.Equal(t, "d29ybGQ=", string(secrets["world"].ValueEncoded)) 4593 } 4594 4595 func TestSecretSettingsDisableScrub(t *testing.T) { 4596 f := newFixture(t) 4597 4598 f.file("secret.yaml", ` 4599 apiVersion: v1 4600 kind: Secret 4601 metadata: 4602 name: my-secret 4603 stringData: 4604 client-id: hello 4605 client-secret: world 4606 `) 4607 f.file("Tiltfile", ` 4608 k8s_yaml('secret.yaml') 4609 secret_settings(disable_scrub=True) 4610 `) 4611 4612 f.load() 4613 4614 secrets := f.loadResult.Secrets 4615 assert.Empty(t, secrets, "expect no secrets to be collected if scrubbing secrets is disabled") 4616 } 4617 4618 func TestDockerPruneSettings(t *testing.T) { 4619 f := newFixture(t) 4620 4621 f.file("Tiltfile", ` 4622 docker_prune_settings(max_age_mins=111, num_builds=222) 4623 `) 4624 4625 f.load() 4626 res := f.loadResult.DockerPruneSettings 4627 4628 assert.True(t, res.Enabled) 4629 assert.Equal(t, time.Minute*111, res.MaxAge) 4630 assert.Equal(t, 222, res.NumBuilds) 4631 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) // default 4632 } 4633 4634 func TestDockerPruneSettingsDefaultsWhenCalled(t *testing.T) { 4635 f := newFixture(t) 4636 4637 f.file("Tiltfile", ` 4638 docker_prune_settings(num_builds=123) 4639 `) 4640 4641 f.load() 4642 res := f.loadResult.DockerPruneSettings 4643 4644 assert.True(t, res.Enabled) 4645 assert.Equal(t, model.DockerPruneDefaultMaxAge, res.MaxAge) 4646 assert.Equal(t, 123, res.NumBuilds) 4647 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) 4648 } 4649 4650 func TestDockerPruneSettingsDefaultsWhenNotCalled(t *testing.T) { 4651 f := newFixture(t) 4652 4653 f.file("Tiltfile", ` 4654 print('nothing to see here') 4655 `) 4656 4657 f.load() 4658 res := f.loadResult.DockerPruneSettings 4659 4660 assert.True(t, res.Enabled) 4661 assert.Equal(t, model.DockerPruneDefaultMaxAge, res.MaxAge) 4662 assert.Equal(t, 0, res.NumBuilds) 4663 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) 4664 } 4665 4666 func TestK8SDependsOn(t *testing.T) { 4667 f := newFixture(t) 4668 4669 f.setupFooAndBar() 4670 f.file("Tiltfile", ` 4671 docker_build('gcr.io/foo', 'foo') 4672 k8s_yaml('foo.yaml') 4673 4674 docker_build('gcr.io/bar', 'bar') 4675 k8s_yaml('bar.yaml') 4676 k8s_resource('bar', resource_deps=['foo']) 4677 `) 4678 4679 f.load() 4680 f.assertNextManifest("foo", resourceDeps()) 4681 f.assertNextManifest("bar", resourceDeps("foo")) 4682 } 4683 4684 func TestLocalDependsOn(t *testing.T) { 4685 f := newFixture(t) 4686 4687 f.file("Tiltfile", ` 4688 local_resource('foo', 'echo foo') 4689 local_resource('bar', 'echo bar', resource_deps=['foo']) 4690 `) 4691 4692 f.load() 4693 f.assertNextManifest("foo", resourceDeps()) 4694 f.assertNextManifest("bar", resourceDeps("foo")) 4695 } 4696 4697 func TestDependsOnMissingResource(t *testing.T) { 4698 f := newFixture(t) 4699 4700 f.file("Tiltfile", ` 4701 local_resource('baz', 'echo baz') 4702 local_resource('bar', 'echo bar', resource_deps=['foo', 'baz']) 4703 `) 4704 4705 f.loadAssertWarnings("resource bar specified a dependency on unknown resource foo - dependency ignored") 4706 f.assertNumManifests(2) 4707 f.assertNextManifest("baz", resourceDeps()) 4708 f.assertNextManifest("bar", resourceDeps("baz")) 4709 } 4710 4711 func TestDependsOnSelf(t *testing.T) { 4712 f := newFixture(t) 4713 4714 f.file("Tiltfile", ` 4715 local_resource('bar', 'echo bar', resource_deps=['bar']) 4716 `) 4717 4718 f.loadErrString("resource bar specified a dependency on itself") 4719 } 4720 4721 func TestDependsOnCycle(t *testing.T) { 4722 f := newFixture(t) 4723 4724 f.file("Tiltfile", ` 4725 local_resource('foo', 'echo foo', resource_deps=['baz']) 4726 local_resource('bar', 'echo bar', resource_deps=['foo']) 4727 local_resource('baz', 'echo baz', resource_deps=['bar']) 4728 `) 4729 4730 f.loadErrString("cycle detected in resource dependency graph", "bar -> foo", "foo -> baz", "baz -> bar") 4731 } 4732 4733 func TestDependsOnPulledInOnPartialLoad(t *testing.T) { 4734 for _, tc := range []struct { 4735 name string 4736 resourcesToLoad []model.ManifestName 4737 expected []model.ManifestName 4738 }{ 4739 { 4740 name: "a", 4741 resourcesToLoad: []model.ManifestName{"a"}, 4742 expected: []model.ManifestName{"a"}, 4743 }, 4744 { 4745 name: "c", 4746 resourcesToLoad: []model.ManifestName{"c"}, 4747 expected: []model.ManifestName{"a", "b", "c"}, 4748 }, 4749 { 4750 name: "d, e", 4751 resourcesToLoad: []model.ManifestName{"d", "e"}, 4752 expected: []model.ManifestName{"a", "b", "d", "e"}, 4753 }, 4754 { 4755 name: "e", 4756 resourcesToLoad: []model.ManifestName{"e"}, 4757 expected: []model.ManifestName{"e"}, 4758 }, 4759 } { 4760 t.Run(tc.name, func(t *testing.T) { 4761 f := newFixture(t) 4762 4763 f.file("Tiltfile", ` 4764 local_resource('a', 'echo a') 4765 local_resource('b', 'echo b', resource_deps=['a']) 4766 local_resource('c', 'echo c', resource_deps=['b']) 4767 local_resource('d', 'echo d', resource_deps=['b']) 4768 local_resource('e', 'echo e') 4769 `) 4770 4771 var args []string 4772 for _, r := range tc.resourcesToLoad { 4773 args = append(args, string(r)) 4774 } 4775 f.load(args...) 4776 require.Equal(t, tc.expected, f.loadResult.EnabledManifests) 4777 }) 4778 } 4779 } 4780 4781 func TestLocalResourceAllowParallel(t *testing.T) { 4782 f := newFixture(t) 4783 4784 f.file("Tiltfile", ` 4785 local_resource("a", ["echo", "hi"], allow_parallel=True) 4786 local_resource("b", ["echo", "hi"]) 4787 local_resource("c", serve_cmd=["echo", "hi"]) 4788 `) 4789 4790 f.load() 4791 a := f.assertNextManifest("a") 4792 assert.True(t, a.LocalTarget().AllowParallel) 4793 b := f.assertNextManifest("b") 4794 assert.False(t, b.LocalTarget().AllowParallel) 4795 4796 // local_resource serve_cmd is currently modeled as a no-op local cmd that 4797 // triggers a server restart. It's always OK for those no-op local cmds to 4798 // run in parallel. 4799 c := f.assertNextManifest("c") 4800 assert.True(t, c.LocalTarget().AllowParallel) 4801 } 4802 4803 func TestLocalResourceInvalidName(t *testing.T) { 4804 f := newFixture(t) 4805 4806 f.file("Tiltfile", ` 4807 local_resource("a/b", ["echo", "hi"]) 4808 `) 4809 4810 f.loadErrString( 4811 // Verify the validation message 4812 "invalid value \"a/b\": may not contain '/'", 4813 4814 // Verify the stack trace points to the local_resource 4815 "Tiltfile:2:15: in <toplevel>") 4816 } 4817 4818 func TestMaxParallelUpdates(t *testing.T) { 4819 for _, tc := range []struct { 4820 name string 4821 tiltfile string 4822 expectErrorContains string 4823 expectedMaxParallelUpdates int 4824 }{ 4825 { 4826 name: "default value if func not called", 4827 tiltfile: "print('hello world')", 4828 expectedMaxParallelUpdates: model.DefaultMaxParallelUpdates, 4829 }, 4830 { 4831 name: "default value if arg not specified", 4832 tiltfile: "update_settings(k8s_upsert_timeout_secs=123)", 4833 expectedMaxParallelUpdates: model.DefaultMaxParallelUpdates, 4834 }, 4835 { 4836 name: "set max parallel updates", 4837 tiltfile: "update_settings(max_parallel_updates=42)", 4838 expectedMaxParallelUpdates: 42, 4839 }, 4840 { 4841 name: "NaN error", 4842 tiltfile: "update_settings(max_parallel_updates='boop')", 4843 expectErrorContains: "got starlark.String, want int", 4844 }, 4845 { 4846 name: "must be positive int", 4847 tiltfile: "update_settings(max_parallel_updates=-1)", 4848 expectErrorContains: "must be >= 1", 4849 }, 4850 } { 4851 t.Run(tc.name, func(t *testing.T) { 4852 f := newFixture(t) 4853 4854 f.file("Tiltfile", tc.tiltfile) 4855 4856 if tc.expectErrorContains != "" { 4857 f.loadErrString(tc.expectErrorContains) 4858 return 4859 } 4860 4861 f.load() 4862 actualBuildSlots := f.loadResult.UpdateSettings.MaxParallelUpdates() 4863 assert.Equal(t, tc.expectedMaxParallelUpdates, actualBuildSlots, "expected vs. actual maxParallelUpdates") 4864 }) 4865 } 4866 } 4867 4868 func TestK8sUpsertTimeout(t *testing.T) { 4869 for _, tc := range []struct { 4870 name string 4871 tiltfile string 4872 expectErrorContains string 4873 expectedTimeout time.Duration 4874 }{ 4875 { 4876 name: "default value if func not called", 4877 tiltfile: "print('hello world')", 4878 expectedTimeout: v1alpha1.KubernetesApplyTimeoutDefault, 4879 }, 4880 { 4881 name: "default value if arg not specified", 4882 tiltfile: "update_settings(max_parallel_updates=123)", 4883 expectedTimeout: v1alpha1.KubernetesApplyTimeoutDefault, 4884 }, 4885 { 4886 name: "set max parallel updates", 4887 tiltfile: "update_settings(k8s_upsert_timeout_secs=42)", 4888 expectedTimeout: 42 * time.Second, 4889 }, 4890 { 4891 name: "NaN error", 4892 tiltfile: "update_settings(k8s_upsert_timeout_secs='boop')", 4893 expectErrorContains: "got starlark.String, want int", 4894 }, 4895 { 4896 name: "must be positive int", 4897 tiltfile: "update_settings(k8s_upsert_timeout_secs=-1)", 4898 expectErrorContains: "minimum k8s upsert timeout is 1s", 4899 }, 4900 } { 4901 t.Run(tc.name, func(t *testing.T) { 4902 f := newFixture(t) 4903 4904 f.file("Tiltfile", tc.tiltfile) 4905 4906 if tc.expectErrorContains != "" { 4907 f.loadErrString(tc.expectErrorContains) 4908 return 4909 } 4910 4911 f.load() 4912 actualTimeout := f.loadResult.UpdateSettings.K8sUpsertTimeout() 4913 assert.Equal(t, tc.expectedTimeout, actualTimeout, "expected vs. actual k8sUpsertTimeout") 4914 }) 4915 } 4916 } 4917 4918 func TestUpdateSettingsCalledTwice(t *testing.T) { 4919 f := newFixture(t) 4920 4921 f.file("Tiltfile", `update_settings(max_parallel_updates=123) 4922 update_settings(k8s_upsert_timeout_secs=456)`) 4923 4924 f.load() 4925 assert.Equal(t, 123, f.loadResult.UpdateSettings.MaxParallelUpdates(), "expected vs. actual MaxParallelUpdates") 4926 assert.Equal(t, 456*time.Second, f.loadResult.UpdateSettings.K8sUpsertTimeout(), "expected vs. actual k8sUpsertTimeout") 4927 } 4928 4929 // recursion is disabled by default in Starlark. Make sure we've enabled it for Tiltfiles. 4930 func TestRecursionEnabled(t *testing.T) { 4931 f := newFixture(t) 4932 4933 f.file("Tiltfile", ` 4934 def fact(n): 4935 if not n: 4936 return 1 4937 return n * fact(n - 1) 4938 4939 print("fact: %d" % (fact(10))) 4940 `) 4941 4942 f.load() 4943 4944 require.Contains(t, f.out.String(), fmt.Sprintf("fact: %d", 10*9*8*7*6*5*4*3*2*1)) 4945 } 4946 4947 func TestBuiltinAnalytics(t *testing.T) { 4948 f := newFixture(t) 4949 4950 // covering: 4951 // 1. a positional arg 4952 // 2. a keyword arg 4953 // 3. a mix of both for the same arg 4954 // 4. a builtin from a starkit plugin 4955 // 5. loading a Tilt plugin 4956 4957 f.file("Tiltfile", ` 4958 local('echo hi') 4959 local(command='echo hi') 4960 local('echo hi', quiet=True) 4961 allow_k8s_contexts("hello") 4962 load('ext://fooExt', 'printFoo') 4963 `) 4964 4965 // write the plugin locally so we don't need to deal with fake fetchers etc. 4966 f.WriteFile(f.JoinPath("tilt-extensions", "fooExt", "Tiltfile"), ` 4967 def printFoo(): 4968 print("foo") 4969 `) 4970 4971 f.load() 4972 4973 countEvent := f.SingleAnalyticsEvent("tiltfile.loaded") 4974 4975 // make sure it has all the expected builtin call counts 4976 expectedCounts := map[string]string{ 4977 "tiltfile.invoked.local": "3", 4978 "tiltfile.invoked.local.arg.command": "3", 4979 "tiltfile.invoked.local.arg.quiet": "1", 4980 "tiltfile.invoked.allow_k8s_contexts": "1", 4981 "tiltfile.invoked.allow_k8s_contexts.arg.contexts": "1", 4982 } 4983 4984 for k, v := range expectedCounts { 4985 require.Equal(t, v, countEvent.Tags[k], "count for %s", k) 4986 } 4987 4988 pluginEvent := f.SingleAnalyticsEvent("tiltfile.loaded.plugin") 4989 expectedTags := map[string]string{ 4990 "ext_name": "fooExt", 4991 "env": "docker-for-desktop", 4992 } 4993 require.Equal(t, expectedTags, pluginEvent.Tags) 4994 } 4995 4996 func TestCustomTagsReported(t *testing.T) { 4997 f := newFixture(t) 4998 4999 f.file("Tiltfile", ` 5000 experimental_analytics_report({'foo': 'bar'}) 5001 `) 5002 5003 f.load() 5004 5005 countEvent := f.SingleAnalyticsEvent("tiltfile.custom.report") 5006 5007 require.Equal(t, map[string]string{"foo": "bar"}, countEvent.Tags) 5008 } 5009 5010 func TestK8sResourceObjectsAddsNonWorkload(t *testing.T) { 5011 f := newFixture(t) 5012 5013 f.setupFoo() 5014 f.yaml("secret.yaml", secret("bar")) 5015 f.yaml("namespace.yaml", namespace("baz")) 5016 5017 f.file("Tiltfile", ` 5018 docker_build('gcr.io/foo', 'foo') 5019 k8s_yaml('foo.yaml') 5020 k8s_yaml('secret.yaml') 5021 k8s_yaml('namespace.yaml') 5022 k8s_resource('foo', objects=['bar', 'baz:namespace:default']) 5023 `) 5024 5025 f.load() 5026 5027 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("baz", "Namespace"), 5028 podReadiness(model.PodReadinessWait)) 5029 f.assertNoMoreManifests() 5030 } 5031 5032 func TestK8sResourceObjectsWithSameName(t *testing.T) { 5033 f := newFixture(t) 5034 5035 f.setupFoo() 5036 f.yaml("secret.yaml", secret("bar")) 5037 f.yaml("namespace.yaml", namespace("bar")) 5038 5039 f.file("Tiltfile", ` 5040 docker_build('gcr.io/foo', 'foo') 5041 k8s_yaml('foo.yaml') 5042 k8s_yaml('secret.yaml') 5043 k8s_yaml('namespace.yaml') 5044 k8s_resource('foo', objects=['bar', 'bar:namespace:default']) 5045 `) 5046 5047 f.loadErrString("\"bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5048 } 5049 5050 func TestK8sResourceObjectsCantIncludeSameObjectTwice(t *testing.T) { 5051 f := newFixture(t) 5052 5053 f.setupFoo() 5054 f.yaml("secret1.yaml", secret("bar")) 5055 f.yaml("secret2.yaml", secret("qux")) 5056 f.yaml("namespace.yaml", namespace("bar")) 5057 5058 f.file("Tiltfile", ` 5059 docker_build('gcr.io/foo', 'foo') 5060 k8s_yaml('foo.yaml') 5061 k8s_yaml('secret1.yaml') 5062 k8s_yaml('secret2.yaml') 5063 k8s_resource('foo', objects=['bar', 'bar:secret:default']) 5064 `) 5065 5066 f.loadErrString("No object identified by the fragment \"bar:secret:default\" could be found in remaining YAML. Valid remaining fragments are: \"qux:Secret:default\"") 5067 } 5068 5069 func TestK8sResourceObjectsMultipleAmbiguous(t *testing.T) { 5070 f := newFixture(t) 5071 5072 f.setupFoo() 5073 f.yaml("secret.yaml", secret("bar")) 5074 f.yaml("namespace.yaml", namespace("bar")) 5075 5076 f.file("Tiltfile", ` 5077 docker_build('gcr.io/foo', 'foo') 5078 k8s_yaml('foo.yaml') 5079 k8s_yaml('secret.yaml') 5080 k8s_yaml('namespace.yaml') 5081 k8s_resource('foo', objects=['bar', 'bar']) 5082 `) 5083 5084 f.loadErrString("bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5085 } 5086 5087 func TestK8sResourceObjectEmptySelector(t *testing.T) { 5088 f := newFixture(t) 5089 5090 f.setupFoo() 5091 f.yaml("secret.yaml", secret("bar")) 5092 f.yaml("namespace.yaml", namespace("baz")) 5093 5094 f.file("Tiltfile", ` 5095 docker_build('gcr.io/foo', 'foo') 5096 k8s_yaml('foo.yaml') 5097 k8s_yaml('secret.yaml') 5098 k8s_yaml('namespace.yaml') 5099 k8s_resource('foo', objects=['']) 5100 `) 5101 5102 f.loadErrString("Error making selector from string \"\"") 5103 } 5104 5105 func TestK8sResourceObjectInvalidSelector(t *testing.T) { 5106 f := newFixture(t) 5107 5108 f.setupFoo() 5109 f.yaml("secret.yaml", secret("bar")) 5110 f.yaml("namespace.yaml", namespace("baz")) 5111 5112 f.file("Tiltfile", ` 5113 docker_build('gcr.io/foo', 'foo') 5114 k8s_yaml('foo.yaml') 5115 k8s_yaml('secret.yaml') 5116 k8s_yaml('namespace.yaml') 5117 k8s_resource('foo', objects=['baz:namespace:default:wot']) 5118 `) 5119 5120 f.loadErrString("Error making selector from string \"baz:namespace:default:wot\"") 5121 } 5122 5123 func TestK8sResourceObjectSelectorWithEscapedColon(t *testing.T) { 5124 f := newFixture(t) 5125 5126 f.setupFoo() 5127 f.yaml("secret.yaml", secret("quu:bar")) 5128 f.yaml("namespace.yaml", namespace("baz")) 5129 5130 f.file("Tiltfile", ` 5131 docker_build('gcr.io/foo', 'foo') 5132 k8s_yaml('foo.yaml') 5133 k8s_yaml('secret.yaml') 5134 k8s_yaml('namespace.yaml') 5135 k8s_resource('foo', objects=['quu\\:bar', 'baz:namespace:default']) 5136 `) 5137 5138 f.load() 5139 5140 f.assertNextManifest("foo", deployment("foo"), k8sObject("quu:bar", "Secret"), k8sObject("baz", "Namespace"), 5141 podReadiness(model.PodReadinessWait)) 5142 f.assertNoMoreManifests() 5143 } 5144 5145 func TestK8sResourceObjectSelectorWithEscapedBackslash(t *testing.T) { 5146 f := newFixture(t) 5147 5148 f.setupFoo() 5149 f.yaml("secret.yaml", secret("quu\\bar")) 5150 f.yaml("namespace.yaml", namespace("baz")) 5151 5152 f.file("Tiltfile", ` 5153 docker_build('gcr.io/foo', 'foo') 5154 k8s_yaml('foo.yaml') 5155 k8s_yaml('secret.yaml') 5156 k8s_yaml('namespace.yaml') 5157 k8s_resource('foo', objects=['quu\\\\bar', 'baz:namespace:default']) 5158 `) 5159 5160 f.load() 5161 5162 f.assertNextManifest("foo", deployment("foo"), k8sObject("quu\\bar", "Secret"), k8sObject("baz", "Namespace"), 5163 podReadiness(model.PodReadinessWait)) 5164 f.assertNoMoreManifests() 5165 } 5166 5167 func TestK8sResourceObjectSelectorSuggestedObjectsAreEscaped(t *testing.T) { 5168 f := newFixture(t) 5169 5170 f.setupFoo() 5171 f.yaml("secret.yaml", secret("quu:bar")) 5172 f.yaml("namespace.yaml", namespace("baz")) 5173 5174 f.file("Tiltfile", ` 5175 docker_build('gcr.io/foo', 'foo') 5176 k8s_yaml('foo.yaml') 5177 k8s_yaml('secret.yaml') 5178 k8s_yaml('namespace.yaml') 5179 k8s_resource('foo', objects=['quu:bar', 'baz:namespace:default']) 5180 `) 5181 5182 f.loadErrString( 5183 `No object identified by the fragment "quu:bar" could be found`, 5184 `Possible objects are: "foo:Deployment:default", "quu\\:bar:Secret:default", "baz:Namespace:default"`, 5185 ) 5186 } 5187 5188 func TestK8sResourceObjectSelectorInvalidEscapeSequence(t *testing.T) { 5189 f := newFixture(t) 5190 5191 f.setupFoo() 5192 f.yaml("secret.yaml", secret("quu:bar")) 5193 f.yaml("namespace.yaml", namespace("baz")) 5194 5195 f.file("Tiltfile", ` 5196 docker_build('gcr.io/foo', 'foo') 5197 k8s_yaml('foo.yaml') 5198 k8s_yaml('secret.yaml') 5199 k8s_yaml('namespace.yaml') 5200 k8s_resource('foo', objects=['qu\\u:bar', 'baz:namespace:default']) 5201 `) 5202 5203 f.loadErrString( 5204 `invalid escape sequence '\u' in 'qu\u:b'`, 5205 ) 5206 } 5207 5208 func TestK8sResourceObjectIncludesSelectorThatDoesntExist(t *testing.T) { 5209 f := newFixture(t) 5210 5211 f.setupFoo() 5212 f.yaml("secret.yaml", secret("bar")) 5213 f.yaml("namespace.yaml", namespace("baz")) 5214 5215 f.file("Tiltfile", ` 5216 docker_build('gcr.io/foo', 'foo') 5217 k8s_yaml('foo.yaml') 5218 k8s_yaml('secret.yaml') 5219 k8s_yaml('namespace.yaml') 5220 k8s_resource('foo', objects=['baz:secret:default']) 5221 `) 5222 5223 f.loadErrString("No object identified by the fragment \"baz:secret:default\" could be found. Possible objects are: \"foo:Deployment:default\", \"bar:Secret:default\", \"baz:Namespace:default\"") 5224 } 5225 5226 func TestK8sResourceObjectsPartialNames(t *testing.T) { 5227 f := newFixture(t) 5228 5229 f.setupFoo() 5230 f.yaml("secret.yaml", secret("bar")) 5231 f.yaml("namespace.yaml", namespace("bar")) 5232 5233 f.file("Tiltfile", ` 5234 docker_build('gcr.io/foo', 'foo') 5235 k8s_yaml('foo.yaml') 5236 k8s_yaml('secret.yaml') 5237 k8s_yaml('namespace.yaml') 5238 k8s_resource('foo', objects=['bar:secret', 'bar:namespace']) 5239 `) 5240 5241 f.load() 5242 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("bar", "Namespace")) 5243 f.assertNoMoreManifests() 5244 } 5245 5246 func TestK8sResourcePrefixesShouldntMatch(t *testing.T) { 5247 f := newFixture(t) 5248 5249 f.setupFoo() 5250 f.yaml("secret.yaml", secret("bar")) 5251 5252 f.file("Tiltfile", ` 5253 docker_build('gcr.io/foo', 'foo') 5254 k8s_yaml('foo.yaml') 5255 k8s_yaml('secret.yaml') 5256 k8s_resource('foo', objects=['ba']) 5257 `) 5258 5259 f.loadErrString("No object identified by the fragment \"ba\" could be found. Possible objects are: \"foo:Deployment:default\", \"bar:Secret:default\"") 5260 } 5261 5262 func TestK8sResourceAmbiguousSelector(t *testing.T) { 5263 f := newFixture(t) 5264 5265 f.setupFoo() 5266 f.yaml("secret.yaml", secret("bar")) 5267 f.yaml("namespace.yaml", namespace("bar")) 5268 5269 f.file("Tiltfile", ` 5270 docker_build('gcr.io/foo', 'foo') 5271 k8s_yaml('foo.yaml') 5272 k8s_yaml('secret.yaml') 5273 k8s_yaml('namespace.yaml') 5274 k8s_resource('foo', objects=['bar']) 5275 `) 5276 5277 f.loadErrString("\"bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5278 } 5279 5280 func TestK8sResourceObjectDuplicate(t *testing.T) { 5281 f := newFixture(t) 5282 5283 f.setupFoo() 5284 f.yaml("secret.yaml", secret("bar")) 5285 f.yaml("anotherworkload.yaml", deployment("baz")) 5286 5287 f.file("Tiltfile", ` 5288 docker_build('gcr.io/foo', 'foo') 5289 k8s_yaml('foo.yaml') 5290 k8s_yaml('anotherworkload.yaml') 5291 k8s_yaml('secret.yaml') 5292 k8s_resource('foo', objects=['bar']) 5293 k8s_resource('baz', objects=['bar']) 5294 `) 5295 5296 f.loadErrString("No object identified by the fragment \"bar\" could be found in remaining YAML. Valid remaining fragments are:") 5297 } 5298 5299 func TestK8sResourceObjectMultipleResources(t *testing.T) { 5300 f := newFixture(t) 5301 5302 f.setupFoo() 5303 f.yaml("secret.yaml", secret("bar")) 5304 f.yaml("namespace.yaml", namespace("qux")) 5305 f.yaml("anotherworkload.yaml", deployment("baz")) 5306 5307 f.file("Tiltfile", ` 5308 docker_build('gcr.io/foo', 'foo') 5309 k8s_yaml('foo.yaml') 5310 k8s_yaml('secret.yaml') 5311 k8s_yaml('namespace.yaml') 5312 k8s_yaml('anotherworkload.yaml') 5313 k8s_resource('foo', objects=['bar']) 5314 k8s_resource('baz') 5315 `) 5316 5317 f.load() 5318 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret")) 5319 f.assertNextManifest("baz", deployment("baz")) 5320 f.assertNextManifestUnresourced("qux") 5321 f.assertNoMoreManifests() 5322 } 5323 5324 func TestMultipleResourcesMultipleObjects(t *testing.T) { 5325 f := newFixture(t) 5326 5327 f.setupFoo() 5328 f.yaml("secret.yaml", secret("bar")) 5329 f.yaml("namespace.yaml", namespace("qux")) 5330 f.yaml("anotherworkload.yaml", deployment("baz")) 5331 5332 f.file("Tiltfile", ` 5333 docker_build('gcr.io/foo', 'foo') 5334 k8s_yaml('foo.yaml') 5335 k8s_yaml('secret.yaml') 5336 k8s_yaml('namespace.yaml') 5337 k8s_yaml('anotherworkload.yaml') 5338 k8s_resource('foo', objects=['bar']) 5339 k8s_resource('baz', objects=['qux']) 5340 `) 5341 5342 f.load() 5343 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret")) 5344 f.assertNextManifest("baz", deployment("baz"), namespace("qux")) 5345 f.assertNoMoreManifests() 5346 } 5347 5348 func TestK8sResourceAmbiguousWorkloadAmbiguousObject(t *testing.T) { 5349 f := newFixture(t) 5350 5351 f.setupFoo() 5352 f.yaml("secret.yaml", secret("foo")) 5353 5354 f.file("Tiltfile", ` 5355 docker_build('gcr.io/foo', 'foo') 5356 k8s_yaml('foo.yaml') 5357 k8s_yaml('secret.yaml') 5358 k8s_resource('foo', objects=['foo']) 5359 `) 5360 5361 f.loadErrString("\"foo\" is not a unique fragment. Objects that match \"foo\" are \"foo:Deployment:default\", \"foo:Secret:default\"") 5362 } 5363 5364 func TestK8sResourceObjectsWithWorkloadToResourceFunction(t *testing.T) { 5365 f := newFixture(t) 5366 5367 f.setupFoo() 5368 f.yaml("secret.yaml", secret("foo")) 5369 5370 f.file("Tiltfile", ` 5371 docker_build('gcr.io/foo', 'foo') 5372 k8s_yaml('foo.yaml') 5373 k8s_yaml('secret.yaml') 5374 def wtrf(id): 5375 return 'hello-' + id.name 5376 workload_to_resource_function(wtrf) 5377 k8s_resource('hello-foo', objects=['foo:secret']) 5378 `) 5379 5380 f.load() 5381 f.assertNumManifests(1) 5382 f.assertNextManifest("hello-foo", k8sObject("foo", "Secret")) 5383 f.assertNoMoreManifests() 5384 } 5385 5386 func TestK8sResourceNewNameWithoutObjects(t *testing.T) { 5387 f := newFixture(t) 5388 5389 f.file("Tiltfile", ` 5390 k8s_resource(new_name='foo') 5391 `) 5392 5393 f.loadErrString("k8s_resource doesn't specify a workload or any objects") 5394 } 5395 5396 func TestK8sResourceObjectsWithGroup(t *testing.T) { 5397 f := newFixture(t) 5398 5399 f.setupFoo() 5400 f.yaml("secret.yaml", secret("bar")) 5401 f.yaml("namespace.yaml", namespace("baz")) 5402 5403 f.file("Tiltfile", ` 5404 docker_build('gcr.io/foo', 'foo') 5405 k8s_yaml('foo.yaml') 5406 k8s_yaml('secret.yaml') 5407 k8s_yaml('namespace.yaml') 5408 k8s_resource('foo', objects=['bar', 'baz:namespace:default:core']) 5409 `) 5410 5411 // TODO(dmiller): see comment on fullNameFromK8sEntity for info on why we don't support specifying group right now 5412 f.loadErrString("Error making selector from string \"baz:namespace:default:core\": Too many parts in selector. Selectors must contain between 1 and 3 parts (colon separated), found 4 parts in baz:namespace:default:core") 5413 // f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("baz", "Namespace")) 5414 // f.assertNoMoreManifests() 5415 } 5416 5417 func TestK8sResourceObjectClusterScoped(t *testing.T) { 5418 f := newFixture(t) 5419 5420 f.setupFoo() 5421 f.yaml("namespace.yaml", namespace("baz")) 5422 5423 f.file("Tiltfile", ` 5424 docker_build('gcr.io/foo', 'foo') 5425 k8s_yaml('foo.yaml') 5426 k8s_yaml('namespace.yaml') 5427 k8s_resource('foo', objects=['baz:namespace']) 5428 `) 5429 5430 f.load() 5431 5432 f.assertNextManifest("foo", deployment("foo"), k8sObject("baz", "Namespace")) 5433 f.assertNoMoreManifests() 5434 } 5435 5436 // TODO(dmiller): I'm not sure if this makes sense ... cluster scoped things like namespaces _can't_ have 5437 // namespaces, so should we allow you to specify namespaces for them? 5438 // For now we just leave them as "default" 5439 func TestK8sResourceObjectClusterScopedWithNamespace(t *testing.T) { 5440 f := newFixture(t) 5441 5442 f.setupFoo() 5443 f.yaml("namespace.yaml", namespace("baz")) 5444 5445 f.file("Tiltfile", ` 5446 docker_build('gcr.io/foo', 'foo') 5447 k8s_yaml('foo.yaml') 5448 k8s_yaml('namespace.yaml') 5449 k8s_resource('foo', objects=['baz:namespace:qux']) 5450 `) 5451 5452 f.loadErrString("No object identified by the fragment \"baz:namespace:qux\" could be found. Possible objects are: \"foo:Deployment:default\", \"baz:Namespace:default\"") 5453 } 5454 5455 func TestK8sResourceObjectsNonWorkloadOnly(t *testing.T) { 5456 f := newFixture(t) 5457 5458 f.yaml("secret.yaml", secret("bar")) 5459 f.yaml("namespace.yaml", namespace("baz")) 5460 5461 f.file("Tiltfile", ` 5462 k8s_yaml('secret.yaml') 5463 k8s_yaml('namespace.yaml') 5464 k8s_resource(new_name='foo', objects=['bar', 'baz:namespace:default']) 5465 `) 5466 5467 f.load() 5468 5469 f.assertNextManifest("foo", k8sObject("bar", "Secret"), k8sObject("baz", "Namespace"), podReadiness(model.PodReadinessIgnore)) 5470 f.assertNoMoreManifests() 5471 } 5472 5473 func TestK8sResourceNewNameAdditive(t *testing.T) { 5474 f := newFixture(t) 5475 5476 f.yaml("a.yaml", namespace("a")) 5477 f.yaml("b.yaml", namespace("b")) 5478 5479 f.file("Tiltfile", ` 5480 k8s_yaml('a.yaml') 5481 k8s_yaml('b.yaml') 5482 k8s_resource(new_name='namespaces', objects=['a']) 5483 k8s_resource('namespaces', objects=['b']) 5484 `) 5485 5486 f.load() 5487 f.assertNextManifest("namespaces", k8sObject("a", "Namespace"), k8sObject("b", "Namespace")) 5488 } 5489 5490 func TestK8sExistingResourceAdditive(t *testing.T) { 5491 f := newFixture(t) 5492 5493 f.yaml("a.yaml", deployment("a")) 5494 f.yaml("b.yaml", namespace("b")) 5495 f.yaml("c.yaml", namespace("c")) 5496 5497 f.file("Tiltfile", ` 5498 k8s_yaml('a.yaml') 5499 k8s_yaml('b.yaml') 5500 k8s_yaml('c.yaml') 5501 k8s_resource('a', objects=['b']) 5502 k8s_resource('a', objects=['c']) 5503 `) 5504 5505 f.load() 5506 f.assertNextManifest("a", 5507 k8sObject("a", "Deployment"), k8sObject("b", "Namespace"), k8sObject("c", "Namespace")) 5508 } 5509 5510 func TestK8sExistingResourceNewNameAdditive(t *testing.T) { 5511 f := newFixture(t) 5512 5513 // this was working non-deterministically based on hashtable order, so generate a bunch of resources 5514 // to reduce the chance of false positives 5515 // https://github.com/tilt-dev/tilt/issues/4808 5516 for i := 1; i <= 25; i++ { 5517 f.yaml(fmt.Sprintf("deploy%d.yaml", i), deployment(fmt.Sprintf("deploy%d", i))) 5518 } 5519 5520 f.file("Tiltfile", ` 5521 for i in range(1, 26): 5522 k8s_yaml('deploy%d.yaml' % (i)) 5523 k8s_resource('deploy%d' % (i), new_name='deploy%d-renamed' % (i), labels=['a']) 5524 k8s_resource('deploy%d-renamed' % (i), labels=['b']) 5525 `) 5526 5527 f.load() 5528 for i := 1; i <= 25; i++ { 5529 f.assertNextManifest(model.ManifestName(fmt.Sprintf("deploy%d-renamed", i)), 5530 k8sObject(fmt.Sprintf("deploy%d", i), "Deployment"), resourceLabels("a", "b")) 5531 } 5532 } 5533 5534 func TestK8sExistingResourceNewNameAlreadyTaken(t *testing.T) { 5535 f := newFixture(t) 5536 5537 f.yaml("a.yaml", deployment("a")) 5538 f.yaml("b.yaml", namespace("b")) 5539 f.yaml("c.yaml", namespace("c")) 5540 5541 f.file("Tiltfile", ` 5542 k8s_yaml('a.yaml') 5543 k8s_yaml('b.yaml') 5544 k8s_yaml('c.yaml') 5545 k8s_resource('a', objects=['b']) 5546 k8s_resource(new_name='a', objects=['c']) 5547 `) 5548 5549 f.loadErrString(`k8s_resource named "a" already exists`) 5550 } 5551 5552 func TestK8sNonWorkloadOnlyResourceWithAllTheOptions(t *testing.T) { 5553 f := newFixture(t) 5554 5555 f.setupFoo() 5556 f.yaml("secret.yaml", secret("bar")) 5557 f.yaml("namespace.yaml", namespace("baz")) 5558 5559 f.file("Tiltfile", ` 5560 docker_build('gcr.io/foo', 'foo') 5561 k8s_yaml('foo.yaml') 5562 k8s_yaml('secret.yaml') 5563 k8s_yaml('namespace.yaml') 5564 k8s_resource(new_name='bar', objects=['bar', 'baz:namespace:default'], port_forwards=9876, extra_pod_selectors=[{'quux': 'corge'}], trigger_mode=TRIGGER_MODE_MANUAL, resource_deps=['foo']) 5565 `) 5566 5567 f.load() 5568 5569 f.assertNextManifest("foo") 5570 f.assertNextManifest("bar", k8sObject("bar", "Secret"), k8sObject("baz", "Namespace")) 5571 f.assertNoMoreManifests() 5572 } 5573 5574 func TestK8sResourceEmptyWorkloadSpecifierAndNoObjects(t *testing.T) { 5575 f := newFixture(t) 5576 5577 f.setupFoo() 5578 5579 f.file("Tiltfile", ` 5580 5581 k8s_yaml('foo.yaml') 5582 k8s_resource('', port_forwards=8000) 5583 `) 5584 5585 f.loadErrString("Resource name missing. Must give a name for an existing resource or a new_name to create a new resource.") 5586 } 5587 5588 func TestK8sResourceNonWorkloadRequiresNewName(t *testing.T) { 5589 f := newFixture(t) 5590 5591 f.yaml("secret.yaml", secret("bar")) 5592 f.yaml("namespace.yaml", namespace("baz")) 5593 5594 f.file("Tiltfile", ` 5595 k8s_yaml('secret.yaml') 5596 k8s_yaml('namespace.yaml') 5597 k8s_resource(objects=['bar', 'baz:namespace:default']) 5598 `) 5599 5600 f.loadErrString("Resource name missing. Must give a name for an existing resource or a new_name to create a new resource.") 5601 } 5602 5603 func TestK8sResourceNewNameCantOverwriteWorkload(t *testing.T) { 5604 f := newFixture(t) 5605 5606 f.setupFoo() 5607 f.yaml("secret.yaml", secret("bar")) 5608 5609 f.file("Tiltfile", ` 5610 k8s_yaml('foo.yaml') 5611 k8s_yaml('secret.yaml') 5612 k8s_resource('foo', new_name='bar') 5613 k8s_resource(new_name='bar', objects=['bar:secret']) 5614 `) 5615 5616 // NOTE(dmiller): because `range`ing over maps is unstable we don't know which error we will encounter: 5617 // 1. Trying to create a non-workload resource when a resource by that name already exists 5618 // 2. Trying to rename a resource to a name that already exists 5619 // so we match a string that appears in both error messages 5620 f.loadErrString("already exists") 5621 } 5622 5623 func TestK8sResourceObjectsNonAmbiguousDefaultNamespace(t *testing.T) { 5624 f := newFixture(t) 5625 5626 f.file("serving-core.yaml", testyaml.KnativeServingCore) 5627 5628 f.file("Tiltfile", ` 5629 k8s_yaml([ 5630 'serving-core.yaml', 5631 ]) 5632 5633 k8s_resource( 5634 objects=[ 5635 'queue-proxy:Image', 5636 ], 5637 new_name='knative-gateways') 5638 `) 5639 5640 f.load() 5641 f.assertNextManifest("knative-gateways") 5642 f.assertNoMoreManifests() 5643 } 5644 5645 func TestK8sResourceObjectsAreNotCaseSensitive(t *testing.T) { 5646 f := newFixture(t) 5647 5648 f.file("serving-core.yaml", testyaml.KnativeServingCore) 5649 5650 f.file("Tiltfile", ` 5651 k8s_yaml([ 5652 'serving-core.yaml', 5653 ]) 5654 5655 k8s_resource( 5656 objects=[ 5657 'queue-proxy:image', 5658 ], 5659 new_name='knative-gateways') 5660 `) 5661 5662 f.load() 5663 f.assertNextManifest("knative-gateways") 5664 f.assertNoMoreManifests() 5665 } 5666 5667 func TestK8sResourceLabels(t *testing.T) { 5668 f := newFixture(t) 5669 5670 f.setupFoo() 5671 5672 f.file("Tiltfile", ` 5673 k8s_yaml('foo.yaml') 5674 k8s_resource('foo', labels="test") 5675 `) 5676 5677 f.load() 5678 f.assertNumManifests(1) 5679 f.assertNextManifest("foo", resourceLabels("test")) 5680 } 5681 5682 func TestK8sResourceLabelsAppend(t *testing.T) { 5683 f := newFixture(t) 5684 5685 f.setupFoo() 5686 5687 f.file("Tiltfile", ` 5688 k8s_yaml('foo.yaml') 5689 k8s_resource('foo', labels="test") 5690 k8s_resource('foo', labels="test2") 5691 `) 5692 5693 f.load() 5694 f.assertNumManifests(1) 5695 f.assertNextManifest("foo", resourceLabels("test", "test2")) 5696 } 5697 5698 func TestLocalResourceLabels(t *testing.T) { 5699 f := newFixture(t) 5700 5701 f.file("Tiltfile", ` 5702 local_resource("test", cmd="echo hi", labels="foo") 5703 local_resource("test2", cmd="echo hi2", labels=["bar", "baz"]) 5704 `) 5705 5706 f.load() 5707 f.assertNumManifests(2) 5708 f.assertNextManifest("test", resourceLabels("foo")) 5709 f.assertNextManifest("test2", resourceLabels("bar", "baz")) 5710 } 5711 5712 // https://github.com/tilt-dev/tilt/issues/5467 5713 func TestLoadErrorWithArgs(t *testing.T) { 5714 f := newFixture(t) 5715 5716 f.file("Tiltfile", "asdf") 5717 f.loadArgsErrString([]string{"foo"}, "undefined: asdf") 5718 } 5719 5720 func TestContentsChangedTag(t *testing.T) { 5721 f := newFixture(t) 5722 5723 f.file("Tiltfile", "print('Hello')") 5724 tiltfile := ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), []string{}) 5725 loader := f.newTiltfileLoader() 5726 5727 // *.changed = false on first load (no previous hash values) 5728 tlr := loader.Load(f.ctx, tiltfile, nil) 5729 assert.Equal(t, "0d4b93146f79968657afdad8b23d423973bf7a7e97690d146e6b6cfcc24e617e", tlr.Hashes.TiltfileSHA256) 5730 assert.Equal(t, "0d4b93146f79968657afdad8b23d423973bf7a7e97690d146e6b6cfcc24e617e", tlr.Hashes.AllFilesSHA256) 5731 5732 event := f.SingleAnalyticsEvent("tiltfile.loaded") 5733 assert.Equal(t, "false", event.Tags["tiltfile.changed"]) 5734 assert.Equal(t, "false", event.Tags["allfiles.changed"]) 5735 5736 // *.changed = true because hash values differ 5737 f.an.Counts = []analytics.CountEvent{} 5738 tlr.Hashes = hasher.Hashes{TiltfileSHA256: "abc123", AllFilesSHA256: "abc123"} 5739 tlr = loader.Load(f.ctx, tiltfile, &tlr) 5740 event = f.SingleAnalyticsEvent("tiltfile.loaded") 5741 assert.Equal(t, "true", event.Tags["tiltfile.changed"]) 5742 assert.Equal(t, "true", event.Tags["allfiles.changed"]) 5743 5744 // *.changed = false because hash values match 5745 f.an.Counts = []analytics.CountEvent{} 5746 tlr = loader.Load(f.ctx, tiltfile, &tlr) 5747 event = f.SingleAnalyticsEvent("tiltfile.loaded") 5748 assert.Equal(t, "false", event.Tags["tiltfile.changed"]) 5749 assert.Equal(t, "false", event.Tags["allfiles.changed"]) 5750 } 5751 5752 type fixture struct { 5753 ctx context.Context 5754 out *bytes.Buffer 5755 t *testing.T 5756 *tempdir.TempDirFixture 5757 k8sContext k8s.KubeContext 5758 k8sNamespace k8s.Namespace 5759 k8sEnv clusterid.Product 5760 webHost model.WebHost 5761 5762 ta *tiltanalytics.TiltAnalytics 5763 an *analytics.MemoryAnalytics 5764 5765 loadResult TiltfileLoadResult 5766 warnings []string 5767 features feature.Defaults 5768 } 5769 5770 func (f *fixture) newTiltfileLoader() TiltfileLoader { 5771 dcc := dockercompose.NewDockerComposeClient(docker.LocalEnv{}) 5772 5773 k8sContextPlugin := k8scontext.NewPlugin(f.k8sContext, f.k8sNamespace, f.k8sEnv) 5774 versionPlugin := version.NewPlugin(model.TiltBuild{Version: "0.5.0"}) 5775 configPlugin := config.NewPlugin("up") 5776 localEnv := localexec.DefaultEnv(12345, f.webHost) 5777 execer := localexec.NewProcessExecer(localEnv) 5778 extr := tiltextension.NewFakeExtReconciler(f.Path()) 5779 extrr := tiltextension.NewFakeExtRepoReconciler(f.Path()) 5780 extPlugin := tiltextension.NewFakePlugin(extrr, extr) 5781 ciSettingsPlugin := cisettings.NewPlugin(0) 5782 return ProvideTiltfileLoader(f.ta, k8sContextPlugin, versionPlugin, configPlugin, 5783 extPlugin, ciSettingsPlugin, dcc, f.webHost, execer, f.features, f.k8sEnv) 5784 } 5785 5786 func newFixture(t *testing.T) *fixture { 5787 out := new(bytes.Buffer) 5788 ctx, ma, ta := testutils.ForkedCtxAndAnalyticsForTest(out) 5789 f := tempdir.NewTempDirFixture(t) 5790 f.Chdir() 5791 5792 // copy the features to avoid unintentional mutation by tests 5793 features := make(feature.Defaults) 5794 for k, v := range feature.MainDefaults { 5795 features[k] = v 5796 } 5797 5798 r := &fixture{ 5799 ctx: ctx, 5800 out: out, 5801 t: t, 5802 TempDirFixture: f, 5803 an: ma, 5804 ta: ta, 5805 k8sContext: "fake-context", 5806 k8sNamespace: "fake-namespace", 5807 k8sEnv: clusterid.ProductDockerDesktop, 5808 features: features, 5809 } 5810 5811 // Collect the warnings 5812 l := logger.NewFuncLogger(false, logger.DebugLvl, func(level logger.Level, fields logger.Fields, msg []byte) error { 5813 if level == logger.WarnLvl { 5814 r.warnings = append(r.warnings, string(msg)) 5815 } 5816 out.Write(msg) 5817 return nil 5818 }) 5819 r.ctx = logger.WithLogger(r.ctx, l) 5820 5821 return r 5822 } 5823 5824 func (f *fixture) file(path string, contents string) { 5825 f.WriteFile(path, contents) 5826 } 5827 5828 type k8sOpts interface{} 5829 5830 func (f *fixture) dockerfile(path string) { 5831 f.file(path, simpleDockerfile) 5832 } 5833 5834 func (f *fixture) dockerignore(path string) { 5835 f.file(path, simpleDockerignore) 5836 } 5837 5838 func (f *fixture) yaml(path string, entities ...k8sOpts) { 5839 var entityObjs []k8s.K8sEntity 5840 5841 for _, e := range entities { 5842 switch e := e.(type) { 5843 case deploymentHelper: 5844 s := testyaml.SnackYaml 5845 if e.image != "" { 5846 s = strings.ReplaceAll(s, testyaml.SnackImage, e.image) 5847 } 5848 s = strings.ReplaceAll(s, testyaml.SnackName, e.name) 5849 objs, err := k8s.ParseYAMLFromString(s) 5850 if err != nil { 5851 f.t.Fatal(err) 5852 } 5853 5854 if len(e.templateLabels) > 0 { 5855 for i, obj := range objs { 5856 withLabels, err := k8s.OverwriteLabels(obj, model.ToLabelPairs(e.templateLabels)) 5857 if err != nil { 5858 f.t.Fatal(err) 5859 } 5860 objs[i] = withLabels 5861 } 5862 } 5863 5864 for i, obj := range objs { 5865 de := obj.Obj.(*appsv1.Deployment) 5866 for i, c := range de.Spec.Template.Spec.Containers { 5867 for _, ev := range e.envVars { 5868 c.Env = append(c.Env, v1.EnvVar{ 5869 Name: ev.name, 5870 Value: ev.value, 5871 }) 5872 } 5873 de.Spec.Template.Spec.Containers[i] = c 5874 } 5875 if e.namespace != "" { 5876 de.Namespace = e.namespace 5877 } 5878 obj.Obj = de 5879 objs[i] = obj 5880 } 5881 5882 entityObjs = append(entityObjs, objs...) 5883 case serviceHelper: 5884 s := testyaml.DoggosServiceYaml 5885 s = strings.ReplaceAll(s, testyaml.DoggosName, e.name) 5886 objs, err := k8s.ParseYAMLFromString(s) 5887 if err != nil { 5888 f.t.Fatal(err) 5889 } 5890 5891 if e.selectorLabels != nil { 5892 for _, obj := range objs { 5893 err := overwriteSelectorsForService(&obj, e.selectorLabels) 5894 if err != nil { 5895 f.t.Fatal(err) 5896 } 5897 } 5898 } 5899 5900 entityObjs = append(entityObjs, objs...) 5901 5902 case secretHelper: 5903 s := testyaml.SecretYaml 5904 s = strings.ReplaceAll(s, testyaml.SecretName, e.name) 5905 objs, err := k8s.ParseYAMLFromString(s) 5906 if err != nil { 5907 f.t.Fatal(err) 5908 } 5909 5910 entityObjs = append(entityObjs, objs...) 5911 case namespaceHelper: 5912 s := testyaml.MyNamespaceYAML 5913 s = strings.ReplaceAll(s, testyaml.MyNamespaceName, e.namespace) 5914 objs, err := k8s.ParseYAMLFromString(s) 5915 if err != nil { 5916 f.t.Fatal(err) 5917 } 5918 entityObjs = append(entityObjs, objs...) 5919 default: 5920 f.t.Fatalf("unexpected entity %T %v", e, e) 5921 } 5922 } 5923 5924 s, err := k8s.SerializeSpecYAML(entityObjs) 5925 if err != nil { 5926 f.t.Fatal(err) 5927 } 5928 f.file(path, s) 5929 } 5930 5931 // Default load. Fails if there are any warnings. 5932 func (f *fixture) load(args ...string) { 5933 f.t.Helper() 5934 f.loadAllowWarnings(args...) 5935 if len(f.warnings) != 0 { 5936 f.t.Fatalf("Unexpected warnings. Actual: %s", f.warnings) 5937 } 5938 } 5939 5940 // Load the manifests, expecting warnings. 5941 // Warnings should be asserted later with assertWarnings 5942 func (f *fixture) loadAllowWarnings(args ...string) { 5943 f.t.Helper() 5944 tlr := f.newTiltfileLoader().Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), args), nil) 5945 err := tlr.Error 5946 if err != nil { 5947 f.t.Fatal(err) 5948 } 5949 f.loadResult = tlr 5950 5951 for _, m := range f.loadResult.Manifests { 5952 err := m.InferImageProperties() 5953 require.NoError(f.t, err) 5954 } 5955 } 5956 5957 func unusedImageWarning(unusedImage string, suggestedImages []string, configType string) string { 5958 ret := fmt.Sprintf("Image not used in any %s config:\n ✕ %s", configType, unusedImage) 5959 if len(suggestedImages) > 0 { 5960 ret += "\nDid you mean…" 5961 for _, s := range suggestedImages { 5962 ret += fmt.Sprintf("\n - %s", s) 5963 } 5964 } 5965 ret += "\nSkipping this image build" 5966 ret += fmt.Sprintf("\nIf this is deliberate, suppress this warning with: update_settings(suppress_unused_image_warnings=[%q])", unusedImage) 5967 return ret 5968 } 5969 5970 // Load the manifests, expecting warnings. 5971 func (f *fixture) loadAssertWarnings(warnings ...string) { 5972 f.loadAllowWarnings() 5973 f.assertWarnings(warnings...) 5974 } 5975 5976 func (f *fixture) loadErrString(msgs ...string) { 5977 f.loadArgsErrString(nil, msgs...) 5978 } 5979 5980 func (f *fixture) loadArgsErrString(args []string, msgs ...string) { 5981 f.t.Helper() 5982 tlr := f.newTiltfileLoader().Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), args), nil) 5983 err := tlr.Error 5984 5985 if err == nil { 5986 f.t.Fatalf("expected error but got nil") 5987 } 5988 f.loadResult = tlr 5989 errText := err.Error() 5990 5991 for _, msg := range msgs { 5992 if !strings.Contains(errText, msg) { 5993 f.t.Fatalf("error %q does not contain string %q", errText, msg) 5994 } 5995 } 5996 5997 for _, m := range f.loadResult.Manifests { 5998 err := m.InferImageProperties() 5999 require.NoError(f.t, err) 6000 } 6001 } 6002 6003 func (f *fixture) gitInit(path string) { 6004 if err := os.MkdirAll(f.JoinPath(path, ".git"), os.FileMode(0777)); err != nil { 6005 f.t.Fatal(err) 6006 } 6007 } 6008 6009 func (f *fixture) assertNoMoreManifests() { 6010 if len(f.loadResult.Manifests) != 0 { 6011 names := make([]string, len(f.loadResult.Manifests)) 6012 for i, m := range f.loadResult.Manifests { 6013 names[i] = m.Name.String() 6014 } 6015 f.t.Fatalf("expected no more manifests but found %d: %s", 6016 len(names), strings.Join(names, ", ")) 6017 } 6018 } 6019 6020 // Helper func for asserting that the next manifest is Unresourced 6021 // k8s YAML containing the given k8s entities. 6022 func (f *fixture) assertNextManifestUnresourced(expectedEntities ...string) model.Manifest { 6023 lowercaseExpected := []string{} 6024 for _, e := range expectedEntities { 6025 lowercaseExpected = append(lowercaseExpected, strings.ToLower(e)) 6026 } 6027 next := f.assertNextManifest(model.UnresourcedYAMLManifestName) 6028 6029 entities, err := k8s.ParseYAML(bytes.NewBufferString(next.K8sTarget().YAML)) 6030 assert.NoError(f.t, err) 6031 6032 entityNames := make([]string, len(entities)) 6033 for i, e := range entities { 6034 entityNames[i] = strings.ToLower(e.Name()) 6035 } 6036 assert.Equal(f.t, lowercaseExpected, entityNames) 6037 return next 6038 } 6039 6040 type funcOpt func(*testing.T, model.Manifest) bool 6041 6042 // assert functions and helpers 6043 func (f *fixture) assertNextManifest(name model.ManifestName, opts ...interface{}) model.Manifest { 6044 f.t.Helper() 6045 6046 if len(f.loadResult.Manifests) == 0 { 6047 f.t.Fatalf("no more manifests; trying to find %q (did you call `f.load`?)", name) 6048 } 6049 6050 m := f.loadResult.Manifests[0] 6051 if m.Name != name { 6052 f.t.Fatalf("expected next manifest to be '%s' but found '%s'", name, m.Name) 6053 } 6054 6055 f.loadResult.Manifests = f.loadResult.Manifests[1:] 6056 6057 imageIndex := 0 6058 nextImageTarget := func() model.ImageTarget { 6059 ret := m.ImageTargetAt(imageIndex) 6060 imageIndex++ 6061 return ret 6062 } 6063 6064 for _, opt := range opts { 6065 switch opt := opt.(type) { 6066 case dbHelper: 6067 image := nextImageTarget() 6068 6069 refs, err := image.Refs(f.cluster(m)) 6070 require.NoError(f.t, err, "Determining image refs") 6071 ref := refs.ConfigurationRef 6072 if ref.Empty() { 6073 f.t.Fatalf("manifest %v has no more image refs; expected %q", m.Name, opt.image.ref) 6074 } 6075 6076 expectedConfigRef := container.MustParseNamed(opt.image.ref) 6077 if !assert.Equal(f.t, expectedConfigRef.String(), ref.String(), "manifest %v image ref", m.Name) { 6078 f.t.FailNow() 6079 } 6080 6081 expectedLocalRef := container.MustParseNamed(opt.image.localRef) 6082 require.Equal(f.t, expectedLocalRef.String(), refs.LocalRef().String(), "manifest %v localRef", m.Name) 6083 6084 if opt.image.clusterRef != "" { 6085 expectedClusterRef := container.MustParseNamed(opt.image.clusterRef) 6086 require.Equal(f.t, expectedClusterRef.String(), refs.ClusterRef().String(), "manifest %v clusterRef", m.Name) 6087 } 6088 6089 assert.Equal(f.t, opt.image.matchInEnvVars, image.MatchInEnvVars) 6090 6091 if !image.IsDockerBuild() { 6092 f.t.Fatalf("expected docker build but manifest %v has no docker build info", m.Name) 6093 } 6094 6095 for _, matcher := range opt.matchers { 6096 switch matcher := matcher.(type) { 6097 case entrypointHelper: 6098 if !sliceutils.StringSliceEquals(matcher.cmd.Argv, image.OverrideCommand.Command) { 6099 f.t.Fatalf("expected OverrideCommand (aka entrypoint) %v, got %v", 6100 matcher.cmd.Argv, image.OverrideCommand.Command) 6101 } 6102 case v1alpha1.LiveUpdateSpec: 6103 lu := image.LiveUpdateSpec 6104 assert.False(f.t, liveupdate.IsEmptySpec(lu)) 6105 assert.Equal(f.t, matcher, lu) 6106 default: 6107 f.t.Fatalf("unknown dbHelper matcher: %T %v", matcher, matcher) 6108 } 6109 } 6110 case cbHelper: 6111 image := nextImageTarget() 6112 6113 refs, err := image.Refs(f.cluster(m)) 6114 require.NoError(f.t, err, "Determining image refs") 6115 6116 ref := refs.ConfigurationRef 6117 expectedRef := container.MustParseNamed(opt.image.ref) 6118 if !assert.Equal(f.t, expectedRef.String(), ref.String(), "manifest %v image ref", m.Name) { 6119 f.t.FailNow() 6120 } 6121 6122 if !image.IsCustomBuild() { 6123 f.t.Fatalf("Expected custom build but manifest %v has no custom build info", m.Name) 6124 } 6125 cbInfo := image.CustomBuildInfo() 6126 6127 for _, matcher := range opt.matchers { 6128 switch matcher := matcher.(type) { 6129 case depsHelper: 6130 assert.Equal(f.t, matcher.deps, cbInfo.Deps) 6131 case cmdHelper: 6132 assert.Equal(f.t, matcher.cmd.Argv, cbInfo.Args) 6133 case tagHelper: 6134 assert.Equal(f.t, matcher.tag, cbInfo.OutputTag) 6135 case disablePushHelper: 6136 assert.Equal(f.t, matcher.disabled, cbInfo.OutputMode == v1alpha1.CmdImageOutputLocalDockerAndRemote) 6137 case entrypointHelper: 6138 if !sliceutils.StringSliceEquals(matcher.cmd.Argv, image.OverrideCommand.Command) { 6139 f.t.Fatalf("expected OverrideCommand (aka entrypoint) %v, got %v", 6140 matcher.cmd.Argv, image.OverrideCommand.Command) 6141 } 6142 case v1alpha1.LiveUpdateSpec: 6143 lu := image.LiveUpdateSpec 6144 assert.False(f.t, liveupdate.IsEmptySpec(lu)) 6145 assert.Equal(f.t, matcher, lu) 6146 } 6147 } 6148 6149 case deploymentHelper: 6150 yaml := m.K8sTarget().YAML 6151 found := false 6152 for _, e := range f.entities(yaml) { 6153 if e.GVK().Kind == "Deployment" && e.Name() == opt.name { 6154 found = true 6155 break 6156 } 6157 } 6158 if !found { 6159 f.t.Fatalf("deployment %v not found in yaml %q", opt.name, yaml) 6160 } 6161 case v1alpha1.KubernetesDiscoveryStrategy: 6162 assert.Equal(f.t, opt, m.K8sTarget().DiscoveryStrategy) 6163 case podReadinessHelper: 6164 assert.Equal(f.t, opt.podReadiness, m.K8sTarget().PodReadinessMode) 6165 case namespaceHelper: 6166 yaml := m.K8sTarget().YAML 6167 found := false 6168 for _, e := range f.entities(yaml) { 6169 if e.GVK().Kind == "Namespace" && e.Name() == opt.namespace { 6170 found = true 6171 break 6172 } 6173 } 6174 if !found { 6175 f.t.Fatalf("namespace %s not found in yaml %q", opt.namespace, yaml) 6176 } 6177 case serviceHelper: 6178 yaml := m.K8sTarget().YAML 6179 found := false 6180 for _, e := range f.entities(yaml) { 6181 if e.GVK().Kind == "Service" && e.Name() == opt.name { 6182 found = true 6183 break 6184 } 6185 } 6186 if !found { 6187 f.t.Fatalf("service %v not found in yaml %q", opt.name, yaml) 6188 } 6189 case k8sObjectHelper: 6190 yaml := m.K8sTarget().YAML 6191 found := false 6192 for _, e := range f.entities(yaml) { 6193 if e.GVK().Kind == opt.kind && e.Name() == opt.name { 6194 found = true 6195 break 6196 } 6197 } 6198 if !found { 6199 f.t.Fatalf("entity of kind %s with name %s not found in yaml %q", opt.kind, opt.name, yaml) 6200 } 6201 case extraPodSelectorsHelper: 6202 actual := m.K8sTarget().KubernetesApplySpec.KubernetesDiscoveryTemplateSpec.ExtraSelectors 6203 assert.ElementsMatch(f.t, k8s.SetsAsLabelSelectors(opt.labels), actual) 6204 case numEntitiesHelper: 6205 yaml := m.K8sTarget().YAML 6206 entities := f.entities(yaml) 6207 if opt.num != len(f.entities(yaml)) { 6208 f.t.Fatalf("manifest %v has %v entities in %v; expected %v", m.Name, len(entities), yaml, opt.num) 6209 } 6210 6211 case matchPathHelper: 6212 // Make sure the paths matches one of the syncs. 6213 isDep := false 6214 path := f.JoinPath(opt.path) 6215 for _, d := range m.LocalPaths() { 6216 if ospath.IsChild(d, path) { 6217 isDep = true 6218 } 6219 } 6220 6221 if !isDep { 6222 f.t.Errorf("Path %s is not a dependency of manifest %s", path, m.Name) 6223 } 6224 6225 expectedFilter := opt.missing 6226 6227 var filterName string 6228 var filter model.PathMatcher 6229 if opt.fileChange { 6230 filter = ignore.CreateFileChangeFilter(m.ImageTargetAt(0).GetFileWatchIgnores()) 6231 filterName = "FileChangeFilter" 6232 } else { 6233 db, ok := m.ImageTargetAt(0).BuildDetails.(model.DockerBuild) 6234 if !ok { 6235 f.t.Fatalf("BuildContextFilter only applies to docker_build") 6236 } 6237 filter = ignore.CreateBuildContextFilter(db.DockerImageSpec.ContextIgnores) 6238 filterName = "BuildContextFilter" 6239 } 6240 6241 actualFilter, err := filter.Matches(path) 6242 if err != nil { 6243 f.t.Fatalf("Error matching filter (%s): %v", path, err) 6244 } 6245 if actualFilter != expectedFilter { 6246 if expectedFilter { 6247 f.t.Errorf("%s should filter %s", filterName, path) 6248 } else { 6249 f.t.Errorf("%s should not filter %s", filterName, path) 6250 } 6251 } 6252 6253 case []model.PortForward: 6254 if len(opt) == 0 { 6255 assert.Nil(f.t, m.K8sTarget().KubernetesApplySpec.PortForwardTemplateSpec) 6256 } else { 6257 var expectedForwards []v1alpha1.Forward 6258 for _, pf := range opt { 6259 expectedForwards = append(expectedForwards, v1alpha1.Forward{ 6260 LocalPort: int32(pf.LocalPort), 6261 ContainerPort: int32(pf.ContainerPort), 6262 Host: pf.Host, 6263 Name: pf.Name, 6264 Path: pf.PathForAppend(), 6265 }) 6266 } 6267 assert.ElementsMatch(f.t, 6268 expectedForwards, 6269 m.K8sTarget().KubernetesApplySpec.PortForwardTemplateSpec.Forwards) 6270 } 6271 case dcResourceLinks: 6272 f.assertLinks(opt, m.DockerComposeTarget().Links) 6273 case localResourceLinks: 6274 f.assertLinks(opt, m.LocalTarget().Links) 6275 case k8sResourceLinks: 6276 f.assertLinks(opt, m.K8sTarget().Links) 6277 case model.TriggerMode: 6278 assert.Equal(f.t, opt, m.TriggerMode) 6279 case resourceDependenciesHelper: 6280 assert.Equal(f.t, opt.deps, m.ResourceDependencies) 6281 case funcOpt: 6282 assert.True(f.t, opt(f.t, m)) 6283 case localTargetHelper: 6284 lt := m.LocalTarget() 6285 for _, matcher := range opt.matchers { 6286 switch matcher := matcher.(type) { 6287 case updateCmdHelper: 6288 assert.Equal(f.t, matcher.cmd.Argv, lt.UpdateCmdSpec.Args) 6289 assert.Equal(f.t, matcher.cmd.Dir, lt.UpdateCmdSpec.Dir) 6290 assert.Equal(f.t, matcher.cmd.Env, lt.UpdateCmdSpec.Env) 6291 case serveCmdHelper: 6292 assert.Equal(f.t, matcher.cmd, lt.ServeCmd) 6293 case depsHelper: 6294 deps := f.JoinPaths(matcher.deps) 6295 assert.ElementsMatch(f.t, deps, lt.Dependencies()) 6296 case readinessProbeHelper: 6297 assert.EqualValues(f.t, matcher.probeSpec, lt.ReadinessProbe) 6298 default: 6299 f.t.Fatalf("unknown matcher for local target %T", matcher) 6300 } 6301 } 6302 case resourceLabelsHelper: 6303 assert.Equal(f.t, opt.labels, m.Labels) 6304 default: 6305 f.t.Fatalf("unexpected arg to assertNextManifest: %T %v", opt, opt) 6306 } 6307 } 6308 6309 f.assertManifestConsistency(m) 6310 6311 return m 6312 } 6313 6314 // All manifests currently contain redundant information 6315 // such that each Deploy target lists its image ID dependencies. 6316 func (f *fixture) assertManifestConsistency(m model.Manifest) { 6317 iTargetIDs := map[model.TargetID]bool{} 6318 for _, iTarget := range m.ImageTargets { 6319 if iTargetIDs[iTarget.ID()] { 6320 f.t.Fatalf("Image Target %s appears twice in manifest: %s", iTarget.ID(), m.Name) 6321 } 6322 iTargetIDs[iTarget.ID()] = true 6323 } 6324 6325 deployTarget := m.DeployTarget 6326 for _, depID := range deployTarget.DependencyIDs() { 6327 if !iTargetIDs[depID] { 6328 f.t.Fatalf("Image Target needed by deploy target is missing: %s", depID) 6329 } 6330 } 6331 } 6332 6333 func (f *fixture) imageTargetNames(m model.Manifest) []string { 6334 result := []string{} 6335 for _, iTarget := range m.ImageTargets { 6336 result = append(result, iTarget.ID().Name.String()) 6337 } 6338 return result 6339 } 6340 6341 func (f *fixture) idNames(ids []model.TargetID) []string { 6342 result := []string{} 6343 for _, id := range ids { 6344 result = append(result, id.Name.String()) 6345 } 6346 return result 6347 } 6348 6349 func (f *fixture) assertNumManifests(expected int) { 6350 assert.Equal(f.t, expected, len(f.loadResult.Manifests)) 6351 } 6352 6353 func (f *fixture) assertConfigFiles(filenames ...string) { 6354 f.t.Helper() 6355 var expected []string 6356 for _, filename := range filenames { 6357 expected = append(expected, f.JoinPath(filename)) 6358 } 6359 sort.Strings(expected) 6360 sort.Strings(f.loadResult.ConfigFiles) 6361 assert.Equal(f.t, expected, f.loadResult.ConfigFiles) 6362 } 6363 6364 func (f *fixture) assertWarnings(warnings ...string) { 6365 var expected []string 6366 for _, warning := range warnings { 6367 expected = append(expected, warning+"\n") 6368 } 6369 sort.Strings(expected) 6370 sort.Strings(f.warnings) 6371 assert.Equal(f.t, expected, f.warnings) 6372 } 6373 6374 func (f *fixture) entities(y string) []k8s.K8sEntity { 6375 es, err := k8s.ParseYAMLFromString(y) 6376 if err != nil { 6377 f.t.Fatal(err) 6378 } 6379 return es 6380 } 6381 6382 func (f *fixture) assertFeature(key string, enabled bool) { 6383 assert.Equal(f.t, enabled, f.loadResult.FeatureFlags[key]) 6384 } 6385 6386 func (f *fixture) assertLinks(expected, actual []model.Link) { 6387 require.Len(f.t, actual, len(expected), "comparing # of links") 6388 for i, exp := range expected { 6389 require.Equalf(f.t, exp.URLString(), actual[i].URLString(), "link at index %d", i) 6390 require.Equalf(f.t, exp.Name, actual[i].Name, "link at index %d", i) 6391 } 6392 } 6393 6394 func (f *fixture) cluster(m model.Manifest) *v1alpha1.Cluster { 6395 f.t.Helper() 6396 6397 tlr := f.loadResult 6398 6399 if m.IsK8s() { 6400 return &v1alpha1.Cluster{ 6401 ObjectMeta: metav1.ObjectMeta{ 6402 Name: v1alpha1.ClusterNameDefault, 6403 }, 6404 Spec: v1alpha1.ClusterSpec{ 6405 Connection: &v1alpha1.ClusterConnection{ 6406 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 6407 }, 6408 DefaultRegistry: tlr.DefaultRegistry, 6409 }, 6410 } 6411 } 6412 6413 if m.IsDC() { 6414 return &v1alpha1.Cluster{ 6415 ObjectMeta: metav1.ObjectMeta{ 6416 Name: v1alpha1.ClusterNameDocker, 6417 }, 6418 Spec: v1alpha1.ClusterSpec{ 6419 Connection: &v1alpha1.ClusterConnection{ 6420 Docker: &v1alpha1.DockerClusterConnection{}, 6421 }, 6422 DefaultRegistry: tlr.DefaultRegistry, 6423 }, 6424 } 6425 } 6426 6427 return &v1alpha1.Cluster{} 6428 } 6429 6430 type secretHelper struct { 6431 name string 6432 } 6433 6434 func secret(name string) secretHelper { 6435 return secretHelper{name: name} 6436 } 6437 6438 type namespaceHelper struct { 6439 namespace string 6440 } 6441 6442 func namespace(namespace string) namespaceHelper { 6443 return namespaceHelper{namespace} 6444 } 6445 6446 type deploymentHelper struct { 6447 name string 6448 image string 6449 templateLabels map[string]string 6450 envVars []envVar 6451 namespace string 6452 } 6453 6454 func deployment(name string, opts ...interface{}) deploymentHelper { 6455 r := deploymentHelper{name: name} 6456 for _, opt := range opts { 6457 switch opt := opt.(type) { 6458 case imageHelper: 6459 r.image = opt.ref 6460 case labelsHelper: 6461 r.templateLabels = opt.labels 6462 case envVarHelper: 6463 r.envVars = opt.envVars 6464 case namespaceHelper: 6465 r.namespace = opt.namespace 6466 default: 6467 panic(fmt.Errorf("unexpected arg to deployment: %T %v", opt, opt)) 6468 } 6469 } 6470 return r 6471 } 6472 6473 type podReadinessHelper struct { 6474 podReadiness model.PodReadinessMode 6475 } 6476 6477 func podReadiness(podReadiness model.PodReadinessMode) podReadinessHelper { 6478 return podReadinessHelper{podReadiness: podReadiness} 6479 } 6480 6481 type serviceHelper struct { 6482 name string 6483 selectorLabels map[string]string 6484 } 6485 6486 func service(name string, opts ...interface{}) serviceHelper { 6487 r := serviceHelper{name: name} 6488 for _, opt := range opts { 6489 switch opt := opt.(type) { 6490 case labelsHelper: 6491 r.selectorLabels = opt.labels 6492 default: 6493 panic(fmt.Errorf("unexpected arg to deployment: %T %v", opt, opt)) 6494 } 6495 } 6496 return r 6497 } 6498 6499 type k8sObjectHelper struct { 6500 name string 6501 kind string 6502 } 6503 6504 func k8sObject(name string, kind string) k8sObjectHelper { 6505 return k8sObjectHelper{name: name, kind: kind} 6506 } 6507 6508 type extraPodSelectorsHelper struct { 6509 labels []labels.Set 6510 } 6511 6512 func extraPodSelectors(labelSets ...labels.Set) extraPodSelectorsHelper { 6513 ret := extraPodSelectorsHelper{ 6514 labels: append([]labels.Set(nil), labelSets...), 6515 } 6516 return ret 6517 } 6518 6519 type numEntitiesHelper struct { 6520 num int 6521 } 6522 6523 func numEntities(num int) numEntitiesHelper { 6524 return numEntitiesHelper{num} 6525 } 6526 6527 type matchPathHelper struct { 6528 path string 6529 missing bool 6530 fileChange bool 6531 } 6532 6533 func buildMatches(path string) matchPathHelper { 6534 return matchPathHelper{ 6535 path: path, 6536 } 6537 } 6538 6539 func buildFilters(path string) matchPathHelper { 6540 return matchPathHelper{ 6541 path: path, 6542 missing: true, 6543 } 6544 } 6545 6546 func fileChangeMatches(path string) matchPathHelper { 6547 return matchPathHelper{ 6548 path: path, 6549 fileChange: true, 6550 } 6551 } 6552 6553 func fileChangeFilters(path string) matchPathHelper { 6554 return matchPathHelper{ 6555 path: path, 6556 missing: true, 6557 fileChange: true, 6558 } 6559 } 6560 6561 type resourceDependenciesHelper struct { 6562 deps []model.ManifestName 6563 } 6564 6565 func resourceDeps(deps ...string) resourceDependenciesHelper { 6566 var mns []model.ManifestName 6567 for _, d := range deps { 6568 mns = append(mns, model.ManifestName(d)) 6569 } 6570 return resourceDependenciesHelper{deps: mns} 6571 } 6572 6573 type resourceLabelsHelper struct { 6574 labels map[string]string 6575 } 6576 6577 func resourceLabels(labels ...string) resourceLabelsHelper { 6578 ret := resourceLabelsHelper{ 6579 labels: map[string]string{}, 6580 } 6581 for _, l := range labels { 6582 ret.labels[l] = l 6583 } 6584 return ret 6585 } 6586 6587 type imageHelper struct { 6588 ref string 6589 localRef string 6590 clusterRef string 6591 matchInEnvVars bool 6592 } 6593 6594 func image(ref string) imageHelper { 6595 return imageHelper{ref: ref, localRef: ref} 6596 } 6597 6598 func (ih imageHelper) withLocalRef(localRef string) imageHelper { 6599 ih.localRef = localRef 6600 return ih 6601 } 6602 6603 func (ih imageHelper) withClusterRef(clusterRef string) imageHelper { 6604 ih.clusterRef = clusterRef 6605 return ih 6606 } 6607 6608 func (ih imageHelper) withMatchInEnvVars() imageHelper { 6609 ih.matchInEnvVars = true 6610 return ih 6611 } 6612 6613 type labelsHelper struct { 6614 labels map[string]string 6615 } 6616 6617 func withLabels(labels map[string]string) labelsHelper { 6618 return labelsHelper{labels: labels} 6619 } 6620 6621 type envVar struct { 6622 name string 6623 value string 6624 } 6625 6626 type envVarHelper struct { 6627 envVars []envVar 6628 } 6629 6630 // usage: withEnvVars("key1", "value1", "key2", "value2") 6631 func withEnvVars(envVars ...string) envVarHelper { 6632 var ret envVarHelper 6633 6634 for i := 0; i < len(envVars); i += 2 { 6635 if i+1 >= len(envVars) { 6636 panic("withEnvVars called with odd number of strings") 6637 } 6638 ret.envVars = append(ret.envVars, envVar{envVars[i], envVars[i+1]}) 6639 } 6640 6641 return ret 6642 } 6643 6644 // docker build helper 6645 type dbHelper struct { 6646 image imageHelper 6647 matchers []interface{} 6648 } 6649 6650 func db(img imageHelper, opts ...interface{}) dbHelper { 6651 return dbHelper{image: img, matchers: opts} 6652 } 6653 6654 // custom build helper 6655 type cbHelper struct { 6656 image imageHelper 6657 matchers []interface{} 6658 } 6659 6660 func cb(img imageHelper, opts ...interface{}) cbHelper { 6661 return cbHelper{img, opts} 6662 } 6663 6664 type entrypointHelper struct { 6665 cmd model.Cmd 6666 } 6667 6668 func entrypoint(command model.Cmd) entrypointHelper { 6669 return entrypointHelper{command} 6670 } 6671 6672 type cmdHelper struct { 6673 cmd model.Cmd 6674 } 6675 6676 func cmd(cmd string, dir string) cmdHelper { 6677 return cmdHelper{cmd: model.ToHostCmdInDir(cmd, dir)} 6678 } 6679 6680 type tagHelper struct { 6681 tag string 6682 } 6683 6684 func tag(tag string) tagHelper { 6685 return tagHelper{tag} 6686 } 6687 6688 type depsHelper struct { 6689 deps []string 6690 } 6691 6692 func deps(deps ...string) depsHelper { 6693 return depsHelper{deps} 6694 } 6695 6696 type disablePushHelper struct { 6697 disabled bool 6698 } 6699 6700 func disablePush(disable bool) disablePushHelper { 6701 return disablePushHelper{disable} 6702 } 6703 6704 type updateCmdHelper struct { 6705 cmd model.Cmd 6706 } 6707 6708 func updateCmd(dir string, cmd string, env []string) updateCmdHelper { 6709 return updateCmdHelper{cmd: model.ToHostCmdInDirWithEnv(cmd, dir, env)} 6710 } 6711 6712 func updateCmdArray(dir string, argv []string, env []string) updateCmdHelper { 6713 return updateCmdHelper{cmd: model.Cmd{Argv: argv, Dir: dir, Env: env}} 6714 } 6715 6716 type serveCmdHelper struct { 6717 cmd model.Cmd 6718 } 6719 6720 func serveCmd(dir string, cmd string, env []string) serveCmdHelper { 6721 return serveCmdHelper{cmd: model.ToHostCmdInDirWithEnv(cmd, dir, env)} 6722 } 6723 6724 func serveCmdArray(dir string, argv []string, env []string) serveCmdHelper { 6725 return serveCmdHelper{model.Cmd{Argv: argv, Dir: dir, Env: env}} 6726 } 6727 6728 type readinessProbeHelper struct { 6729 probeSpec *v1alpha1.Probe 6730 } 6731 6732 type localTargetHelper struct { 6733 matchers []interface{} 6734 } 6735 6736 func localTarget(opts ...interface{}) localTargetHelper { 6737 return localTargetHelper{matchers: opts} 6738 } 6739 6740 // useful scenarios to setup 6741 6742 // foo just has one image and one yaml 6743 func (f *fixture) setupFoo() { 6744 f.dockerfile("foo/Dockerfile") 6745 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 6746 f.gitInit("") 6747 } 6748 6749 // bar just has one image and one yaml 6750 func (f *fixture) setupFooAndBar() { 6751 f.dockerfile("foo/Dockerfile") 6752 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 6753 6754 f.dockerfile("bar/Dockerfile") 6755 f.yaml("bar.yaml", deployment("bar", image("gcr.io/bar"))) 6756 6757 f.gitInit("") 6758 } 6759 6760 // expand has 4 images, a-d, and a yaml with all of it 6761 func (f *fixture) setupExpand() { 6762 f.dockerfile("a/Dockerfile") 6763 f.dockerfile("b/Dockerfile") 6764 f.dockerfile("c/Dockerfile") 6765 f.dockerfile("d/Dockerfile") 6766 6767 f.yaml("all.yaml", 6768 deployment("a", image("gcr.io/a")), 6769 deployment("b", image("gcr.io/b")), 6770 deployment("c", image("gcr.io/c")), 6771 deployment("d", image("gcr.io/d")), 6772 ) 6773 6774 f.gitInit("") 6775 } 6776 6777 func (f *fixture) setupHelm() { 6778 f.file("helm/Chart.yaml", chartYAML) 6779 f.file("helm/values.yaml", valuesYAML) 6780 f.file("dev/helm/values-dev.yaml", valuesDevYAML) // make sure we can pull in a values.yaml file from outside chart dir 6781 6782 f.file("helm/templates/_helpers.tpl", helpersTPL) 6783 f.file("helm/templates/deployment.yaml", deploymentYAML) 6784 f.file("helm/templates/ingress.yaml", ingressYAML) 6785 f.file("helm/templates/service.yaml", serviceYAML) 6786 f.file("helm/templates/namespace.yaml", namespaceYAML) 6787 } 6788 6789 func (f *fixture) setupHelmWithRequirements() { 6790 f.setupHelm() 6791 6792 nginxIngressChartPath := testdata.NginxIngressChartPath() 6793 f.CopyFile(nginxIngressChartPath, filepath.Join("helm/charts", filepath.Base(nginxIngressChartPath))) 6794 } 6795 6796 func (f *fixture) setupHelmWithTest() { 6797 f.setupHelm() 6798 f.file("helm/templates/tests/test-mariadb-connection.yaml", helmTestYAML) 6799 } 6800 6801 func (f *fixture) setupExtraPodSelectors(s string) { 6802 f.setupFoo() 6803 6804 tiltfile := fmt.Sprintf(` 6805 6806 docker_build('gcr.io/foo', 'foo') 6807 k8s_yaml('foo.yaml') 6808 k8s_resource('foo', extra_pod_selectors=%s) 6809 `, s) 6810 6811 f.file("Tiltfile", tiltfile) 6812 } 6813 6814 func (f *fixture) setupCRD() { 6815 f.file("crd.yaml", `apiVersion: fission.io/v1 6816 kind: Environment 6817 metadata: 6818 name: mycrd 6819 spec: 6820 builder: 6821 command: build 6822 image: test/mycrd-builder 6823 poolsize: 1 6824 runtime: 6825 image: test/mycrd-env`) 6826 } 6827 6828 func overwriteSelectorsForService(entity *k8s.K8sEntity, labels map[string]string) error { 6829 svc, ok := entity.Obj.(*v1.Service) 6830 if !ok { 6831 return fmt.Errorf("don't know how to set selectors for %T", entity.Obj) 6832 } 6833 svc.Spec.Selector = labels 6834 return nil 6835 } 6836 6837 func (f *fixture) SingleAnalyticsEvent(name string) analytics.CountEvent { 6838 var ret analytics.CountEvent 6839 for _, ce := range f.an.Counts { 6840 if ce.Name == name { 6841 require.Equalf(f.t, "", ret.Name, "two count events named %s", name) 6842 ret = ce 6843 } 6844 } 6845 require.NotEqualf(f.t, "", ret.Name, "no count event named %s", name) 6846 6847 return ret 6848 }