github.com/tilt-dev/tilt@v0.36.0/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 TestFilterYamlByLabelCustomResource(t *testing.T) { 1505 f := newFixture(t) 1506 f.file("Tiltfile", ` 1507 test=""" 1508 apiVersion: apiregistration.k8s.io/v1 1509 kind: APIService 1510 metadata: 1511 labels: 1512 app.kubernetes.io/instance: metrics-server 1513 app.kubernetes.io/managed-by: Helm 1514 app.kubernetes.io/name: metrics-server 1515 app.kubernetes.io/version: 0.7.1 1516 helm.sh/chart: metrics-server-3.12.1 1517 name: v1beta1.metrics.k8s.io 1518 spec: 1519 group: metrics.k8s.io 1520 groupPriorityMinimum: 100 1521 insecureSkipTLSVerify: true 1522 service: 1523 name: metrics-server 1524 namespace: kube-system 1525 port: 443 1526 version: v1beta1 1527 versionPriority: 100 1528 --- 1529 apiVersion: v1 1530 kind: Namespace 1531 metadata: 1532 labels: 1533 app.kubernetes.io/instance: longhorn 1534 app.kubernetes.io/managed-by: Helm 1535 app.kubernetes.io/name: longhorn 1536 name: longhorn-system 1537 """ 1538 a, b = filter_yaml( 1539 blob(test), 1540 labels={"app.kubernetes.io/name": "longhorn"} 1541 ) 1542 print('a: %d' % len(decode_yaml_stream(a))) 1543 print('b: %d' % len(decode_yaml_stream(b))) 1544 `) 1545 1546 f.load() 1547 1548 require.Contains(t, f.out.String(), "a: 1") 1549 require.Contains(t, f.out.String(), "b: 1") 1550 } 1551 1552 func TestFilterYamlByName(t *testing.T) { 1553 f := newFixture(t) 1554 f.file("k8s.yaml", yaml.ConcatYAML( 1555 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1556 testyaml.SnackYaml, testyaml.SanchoYAML)) 1557 f.file("Tiltfile", ` 1558 doggos, rest = filter_yaml('k8s.yaml', name='doggos') 1559 k8s_yaml(doggos) 1560 `) 1561 1562 f.load() 1563 f.assertNextManifest("doggos", deployment("doggos"), service("doggos")) 1564 f.assertNoMoreManifests() 1565 } 1566 1567 func TestFilterYamlByNameKind(t *testing.T) { 1568 f := newFixture(t) 1569 f.file("k8s.yaml", yaml.ConcatYAML( 1570 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1571 testyaml.SnackYaml, testyaml.SanchoYAML)) 1572 f.file("Tiltfile", ` 1573 doggos, rest = filter_yaml('k8s.yaml', name='doggos', kind='deployment') 1574 k8s_yaml(doggos) 1575 `) 1576 1577 f.load() 1578 f.assertNextManifest("doggos", deployment("doggos")) 1579 f.assertNoMoreManifests() 1580 } 1581 1582 func TestFilterYamlByNamespace(t *testing.T) { 1583 f := newFixture(t) 1584 f.file("k8s.yaml", yaml.ConcatYAML( 1585 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1586 testyaml.SnackYaml, testyaml.SanchoYAML)) 1587 f.file("Tiltfile", ` 1588 doggos, rest = filter_yaml('k8s.yaml', namespace='the-dog-zone') 1589 k8s_yaml(doggos) 1590 `) 1591 1592 f.load() 1593 f.assertNextManifest("doggos", deployment("doggos")) 1594 f.assertNoMoreManifests() 1595 } 1596 1597 func TestFilterYamlByApiVersion(t *testing.T) { 1598 f := newFixture(t) 1599 f.file("k8s.yaml", yaml.ConcatYAML( 1600 testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml, 1601 testyaml.SnackYaml, testyaml.SanchoYAML)) 1602 f.file("Tiltfile", ` 1603 doggos, rest = filter_yaml('k8s.yaml', name='doggos', api_version='apps/v1') 1604 k8s_yaml(doggos) 1605 `) 1606 1607 f.load() 1608 f.assertNextManifest("doggos", deployment("doggos")) 1609 f.assertNoMoreManifests() 1610 } 1611 1612 func TestFilterYamlNoMatch(t *testing.T) { 1613 f := newFixture(t) 1614 f.file("k8s.yaml", yaml.ConcatYAML(testyaml.DoggosDeploymentYaml, testyaml.DoggosServiceYaml)) 1615 f.file("Tiltfile", ` 1616 doggos, rest = filter_yaml('k8s.yaml', namespace='dne', kind='deployment') 1617 k8s_yaml(doggos) 1618 `) 1619 f.loadErrString(emptyYAMLError.Error()) 1620 } 1621 1622 func TestYamlNone(t *testing.T) { 1623 f := newFixture(t) 1624 1625 f.setupFoo() 1626 1627 f.file("Tiltfile", ` 1628 k8s_yaml(None) 1629 `) 1630 f.loadErrString(emptyYAMLError.Error()) 1631 } 1632 1633 func TestYamlEmptyBlob(t *testing.T) { 1634 f := newFixture(t) 1635 1636 f.setupFoo() 1637 1638 f.file("Tiltfile", ` 1639 k8s_yaml(blob('')) 1640 `) 1641 f.loadErrString(emptyYAMLError.Error()) 1642 } 1643 1644 func TestDuplicateLocalResources(t *testing.T) { 1645 f := newFixture(t) 1646 1647 f.setupFoo() 1648 1649 f.file("Tiltfile", ` 1650 local_resource('foo', 'echo foo') 1651 local_resource('foo', 'echo foo') 1652 `) 1653 1654 f.loadErrString(`local_resource named "foo" already exists`) 1655 } 1656 1657 // These tests are for behavior that we specifically enabled in Starlark 1658 // in the init() function 1659 func TestTopLevelIfStatement(t *testing.T) { 1660 f := newFixture(t) 1661 1662 f.setupFoo() 1663 1664 f.file("Tiltfile", ` 1665 if True: 1666 docker_build('gcr.io/foo', 'foo') 1667 k8s_yaml('foo.yaml') 1668 `) 1669 1670 f.load() 1671 1672 f.assertNextManifest("foo", 1673 db(image("gcr.io/foo")), 1674 deployment("foo")) 1675 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 1676 } 1677 1678 func TestTopLevelForLoop(t *testing.T) { 1679 f := newFixture(t) 1680 1681 f.setupFoo() 1682 1683 f.file("Tiltfile", ` 1684 for i in range(1, 3): 1685 print(i) 1686 `) 1687 1688 f.load() 1689 } 1690 1691 func TestTopLevelVariableRename(t *testing.T) { 1692 f := newFixture(t) 1693 1694 f.setupFoo() 1695 1696 f.file("Tiltfile", ` 1697 x = 1 1698 x = 2 1699 `) 1700 1701 f.load() 1702 } 1703 1704 func TestEmptyDockerfileDockerBuild(t *testing.T) { 1705 f := newFixture(t) 1706 f.setupFoo() 1707 f.file("foo/Dockerfile", "") 1708 f.file("Tiltfile", ` 1709 docker_build('gcr.io/foo', 'foo') 1710 k8s_yaml('foo.yaml') 1711 `) 1712 f.load() 1713 m := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 1714 assert.True(t, m.ImageTargetAt(0).IsDockerBuild()) 1715 } 1716 1717 func TestSanchoSidecar(t *testing.T) { 1718 f := newFixture(t) 1719 f.setupFoo() 1720 f.file("Dockerfile", "FROM golang:1.10") 1721 f.file("k8s.yaml", testyaml.SanchoSidecarYAML) 1722 f.file("Tiltfile", ` 1723 k8s_yaml('k8s.yaml') 1724 docker_build('gcr.io/some-project-162817/sancho', '.') 1725 docker_build('gcr.io/some-project-162817/sancho-sidecar', '.') 1726 `) 1727 f.load() 1728 1729 assert.Equal(t, 1, len(f.loadResult.Manifests)) 1730 m := f.assertNextManifest("sancho") 1731 assert.Equal(t, 2, len(m.ImageTargets)) 1732 assert.Equal(t, "gcr.io/some-project-162817/sancho", 1733 m.ImageTargetAt(0).ImageMapSpec.Selector) 1734 assert.Equal(t, "gcr.io/some-project-162817/sancho-sidecar", 1735 m.ImageTargetAt(1).ImageMapSpec.Selector) 1736 } 1737 1738 func TestSanchoRedisSidecar(t *testing.T) { 1739 f := newFixture(t) 1740 f.setupFoo() 1741 f.file("Dockerfile", "FROM golang:1.10") 1742 f.file("k8s.yaml", testyaml.SanchoRedisSidecarYAML) 1743 f.file("Tiltfile", ` 1744 k8s_yaml('k8s.yaml') 1745 docker_build('gcr.io/some-project-162817/sancho', '.') 1746 `) 1747 f.load() 1748 1749 assert.Equal(t, 1, len(f.loadResult.Manifests)) 1750 m := f.assertNextManifest("sancho") 1751 assert.Equal(t, 1, len(m.ImageTargets)) 1752 assert.Equal(t, "gcr.io/some-project-162817/sancho", 1753 m.ImageTargetAt(0).ImageMapSpec.Selector) 1754 } 1755 1756 func TestExtraPodSelectors(t *testing.T) { 1757 f := newFixture(t) 1758 1759 f.setupExtraPodSelectors("[{'foo': 'bar', 'baz': 'qux'}, {'quux': 'corge'}]") 1760 f.load() 1761 1762 f.assertNextManifest("foo", 1763 extraPodSelectors(labels.Set{"foo": "bar", "baz": "qux"}, labels.Set{"quux": "corge"}), 1764 podReadiness(model.PodReadinessWait)) 1765 } 1766 1767 func TestExtraPodSelectorsNotList(t *testing.T) { 1768 f := newFixture(t) 1769 1770 f.setupExtraPodSelectors("'hello'") 1771 f.loadErrString("got starlark.String", "dict or a list") 1772 } 1773 1774 func TestExtraPodSelectorsDict(t *testing.T) { 1775 f := newFixture(t) 1776 1777 f.setupExtraPodSelectors("{'foo': 'bar'}") 1778 f.load() 1779 f.assertNextManifest("foo", 1780 extraPodSelectors(labels.Set{"foo": "bar"}), 1781 podReadiness(model.PodReadinessWait)) 1782 } 1783 1784 func TestExtraPodSelectorsElementNotDict(t *testing.T) { 1785 f := newFixture(t) 1786 1787 f.setupExtraPodSelectors("['hello']") 1788 f.loadErrString("must be dicts", "starlark.String") 1789 } 1790 1791 func TestExtraPodSelectorsKeyNotString(t *testing.T) { 1792 f := newFixture(t) 1793 1794 f.setupExtraPodSelectors("[{54321: 'hello'}]") 1795 f.loadErrString("keys must be strings", "54321") 1796 } 1797 1798 func TestExtraPodSelectorsValueNotString(t *testing.T) { 1799 f := newFixture(t) 1800 1801 f.setupExtraPodSelectors("[{'hello': 54321}]") 1802 f.loadErrString("values must be strings", "54321") 1803 } 1804 1805 func TestPodReadinessDefaultDeployment(t *testing.T) { 1806 f := newFixture(t) 1807 1808 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1809 f.file("Tiltfile", ` 1810 k8s_yaml('foo.yaml') 1811 `) 1812 1813 f.load("foo") 1814 f.assertNextManifest("foo", 1815 deployment("foo"), 1816 podReadiness(model.PodReadinessWait), 1817 ) 1818 } 1819 1820 func TestPodReadinessDefaultConfigMap(t *testing.T) { 1821 f := newFixture(t) 1822 1823 f.file("config.yaml", `apiVersion: v1 1824 kind: ConfigMap 1825 metadata: 1826 name: config 1827 data: 1828 foo: bar 1829 `) 1830 f.file("Tiltfile", ` 1831 k8s_yaml('config.yaml') 1832 k8s_resource(new_name='config', objects=['config']) 1833 `) 1834 1835 f.load("config") 1836 f.assertNextManifest("config", 1837 podReadiness(model.PodReadinessIgnore), 1838 ) 1839 } 1840 1841 func TestPodReadinessDefaultJob(t *testing.T) { 1842 f := newFixture(t) 1843 1844 f.file("job.yaml", `apiVersion: batch/v1 1845 kind: Job 1846 metadata: 1847 name: myjob 1848 `) 1849 f.file("Tiltfile", ` 1850 k8s_yaml('job.yaml') 1851 `) 1852 1853 f.load("myjob") 1854 f.assertNextManifest("myjob", 1855 podReadiness(model.PodReadinessSucceeded), 1856 ) 1857 } 1858 1859 func TestK8sDiscoveryStrategy(t *testing.T) { 1860 f := newFixture(t) 1861 1862 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1863 f.file("Tiltfile", ` 1864 k8s_yaml('foo.yaml') 1865 k8s_resource('foo', discovery_strategy='selectors-only') 1866 `) 1867 1868 f.load("foo") 1869 f.assertNextManifest("foo", 1870 deployment("foo"), 1871 v1alpha1.KubernetesDiscoveryStrategySelectorsOnly, 1872 ) 1873 } 1874 1875 func TestK8sDiscoveryStrategyInvalid(t *testing.T) { 1876 f := newFixture(t) 1877 1878 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1879 f.file("Tiltfile", ` 1880 k8s_yaml('foo.yaml') 1881 k8s_resource('foo', discovery_strategy='typo') 1882 `) 1883 1884 f.loadErrString("Invalid. Must be one of: \"default\", \"selectors-only\"") 1885 } 1886 1887 func TestPodReadinessOverrideDeployment(t *testing.T) { 1888 f := newFixture(t) 1889 1890 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1891 f.file("Tiltfile", ` 1892 k8s_yaml('foo.yaml') 1893 k8s_resource('foo', pod_readiness='ignore') 1894 `) 1895 1896 f.load("foo") 1897 f.assertNextManifest("foo", 1898 deployment("foo"), 1899 podReadiness(model.PodReadinessIgnore), 1900 ) 1901 } 1902 1903 func TestPodReadinessOverrideConfigMap(t *testing.T) { 1904 f := newFixture(t) 1905 1906 f.file("config.yaml", `apiVersion: v1 1907 kind: ConfigMap 1908 metadata: 1909 name: config 1910 data: 1911 foo: "bar" 1912 `) 1913 f.file("Tiltfile", ` 1914 k8s_yaml('config.yaml') 1915 k8s_resource(new_name='config', objects=['config'], pod_readiness='wait') 1916 `) 1917 1918 f.load("config") 1919 f.assertNextManifest("config", 1920 podReadiness(model.PodReadinessWait), 1921 ) 1922 } 1923 1924 func TestPodReadinessInvalid(t *testing.T) { 1925 f := newFixture(t) 1926 1927 f.file("config.yaml", `apiVersion: v1 1928 kind: ConfigMap 1929 metadata: 1930 name: config 1931 data: 1932 foo: bar 1933 `) 1934 f.file("Tiltfile", ` 1935 k8s_yaml('config.yaml') 1936 k8s_resource(new_name='config', objects=['config'], pod_readiness='w') 1937 `) 1938 1939 f.loadErrString("Invalid value. Allowed: {ignore, wait}. Got: w") 1940 } 1941 1942 func TestDockerBuildMatchingTag(t *testing.T) { 1943 f := newFixture(t) 1944 1945 f.gitInit("") 1946 f.file("Dockerfile", "FROM golang:1.10") 1947 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:stable"))) 1948 f.file("Tiltfile", ` 1949 docker_build('gcr.io/foo:stable', '.') 1950 k8s_yaml('foo.yaml') 1951 `) 1952 1953 f.load("foo") 1954 f.assertNextManifest("foo", 1955 deployment("foo"), 1956 ) 1957 } 1958 1959 func TestDockerBuildButK8sMissing(t *testing.T) { 1960 f := newFixture(t) 1961 1962 f.gitInit("") 1963 f.file("Dockerfile", "FROM golang:1.10") 1964 f.file("Tiltfile", ` 1965 docker_build('gcr.io/foo:stable', '.') 1966 `) 1967 1968 f.loadAssertWarnings(unmatchedImageNoConfigsWarning) 1969 } 1970 1971 func TestDockerBuildButK8sMissingTag(t *testing.T) { 1972 f := newFixture(t) 1973 1974 f.gitInit("") 1975 f.file("Dockerfile", "FROM golang:1.10") 1976 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 1977 f.file("Tiltfile", ` 1978 docker_build('gcr.io/foo:stable', '.') 1979 k8s_yaml('foo.yaml') 1980 `) 1981 1982 w := unusedImageWarning("gcr.io/foo:stable", []string{"gcr.io/foo"}, "Kubernetes") 1983 f.loadAssertWarnings(w) 1984 } 1985 1986 func TestDockerBuildUnusedSuppressWarning(t *testing.T) { 1987 f := newFixture(t) 1988 1989 f.gitInit("") 1990 f.file("Dockerfile", "FROM golang:1.10") 1991 f.file("Tiltfile", ` 1992 docker_build('a', '.') 1993 docker_build('b', '.') 1994 update_settings(suppress_unused_image_warnings=['a']) 1995 update_settings(suppress_unused_image_warnings=['b']) 1996 `) 1997 1998 f.load() 1999 } 2000 2001 func TestDockerBuildButK8sNonMatchingTag(t *testing.T) { 2002 f := newFixture(t) 2003 2004 f.gitInit("") 2005 f.file("Dockerfile", "FROM golang:1.10") 2006 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo:beta"))) 2007 f.file("Tiltfile", ` 2008 docker_build('gcr.io/foo:stable', '.') 2009 k8s_yaml('foo.yaml') 2010 `) 2011 2012 w := unusedImageWarning("gcr.io/foo:stable", []string{"gcr.io/foo"}, "Kubernetes") 2013 f.loadAssertWarnings(w) 2014 } 2015 2016 func TestFail(t *testing.T) { 2017 f := newFixture(t) 2018 2019 f.file("Tiltfile", ` 2020 fail("this is an error") 2021 print("not this") 2022 fail("or this") 2023 `) 2024 2025 f.loadErrString("this is an error") 2026 } 2027 2028 func TestBlob(t *testing.T) { 2029 f := newFixture(t) 2030 2031 f.file( 2032 "Tiltfile", 2033 fmt.Sprintf(`k8s_yaml(blob('''%s'''))`, testyaml.SnackYaml), 2034 ) 2035 2036 f.load() 2037 2038 f.assertNextManifest("snack", deployment("snack")) 2039 } 2040 2041 func TestBlobErr(t *testing.T) { 2042 f := newFixture(t) 2043 2044 f.file( 2045 "Tiltfile", 2046 `blob(42)`, 2047 ) 2048 2049 f.loadErrString("for parameter input: got int, want string") 2050 } 2051 2052 func TestImageDependency(t *testing.T) { 2053 f := newFixture(t) 2054 2055 f.gitInit("") 2056 f.file("imageA.dockerfile", "FROM golang:1.10") 2057 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2058 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2059 f.file("Tiltfile", ` 2060 2061 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2062 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2063 k8s_yaml('foo.yaml') 2064 `) 2065 2066 f.load() 2067 f.assertNextManifest("foo", deployment("foo", image("gcr.io/image-a"), image("gcr.io/image-b"))) 2068 } 2069 2070 func TestImageDependencyLiveUpdate(t *testing.T) { 2071 f := newFixture(t) 2072 2073 f.gitInit("") 2074 f.file("message.txt", "Hello!") 2075 f.file("imageA.dockerfile", "FROM golang:1.10") 2076 f.file("imageB.dockerfile", `FROM gcr.io/image-a 2077 ADD message.txt /tmp/message.txt`) 2078 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2079 f.file("Tiltfile", ` 2080 2081 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile', 2082 live_update=[sync('message.txt', '/tmp/message.txt')]) 2083 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2084 k8s_yaml('foo.yaml') 2085 `) 2086 2087 f.load() 2088 m := f.assertNextManifest("foo", 2089 deployment("foo", image("gcr.io/image-a"), image("gcr.io/image-b"))) 2090 2091 assert.True(t, liveupdate.IsEmptySpec(m.ImageTargetAt(0).LiveUpdateSpec)) 2092 assert.False(t, liveupdate.IsEmptySpec(m.ImageTargetAt(1).LiveUpdateSpec)) 2093 } 2094 2095 func TestImageDependencyCycle(t *testing.T) { 2096 f := newFixture(t) 2097 2098 f.gitInit("") 2099 f.file("imageA.dockerfile", "FROM gcr.io/image-b") 2100 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2101 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b"))) 2102 f.file("Tiltfile", ` 2103 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2104 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2105 k8s_yaml('foo.yaml') 2106 `) 2107 2108 f.loadErrString("Image dependency cycle: gcr.io/image-b") 2109 } 2110 2111 func TestImageDependencyDiamond(t *testing.T) { 2112 f := newFixture(t) 2113 2114 f.gitInit("") 2115 f.file("imageA.dockerfile", "FROM golang:1.10") 2116 f.file("imageB.dockerfile", "FROM gcr.io/image-a") 2117 f.file("imageC.dockerfile", "FROM gcr.io/image-a") 2118 f.file("imageD.dockerfile", ` 2119 FROM gcr.io/image-b 2120 FROM gcr.io/image-c 2121 `) 2122 f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-d"))) 2123 f.file("Tiltfile", ` 2124 2125 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2126 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2127 docker_build('gcr.io/image-c', '.', dockerfile='imageC.dockerfile') 2128 docker_build('gcr.io/image-d', '.', dockerfile='imageD.dockerfile') 2129 k8s_yaml('foo.yaml') 2130 `) 2131 2132 f.load() 2133 2134 m := f.assertNextManifest("foo", deployment("foo")) 2135 assert.Equal(t, []string{ 2136 "gcr.io_image-a", 2137 "gcr.io_image-b", 2138 "gcr.io_image-c", 2139 "gcr.io_image-d", 2140 }, f.imageTargetNames(m)) 2141 } 2142 2143 func TestImageDependencyTwice(t *testing.T) { 2144 f := newFixture(t) 2145 2146 f.gitInit("") 2147 f.file("imageA.dockerfile", "FROM golang:1.10") 2148 f.file("imageB.dockerfile", `FROM golang:1.10 2149 COPY --from=gcr.io/image-a /src/package.json /src/package.json 2150 COPY --from=gcr.io/image-a /src/package.lock /src/package.lock 2151 `) 2152 f.file("snack.yaml", ` 2153 apiVersion: apps/v1 2154 kind: Deployment 2155 metadata: 2156 name: snack 2157 labels: 2158 app: snack 2159 spec: 2160 selector: 2161 matchLabels: 2162 app: snack 2163 template: 2164 metadata: 2165 labels: 2166 app: snack 2167 spec: 2168 containers: 2169 - name: snack1 2170 image: gcr.io/image-b 2171 command: ["/go/bin/snack"] 2172 - name: snack2 2173 image: gcr.io/image-b 2174 command: ["/go/bin/snack"] 2175 `) 2176 f.file("Tiltfile", ` 2177 2178 docker_build('gcr.io/image-a', '.', dockerfile='imageA.dockerfile') 2179 docker_build('gcr.io/image-b', '.', dockerfile='imageB.dockerfile') 2180 k8s_yaml('snack.yaml') 2181 `) 2182 2183 f.load() 2184 2185 m := f.assertNextManifest("snack") 2186 assert.Equal(t, []string{ 2187 "gcr.io_image-a", 2188 "gcr.io_image-b", 2189 }, f.imageTargetNames(m)) 2190 assert.Equal(t, []string{ 2191 "gcr.io_image-a", 2192 "gcr.io_image-b", 2193 "snack", // the deploy name 2194 }, f.idNames(m.DependencyIDs())) 2195 assert.Equal(t, []string{}, f.idNames(m.ImageTargets[0].DependencyIDs())) 2196 assert.Equal(t, []string{"gcr.io_image-a"}, f.idNames(m.ImageTargets[1].DependencyIDs())) 2197 assert.Equal(t, []string{"gcr.io_image-b"}, f.idNames(m.DeployTarget.DependencyIDs())) 2198 } 2199 2200 func TestImageDependencyNormalization(t *testing.T) { 2201 f := newFixture(t) 2202 2203 f.gitInit("") 2204 f.file("common.dockerfile", "FROM golang:1.10") 2205 f.file("auth.dockerfile", "FROM vandelay/common") 2206 f.yaml("auth.yaml", deployment("auth", image("vandelay/auth"))) 2207 f.file("Tiltfile", ` 2208 docker_build('vandelay/common', '.', dockerfile='common.dockerfile') 2209 docker_build('vandelay/auth', '.', dockerfile='auth.dockerfile') 2210 k8s_yaml('auth.yaml') 2211 `) 2212 2213 f.load() 2214 2215 m := f.assertNextManifest("auth", deployment("auth")) 2216 assert.Equal(t, []string{ 2217 "vandelay_common", 2218 "vandelay_auth", 2219 }, f.imageTargetNames(m)) 2220 } 2221 2222 func TestImagesWithSameNameAssembly(t *testing.T) { 2223 f := newFixture(t) 2224 2225 f.gitInit("") 2226 f.file("app.dockerfile", "FROM golang:1.10") 2227 f.file("app-jessie.dockerfile", "FROM golang:1.10-jessie") 2228 f.yaml("app.yaml", 2229 deployment("app", image("vandelay/app")), 2230 deployment("app-jessie", image("vandelay/app:jessie"))) 2231 f.file("Tiltfile", ` 2232 2233 docker_build('vandelay/app', '.', dockerfile='app.dockerfile') 2234 docker_build('vandelay/app:jessie', '.', dockerfile='app-jessie.dockerfile') 2235 k8s_yaml('app.yaml') 2236 `) 2237 2238 f.load() 2239 2240 f.assertNextManifest("app", deployment("app", image("vandelay/app"))) 2241 f.assertNextManifest("app-jessie", deployment("app-jessie", image("vandelay/app:jessie"))) 2242 } 2243 2244 func TestImagesWithSameNameDifferentManifests(t *testing.T) { 2245 f := newFixture(t) 2246 2247 f.gitInit("") 2248 f.file("app.dockerfile", "FROM golang:1.10") 2249 f.file("app-jessie.dockerfile", "FROM golang:1.10-jessie") 2250 f.yaml("app.yaml", 2251 deployment("app", image("vandelay/app")), 2252 deployment("app-jessie", image("vandelay/app:jessie"))) 2253 f.file("Tiltfile", ` 2254 docker_build('vandelay/app', '.', dockerfile='app.dockerfile') 2255 docker_build('vandelay/app:jessie', '.', dockerfile='app-jessie.dockerfile') 2256 k8s_yaml('app.yaml') 2257 `) 2258 2259 f.load() 2260 2261 m := f.assertNextManifest("app", deployment("app")) 2262 assert.Equal(t, []string{ 2263 "vandelay_app", 2264 }, f.imageTargetNames(m)) 2265 2266 m = f.assertNextManifest("app-jessie", deployment("app-jessie")) 2267 assert.Equal(t, []string{ 2268 "vandelay_app:jessie", 2269 }, f.imageTargetNames(m)) 2270 } 2271 2272 func TestImageRefSuggestion(t *testing.T) { 2273 f := newFixture(t) 2274 2275 f.setupFoo() 2276 f.file("Tiltfile", ` 2277 docker_build('gcr.typo.io/foo', 'foo') 2278 k8s_yaml('foo.yaml') 2279 `) 2280 2281 w := unusedImageWarning("gcr.typo.io/foo", []string{"gcr.io/foo"}, "Kubernetes") 2282 f.loadAssertWarnings(w) 2283 } 2284 2285 func TestDir(t *testing.T) { 2286 f := newFixture(t) 2287 2288 f.gitInit("") 2289 f.yaml("config/foo.yaml", deployment("foo", image("gcr.io/foo"))) 2290 f.yaml("config/bar.yaml", deployment("bar", image("gcr.io/bar"))) 2291 f.file("Tiltfile", `k8s_yaml(listdir('config'))`) 2292 2293 f.load("foo", "bar") 2294 f.assertNumManifests(2) 2295 f.assertConfigFiles("Tiltfile", ".tiltignore", "config/foo.yaml", "config/bar.yaml") 2296 } 2297 2298 func TestDirRecursive(t *testing.T) { 2299 f := newFixture(t) 2300 2301 f.gitInit("") 2302 f.file("foo/bar", "bar") 2303 f.file("foo/baz/qux", "qux") 2304 f.file("Tiltfile", `files = listdir('foo', recursive=True) 2305 2306 for f in files: 2307 read_file(f) 2308 `) 2309 2310 f.load() 2311 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo", "foo/bar", "foo/baz/qux") 2312 } 2313 2314 func TestCallCounts(t *testing.T) { 2315 f := newFixture(t) 2316 2317 f.gitInit("") 2318 f.file("Dockerfile", "FROM golang:1.10") 2319 f.yaml("foo.yaml", 2320 deployment("foo", image("gcr.io/foo")), 2321 deployment("bar", image("gcr.io/bar"))) 2322 f.file("Tiltfile", ` 2323 docker_build('gcr.io/foo', '.') 2324 docker_build('gcr.io/bar', '.') 2325 k8s_yaml('foo.yaml') 2326 `) 2327 2328 f.load() 2329 2330 require.Len(t, f.an.Counts, 1) 2331 expectedCallCounts := map[string]int{ 2332 "docker_build": 2, 2333 "k8s_yaml": 1, 2334 } 2335 tags := f.an.Counts[0].Tags 2336 for arg, expectedCount := range expectedCallCounts { 2337 count, ok := tags[fmt.Sprintf("tiltfile.invoked.%s", arg)] 2338 require.True(t, ok, "arg %s was not counted in %v", arg, tags) 2339 require.Equal(t, strconv.Itoa(expectedCount), count, "arg %s had the wrong count in %v", arg, tags) 2340 } 2341 } 2342 2343 func TestArgCounts(t *testing.T) { 2344 f := newFixture(t) 2345 2346 f.gitInit("") 2347 f.file("Dockerfile", "FROM golang:1.10") 2348 f.yaml("foo.yaml", 2349 deployment("foo", image("gcr.io/foo")), 2350 deployment("bar", image("gcr.io/bar"))) 2351 f.file("Tiltfile", ` 2352 docker_build(ref='gcr.io/foo', context='.', dockerfile='Dockerfile') 2353 docker_build('gcr.io/bar', '.') 2354 k8s_yaml('foo.yaml') 2355 `) 2356 2357 f.load() 2358 2359 require.Len(t, f.an.Counts, 1) 2360 expectedArgCounts := map[string]int{ 2361 "docker_build.arg.context": 2, 2362 "docker_build.arg.dockerfile": 1, 2363 "docker_build.arg.ref": 2, 2364 "k8s_yaml.arg.yaml": 1, 2365 } 2366 tags := f.an.Counts[0].Tags 2367 for arg, expectedCount := range expectedArgCounts { 2368 count, ok := tags[fmt.Sprintf("tiltfile.invoked.%s", arg)] 2369 require.True(t, ok, "tiltfile.invoked.%s was not counted in %v", arg, tags) 2370 require.Equal(t, strconv.Itoa(expectedCount), count, "tiltfile.invoked.%s had the wrong count in %v", arg, tags) 2371 } 2372 } 2373 2374 func TestK8sManifestRefInjectCounts(t *testing.T) { 2375 f := newFixture(t) 2376 2377 f.gitInit("") 2378 f.file("Dockerfile", "FROM golang:1.10") 2379 f.file("sancho_twin.yaml", testyaml.SanchoTwoContainersOneImageYAML) // 1 img x 2 c 2380 f.file("sancho_sidecar.yaml", testyaml.SanchoSidecarYAML) // 2 imgs (1 c each) 2381 f.file("blorg.yaml", testyaml.BlorgJobYAML) 2382 2383 f.file("Tiltfile", ` 2384 docker_build('gcr.io/some-project-162817/sancho', '.') 2385 docker_build('gcr.io/some-project-162817/sancho-sidecar', '.') 2386 docker_build('gcr.io/blorg-dev/blorg-backend:devel-nick', '.') 2387 2388 k8s_yaml(['sancho_twin.yaml', 'sancho_sidecar.yaml', 'blorg.yaml']) 2389 `) 2390 2391 f.load() 2392 2393 sanchoTwin := f.assertNextManifest("sancho-2c1i") 2394 sTwinInjectCounts := sanchoTwin.K8sTarget().RefInjectCounts() 2395 assert.Len(t, sTwinInjectCounts, 1) 2396 assert.Equal(t, sTwinInjectCounts[testyaml.SanchoImage], 2) 2397 2398 sanchoSidecar := f.assertNextManifest("sancho") 2399 ssInjectCounts := sanchoSidecar.K8sTarget().RefInjectCounts() 2400 assert.Len(t, ssInjectCounts, 2) 2401 assert.Equal(t, ssInjectCounts[testyaml.SanchoImage], 1) 2402 assert.Equal(t, ssInjectCounts[testyaml.SanchoSidecarImage], 1) 2403 2404 blorgJob := f.assertNextManifest("blorg-job") 2405 blorgInjectCounts := blorgJob.K8sTarget().RefInjectCounts() 2406 assert.Len(t, blorgInjectCounts, 1) 2407 assert.Equal(t, blorgJob.K8sTarget().RefInjectCounts()["gcr.io/blorg-dev/blorg-backend:devel-nick"], 1) 2408 } 2409 2410 func TestYamlErrorFromLocal(t *testing.T) { 2411 f := newFixture(t) 2412 f.file("Tiltfile", ` 2413 yaml = local('echo hi') 2414 k8s_yaml(yaml) 2415 `) 2416 f.loadErrString("echo hi") 2417 } 2418 2419 func TestYamlErrorFromReadFile(t *testing.T) { 2420 f := newFixture(t) 2421 f.file("foo.yaml", "hi") 2422 f.file("Tiltfile", ` 2423 k8s_yaml(read_file('foo.yaml')) 2424 `) 2425 f.loadErrString(fmt.Sprintf("file: %s", f.JoinPath("foo.yaml"))) 2426 } 2427 2428 func TestYamlErrorFromBlob(t *testing.T) { 2429 f := newFixture(t) 2430 f.file("Tiltfile", ` 2431 k8s_yaml(blob('hi')) 2432 `) 2433 f.loadErrString("from Tiltfile blob() call") 2434 } 2435 2436 func TestCustomBuildWithTag(t *testing.T) { 2437 f := newFixture(t) 2438 2439 tiltfile := `k8s_yaml('foo.yaml') 2440 custom_build( 2441 'gcr.io/foo', 2442 'docker build -t gcr.io/foo:my-great-tag foo', 2443 ['foo'], 2444 tag='my-great-tag' 2445 )` 2446 2447 f.setupFoo() 2448 f.file("Tiltfile", tiltfile) 2449 2450 f.load("foo") 2451 f.assertNumManifests(1) 2452 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/.dockerignore") 2453 m := f.assertNextManifest("foo", 2454 cb( 2455 image("gcr.io/foo"), 2456 deps(f.JoinPath("foo")), 2457 cmd("docker build -t gcr.io/foo:my-great-tag foo", f.Path()), 2458 tag("my-great-tag"), 2459 ), 2460 deployment("foo")) 2461 assert.False(t, m.ImageTargets[0].CustomBuildInfo().SkipsPush()) 2462 } 2463 2464 func TestCustomBuildDisablePush(t *testing.T) { 2465 f := newFixture(t) 2466 2467 tiltfile := `k8s_yaml('foo.yaml') 2468 hfb = custom_build( 2469 'gcr.io/foo', 2470 'docker build -t $TAG foo', 2471 ['foo'], 2472 disable_push=True, 2473 )` 2474 2475 f.setupFoo() 2476 f.file("Tiltfile", tiltfile) 2477 2478 f.load("foo") 2479 f.assertNumManifests(1) 2480 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/.dockerignore") 2481 f.assertNextManifest("foo", 2482 cb( 2483 image("gcr.io/foo"), 2484 deps(f.JoinPath("foo")), 2485 cmd("docker build -t $TAG foo", f.Path()), 2486 disablePush(true), 2487 ), 2488 deployment("foo")) 2489 } 2490 2491 func TestCustomBuildSkipsLocalDocker(t *testing.T) { 2492 f := newFixture(t) 2493 2494 tiltfile := ` 2495 k8s_yaml('foo.yaml') 2496 custom_build( 2497 'gcr.io/foo', 2498 'buildah bud -t $TAG foo && buildah push $TAG $TAG', 2499 ['foo'], 2500 skips_local_docker=True, 2501 )` 2502 2503 f.setupFoo() 2504 f.file("Tiltfile", tiltfile) 2505 2506 f.load("foo") 2507 m := f.assertNextManifest("foo", 2508 cb( 2509 image("gcr.io/foo"), 2510 ), 2511 deployment("foo")) 2512 assert.Equal(t, v1alpha1.CmdImageOutputRemote, m.ImageTargets[0].CustomBuildInfo().OutputMode) 2513 assert.True(t, m.ImageTargets[0].CustomBuildInfo().SkipsPush()) 2514 } 2515 2516 func TestImageObjectJSONPath(t *testing.T) { 2517 f := newFixture(t) 2518 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2519 kind: UselessMachine 2520 metadata: 2521 name: um 2522 spec: 2523 image: 2524 repo: tilt.dev/frontend`) 2525 f.dockerfile("Dockerfile") 2526 f.file("Tiltfile", ` 2527 k8s_yaml('um.yaml') 2528 k8s_kind(kind='UselessMachine', image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2529 docker_build('tilt.dev/frontend', '.') 2530 `) 2531 2532 f.load() 2533 m := f.assertNextManifest("um", 2534 podReadiness(model.PodReadinessWait)) 2535 assert.Equal(t, "tilt.dev/frontend", 2536 m.ImageTargets[0].ImageMapSpec.Selector) 2537 } 2538 2539 func TestImageObjectJSONPathNoMatch(t *testing.T) { 2540 f := newFixture(t) 2541 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2542 kind: UselessMachine 2543 metadata: 2544 name: um 2545 spec: 2546 repo: tilt.dev/frontend`) 2547 f.dockerfile("Dockerfile") 2548 f.file("Tiltfile", ` 2549 k8s_yaml('um.yaml') 2550 k8s_kind(kind='UselessMachine', image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2551 docker_build('tilt.dev/frontend', '.') 2552 `) 2553 2554 f.loadErrString("finding image", "UselessMachine/um", ".spec.image") 2555 } 2556 2557 func TestImageObjectJSONPathPodReadinessIgnore(t *testing.T) { 2558 f := newFixture(t) 2559 f.file("um.yaml", `apiVersion: tilt.dev/v1alpha1 2560 kind: UselessMachine 2561 metadata: 2562 name: um 2563 spec: 2564 image: 2565 repo: tilt.dev/frontend`) 2566 f.dockerfile("Dockerfile") 2567 f.file("Tiltfile", ` 2568 k8s_yaml('um.yaml') 2569 k8s_kind(kind='UselessMachine', pod_readiness='ignore', 2570 image_object={'json_path': '{.spec.image}', 'repo_field': 'repo', 'tag_field': 'tag'}) 2571 docker_build('tilt.dev/frontend', '.') 2572 `) 2573 2574 f.load() 2575 m := f.assertNextManifest("um", 2576 podReadiness(model.PodReadinessIgnore)) 2577 assert.Equal(t, "tilt.dev/frontend", 2578 m.ImageTargets[0].ImageMapSpec.Selector) 2579 } 2580 2581 func TestExtraImageLocationOneImage(t *testing.T) { 2582 f := newFixture(t) 2583 f.setupCRD() 2584 f.dockerfile("env/Dockerfile") 2585 f.dockerfile("builder/Dockerfile") 2586 f.file("Tiltfile", ` 2587 2588 k8s_yaml('crd.yaml') 2589 k8s_image_json_path(kind='Environment', paths='{.spec.runtime.image}') 2590 docker_build('test/mycrd-env', 'env') 2591 `) 2592 2593 f.load("mycrd") 2594 f.assertNextManifest("mycrd", 2595 db( 2596 image("test/mycrd-env"), 2597 ), 2598 k8sObject("mycrd", "Environment"), 2599 ) 2600 } 2601 2602 func TestConflictingWorkloadNames(t *testing.T) { 2603 f := newFixture(t) 2604 2605 f.dockerfile("foo1/Dockerfile") 2606 f.dockerfile("foo2/Dockerfile") 2607 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 2608 f.yaml("foo2.yaml", deployment("foo", image("gcr.io/foo2"), namespace("ns2"))) 2609 2610 f.file("Tiltfile", ` 2611 2612 k8s_yaml(['foo1.yaml', 'foo2.yaml']) 2613 docker_build('gcr.io/foo1', 'foo1') 2614 docker_build('gcr.io/foo2', 'foo2') 2615 `) 2616 f.load("foo:deployment:ns1", "foo:deployment:ns2") 2617 2618 f.assertNextManifest("foo:deployment:ns1", db(image("gcr.io/foo1"))) 2619 f.assertNextManifest("foo:deployment:ns2", db(image("gcr.io/foo2"))) 2620 } 2621 2622 type k8sKindTest struct { 2623 name string 2624 k8sKindArgs string 2625 expectWorkload bool 2626 expectImage bool 2627 expectedError string 2628 preamble string 2629 expectedResourceName model.ManifestName 2630 } 2631 2632 func TestK8sKind(t *testing.T) { 2633 tests := []k8sKindTest{ 2634 {name: "match kind", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}'", expectWorkload: true, expectImage: true}, 2635 {name: "don't match kind", k8sKindArgs: "'fdas', image_json_path='{.spec.runtime.image}'", expectWorkload: false}, 2636 {name: "match apiVersion", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}', api_version='fission.io/v1'", expectWorkload: true, expectImage: true}, 2637 {name: "don't match apiVersion", k8sKindArgs: "'Environment', image_json_path='{.spec.runtime.image}', api_version='fission.io/v2'"}, 2638 {name: "invalid kind regexp", k8sKindArgs: "'*', image_json_path='{.spec.runtime.image}'", expectedError: "error parsing kind regexp"}, 2639 {name: "invalid apiVersion regexp", k8sKindArgs: "'Environment', api_version='*', image_json_path='{.spec.runtime.image}'", expectedError: "error parsing apiVersion regexp"}, 2640 {name: "no image", k8sKindArgs: "'Environment'", expectWorkload: true}, 2641 } 2642 2643 for _, test := range tests { 2644 t.Run(test.name, func(t *testing.T) { 2645 f := newFixture(t) 2646 f.setupCRD() 2647 f.dockerfile("env/Dockerfile") 2648 f.dockerfile("builder/Dockerfile") 2649 img := "" 2650 if !test.expectWorkload || test.expectImage { 2651 img = "docker_build('test/mycrd-env', 'env')" 2652 } 2653 f.file("Tiltfile", fmt.Sprintf(` 2654 2655 %s 2656 k8s_yaml('crd.yaml') 2657 k8s_kind(%s) 2658 %s 2659 `, test.preamble, test.k8sKindArgs, img)) 2660 2661 if test.expectWorkload { 2662 if test.expectedError != "" { 2663 t.Fatal("invalid test: cannot expect both workload and error") 2664 } 2665 expectedResourceName := model.ManifestName("mycrd") 2666 if test.expectedResourceName != "" { 2667 expectedResourceName = test.expectedResourceName 2668 } 2669 f.load(string(expectedResourceName)) 2670 var imageOpt interface{} 2671 if test.expectImage { 2672 imageOpt = db(image("test/mycrd-env")) 2673 } else { 2674 imageOpt = funcOpt(func(t *testing.T, m model.Manifest) bool { 2675 return assert.Equal(t, 0, len(m.ImageTargets)) 2676 }) 2677 } 2678 f.assertNextManifest( 2679 expectedResourceName, 2680 k8sObject("mycrd", "Environment"), 2681 imageOpt) 2682 } else { 2683 if test.expectImage { 2684 t.Fatal("invalid test: cannot expect image without expecting workload") 2685 } 2686 if test.expectedError == "" { 2687 f.loadAssertWarnings(unmatchedImageAllUnresourcedWarning) 2688 } else { 2689 f.loadErrString(test.expectedError) 2690 } 2691 } 2692 }) 2693 } 2694 } 2695 2696 func TestK8sKindImageJSONPathPositional(t *testing.T) { 2697 f := newFixture(t) 2698 f.setupCRD() 2699 f.dockerfile("env/Dockerfile") 2700 f.dockerfile("builder/Dockerfile") 2701 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2702 k8s_kind('Environment', '{.spec.runtime.image}') 2703 docker_build('test/mycrd-env', 'env') 2704 `) 2705 2706 f.loadErrString("got 2 arguments, want at most 1") 2707 } 2708 2709 func TestExtraImageLocationTwoImages(t *testing.T) { 2710 f := newFixture(t) 2711 f.setupCRD() 2712 f.dockerfile("env/Dockerfile") 2713 f.dockerfile("builder/Dockerfile") 2714 f.file("Tiltfile", ` 2715 2716 k8s_yaml('crd.yaml') 2717 k8s_image_json_path(['{.spec.runtime.image}', '{.spec.builder.image}'], kind='Environment') 2718 docker_build('test/mycrd-builder', 'builder') 2719 docker_build('test/mycrd-env', 'env') 2720 `) 2721 2722 f.load("mycrd") 2723 f.assertNextManifest("mycrd", 2724 db( 2725 image("test/mycrd-env"), 2726 ), 2727 db( 2728 image("test/mycrd-builder"), 2729 ), 2730 k8sObject("mycrd", "Environment"), 2731 ) 2732 } 2733 2734 func TestExtraImageLocationDeploymentEnvVarByName(t *testing.T) { 2735 f := newFixture(t) 2736 2737 f.dockerfile("foo/Dockerfile") 2738 f.dockerfile("foo-fetcher/Dockerfile") 2739 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2740 f.dockerfile("bar/Dockerfile") 2741 // just throwing bar in here to make sure it doesn't error out because it has no FETCHER_IMAGE 2742 f.yaml("bar.yaml", deployment("bar", image("gcr.io/bar"))) 2743 f.gitInit("") 2744 2745 f.file("Tiltfile", `k8s_yaml(['foo.yaml', 'bar.yaml']) 2746 docker_build('gcr.io/foo', 'foo') 2747 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2748 docker_build('gcr.io/bar', 'bar') 2749 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", name='foo') 2750 `) 2751 f.load("foo", "bar") 2752 f.assertNextManifest("foo", 2753 db( 2754 image("gcr.io/foo"), 2755 ), 2756 db( 2757 image("gcr.io/foo-fetcher"), 2758 ), 2759 ) 2760 f.assertNextManifest("bar", 2761 db( 2762 image("gcr.io/bar"), 2763 ), 2764 ) 2765 } 2766 2767 func TestExtraImageLocationDeploymentEnvVarMatch(t *testing.T) { 2768 f := newFixture(t) 2769 2770 f.dockerfile("foo/Dockerfile") 2771 f.dockerfile("foo-fetcher/Dockerfile") 2772 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2773 f.gitInit("") 2774 2775 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2776 docker_build('gcr.io/foo', 'foo') 2777 docker_build('gcr.io/foo-fetcher', 'foo-fetcher', match_in_env_vars=True) 2778 `) 2779 f.load("foo") 2780 f.assertNextManifest("foo", 2781 db( 2782 image("gcr.io/foo"), 2783 ), 2784 db( 2785 image("gcr.io/foo-fetcher").withMatchInEnvVars(), 2786 ), 2787 ) 2788 } 2789 2790 func TestExtraImageLocationDeploymentEnvVarDoesNotMatchIfNotSpecified(t *testing.T) { 2791 f := newFixture(t) 2792 2793 f.dockerfile("foo/Dockerfile") 2794 f.dockerfile("foo-fetcher/Dockerfile") 2795 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2796 f.gitInit("") 2797 2798 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2799 docker_build('gcr.io/foo', 'foo') 2800 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2801 `) 2802 f.loadAssertWarnings(unusedImageWarning("gcr.io/foo-fetcher", []string{"gcr.io/foo"}, "Kubernetes")) 2803 f.assertNextManifest("foo", 2804 db( 2805 image("gcr.io/foo"), 2806 ), 2807 ) 2808 2809 } 2810 2811 func TestK8sImageJSONPathArgs(t *testing.T) { 2812 tests := []struct { 2813 name string 2814 args string 2815 expectMatch bool 2816 expectedError string 2817 }{ 2818 {"match name", "name='foo'", true, ""}, 2819 {"don't match name", "name='bar'", false, ""}, 2820 {"match name w/ regex", "name='.*o'", true, ""}, 2821 {"match kind", "name='foo', kind='Deployment'", true, ""}, 2822 {"don't match kind", "name='bar', kind='asdf'", false, ""}, 2823 {"match apiVersion", "name='foo', api_version='apps/v1'", true, ""}, 2824 {"match apiVersion+kind w/ regex", "name='foo', kind='Deployment', api_version='apps/.*'", true, ""}, 2825 {"don't match apiVersion", "name='bar', api_version='apps/v2'", false, ""}, 2826 {"match namespace", "name='foo', namespace='default'", true, ""}, 2827 {"don't match namespace", "name='bar', namespace='asdf'", false, ""}, 2828 {"invalid name regex", "name='*'", false, "error parsing name regexp"}, 2829 {"invalid kind regex", "kind='*'", false, "error parsing kind regexp"}, 2830 {"invalid apiVersion regex", "name='foo', api_version='*'", false, "error parsing apiVersion regexp"}, 2831 {"invalid namespace regex", "namespace='*'", false, "error parsing namespace regexp"}, 2832 {"regexes are case-insensitive", "name='FOO'", true, ""}, 2833 {"regexes that specify case insensitivity still work", "name='(?i)FOO'", true, ""}, 2834 } 2835 for _, test := range tests { 2836 t.Run(test.name, func(t *testing.T) { 2837 f := newFixture(t) 2838 2839 f.dockerfile("foo/Dockerfile") 2840 f.dockerfile("foo-fetcher/Dockerfile") 2841 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2842 f.gitInit("") 2843 2844 f.file("Tiltfile", fmt.Sprintf(`k8s_yaml('foo.yaml') 2845 docker_build('gcr.io/foo', 'foo') 2846 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2847 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", %s) 2848 `, test.args)) 2849 if test.expectMatch { 2850 if test.expectedError != "" { 2851 t.Fatal("illegal test definition: cannot expect both match and error") 2852 } 2853 f.load("foo") 2854 f.assertNextManifest("foo", 2855 db( 2856 image("gcr.io/foo"), 2857 ), 2858 db( 2859 image("gcr.io/foo-fetcher"), 2860 ), 2861 ) 2862 } else { 2863 if test.expectedError == "" { 2864 w := unusedImageWarning("gcr.io/foo-fetcher", []string{"gcr.io/foo"}, "Kubernetes") 2865 f.loadAssertWarnings(w) 2866 } else { 2867 f.loadErrString(test.expectedError) 2868 } 2869 } 2870 }) 2871 } 2872 } 2873 2874 func TestExtraImageLocationDeploymentEnvVarByNameAndNamespace(t *testing.T) { 2875 f := newFixture(t) 2876 2877 f.dockerfile("foo/Dockerfile") 2878 f.dockerfile("foo-fetcher/Dockerfile") 2879 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"), withEnvVars("FETCHER_IMAGE", "gcr.io/foo-fetcher"))) 2880 f.gitInit("") 2881 2882 f.file("Tiltfile", `k8s_yaml('foo.yaml') 2883 docker_build('gcr.io/foo', 'foo') 2884 docker_build('gcr.io/foo-fetcher', 'foo-fetcher') 2885 k8s_image_json_path("{.spec.template.spec.containers[*].env[?(@.name=='FETCHER_IMAGE')].value}", name='foo', namespace='default') 2886 `) 2887 f.load("foo") 2888 f.assertNextManifest("foo", 2889 db( 2890 image("gcr.io/foo"), 2891 ), 2892 db( 2893 image("gcr.io/foo-fetcher"), 2894 ), 2895 ) 2896 } 2897 2898 func TestExtraImageLocationNoMatch(t *testing.T) { 2899 f := newFixture(t) 2900 f.setupCRD() 2901 f.dockerfile("env/Dockerfile") 2902 f.dockerfile("builder/Dockerfile") 2903 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2904 k8s_image_json_path('{.foobar}', kind='Environment') 2905 docker_build('test/mycrd-env', 'env') 2906 `) 2907 2908 f.loadErrString("{.foobar}", "foobar is not found") 2909 } 2910 2911 func TestExtraImageLocationInvalidJsonPath(t *testing.T) { 2912 f := newFixture(t) 2913 f.setupCRD() 2914 f.dockerfile("env/Dockerfile") 2915 f.dockerfile("builder/Dockerfile") 2916 f.file("Tiltfile", `k8s_yaml('crd.yaml') 2917 k8s_image_json_path('{foobar()}', kind='Environment') 2918 docker_build('test/mycrd-env', 'env') 2919 `) 2920 2921 f.loadErrString("{foobar()}", "unrecognized identifier foobar()") 2922 } 2923 2924 func TestExtraImageLocationNoPaths(t *testing.T) { 2925 f := newFixture(t) 2926 f.file("Tiltfile", `k8s_image_json_path(kind='MyType')`) 2927 f.loadErrString("missing argument for paths") 2928 } 2929 2930 func TestExtraImageLocationNotListOrString(t *testing.T) { 2931 f := newFixture(t) 2932 f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=8)`) 2933 f.loadErrString("for parameter \"paths\": Expected string, got: 8") 2934 } 2935 2936 func TestExtraImageLocationListContainsNonString(t *testing.T) { 2937 f := newFixture(t) 2938 f.file("Tiltfile", `k8s_image_json_path(kind='MyType', paths=["foo", 8])`) 2939 f.loadErrString("for parameter \"paths\": Expected string, got: 8") 2940 } 2941 2942 func TestExtraImageLocationNoSelectorSpecified(t *testing.T) { 2943 f := newFixture(t) 2944 f.file("Tiltfile", `k8s_image_json_path(paths=["foo"])`) 2945 f.loadErrString("at least one of kind, name, or namespace must be specified") 2946 } 2947 2948 func TestDockerBuildEmptyDockerFileArg(t *testing.T) { 2949 f := newFixture(t) 2950 f.file("Tiltfile", ` 2951 docker_build('web/api', '', dockerfile='') 2952 `) 2953 f.loadErrString("error reading dockerfile") 2954 } 2955 2956 func TestK8sYamlEmptyArg(t *testing.T) { 2957 f := newFixture(t) 2958 f.file("Tiltfile", ` 2959 k8s_yaml('') 2960 `) 2961 f.loadErrString("error reading yaml file") 2962 } 2963 2964 func TestTwoDefaultRegistries(t *testing.T) { 2965 f := newFixture(t) 2966 2967 f.file("Tiltfile", ` 2968 default_registry("gcr.io") 2969 default_registry("docker.io")`) 2970 2971 f.loadErrString("default registry already defined") 2972 } 2973 2974 func TestDefaultRegistryInvalid(t *testing.T) { 2975 f := newFixture(t) 2976 2977 f.setupFoo() 2978 f.file("Tiltfile", ` 2979 default_registry("foo") 2980 docker_build('gcr.io/foo', 'foo') 2981 `) 2982 2983 f.loadErrString("Traceback ", "repository name must be canonical") 2984 } 2985 2986 func TestDefaultRegistryHostFromCluster(t *testing.T) { 2987 f := newFixture(t) 2988 2989 f.setupFoo() 2990 f.file("Tiltfile", ` 2991 default_registry("abc.io", host_from_cluster="def.io") 2992 k8s_yaml('foo.yaml') 2993 docker_build('gcr.io/foo', 'foo') 2994 `) 2995 2996 f.load() 2997 2998 f.assertNextManifest("foo", 2999 db(image("gcr.io/foo").withLocalRef("abc.io/gcr.io_foo").withClusterRef("def.io/gcr.io_foo")), 3000 deployment("foo")) 3001 } 3002 3003 func TestDefaultRegistryAtEndOfTiltfile(t *testing.T) { 3004 f := newFixture(t) 3005 3006 f.setupFoo() 3007 // default_registry is the last entry to test that it doesn't only affect subsequently defined images 3008 f.file("Tiltfile", ` 3009 docker_build('gcr.io/foo', 'foo') 3010 k8s_yaml('foo.yaml') 3011 default_registry('bar.com') 3012 `) 3013 3014 f.load() 3015 3016 f.assertNextManifest("foo", 3017 db(image("gcr.io/foo").withLocalRef("bar.com/gcr.io_foo")), 3018 deployment("foo")) 3019 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 3020 } 3021 3022 func TestDefaultRegistryTwoImagesOnlyDifferByTag(t *testing.T) { 3023 f := newFixture(t) 3024 3025 f.dockerfile("bar/Dockerfile") 3026 f.yaml("bar.yaml", deployment("bar", image("gcr.io/foo:bar"))) 3027 3028 f.dockerfile("baz/Dockerfile") 3029 f.yaml("baz.yaml", deployment("baz", image("gcr.io/foo:baz"))) 3030 3031 f.gitInit("") 3032 f.file("Tiltfile", ` 3033 3034 docker_build('gcr.io/foo:bar', 'bar') 3035 docker_build('gcr.io/foo:baz', 'baz') 3036 k8s_yaml('bar.yaml') 3037 k8s_yaml('baz.yaml') 3038 default_registry('example.com') 3039 `) 3040 3041 f.load() 3042 3043 f.assertNextManifest("bar", 3044 db(image("gcr.io/foo:bar").withLocalRef("example.com/gcr.io_foo")), 3045 deployment("bar")) 3046 f.assertNextManifest("baz", 3047 db(image("gcr.io/foo:baz").withLocalRef("example.com/gcr.io_foo")), 3048 deployment("baz")) 3049 f.assertConfigFiles("Tiltfile", ".tiltignore", "bar/Dockerfile", "bar/.dockerignore", "bar.yaml", "baz/Dockerfile", "baz/.dockerignore", "baz.yaml") 3050 } 3051 3052 func TestDefaultRegistrySingleName(t *testing.T) { 3053 f := newFixture(t) 3054 3055 f.dockerfile("fe/Dockerfile") 3056 f.yaml("fe.yaml", deployment("fe", image("fe"))) 3057 3058 f.dockerfile("be/Dockerfile") 3059 f.yaml("be.yaml", deployment("be", image("be"))) 3060 3061 f.gitInit("") 3062 f.file("Tiltfile", ` 3063 3064 docker_build('fe', './fe') 3065 docker_build('be', './be') 3066 k8s_yaml('fe.yaml') 3067 k8s_yaml('be.yaml') 3068 default_registry('123.dkr.ecr.us-east-1.amazonaws.com', single_name='team-a/dev') 3069 `) 3070 3071 f.load() 3072 3073 fe := f.assertNextManifest("fe", 3074 db(image("fe").withLocalRef("123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev")), 3075 deployment("fe")) 3076 3077 feRefs, err := fe.ImageTargets[0].Refs(f.cluster(fe)) 3078 assert.NoError(t, err) 3079 feTaggedRefs, err := feRefs.AddTagSuffix("tilt-build-123") 3080 assert.NoError(t, err) 3081 assert.Equal(t, "123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev:fe-tilt-build-123", 3082 feTaggedRefs.LocalRef.String()) 3083 3084 be := f.assertNextManifest("be", 3085 db(image("be").withLocalRef("123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev")), 3086 deployment("be")) 3087 3088 beRefs, err := be.ImageTargets[0].Refs(f.cluster(be)) 3089 assert.NoError(t, err) 3090 beTaggedRefs, err := beRefs.AddTagSuffix("tilt-build-456") 3091 assert.NoError(t, err) 3092 assert.Equal(t, "123.dkr.ecr.us-east-1.amazonaws.com/team-a/dev:be-tilt-build-456", 3093 beTaggedRefs.LocalRef.String()) 3094 } 3095 3096 func TestDefaultReadFile(t *testing.T) { 3097 f := newFixture(t) 3098 f.setupFooAndBar() 3099 tiltfile := ` 3100 result = read_file("this_file_does_not_exist", default="foo") 3101 docker_build('gcr.io/foo', 'foo') 3102 k8s_yaml(str(result) + '.yaml') 3103 ` 3104 3105 f.file("Tiltfile", tiltfile) 3106 3107 f.load() 3108 3109 f.assertNextManifest("foo", 3110 db(image("gcr.io/foo")), 3111 deployment("foo")) 3112 3113 f.assertConfigFiles("Tiltfile", ".tiltignore", "this_file_does_not_exist", "foo.yaml", "foo/Dockerfile", "foo/.dockerignore") 3114 } 3115 3116 func TestWatchFile(t *testing.T) { 3117 f := newFixture(t) 3118 3119 f.setupFoo() 3120 3121 f.file("hello", "world") 3122 f.file("Tiltfile", ` 3123 docker_build('gcr.io/foo', 'foo') 3124 watch_file('hello') 3125 k8s_yaml('foo.yaml') 3126 `) 3127 3128 f.load() 3129 3130 f.assertNextManifest("foo", 3131 db(image("gcr.io/foo")), 3132 deployment("foo")) 3133 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml", "hello") 3134 } 3135 3136 func TestAssemblyBasic(t *testing.T) { 3137 f := newFixture(t) 3138 3139 f.setupFoo() 3140 3141 f.file("Tiltfile", ` 3142 docker_build('gcr.io/foo', 'foo') 3143 k8s_yaml('foo.yaml') 3144 `) 3145 3146 f.load("foo") 3147 3148 f.assertNextManifest("foo", 3149 db(image("gcr.io/foo")), 3150 deployment("foo")) 3151 3152 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "foo/Dockerfile", "foo/.dockerignore") 3153 } 3154 3155 func TestAssemblyTwoWorkloadsSameImage(t *testing.T) { 3156 f := newFixture(t) 3157 3158 f.setupFoo() 3159 f.yaml("bar.yaml", deployment("bar", image("gcr.io/foo"))) 3160 3161 f.file("Tiltfile", ` 3162 3163 docker_build('gcr.io/foo', 'foo') 3164 k8s_yaml(['foo.yaml', 'bar.yaml']) 3165 `) 3166 3167 f.load("foo", "bar") 3168 3169 f.assertNextManifest("foo", 3170 db(image("gcr.io/foo")), 3171 deployment("foo")) 3172 f.assertNextManifest("bar", 3173 db(image("gcr.io/foo")), 3174 deployment("bar")) 3175 3176 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo.yaml", "bar.yaml", "foo/Dockerfile", "foo/.dockerignore") 3177 } 3178 3179 // Fix a bug where a service with no selectors trivially matched all pods, so Tilt grouped 3180 // it with the first workload (https://github.com/tilt-dev/tilt/issues/4233) 3181 func TestAssemblyServiceWithoutSelectorMatchesNothing(t *testing.T) { 3182 f := newFixture(t) 3183 3184 f.yaml("all.yaml", 3185 deployment("foo", withLabels(map[string]string{"app": "foo"})), 3186 3187 service("service-without-selectors", withLabels(map[string]string{})), 3188 ) 3189 f.file("Tiltfile", ` 3190 k8s_yaml('all.yaml') 3191 `) 3192 3193 f.load() 3194 3195 f.assertNextManifest("foo", deployment("foo")) 3196 3197 f.assertNextManifestUnresourced("service-without-selectors") 3198 } 3199 3200 func TestK8sResourceNoMatch(t *testing.T) { 3201 f := newFixture(t) 3202 3203 f.setupFoo() 3204 f.file("Tiltfile", ` 3205 3206 k8s_yaml('foo.yaml') 3207 k8s_resource('bar', new_name='baz') 3208 `) 3209 3210 f.loadErrString("specified unknown resource \"bar\". known k8s resources: foo") 3211 } 3212 3213 func TestK8sResourceNewName(t *testing.T) { 3214 f := newFixture(t) 3215 3216 f.setupFoo() 3217 f.file("Tiltfile", ` 3218 3219 k8s_yaml('foo.yaml') 3220 k8s_resource('foo', new_name='bar') 3221 `) 3222 3223 f.load() 3224 f.assertNumManifests(1) 3225 f.assertNextManifest("bar", deployment("foo")) 3226 } 3227 3228 func TestK8sResourceRenameTwice(t *testing.T) { 3229 f := newFixture(t) 3230 3231 f.setupFoo() 3232 f.file("Tiltfile", ` 3233 k8s_yaml('foo.yaml') 3234 k8s_resource('foo', new_name='bar') 3235 k8s_resource('bar', new_name='baz') 3236 `) 3237 3238 f.load() 3239 f.assertNumManifests(1) 3240 f.assertNextManifest("baz", deployment("foo")) 3241 } 3242 3243 func TestK8sResourceNewNameConflict(t *testing.T) { 3244 f := newFixture(t) 3245 3246 f.setupFooAndBar() 3247 f.file("Tiltfile", ` 3248 3249 k8s_yaml(['foo.yaml', 'bar.yaml']) 3250 k8s_resource('foo', new_name='bar') 3251 `) 3252 3253 f.loadErrString("\"foo\" to \"bar\"", "already exists") 3254 } 3255 3256 func TestK8sResourceRenameConflictingNames(t *testing.T) { 3257 f := newFixture(t) 3258 3259 f.dockerfile("foo1/Dockerfile") 3260 f.dockerfile("foo2/Dockerfile") 3261 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 3262 f.yaml("foo2.yaml", deployment("foo", image("gcr.io/foo2"), namespace("ns2"))) 3263 3264 f.file("Tiltfile", ` 3265 3266 k8s_yaml(['foo1.yaml', 'foo2.yaml']) 3267 docker_build('gcr.io/foo1', 'foo1') 3268 docker_build('gcr.io/foo2', 'foo2') 3269 k8s_resource('foo:deployment:ns2', new_name='foo') 3270 `) 3271 f.load("foo:deployment:ns1", "foo") 3272 3273 f.assertNextManifest("foo:deployment:ns1", db(image("gcr.io/foo1"))) 3274 f.assertNextManifest("foo", db(image("gcr.io/foo2"))) 3275 } 3276 3277 func TestConflictingNewNames(t *testing.T) { 3278 f := newFixture(t) 3279 3280 f.yaml("ns1.yaml", namespace("ns1")) 3281 f.yaml("ns2.yaml", namespace("ns2")) 3282 f.file("Tiltfile", ` 3283 k8s_yaml(['ns1.yaml', 'ns2.yaml']) 3284 k8s_resource(new_name='foo', objects=['ns1:namespace']) 3285 k8s_resource(new_name='foo', objects=['ns2:namespace']) 3286 `) 3287 3288 f.loadErrString("k8s_resource named \"foo\" already exists") 3289 } 3290 3291 func TestAdditivePortForwards(t *testing.T) { 3292 f := newFixture(t) 3293 3294 f.setupFoo() 3295 3296 f.file("Tiltfile", ` 3297 3298 k8s_yaml('foo.yaml') 3299 k8s_resource('foo', port_forwards=8001) 3300 k8s_resource('foo', port_forwards=8000) 3301 `) 3302 3303 f.load() 3304 f.assertNextManifest("foo", []model.PortForward{{LocalPort: 8001}, {LocalPort: 8000}}) 3305 } 3306 3307 func TestWorkloadToResourceFunction(t *testing.T) { 3308 f := newFixture(t) 3309 3310 f.setupFoo() 3311 3312 f.file("Tiltfile", ` 3313 3314 docker_build('gcr.io/foo', 'foo') 3315 k8s_yaml('foo.yaml') 3316 def wtrf(id): 3317 return 'hello-' + id.name 3318 workload_to_resource_function(wtrf) 3319 k8s_resource('hello-foo', port_forwards=8000) 3320 `) 3321 3322 f.load() 3323 f.assertNumManifests(1) 3324 f.assertNextManifest("hello-foo", db(image("gcr.io/foo")), []model.PortForward{{LocalPort: 8000}}) 3325 } 3326 3327 func TestWorkloadToResourceFunctionConflict(t *testing.T) { 3328 f := newFixture(t) 3329 3330 f.setupFooAndBar() 3331 3332 f.file("Tiltfile", ` 3333 3334 docker_build('gcr.io/foo', 'foo') 3335 docker_build('gcr.io/bar', 'bar') 3336 k8s_yaml(['foo.yaml', 'bar.yaml']) 3337 def wtrf(id): 3338 return 'baz' 3339 workload_to_resource_function(wtrf) 3340 `) 3341 3342 f.loadErrString("workload_to_resource_function", "bar:deployment:default:apps", "foo:deployment:default:apps", "'baz'") 3343 } 3344 3345 func TestWorkloadToResourceFunctionError(t *testing.T) { 3346 f := newFixture(t) 3347 3348 f.setupFoo() 3349 3350 f.file("Tiltfile", ` 3351 3352 docker_build('gcr.io/foo', 'foo') 3353 k8s_yaml('foo.yaml') 3354 def wtrf(id): 3355 return 1 + 'asdf' 3356 workload_to_resource_function(wtrf) 3357 k8s_resource('hello-foo', port_forwards=8000) 3358 `) 3359 3360 f.loadErrString("'foo:deployment:default:apps'", "unknown binary op: int + string", "Tiltfile:5:1", workloadToResourceFunctionN) 3361 } 3362 3363 func TestWorkloadToResourceFunctionReturnsNonString(t *testing.T) { 3364 f := newFixture(t) 3365 3366 f.setupFoo() 3367 3368 f.file("Tiltfile", ` 3369 3370 docker_build('gcr.io/foo', 'foo') 3371 k8s_yaml('foo.yaml') 3372 def wtrf(id): 3373 return 1 3374 workload_to_resource_function(wtrf) 3375 k8s_resource('hello-foo', port_forwards=8000) 3376 `) 3377 3378 f.loadErrString("'foo:deployment:default:apps'", "invalid return value", "wanted: string. got: starlark.Int", "Tiltfile:5:1", workloadToResourceFunctionN) 3379 } 3380 3381 func TestWorkloadToResourceFunctionTakesNoArgs(t *testing.T) { 3382 f := newFixture(t) 3383 3384 f.setupFoo() 3385 3386 f.file("Tiltfile", ` 3387 3388 docker_build('gcr.io/foo', 'foo') 3389 k8s_yaml('foo.yaml') 3390 def wtrf(): 3391 return "hello" 3392 workload_to_resource_function(wtrf) 3393 k8s_resource('hello-foo', port_forwards=8000) 3394 `) 3395 3396 f.loadErrString("workload_to_resource_function arg must take 1 argument. wtrf takes 0") 3397 } 3398 3399 func TestWorkloadToResourceFunctionTakesTwoArgs(t *testing.T) { 3400 f := newFixture(t) 3401 3402 f.setupFoo() 3403 3404 f.file("Tiltfile", ` 3405 3406 docker_build('gcr.io/foo', 'foo') 3407 k8s_yaml('foo.yaml') 3408 def wtrf(a, b): 3409 return "hello" 3410 workload_to_resource_function(wtrf) 3411 k8s_resource('hello-foo', port_forwards=8000) 3412 `) 3413 3414 f.loadErrString("workload_to_resource_function arg must take 1 argument. wtrf takes 2") 3415 } 3416 3417 func TestMultipleLiveUpdatesOnManifest(t *testing.T) { 3418 f := newFixture(t) 3419 3420 f.gitInit("") 3421 f.file("sancho/Dockerfile", "FROM golang:1.10") 3422 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3423 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3424 f.file("Tiltfile", ` 3425 k8s_yaml('sancho.yaml') 3426 docker_build('gcr.io/some-project-162817/sancho', './sancho', 3427 live_update=[sync('./sancho/foo', '/bar')] 3428 ) 3429 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar', 3430 live_update=[sync('./sidecar/baz', '/quux')] 3431 ) 3432 `) 3433 3434 sync1 := v1alpha1.LiveUpdateSync{LocalPath: filepath.Join("sancho", "foo"), ContainerPath: "/bar"} 3435 expectedLU1 := v1alpha1.LiveUpdateSpec{ 3436 BasePath: f.Path(), 3437 Syncs: []v1alpha1.LiveUpdateSync{sync1}, 3438 } 3439 3440 sync2 := v1alpha1.LiveUpdateSync{LocalPath: filepath.Join("sidecar", "baz"), ContainerPath: "/quux"} 3441 expectedLU2 := v1alpha1.LiveUpdateSpec{ 3442 BasePath: f.Path(), 3443 Syncs: []v1alpha1.LiveUpdateSync{sync2}, 3444 } 3445 3446 f.load() 3447 f.assertNextManifest("sancho", 3448 db(image("gcr.io/some-project-162817/sancho"), expectedLU1), 3449 db(image("gcr.io/some-project-162817/sancho-sidecar"), expectedLU2), 3450 ) 3451 } 3452 3453 func TestImpossibleLiveUpdatesOKNoLiveUpdate(t *testing.T) { 3454 f := newFixture(t) 3455 3456 f.gitInit("") 3457 f.file("sancho/Dockerfile", "FROM golang:1.10") 3458 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3459 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3460 f.file("Tiltfile", ` 3461 k8s_yaml('sancho.yaml') 3462 docker_build('gcr.io/some-project-162817/sancho', './sancho') 3463 3464 # no LiveUpdate on this so nothing meriting a warning 3465 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar') 3466 `) 3467 3468 // Expect no warnings! 3469 f.load() 3470 } 3471 3472 func TestImpossibleLiveUpdatesOKSecondContainerLiveUpdate(t *testing.T) { 3473 f := newFixture(t) 3474 3475 f.gitInit("") 3476 f.file("sancho/Dockerfile", "FROM golang:1.10") 3477 f.file("sidecar/Dockerfile", "FROM golang:1.10") 3478 f.file("sancho.yaml", testyaml.SanchoSidecarYAML) // two containers 3479 f.file("Tiltfile", ` 3480 k8s_yaml('sancho.yaml') 3481 3482 # this is the second k8s container, but only the first image target, so should be OK 3483 docker_build('gcr.io/some-project-162817/sancho-sidecar', './sidecar') 3484 `) 3485 3486 // Expect no warnings! 3487 f.load() 3488 } 3489 3490 func TestTriggerModeK8S(t *testing.T) { 3491 for _, testCase := range []struct { 3492 name string 3493 globalSetting triggerMode 3494 k8sResourceSetting triggerMode 3495 specifyAutoInit bool 3496 autoInit bool 3497 expectedTriggerMode model.TriggerMode 3498 }{ 3499 {"default", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3500 {"explicit global auto", TriggerModeAuto, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3501 {"explicit global manual", TriggerModeManual, TriggerModeUnset, false, false, model.TriggerModeManualWithAutoInit}, 3502 {"kr auto", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 3503 {"kr manual", TriggerModeUnset, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 3504 {"kr manual, auto_init=False", TriggerModeUnset, TriggerModeManual, true, false, model.TriggerModeManual}, 3505 {"kr manual, auto_init=True", TriggerModeUnset, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3506 {"kr override auto", TriggerModeManual, TriggerModeAuto, false, false, model.TriggerModeAuto}, 3507 {"kr override manual", TriggerModeAuto, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 3508 {"kr override manual, auto_init=False", TriggerModeAuto, TriggerModeManual, true, false, model.TriggerModeManual}, 3509 {"kr override manual, auto_init=True", TriggerModeAuto, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3510 } { 3511 t.Run(testCase.name, func(t *testing.T) { 3512 f := newFixture(t) 3513 3514 f.setupFoo() 3515 3516 var globalTriggerModeDirective string 3517 switch testCase.globalSetting { 3518 case TriggerModeUnset: 3519 globalTriggerModeDirective = "" 3520 default: 3521 globalTriggerModeDirective = fmt.Sprintf("trigger_mode(%s)", testCase.globalSetting.String()) 3522 } 3523 3524 var k8sResourceDirective string 3525 switch testCase.k8sResourceSetting { 3526 case TriggerModeUnset: 3527 k8sResourceDirective = "" 3528 default: 3529 autoInitOption := "" 3530 if testCase.specifyAutoInit { 3531 autoInitOption = ", auto_init=" 3532 if testCase.autoInit { 3533 autoInitOption += "True" 3534 } else { 3535 autoInitOption += "False" 3536 } 3537 } 3538 k8sResourceDirective = fmt.Sprintf("k8s_resource('foo', trigger_mode=%s%s)", testCase.k8sResourceSetting.String(), autoInitOption) 3539 } 3540 3541 f.file("Tiltfile", fmt.Sprintf(` 3542 %s 3543 docker_build('gcr.io/foo', 'foo') 3544 k8s_yaml('foo.yaml') 3545 %s 3546 `, globalTriggerModeDirective, k8sResourceDirective)) 3547 3548 f.load() 3549 3550 f.assertNumManifests(1) 3551 f.assertNextManifest("foo", testCase.expectedTriggerMode) 3552 }) 3553 } 3554 } 3555 3556 func TestTriggerModeLocal(t *testing.T) { 3557 for _, testCase := range []struct { 3558 name string 3559 globalSetting triggerMode 3560 localResourceSetting triggerMode 3561 specifyAutoInit bool 3562 autoInit bool 3563 expectedTriggerMode model.TriggerMode 3564 }{ 3565 {"default", TriggerModeUnset, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3566 {"explicit global auto", TriggerModeAuto, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3567 {"explicit global manual", TriggerModeManual, TriggerModeUnset, false, true, model.TriggerModeManualWithAutoInit}, 3568 {"explicit global auto, autoInit=True", TriggerModeAuto, TriggerModeUnset, true, true, model.TriggerModeAuto}, 3569 {"explicit global auto, autoInit=False", TriggerModeAuto, TriggerModeUnset, true, false, model.TriggerModeAutoWithManualInit}, 3570 {"explicit global manual, autoInit=True", TriggerModeManual, TriggerModeUnset, true, true, model.TriggerModeManualWithAutoInit}, 3571 {"explicit global manual, autoInit=False", TriggerModeManual, TriggerModeUnset, true, false, model.TriggerModeManual}, 3572 {"local_resource auto", TriggerModeUnset, TriggerModeUnset, false, true, model.TriggerModeAuto}, 3573 {"local_resource manual", TriggerModeUnset, TriggerModeManual, false, true, model.TriggerModeManualWithAutoInit}, 3574 {"local_resource auto, autoInit=True", TriggerModeUnset, TriggerModeAuto, true, true, model.TriggerModeAuto}, 3575 {"local_resource auto, autoInit=False", TriggerModeUnset, TriggerModeAuto, true, false, model.TriggerModeAutoWithManualInit}, 3576 {"local_resource manual, autoInit=True", TriggerModeUnset, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 3577 {"local_resource manual, autoInit=False", TriggerModeUnset, TriggerModeManual, true, false, model.TriggerModeManual}, 3578 {"local_resource override auto", TriggerModeManual, TriggerModeAuto, false, true, model.TriggerModeAuto}, 3579 {"local_resource override manual", TriggerModeAuto, TriggerModeManual, false, true, model.TriggerModeManualWithAutoInit}, 3580 } { 3581 t.Run(testCase.name, func(t *testing.T) { 3582 f := newFixture(t) 3583 3584 var globalTriggerModeDirective string 3585 switch testCase.globalSetting { 3586 case TriggerModeUnset: 3587 globalTriggerModeDirective = "" 3588 case TriggerModeManual: 3589 globalTriggerModeDirective = "trigger_mode(TRIGGER_MODE_MANUAL)" 3590 case TriggerModeAuto: 3591 globalTriggerModeDirective = "trigger_mode(TRIGGER_MODE_AUTO)" 3592 } 3593 3594 resourceTriggerModeArg := "" 3595 switch testCase.localResourceSetting { 3596 case TriggerModeManual: 3597 resourceTriggerModeArg = ", trigger_mode=TRIGGER_MODE_MANUAL" 3598 case TriggerModeAuto: 3599 resourceTriggerModeArg = ", trigger_mode=TRIGGER_MODE_AUTO" 3600 } 3601 3602 autoInitArg := "" 3603 if testCase.specifyAutoInit { 3604 if testCase.autoInit { 3605 autoInitArg = ", auto_init=True" 3606 } else { 3607 autoInitArg = ", auto_init=False" 3608 } 3609 } 3610 3611 localResourceDirective := fmt.Sprintf("local_resource('foo', 'echo hi'%s%s)", resourceTriggerModeArg, autoInitArg) 3612 3613 f.file("Tiltfile", fmt.Sprintf(` 3614 %s 3615 %s 3616 `, globalTriggerModeDirective, localResourceDirective)) 3617 3618 f.load() 3619 3620 f.assertNumManifests(1) 3621 f.assertNextManifest("foo", testCase.expectedTriggerMode) 3622 }) 3623 } 3624 } 3625 3626 func TestTriggerModeInt(t *testing.T) { 3627 f := newFixture(t) 3628 3629 f.file("Tiltfile", ` 3630 trigger_mode(1) 3631 `) 3632 f.loadErrString("got int, want TriggerMode") 3633 } 3634 3635 func TestMultipleTriggerMode(t *testing.T) { 3636 f := newFixture(t) 3637 3638 f.file("Tiltfile", ` 3639 trigger_mode(TRIGGER_MODE_MANUAL) 3640 trigger_mode(TRIGGER_MODE_MANUAL) 3641 `) 3642 f.loadErrString("trigger_mode can only be called once") 3643 } 3644 3645 func TestK8sContext(t *testing.T) { 3646 f := newFixture(t) 3647 3648 f.setupFoo() 3649 3650 f.file("Tiltfile", ` 3651 if k8s_context() != 'fake-context': 3652 fail('bad context') 3653 if k8s_namespace() != 'fake-namespace': 3654 fail('bad namespace') 3655 k8s_yaml('foo.yaml') 3656 docker_build('gcr.io/foo', 'foo') 3657 `) 3658 3659 f.load() 3660 f.assertNextManifest("foo", 3661 db(image("gcr.io/foo")), 3662 deployment("foo")) 3663 f.assertConfigFiles("Tiltfile", ".tiltignore", "foo/Dockerfile", "foo/.dockerignore", "foo.yaml") 3664 3665 } 3666 3667 func TestDockerbuildIgnoreAsString(t *testing.T) { 3668 f := newFixture(t) 3669 3670 f.dockerfile("Dockerfile") 3671 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3672 f.file("Tiltfile", ` 3673 3674 docker_build('gcr.io/foo', '.', ignore="*.txt") 3675 k8s_yaml('foo.yaml') 3676 `) 3677 3678 f.load() 3679 f.assertNextManifest("foo", 3680 buildFilters("a.txt"), 3681 fileChangeFilters("a.txt"), 3682 buildMatches("txt.a"), 3683 fileChangeMatches("txt.a"), 3684 ) 3685 } 3686 3687 func TestDockerbuildIgnoreAsArray(t *testing.T) { 3688 f := newFixture(t) 3689 3690 f.dockerfile("Dockerfile") 3691 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3692 f.file("Tiltfile", ` 3693 3694 docker_build('gcr.io/foo', '.', ignore=["*.txt", "*.md"]) 3695 k8s_yaml('foo.yaml') 3696 `) 3697 3698 f.load() 3699 f.assertNextManifest("foo", 3700 buildFilters("a.txt"), 3701 buildFilters("a.md"), 3702 fileChangeFilters("a.txt"), 3703 fileChangeFilters("a.md"), 3704 buildMatches("txt.a"), 3705 fileChangeMatches("txt.a"), 3706 ) 3707 } 3708 3709 func TestDockerbuildInvalidIgnore(t *testing.T) { 3710 f := newFixture(t) 3711 3712 f.dockerfile("foo/Dockerfile") 3713 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3714 3715 f.file("Tiltfile", ` 3716 3717 docker_build('fooimage', 'foo', ignore=[127]) 3718 k8s_yaml('foo.yaml') 3719 `) 3720 3721 f.loadErrString("ignore must be a string or a sequence of strings; found a starlark.Int") 3722 } 3723 3724 func TestDockerbuildOnly(t *testing.T) { 3725 f := newFixture(t) 3726 3727 f.dockerfile("Dockerfile") 3728 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3729 f.file("Tiltfile", ` 3730 docker_build('gcr.io/foo', '.', only="myservice") 3731 k8s_yaml('foo.yaml') 3732 `) 3733 3734 f.load() 3735 f.assertNextManifest("foo", 3736 buildFilters("otherservice/bar"), 3737 fileChangeFilters("otherservice/bar"), 3738 buildMatches("myservice/bar"), 3739 fileChangeMatches("myservice/bar"), 3740 ) 3741 } 3742 3743 func TestDockerbuildOnlyAsArray(t *testing.T) { 3744 f := newFixture(t) 3745 3746 f.dockerfile("Dockerfile") 3747 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3748 f.file("Tiltfile", ` 3749 docker_build('gcr.io/foo', '.', only=["common", "myservice"]) 3750 k8s_yaml('foo.yaml') 3751 `) 3752 3753 f.load() 3754 f.assertNextManifest("foo", 3755 buildFilters("otherservice/bar"), 3756 fileChangeFilters("otherservice/bar"), 3757 buildMatches("myservice/bar"), 3758 fileChangeMatches("myservice/bar"), 3759 buildMatches("common/bar"), 3760 fileChangeMatches("common/bar"), 3761 ) 3762 } 3763 3764 func TestDockerbuildInvalidOnly(t *testing.T) { 3765 f := newFixture(t) 3766 3767 f.dockerfile("foo/Dockerfile") 3768 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3769 3770 f.file("Tiltfile", ` 3771 3772 docker_build('fooimage', 'foo', only=[127]) 3773 k8s_yaml('foo.yaml') 3774 `) 3775 3776 f.loadErrString("only must be a string or a sequence of strings; found a starlark.Int") 3777 } 3778 3779 func TestDockerbuildInvalidOnlyGlob(t *testing.T) { 3780 f := newFixture(t) 3781 3782 f.dockerfile("foo/Dockerfile") 3783 f.yaml("foo.yaml", deployment("foo", image("fooimage"))) 3784 3785 f.file("Tiltfile", ` 3786 3787 docker_build('fooimage', 'foo', only=["**/common"]) 3788 k8s_yaml('foo.yaml') 3789 `) 3790 3791 f.loadErrString("'only' does not support '*' file globs") 3792 } 3793 3794 func TestDockerbuildOnlyAndIgnore(t *testing.T) { 3795 f := newFixture(t) 3796 3797 f.dockerfile("Dockerfile") 3798 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3799 f.file("Tiltfile", ` 3800 docker_build('gcr.io/foo', '.', ignore="**/*.md", only=["common", "myservice"]) 3801 k8s_yaml('foo.yaml') 3802 `) 3803 3804 f.load() 3805 f.assertNextManifest("foo", 3806 buildFilters("otherservice/bar"), 3807 fileChangeFilters("otherservice/bar"), 3808 buildFilters("myservice/README.md"), 3809 fileChangeFilters("myservice/README.md"), 3810 buildMatches("myservice/bar"), 3811 fileChangeMatches("myservice/bar"), 3812 buildMatches("common/bar"), 3813 fileChangeMatches("common/bar"), 3814 ) 3815 } 3816 3817 // if the same file is ignored and included, the ignore takes precedence 3818 func TestDockerbuildOnlyAndIgnoreSameFile(t *testing.T) { 3819 f := newFixture(t) 3820 3821 f.dockerfile("Dockerfile") 3822 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3823 f.file("Tiltfile", ` 3824 docker_build('gcr.io/foo', '.', ignore="common/README.md", only="common/README.md") 3825 k8s_yaml('foo.yaml') 3826 `) 3827 3828 f.load() 3829 f.assertNextManifest("foo", 3830 buildFilters("common/README.md"), 3831 fileChangeFilters("common/README.md"), 3832 ) 3833 } 3834 3835 // If an only rule starts with a !, we assume that paths starts with a ! 3836 // We don't do a double negative 3837 func TestDockerbuildOnlyHasException(t *testing.T) { 3838 f := newFixture(t) 3839 3840 f.dockerfile("Dockerfile") 3841 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3842 f.file("Tiltfile", ` 3843 docker_build('gcr.io/foo', '.', only="!myservice") 3844 k8s_yaml('foo.yaml') 3845 `) 3846 3847 f.load() 3848 f.assertNextManifest("foo", 3849 buildFilters("otherservice/bar"), 3850 fileChangeFilters("otherservice/bar"), 3851 buildMatches("!myservice/bar"), 3852 fileChangeMatches("!myservice/bar"), 3853 ) 3854 } 3855 3856 // What if you have \n in strings? 3857 // That's hard to make work easily, so let's just throw an error 3858 func TestDockerbuildIgnoreWithNewline(t *testing.T) { 3859 f := newFixture(t) 3860 3861 f.dockerfile("Dockerfile") 3862 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3863 f.file("Tiltfile", ` 3864 docker_build('gcr.io/foo', '.', ignore="\nweirdfile.txt") 3865 k8s_yaml('foo.yaml') 3866 `) 3867 3868 f.loadErrString(`ignore cannot contain newlines; found ignore: "\nweirdfile.txt"`) 3869 } 3870 func TestDockerbuildOnlyWithNewline(t *testing.T) { 3871 f := newFixture(t) 3872 3873 f.dockerfile("Dockerfile") 3874 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3875 f.file("Tiltfile", ` 3876 docker_build('gcr.io/foo', '.', only="\nweirdfile.txt") 3877 k8s_yaml('foo.yaml') 3878 `) 3879 3880 f.loadErrString(`only cannot contain newlines; found only: "\nweirdfile.txt`) 3881 } 3882 3883 // Custom Build Ignores(Single file) 3884 func TestCustomBuildIgnoresSingular(t *testing.T) { 3885 f := newFixture(t) 3886 f.setupFoo() 3887 3888 f.file("Tiltfile", ` 3889 3890 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 3891 ['foo'], ignore="a.txt") 3892 k8s_yaml('foo.yaml') 3893 `) // custom build doesnt support globs for dependencies 3894 f.load() 3895 f.assertNextManifest("foo", 3896 fileChangeFilters("foo/a.txt"), 3897 fileChangeMatches("foo/txt.a"), 3898 ) 3899 } 3900 3901 // Custom Build Ignores(Multiple files) 3902 func TestCustomBuildIgnoresMultiple(t *testing.T) { 3903 f := newFixture(t) 3904 f.setupFoo() 3905 3906 f.file("Tiltfile", ` 3907 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 3908 ['foo'], ignore=["a.md","a.txt"]) 3909 k8s_yaml('foo.yaml') 3910 `) 3911 f.load() 3912 f.assertNextManifest("foo", 3913 fileChangeFilters("foo/a.txt"), 3914 fileChangeFilters("foo/a.md"), 3915 fileChangeMatches("foo/txt.a"), 3916 fileChangeMatches("foo/md.a"), 3917 ) 3918 } 3919 3920 func TestEnableFeature(t *testing.T) { 3921 f := newFixture(t) 3922 f.features["testflag_disabled"] = feature.Value{Enabled: false} 3923 f.setupFoo() 3924 3925 f.file("Tiltfile", `enable_feature('testflag_disabled')`) 3926 f.load() 3927 3928 f.assertFeature("testflag_disabled", true) 3929 } 3930 3931 func TestEnableFeatureWithError(t *testing.T) { 3932 f := newFixture(t) 3933 f.features["testflag_disabled"] = feature.Value{Enabled: false} 3934 f.setupFoo() 3935 3936 f.file("Tiltfile", ` 3937 enable_feature('testflag_disabled') 3938 fail('goodnight moon') 3939 `) 3940 f.loadErrString("goodnight moon") 3941 3942 f.assertFeature("testflag_disabled", true) 3943 } 3944 3945 func TestDisableFeature(t *testing.T) { 3946 f := newFixture(t) 3947 f.features["testflag_enabled"] = feature.Value{Enabled: true} 3948 f.setupFoo() 3949 3950 f.file("Tiltfile", `disable_feature('testflag_enabled')`) 3951 f.load() 3952 3953 f.assertFeature("testflag_enabled", false) 3954 } 3955 3956 func TestEnableFeatureThatDoesNotExist(t *testing.T) { 3957 f := newFixture(t) 3958 f.setupFoo() 3959 3960 f.file("Tiltfile", `enable_feature('testflag')`) 3961 3962 f.loadErrString("Unknown feature flag: testflag") 3963 } 3964 3965 func TestDisableFeatureThatDoesNotExist(t *testing.T) { 3966 f := newFixture(t) 3967 f.setupFoo() 3968 3969 f.file("Tiltfile", `disable_feature('testflag')`) 3970 3971 f.loadErrString("Unknown feature flag: testflag") 3972 } 3973 3974 func TestDisableObsoleteFeature(t *testing.T) { 3975 f := newFixture(t) 3976 f.features["obsoleteflag"] = feature.Value{Status: feature.Obsolete, Enabled: true} 3977 f.setupFoo() 3978 3979 f.file("Tiltfile", `disable_feature('obsoleteflag')`) 3980 f.loadAssertWarnings("Obsolete feature flag: obsoleteflag") 3981 } 3982 3983 func TestDisableSnapshots(t *testing.T) { 3984 f := newFixture(t) 3985 f.setupFoo() 3986 3987 f.file("Tiltfile", `disable_snapshots()`) 3988 f.load() 3989 3990 f.assertFeature(feature.Snapshots, false) 3991 } 3992 3993 func TestDockerBuildEntrypointString(t *testing.T) { 3994 f := newFixture(t) 3995 3996 f.dockerfile("Dockerfile") 3997 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 3998 f.file("Tiltfile", ` 3999 docker_build('gcr.io/foo', '.', entrypoint="/bin/the_app") 4000 k8s_yaml('foo.yaml') 4001 `) 4002 4003 f.load() 4004 f.assertNextManifest("foo", db(image("gcr.io/foo"), entrypoint(model.ToUnixCmdInDir("/bin/the_app", f.Path())))) 4005 } 4006 4007 func TestDockerBuildContainerArgs(t *testing.T) { 4008 f := newFixture(t) 4009 4010 f.dockerfile("Dockerfile") 4011 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4012 f.file("Tiltfile", ` 4013 docker_build('gcr.io/foo', '.', container_args=["bar"]) 4014 k8s_yaml('foo.yaml') 4015 `) 4016 4017 f.load() 4018 4019 m := f.assertNextManifest("foo") 4020 assert.Equal(t, 4021 &v1alpha1.ImageMapOverrideArgs{Args: []string{"bar"}}, 4022 m.ImageTargets[0].OverrideArgs) 4023 } 4024 4025 func TestDockerBuildEntrypointArray(t *testing.T) { 4026 f := newFixture(t) 4027 4028 f.dockerfile("Dockerfile") 4029 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4030 f.file("Tiltfile", ` 4031 docker_build('gcr.io/foo', '.', entrypoint=["/bin/the_app"]) 4032 k8s_yaml('foo.yaml') 4033 `) 4034 4035 f.load() 4036 f.assertNextManifest("foo", db(image("gcr.io/foo"), entrypoint(model.Cmd{Argv: []string{"/bin/the_app"}}))) 4037 } 4038 4039 func TestDockerBuild_buildArgs(t *testing.T) { 4040 f := newFixture(t) 4041 4042 f.setupFoo() 4043 4044 f.file("rev.txt", "hello") 4045 f.file("Tiltfile", ` 4046 cmd = 'cat rev.txt' 4047 if os.name == 'nt': 4048 cmd = 'type rev.txt' 4049 docker_build('gcr.io/foo', 'foo', build_args={'GIT_REV': local(cmd)}) 4050 k8s_yaml('foo.yaml') 4051 `) 4052 4053 f.load("foo") 4054 4055 m := f.assertNextManifest("foo") 4056 assert.Equal(t, 4057 []string{"GIT_REV=hello"}, 4058 m.ImageTargets[0].DockerBuildInfo().Args) 4059 } 4060 4061 func TestCustomBuildEntrypoint(t *testing.T) { 4062 f := newFixture(t) 4063 4064 f.dockerfile("Dockerfile") 4065 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4066 f.file("Tiltfile", ` 4067 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 4068 ['foo'], entrypoint="/bin/the_app") 4069 k8s_yaml('foo.yaml') 4070 `) 4071 4072 f.load() 4073 f.assertNextManifest("foo", cb( 4074 image("gcr.io/foo"), 4075 deps(f.JoinPath("foo")), 4076 cmd("docker build -t $EXPECTED_REF foo", f.Path()), 4077 entrypoint(model.ToUnixCmdInDir("/bin/the_app", f.Path()))), 4078 ) 4079 } 4080 4081 func TestCustomBuildContainerArgs(t *testing.T) { 4082 f := newFixture(t) 4083 4084 f.dockerfile("Dockerfile") 4085 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4086 f.file("Tiltfile", ` 4087 custom_build('gcr.io/foo', 'docker build -t $EXPECTED_REF foo', 4088 ['foo'], container_args=['bar']) 4089 k8s_yaml('foo.yaml') 4090 `) 4091 4092 f.load() 4093 assert.Equal(t, 4094 &v1alpha1.ImageMapOverrideArgs{Args: []string{"bar"}}, 4095 f.assertNextManifest("foo").ImageTargets[0].OverrideArgs) 4096 } 4097 4098 // See comments on ImageTarget#MaybeIgnoreRegistry() 4099 func TestCustomBuildSkipsLocalDockerAndTagPassedIgnoresLocalRegistry(t *testing.T) { 4100 f := newFixture(t) 4101 4102 f.dockerfile("Dockerfile") 4103 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4104 f.file("Tiltfile", ` 4105 default_registry('localhost:5000') 4106 custom_build('gcr.io/foo', ':', ["."], tag='gcr.io/foo:latest', skips_local_docker=True) 4107 k8s_yaml('foo.yaml') 4108 `) 4109 4110 f.load() 4111 m := f.assertNextManifest("foo") 4112 refs, err := m.ImageTargets[0].Refs(f.cluster(m)) 4113 require.NoError(t, err) 4114 assert.Equal(t, "gcr.io/foo", refs.ClusterRef().String()) 4115 } 4116 4117 func TestDuplicateYAMLEntityWithinSingleResource(t *testing.T) { 4118 f := newFixture(t) 4119 4120 f.gitInit("") 4121 f.yaml("resource.yaml", 4122 service("doggos"), 4123 service("doggos")) 4124 f.file("Tiltfile", ` 4125 k8s_yaml('resource.yaml') 4126 `) 4127 stack := fmt.Sprintf(`Traceback (most recent call last): 4128 %s:2:9: in <toplevel> 4129 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4130 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service doggos", stack).Error()) 4131 } 4132 4133 func TestDuplicateYAMLEntityWithinSingleResourceAllowed(t *testing.T) { 4134 f := newFixture(t) 4135 4136 f.gitInit("") 4137 f.yaml("resource.yaml", 4138 service("doggos"), 4139 service("doggos")) 4140 f.file("Tiltfile", ` 4141 k8s_yaml('resource.yaml', allow_duplicates=True) 4142 `) 4143 f.load() 4144 } 4145 4146 func TestDuplicateYAMLEntityAcrossResources(t *testing.T) { 4147 f := newFixture(t) 4148 4149 f.dockerfile("foo1/Dockerfile") 4150 f.yaml("foo1.yaml", deployment("foo", image("gcr.io/foo1"), namespace("ns1"))) 4151 f.file("Tiltfile", ` 4152 4153 k8s_yaml(['foo1.yaml', 'foo1.yaml']) 4154 k8s_resource('foo:deployment:ns1', new_name='foo') 4155 `) 4156 4157 stack := fmt.Sprintf(`Traceback (most recent call last): 4158 %s:3:9: in <toplevel> 4159 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4160 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Deployment foo (Namespace: ns1)", stack).Error()) 4161 } 4162 4163 func TestDuplicateYAMLEntityInSingleWorkload(t *testing.T) { 4164 //Services corresponding to a deployment get pulled into the same resource. 4165 f := newFixture(t) 4166 4167 labelsFoo := map[string]string{"foo": "bar"} 4168 f.yaml("all.yaml", 4169 deployment("foo-deployment", image("gcr.io/foo1"), namespace("ns1"), withLabels(labelsFoo)), 4170 service("foo-service", withLabels(labelsFoo)), 4171 service("foo-service", withLabels(labelsFoo))) 4172 f.file("Tiltfile", ` 4173 k8s_yaml('all.yaml') 4174 `) 4175 4176 stack := fmt.Sprintf(`Traceback (most recent call last): 4177 %s:2:9: in <toplevel> 4178 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4179 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service foo-service", stack).Error()) 4180 } 4181 4182 func TestDuplicateYAMLEntityInUserAssembledNonWorkloadResource(t *testing.T) { 4183 f := newFixture(t) 4184 f.gitInit("") 4185 f.yaml("all.yaml", 4186 service("foo-service"), 4187 service("foo-service")) 4188 f.file("Tiltfile", ` 4189 k8s_yaml('all.yaml') 4190 k8s_resource(objects=['foo-service:Service:default'], new_name='my-services') 4191 `) 4192 4193 stack := fmt.Sprintf(`Traceback (most recent call last): 4194 %s:2:9: in <toplevel> 4195 <builtin>: in k8s_yaml`, f.JoinPath("Tiltfile")) 4196 f.loadErrString(tiltfile_k8s.DuplicateYAMLDetectedError("Service foo-service", stack).Error()) 4197 } 4198 4199 func TestSetTeamID(t *testing.T) { 4200 f := newFixture(t) 4201 4202 f.file("Tiltfile", "set_team('sharks')") 4203 f.load() 4204 4205 assert.Equal(t, "sharks", f.loadResult.TeamID) 4206 } 4207 4208 func TestSetTeamIDEmpty(t *testing.T) { 4209 f := newFixture(t) 4210 4211 f.file("Tiltfile", "set_team('')") 4212 f.loadErrString("team_id cannot be empty") 4213 } 4214 4215 func TestSetTeamIDMultiple(t *testing.T) { 4216 f := newFixture(t) 4217 4218 f.file("Tiltfile", ` 4219 set_team('sharks') 4220 set_team('jets') 4221 `) 4222 f.loadErrString("team_id set multiple times", "'sharks'", "'jets'") 4223 } 4224 4225 func TestK8SContextAcceptance(t *testing.T) { 4226 for _, test := range []struct { 4227 name string 4228 contextName k8s.KubeContext 4229 env clusterid.Product 4230 expectError bool 4231 expectedErrorSubstrings []string 4232 }{ 4233 {"minikube", "minikube", clusterid.ProductMinikube, false, nil}, 4234 {"docker-for-desktop", "docker-for-desktop", clusterid.ProductDockerDesktop, false, nil}, 4235 {"kind", "KIND", clusterid.ProductKIND, false, nil}, 4236 {"gke", "gke", clusterid.ProductGKE, true, []string{"'gke'", "If you're sure", "switch k8s contexts", "allow_k8s_contexts"}}, 4237 {"allowed", "allowed-context", clusterid.ProductGKE, false, nil}, 4238 } { 4239 t.Run(test.name, func(t *testing.T) { 4240 f := newFixture(t) 4241 4242 f.file("Tiltfile", ` 4243 k8s_yaml("foo.yaml") 4244 allow_k8s_contexts("allowed-context") 4245 `) 4246 f.setupFoo() 4247 4248 f.k8sContext = test.contextName 4249 f.k8sEnv = test.env 4250 if !test.expectError { 4251 f.load() 4252 } else { 4253 f.loadErrString(test.expectedErrorSubstrings...) 4254 } 4255 }) 4256 } 4257 } 4258 4259 // Test for fix to https://github.com/tilt-dev/tilt/issues/4234 4260 func TestCheckK8SContextWhenOnlyUncategorizedK8s(t *testing.T) { 4261 f := newFixture(t) 4262 4263 // We'll only have Uncategorized k8s entities, no K8s resources-- 4264 // make sure we still check K8sContext and throw an error if need be 4265 f.yaml("service.yaml", service("some-service")) 4266 4267 f.file("Tiltfile", ` 4268 k8s_yaml("service.yaml") 4269 allow_k8s_contexts("allowed-context") 4270 `) 4271 f.setupFoo() 4272 4273 f.k8sContext = "gke" 4274 f.k8sEnv = clusterid.ProductGKE 4275 4276 f.loadErrString("If you're sure", "switch k8s contexts", "allow_k8s_contexts") 4277 } 4278 4279 func TestLocalObeysAllowedK8sContexts(t *testing.T) { 4280 for _, test := range []struct { 4281 name string 4282 contextName k8s.KubeContext 4283 env clusterid.Product 4284 expectError bool 4285 expectedErrorSubstrings []string 4286 }{ 4287 {"gke", "gke", clusterid.ProductGKE, true, []string{"'gke'", "If you're sure", "switch k8s contexts", "allow_k8s_contexts"}}, 4288 {"allowed", "allowed-context", clusterid.ProductGKE, false, nil}, 4289 {"docker-compose", "unknown", k8s.ProductNone, false, nil}, 4290 } { 4291 t.Run(test.name, func(t *testing.T) { 4292 f := newFixture(t) 4293 4294 f.file("Tiltfile", ` 4295 allow_k8s_contexts("allowed-context") 4296 local('echo hi') 4297 `) 4298 f.setupFoo() 4299 4300 f.k8sContext = test.contextName 4301 f.k8sEnv = test.env 4302 if !test.expectError { 4303 f.load() 4304 } else { 4305 f.loadErrString(test.expectedErrorSubstrings...) 4306 } 4307 }) 4308 } 4309 } 4310 4311 func TestLocalResourceOnlyUpdateCmd(t *testing.T) { 4312 f := newFixture(t) 4313 4314 f.file("Tiltfile", ` 4315 local_resource("test", "echo hi", deps=["foo/bar", "foo/a.txt"]) 4316 `) 4317 4318 f.setupFoo() 4319 f.file(".gitignore", "*.txt") 4320 f.load() 4321 4322 f.assertNumManifests(1) 4323 path1 := "foo/bar" 4324 path2 := "foo/a.txt" 4325 m := f.assertNextManifest("test", localTarget(updateCmd(f.Path(), "echo hi", nil), deps(path1, path2)), fileChangeMatches("foo/a.txt")) 4326 4327 lt := m.LocalTarget() 4328 assert.Equal(t, []v1alpha1.IgnoreDef{ 4329 {BasePath: f.JoinPath(".git")}, 4330 }, lt.GetFileWatchIgnores()) 4331 4332 f.assertConfigFiles("Tiltfile", ".tiltignore") 4333 } 4334 4335 func TestLocalResourceOnlyServeCmd(t *testing.T) { 4336 f := newFixture(t) 4337 4338 f.file("Tiltfile", ` 4339 local_resource("test", serve_cmd="sleep 1000") 4340 `) 4341 4342 f.load() 4343 4344 f.assertNumManifests(1) 4345 f.assertNextManifest("test", localTarget(serveCmd(f.Path(), "sleep 1000", nil))) 4346 4347 f.assertConfigFiles("Tiltfile", ".tiltignore") 4348 } 4349 4350 func TestLocalResourceUpdateAndServeCmd(t *testing.T) { 4351 f := newFixture(t) 4352 4353 f.file("Tiltfile", ` 4354 local_resource("test", cmd="echo hi", serve_cmd="sleep 1000") 4355 `) 4356 4357 f.load() 4358 4359 f.assertNumManifests(1) 4360 f.assertNextManifest("test", localTarget( 4361 updateCmd(f.Path(), "echo hi", nil), 4362 serveCmd(f.Path(), "sleep 1000", nil), 4363 )) 4364 4365 f.assertConfigFiles("Tiltfile", ".tiltignore") 4366 } 4367 4368 func TestLocalResourceNeitherUpdateOrServeCmd(t *testing.T) { 4369 f := newFixture(t) 4370 4371 f.file("Tiltfile", ` 4372 local_resource("test") 4373 `) 4374 4375 f.loadErrString("local_resource must have a cmd and/or a serve_cmd, but both were empty") 4376 } 4377 4378 func TestLocalResourceUpdateCmdArray(t *testing.T) { 4379 f := newFixture(t) 4380 4381 f.file("Tiltfile", ` 4382 local_resource("test", ["echo", "hi"]) 4383 `) 4384 4385 f.load() 4386 f.assertNumManifests(1) 4387 f.assertNextManifest("test", localTarget(updateCmdArray(f.Path(), []string{"echo", "hi"}, nil))) 4388 } 4389 4390 func TestLocalResourceServeCmdArray(t *testing.T) { 4391 f := newFixture(t) 4392 4393 f.file("Tiltfile", ` 4394 local_resource("test", serve_cmd=["echo", "hi"]) 4395 `) 4396 4397 f.load() 4398 f.assertNumManifests(1) 4399 f.assertNextManifest("test", localTarget(serveCmdArray(f.Path(), []string{"echo", "hi"}, nil))) 4400 } 4401 4402 func TestLocalResourceWorkdir(t *testing.T) { 4403 f := newFixture(t) 4404 4405 f.file("nested/Tiltfile", ` 4406 local_resource("nested-local", "echo nested", deps=["foo/bar", "more_nested/repo"]) 4407 `) 4408 f.file("Tiltfile", ` 4409 include('nested/Tiltfile') 4410 local_resource("toplvl-local", "echo hello world", deps=["foo/baz", "foo/a.txt"]) 4411 `) 4412 4413 f.setupFoo() 4414 f.MkdirAll("nested/.git") 4415 f.MkdirAll("nested/more_nested/repo/.git") 4416 f.MkdirAll("foo/baz/.git") 4417 f.MkdirAll("foo/.git") // no Tiltfile lives here, nor is it a LocalResource dep; won't be pulled in as a repo 4418 f.load() 4419 4420 f.assertNumManifests(2) 4421 mNested := f.assertNextManifest("nested-local", 4422 localTarget(updateCmd(f.JoinPath("nested"), "echo nested", nil), 4423 deps("nested/foo/bar", "nested/more_nested/repo"))) 4424 4425 ltNested := mNested.LocalTarget() 4426 assert.Equal(t, []v1alpha1.IgnoreDef{ 4427 {BasePath: f.JoinPath("nested/more_nested/repo", ".git")}, 4428 {BasePath: f.JoinPath("nested", ".git")}, 4429 }, ltNested.GetFileWatchIgnores()) 4430 4431 mTop := f.assertNextManifest("toplvl-local", localTarget(updateCmd(f.Path(), "echo hello world", nil), deps("foo/baz", "foo/a.txt"))) 4432 ltTop := mTop.LocalTarget() 4433 assert.Equal(t, []v1alpha1.IgnoreDef{ 4434 {BasePath: f.JoinPath("foo/baz", ".git")}, 4435 {BasePath: f.JoinPath(".git")}, 4436 }, ltTop.GetFileWatchIgnores()) 4437 } 4438 4439 func TestLocalResourceIgnore(t *testing.T) { 4440 f := newFixture(t) 4441 4442 f.file(".dockerignore", "**/**.c") 4443 f.file("Tiltfile", "include('proj/Tiltfile')") 4444 f.file("proj/Tiltfile", ` 4445 local_resource("test", "echo hi", deps=["foo"], ignore=["**/*.a", "foo/bar.d"]) 4446 `) 4447 4448 f.setupFoo() 4449 f.file(".gitignore", "*.txt") 4450 f.load() 4451 4452 m := f.assertNextManifest("test") 4453 4454 // TODO(dmiller): I can't figure out how to translate these in to (file\build)(Matches\Filters) assert functions 4455 filter := ignore.CreateFileChangeFilter(m.LocalTarget().GetFileWatchIgnores()) 4456 4457 for _, tc := range []struct { 4458 path string 4459 expectMatch bool 4460 }{ 4461 {"proj/foo/bar.a", true}, 4462 {"proj/foo/bar.b", false}, 4463 {"proj/foo/baz/bar.a", true}, 4464 {"proj/foo/bar.d", true}, 4465 } { 4466 matches, err := filter.Matches(f.JoinPath(tc.path)) 4467 require.NoError(t, err) 4468 require.Equal(t, tc.expectMatch, matches, tc.path) 4469 } 4470 } 4471 4472 func TestLocalResourceUpdateCmdEnv(t *testing.T) { 4473 f := newFixture(t) 4474 4475 f.file("Tiltfile", ` 4476 local_resource("test", "echo hi", env={"KEY1": "value1", "KEY2": "value2"}, serve_cmd="sleep 1000") 4477 `) 4478 4479 f.load() 4480 f.assertNumManifests(1) 4481 f.assertNextManifest("test", localTarget( 4482 updateCmd(f.Path(), "echo hi", []string{"KEY1=value1", "KEY2=value2"}), 4483 serveCmd(f.Path(), "sleep 1000", nil), 4484 )) 4485 } 4486 4487 func TestLocalResourceServeCmdEnv(t *testing.T) { 4488 f := newFixture(t) 4489 4490 f.file("Tiltfile", ` 4491 local_resource("test", "echo hi", serve_cmd="sleep 1000", serve_env={"KEY1": "value1", "KEY2": "value2"}) 4492 `) 4493 4494 f.load() 4495 f.assertNumManifests(1) 4496 f.assertNextManifest("test", localTarget( 4497 updateCmd(f.Path(), "echo hi", nil), 4498 serveCmd(f.Path(), "sleep 1000", []string{"KEY1=value1", "KEY2=value2"}), 4499 )) 4500 } 4501 4502 func TestLocalResourceUpdateCmdDir(t *testing.T) { 4503 f := newFixture(t) 4504 4505 f.file("nested/inside.txt", "inside the nested directory") 4506 f.file("Tiltfile", ` 4507 local_resource("test", cmd="cat inside.txt", dir="nested") 4508 `) 4509 4510 f.load() 4511 f.assertNumManifests(1) 4512 f.assertNextManifest("test", localTarget( 4513 updateCmd(f.JoinPath(f.Path(), "nested"), "cat inside.txt", nil), 4514 )) 4515 } 4516 4517 func TestLocalResourceUpdateCmdDirNone(t *testing.T) { 4518 f := newFixture(t) 4519 4520 f.file("here.txt", "same level") 4521 f.file("Tiltfile", ` 4522 local_resource("test", cmd="cat here.txt", dir=None) 4523 `) 4524 4525 f.load() 4526 f.assertNumManifests(1) 4527 f.assertNextManifest("test", localTarget( 4528 updateCmd(f.Path(), "cat here.txt", nil), 4529 )) 4530 } 4531 4532 func TestLocalResourceServeCmdDir(t *testing.T) { 4533 f := newFixture(t) 4534 4535 f.file("nested/inside.txt", "inside the nested directory") 4536 f.file("Tiltfile", ` 4537 local_resource("test", serve_cmd="cat inside.txt", serve_dir="nested") 4538 `) 4539 4540 f.load() 4541 f.assertNumManifests(1) 4542 f.assertNextManifest("test", localTarget( 4543 serveCmd(f.JoinPath(f.Path(), "nested"), "cat inside.txt", nil), 4544 )) 4545 } 4546 4547 func TestLocalResourceServeCmdDirNone(t *testing.T) { 4548 f := newFixture(t) 4549 4550 f.file("here.txt", "same level") 4551 f.file("Tiltfile", ` 4552 local_resource("test", serve_cmd="cat here.txt", serve_dir=None) 4553 `) 4554 4555 f.load() 4556 f.assertNumManifests(1) 4557 f.assertNextManifest("test", localTarget( 4558 serveCmd(f.Path(), "cat here.txt", nil), 4559 )) 4560 } 4561 4562 func TestCustomBuildStoresTiltfilePath(t *testing.T) { 4563 f := newFixture(t) 4564 4565 f.file("Tiltfile", `include('proj/Tiltfile') 4566 k8s_yaml("foo.yaml")`) 4567 f.file("proj/Tiltfile", ` 4568 custom_build( 4569 'gcr.io/foo', 4570 'build.sh', 4571 ['foo'] 4572 ) 4573 `) 4574 f.file("proj/build.sh", "docker build -t $EXPECTED_REF gcr.io/foo") 4575 f.file("proj/Dockerfile", "FROM alpine") 4576 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 4577 4578 f.load() 4579 f.assertNumManifests(1) 4580 f.assertNextManifest("foo", cb( 4581 image("gcr.io/foo"), 4582 deps(f.JoinPath("proj/foo")), 4583 cmd("build.sh", f.JoinPath("proj")), 4584 )) 4585 } 4586 4587 func TestSecretString(t *testing.T) { 4588 f := newFixture(t) 4589 4590 f.file("secret.yaml", ` 4591 apiVersion: v1 4592 kind: Secret 4593 metadata: 4594 name: my-secret 4595 stringData: 4596 client-id: hello 4597 client-secret: world 4598 `) 4599 f.file("Tiltfile", ` 4600 k8s_yaml('secret.yaml') 4601 `) 4602 4603 f.load() 4604 4605 secrets := f.loadResult.Secrets 4606 assert.Equal(t, 2, len(secrets)) 4607 assert.Equal(t, "client-id", secrets["hello"].Key) 4608 assert.Equal(t, "hello", string(secrets["hello"].Value)) 4609 assert.Equal(t, "aGVsbG8=", string(secrets["hello"].ValueEncoded)) 4610 assert.Equal(t, "client-secret", secrets["world"].Key) 4611 assert.Equal(t, "world", string(secrets["world"].Value)) 4612 assert.Equal(t, "d29ybGQ=", string(secrets["world"].ValueEncoded)) 4613 } 4614 4615 func TestSecretBytes(t *testing.T) { 4616 f := newFixture(t) 4617 4618 f.file("secret.yaml", ` 4619 apiVersion: v1 4620 kind: Secret 4621 metadata: 4622 name: my-secret 4623 data: 4624 client-id: aGVsbG8= 4625 client-secret: d29ybGQ= 4626 `) 4627 f.file("Tiltfile", ` 4628 k8s_yaml('secret.yaml') 4629 `) 4630 4631 f.load() 4632 4633 secrets := f.loadResult.Secrets 4634 assert.Equal(t, 2, len(secrets)) 4635 assert.Equal(t, "client-id", secrets["hello"].Key) 4636 assert.Equal(t, "hello", string(secrets["hello"].Value)) 4637 assert.Equal(t, "aGVsbG8=", string(secrets["hello"].ValueEncoded)) 4638 assert.Equal(t, "client-secret", secrets["world"].Key) 4639 assert.Equal(t, "world", string(secrets["world"].Value)) 4640 assert.Equal(t, "d29ybGQ=", string(secrets["world"].ValueEncoded)) 4641 } 4642 4643 func TestSecretSettingsDisableScrub(t *testing.T) { 4644 f := newFixture(t) 4645 4646 f.file("secret.yaml", ` 4647 apiVersion: v1 4648 kind: Secret 4649 metadata: 4650 name: my-secret 4651 stringData: 4652 client-id: hello 4653 client-secret: world 4654 `) 4655 f.file("Tiltfile", ` 4656 k8s_yaml('secret.yaml') 4657 secret_settings(disable_scrub=True) 4658 `) 4659 4660 f.load() 4661 4662 secrets := f.loadResult.Secrets 4663 assert.Empty(t, secrets, "expect no secrets to be collected if scrubbing secrets is disabled") 4664 } 4665 4666 func TestDockerPruneSettings(t *testing.T) { 4667 f := newFixture(t) 4668 4669 f.file("Tiltfile", ` 4670 docker_prune_settings(max_age_mins=111, num_builds=222) 4671 `) 4672 4673 f.load() 4674 res := f.loadResult.DockerPruneSettings 4675 4676 assert.True(t, res.Enabled) 4677 assert.Equal(t, time.Minute*111, res.MaxAge) 4678 assert.Equal(t, 222, res.NumBuilds) 4679 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) // default 4680 } 4681 4682 func TestDockerPruneSettingsDefaultsWhenCalled(t *testing.T) { 4683 f := newFixture(t) 4684 4685 f.file("Tiltfile", ` 4686 docker_prune_settings(num_builds=123) 4687 `) 4688 4689 f.load() 4690 res := f.loadResult.DockerPruneSettings 4691 4692 assert.True(t, res.Enabled) 4693 assert.Equal(t, model.DockerPruneDefaultMaxAge, res.MaxAge) 4694 assert.Equal(t, 123, res.NumBuilds) 4695 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) 4696 } 4697 4698 func TestDockerPruneSettingsDefaultsWhenNotCalled(t *testing.T) { 4699 f := newFixture(t) 4700 4701 f.file("Tiltfile", ` 4702 print('nothing to see here') 4703 `) 4704 4705 f.load() 4706 res := f.loadResult.DockerPruneSettings 4707 4708 assert.True(t, res.Enabled) 4709 assert.Equal(t, model.DockerPruneDefaultMaxAge, res.MaxAge) 4710 assert.Equal(t, 0, res.NumBuilds) 4711 assert.Equal(t, model.DockerPruneDefaultInterval, res.Interval) 4712 } 4713 4714 func TestK8SDependsOn(t *testing.T) { 4715 f := newFixture(t) 4716 4717 f.setupFooAndBar() 4718 f.file("Tiltfile", ` 4719 docker_build('gcr.io/foo', 'foo') 4720 k8s_yaml('foo.yaml') 4721 4722 docker_build('gcr.io/bar', 'bar') 4723 k8s_yaml('bar.yaml') 4724 k8s_resource('bar', resource_deps=['foo']) 4725 `) 4726 4727 f.load() 4728 f.assertNextManifest("foo", resourceDeps()) 4729 f.assertNextManifest("bar", resourceDeps("foo")) 4730 } 4731 4732 func TestLocalDependsOn(t *testing.T) { 4733 f := newFixture(t) 4734 4735 f.file("Tiltfile", ` 4736 local_resource('foo', 'echo foo') 4737 local_resource('bar', 'echo bar', resource_deps=['foo']) 4738 `) 4739 4740 f.load() 4741 f.assertNextManifest("foo", resourceDeps()) 4742 f.assertNextManifest("bar", resourceDeps("foo")) 4743 } 4744 4745 func TestDependsOnMissingResource(t *testing.T) { 4746 f := newFixture(t) 4747 4748 f.file("Tiltfile", ` 4749 local_resource('baz', 'echo baz') 4750 local_resource('bar', 'echo bar', resource_deps=['foo', 'baz']) 4751 `) 4752 4753 f.loadAssertWarnings("resource bar specified a dependency on unknown resource foo - dependency ignored") 4754 f.assertNumManifests(2) 4755 f.assertNextManifest("baz", resourceDeps()) 4756 f.assertNextManifest("bar", resourceDeps("baz")) 4757 } 4758 4759 func TestDependsOnSelf(t *testing.T) { 4760 f := newFixture(t) 4761 4762 f.file("Tiltfile", ` 4763 local_resource('bar', 'echo bar', resource_deps=['bar']) 4764 `) 4765 4766 f.loadErrString("resource bar specified a dependency on itself") 4767 } 4768 4769 func TestDependsOnCycle(t *testing.T) { 4770 f := newFixture(t) 4771 4772 f.file("Tiltfile", ` 4773 local_resource('foo', 'echo foo', resource_deps=['baz']) 4774 local_resource('bar', 'echo bar', resource_deps=['foo']) 4775 local_resource('baz', 'echo baz', resource_deps=['bar']) 4776 `) 4777 4778 f.loadErrString("cycle detected in resource dependency graph", "bar -> foo", "foo -> baz", "baz -> bar") 4779 } 4780 4781 func TestDependsOnPulledInOnPartialLoad(t *testing.T) { 4782 for _, tc := range []struct { 4783 name string 4784 resourcesToLoad []model.ManifestName 4785 expected []model.ManifestName 4786 }{ 4787 { 4788 name: "a", 4789 resourcesToLoad: []model.ManifestName{"a"}, 4790 expected: []model.ManifestName{"a"}, 4791 }, 4792 { 4793 name: "c", 4794 resourcesToLoad: []model.ManifestName{"c"}, 4795 expected: []model.ManifestName{"a", "b", "c"}, 4796 }, 4797 { 4798 name: "d, e", 4799 resourcesToLoad: []model.ManifestName{"d", "e"}, 4800 expected: []model.ManifestName{"a", "b", "d", "e"}, 4801 }, 4802 { 4803 name: "e", 4804 resourcesToLoad: []model.ManifestName{"e"}, 4805 expected: []model.ManifestName{"e"}, 4806 }, 4807 } { 4808 t.Run(tc.name, func(t *testing.T) { 4809 f := newFixture(t) 4810 4811 f.file("Tiltfile", ` 4812 local_resource('a', 'echo a') 4813 local_resource('b', 'echo b', resource_deps=['a']) 4814 local_resource('c', 'echo c', resource_deps=['b']) 4815 local_resource('d', 'echo d', resource_deps=['b']) 4816 local_resource('e', 'echo e') 4817 `) 4818 4819 var args []string 4820 for _, r := range tc.resourcesToLoad { 4821 args = append(args, string(r)) 4822 } 4823 f.load(args...) 4824 require.Equal(t, tc.expected, f.loadResult.EnabledManifests) 4825 }) 4826 } 4827 } 4828 4829 func TestLocalResourceAllowParallel(t *testing.T) { 4830 f := newFixture(t) 4831 4832 f.file("Tiltfile", ` 4833 local_resource("a", ["echo", "hi"], allow_parallel=True) 4834 local_resource("b", ["echo", "hi"]) 4835 local_resource("c", serve_cmd=["echo", "hi"]) 4836 `) 4837 4838 f.load() 4839 a := f.assertNextManifest("a") 4840 assert.True(t, a.LocalTarget().AllowParallel) 4841 b := f.assertNextManifest("b") 4842 assert.False(t, b.LocalTarget().AllowParallel) 4843 4844 // local_resource serve_cmd is currently modeled as a no-op local cmd that 4845 // triggers a server restart. It's always OK for those no-op local cmds to 4846 // run in parallel. 4847 c := f.assertNextManifest("c") 4848 assert.True(t, c.LocalTarget().AllowParallel) 4849 } 4850 4851 func TestLocalResourceInvalidName(t *testing.T) { 4852 f := newFixture(t) 4853 4854 f.file("Tiltfile", ` 4855 local_resource("a/b", ["echo", "hi"]) 4856 `) 4857 4858 f.loadErrString( 4859 // Verify the validation message 4860 "invalid value \"a/b\": may not contain '/'", 4861 4862 // Verify the stack trace points to the local_resource 4863 "Tiltfile:2:15: in <toplevel>") 4864 } 4865 4866 func TestMaxParallelUpdates(t *testing.T) { 4867 for _, tc := range []struct { 4868 name string 4869 tiltfile string 4870 expectErrorContains string 4871 expectedMaxParallelUpdates int 4872 }{ 4873 { 4874 name: "default value if func not called", 4875 tiltfile: "print('hello world')", 4876 expectedMaxParallelUpdates: model.DefaultMaxParallelUpdates, 4877 }, 4878 { 4879 name: "default value if arg not specified", 4880 tiltfile: "update_settings(k8s_upsert_timeout_secs=123)", 4881 expectedMaxParallelUpdates: model.DefaultMaxParallelUpdates, 4882 }, 4883 { 4884 name: "set max parallel updates", 4885 tiltfile: "update_settings(max_parallel_updates=42)", 4886 expectedMaxParallelUpdates: 42, 4887 }, 4888 { 4889 name: "NaN error", 4890 tiltfile: "update_settings(max_parallel_updates='boop')", 4891 expectErrorContains: "got starlark.String, want int", 4892 }, 4893 { 4894 name: "must be positive int", 4895 tiltfile: "update_settings(max_parallel_updates=-1)", 4896 expectErrorContains: "must be >= 1", 4897 }, 4898 } { 4899 t.Run(tc.name, func(t *testing.T) { 4900 f := newFixture(t) 4901 4902 f.file("Tiltfile", tc.tiltfile) 4903 4904 if tc.expectErrorContains != "" { 4905 f.loadErrString(tc.expectErrorContains) 4906 return 4907 } 4908 4909 f.load() 4910 actualBuildSlots := f.loadResult.UpdateSettings.MaxParallelUpdates() 4911 assert.Equal(t, tc.expectedMaxParallelUpdates, actualBuildSlots, "expected vs. actual maxParallelUpdates") 4912 }) 4913 } 4914 } 4915 4916 func TestK8sUpsertTimeout(t *testing.T) { 4917 for _, tc := range []struct { 4918 name string 4919 tiltfile string 4920 expectErrorContains string 4921 expectedTimeout time.Duration 4922 }{ 4923 { 4924 name: "default value if func not called", 4925 tiltfile: "print('hello world')", 4926 expectedTimeout: v1alpha1.KubernetesApplyTimeoutDefault, 4927 }, 4928 { 4929 name: "default value if arg not specified", 4930 tiltfile: "update_settings(max_parallel_updates=123)", 4931 expectedTimeout: v1alpha1.KubernetesApplyTimeoutDefault, 4932 }, 4933 { 4934 name: "set max parallel updates", 4935 tiltfile: "update_settings(k8s_upsert_timeout_secs=42)", 4936 expectedTimeout: 42 * time.Second, 4937 }, 4938 { 4939 name: "NaN error", 4940 tiltfile: "update_settings(k8s_upsert_timeout_secs='boop')", 4941 expectErrorContains: "got starlark.String, want int", 4942 }, 4943 { 4944 name: "must be positive int", 4945 tiltfile: "update_settings(k8s_upsert_timeout_secs=-1)", 4946 expectErrorContains: "minimum k8s upsert timeout is 1s", 4947 }, 4948 } { 4949 t.Run(tc.name, func(t *testing.T) { 4950 f := newFixture(t) 4951 4952 f.file("Tiltfile", tc.tiltfile) 4953 4954 if tc.expectErrorContains != "" { 4955 f.loadErrString(tc.expectErrorContains) 4956 return 4957 } 4958 4959 f.load() 4960 actualTimeout := f.loadResult.UpdateSettings.K8sUpsertTimeout() 4961 assert.Equal(t, tc.expectedTimeout, actualTimeout, "expected vs. actual k8sUpsertTimeout") 4962 }) 4963 } 4964 } 4965 4966 func TestUpdateSettingsCalledTwice(t *testing.T) { 4967 f := newFixture(t) 4968 4969 f.file("Tiltfile", `update_settings(max_parallel_updates=123) 4970 update_settings(k8s_upsert_timeout_secs=456)`) 4971 4972 f.load() 4973 assert.Equal(t, 123, f.loadResult.UpdateSettings.MaxParallelUpdates(), "expected vs. actual MaxParallelUpdates") 4974 assert.Equal(t, 456*time.Second, f.loadResult.UpdateSettings.K8sUpsertTimeout(), "expected vs. actual k8sUpsertTimeout") 4975 } 4976 4977 // recursion is disabled by default in Starlark. Make sure we've enabled it for Tiltfiles. 4978 func TestRecursionEnabled(t *testing.T) { 4979 f := newFixture(t) 4980 4981 f.file("Tiltfile", ` 4982 def fact(n): 4983 if not n: 4984 return 1 4985 return n * fact(n - 1) 4986 4987 print("fact: %d" % (fact(10))) 4988 `) 4989 4990 f.load() 4991 4992 require.Contains(t, f.out.String(), fmt.Sprintf("fact: %d", 10*9*8*7*6*5*4*3*2*1)) 4993 } 4994 4995 func TestBuiltinAnalytics(t *testing.T) { 4996 f := newFixture(t) 4997 4998 // covering: 4999 // 1. a positional arg 5000 // 2. a keyword arg 5001 // 3. a mix of both for the same arg 5002 // 4. a builtin from a starkit plugin 5003 // 5. loading a Tilt plugin 5004 5005 f.file("Tiltfile", ` 5006 local('echo hi') 5007 local(command='echo hi') 5008 local('echo hi', quiet=True) 5009 allow_k8s_contexts("hello") 5010 load('ext://fooExt', 'printFoo') 5011 `) 5012 5013 // write the plugin locally so we don't need to deal with fake fetchers etc. 5014 f.WriteFile(f.JoinPath("tilt-extensions", "fooExt", "Tiltfile"), ` 5015 def printFoo(): 5016 print("foo") 5017 `) 5018 5019 f.load() 5020 5021 countEvent := f.SingleAnalyticsEvent("tiltfile.loaded") 5022 5023 // make sure it has all the expected builtin call counts 5024 expectedCounts := map[string]string{ 5025 "tiltfile.invoked.local": "3", 5026 "tiltfile.invoked.local.arg.command": "3", 5027 "tiltfile.invoked.local.arg.quiet": "1", 5028 "tiltfile.invoked.allow_k8s_contexts": "1", 5029 "tiltfile.invoked.allow_k8s_contexts.arg.contexts": "1", 5030 } 5031 5032 for k, v := range expectedCounts { 5033 require.Equal(t, v, countEvent.Tags[k], "count for %s", k) 5034 } 5035 5036 pluginEvent := f.SingleAnalyticsEvent("tiltfile.loaded.plugin") 5037 expectedTags := map[string]string{ 5038 "ext_name": "fooExt", 5039 "env": "docker-for-desktop", 5040 } 5041 require.Equal(t, expectedTags, pluginEvent.Tags) 5042 } 5043 5044 func TestCustomTagsReported(t *testing.T) { 5045 f := newFixture(t) 5046 5047 f.file("Tiltfile", ` 5048 experimental_analytics_report({'foo': 'bar'}) 5049 `) 5050 5051 f.load() 5052 5053 countEvent := f.SingleAnalyticsEvent("tiltfile.custom.report") 5054 5055 require.Equal(t, map[string]string{"foo": "bar"}, countEvent.Tags) 5056 } 5057 5058 func TestK8sResourceObjectsAddsNonWorkload(t *testing.T) { 5059 f := newFixture(t) 5060 5061 f.setupFoo() 5062 f.yaml("secret.yaml", secret("bar")) 5063 f.yaml("namespace.yaml", namespace("baz")) 5064 5065 f.file("Tiltfile", ` 5066 docker_build('gcr.io/foo', 'foo') 5067 k8s_yaml('foo.yaml') 5068 k8s_yaml('secret.yaml') 5069 k8s_yaml('namespace.yaml') 5070 k8s_resource('foo', objects=['bar', 'baz:namespace:default']) 5071 `) 5072 5073 f.load() 5074 5075 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("baz", "Namespace"), 5076 podReadiness(model.PodReadinessWait)) 5077 f.assertNoMoreManifests() 5078 } 5079 5080 func TestK8sResourceObjectsWithSameName(t *testing.T) { 5081 f := newFixture(t) 5082 5083 f.setupFoo() 5084 f.yaml("secret.yaml", secret("bar")) 5085 f.yaml("namespace.yaml", namespace("bar")) 5086 5087 f.file("Tiltfile", ` 5088 docker_build('gcr.io/foo', 'foo') 5089 k8s_yaml('foo.yaml') 5090 k8s_yaml('secret.yaml') 5091 k8s_yaml('namespace.yaml') 5092 k8s_resource('foo', objects=['bar', 'bar:namespace:default']) 5093 `) 5094 5095 f.loadErrString("\"bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5096 } 5097 5098 func TestK8sResourceObjectsCantIncludeSameObjectTwice(t *testing.T) { 5099 f := newFixture(t) 5100 5101 f.setupFoo() 5102 f.yaml("secret1.yaml", secret("bar")) 5103 f.yaml("secret2.yaml", secret("qux")) 5104 f.yaml("namespace.yaml", namespace("bar")) 5105 5106 f.file("Tiltfile", ` 5107 docker_build('gcr.io/foo', 'foo') 5108 k8s_yaml('foo.yaml') 5109 k8s_yaml('secret1.yaml') 5110 k8s_yaml('secret2.yaml') 5111 k8s_resource('foo', objects=['bar', 'bar:secret:default']) 5112 `) 5113 5114 f.loadErrString("No object identified by the fragment \"bar:secret:default\" could be found in remaining YAML. Valid remaining fragments are: \"qux:Secret:default\"") 5115 } 5116 5117 func TestK8sResourceObjectsMultipleAmbiguous(t *testing.T) { 5118 f := newFixture(t) 5119 5120 f.setupFoo() 5121 f.yaml("secret.yaml", secret("bar")) 5122 f.yaml("namespace.yaml", namespace("bar")) 5123 5124 f.file("Tiltfile", ` 5125 docker_build('gcr.io/foo', 'foo') 5126 k8s_yaml('foo.yaml') 5127 k8s_yaml('secret.yaml') 5128 k8s_yaml('namespace.yaml') 5129 k8s_resource('foo', objects=['bar', 'bar']) 5130 `) 5131 5132 f.loadErrString("bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5133 } 5134 5135 func TestK8sResourceObjectEmptySelector(t *testing.T) { 5136 f := newFixture(t) 5137 5138 f.setupFoo() 5139 f.yaml("secret.yaml", secret("bar")) 5140 f.yaml("namespace.yaml", namespace("baz")) 5141 5142 f.file("Tiltfile", ` 5143 docker_build('gcr.io/foo', 'foo') 5144 k8s_yaml('foo.yaml') 5145 k8s_yaml('secret.yaml') 5146 k8s_yaml('namespace.yaml') 5147 k8s_resource('foo', objects=['']) 5148 `) 5149 5150 f.loadErrString("Error making selector from string \"\"") 5151 } 5152 5153 func TestK8sResourceObjectInvalidSelector(t *testing.T) { 5154 f := newFixture(t) 5155 5156 f.setupFoo() 5157 f.yaml("secret.yaml", secret("bar")) 5158 f.yaml("namespace.yaml", namespace("baz")) 5159 5160 f.file("Tiltfile", ` 5161 docker_build('gcr.io/foo', 'foo') 5162 k8s_yaml('foo.yaml') 5163 k8s_yaml('secret.yaml') 5164 k8s_yaml('namespace.yaml') 5165 k8s_resource('foo', objects=['baz:namespace:default:wot']) 5166 `) 5167 5168 f.loadErrString("Error making selector from string \"baz:namespace:default:wot\"") 5169 } 5170 5171 func TestK8sResourceObjectSelectorWithEscapedColon(t *testing.T) { 5172 f := newFixture(t) 5173 5174 f.setupFoo() 5175 f.yaml("secret.yaml", secret("quu:bar")) 5176 f.yaml("namespace.yaml", namespace("baz")) 5177 5178 f.file("Tiltfile", ` 5179 docker_build('gcr.io/foo', 'foo') 5180 k8s_yaml('foo.yaml') 5181 k8s_yaml('secret.yaml') 5182 k8s_yaml('namespace.yaml') 5183 k8s_resource('foo', objects=['quu\\:bar', 'baz:namespace:default']) 5184 `) 5185 5186 f.load() 5187 5188 f.assertNextManifest("foo", deployment("foo"), k8sObject("quu:bar", "Secret"), k8sObject("baz", "Namespace"), 5189 podReadiness(model.PodReadinessWait)) 5190 f.assertNoMoreManifests() 5191 } 5192 5193 func TestK8sResourceObjectSelectorWithEscapedBackslash(t *testing.T) { 5194 f := newFixture(t) 5195 5196 f.setupFoo() 5197 f.yaml("secret.yaml", secret("quu\\bar")) 5198 f.yaml("namespace.yaml", namespace("baz")) 5199 5200 f.file("Tiltfile", ` 5201 docker_build('gcr.io/foo', 'foo') 5202 k8s_yaml('foo.yaml') 5203 k8s_yaml('secret.yaml') 5204 k8s_yaml('namespace.yaml') 5205 k8s_resource('foo', objects=['quu\\\\bar', 'baz:namespace:default']) 5206 `) 5207 5208 f.load() 5209 5210 f.assertNextManifest("foo", deployment("foo"), k8sObject("quu\\bar", "Secret"), k8sObject("baz", "Namespace"), 5211 podReadiness(model.PodReadinessWait)) 5212 f.assertNoMoreManifests() 5213 } 5214 5215 func TestK8sResourceObjectSelectorSuggestedObjectsAreEscaped(t *testing.T) { 5216 f := newFixture(t) 5217 5218 f.setupFoo() 5219 f.yaml("secret.yaml", secret("quu:bar")) 5220 f.yaml("namespace.yaml", namespace("baz")) 5221 5222 f.file("Tiltfile", ` 5223 docker_build('gcr.io/foo', 'foo') 5224 k8s_yaml('foo.yaml') 5225 k8s_yaml('secret.yaml') 5226 k8s_yaml('namespace.yaml') 5227 k8s_resource('foo', objects=['quu:bar', 'baz:namespace:default']) 5228 `) 5229 5230 f.loadErrString( 5231 `No object identified by the fragment "quu:bar" could be found`, 5232 `Possible objects are: "foo:Deployment:default", "quu\\:bar:Secret:default", "baz:Namespace:default"`, 5233 ) 5234 } 5235 5236 func TestK8sResourceObjectSelectorInvalidEscapeSequence(t *testing.T) { 5237 f := newFixture(t) 5238 5239 f.setupFoo() 5240 f.yaml("secret.yaml", secret("quu:bar")) 5241 f.yaml("namespace.yaml", namespace("baz")) 5242 5243 f.file("Tiltfile", ` 5244 docker_build('gcr.io/foo', 'foo') 5245 k8s_yaml('foo.yaml') 5246 k8s_yaml('secret.yaml') 5247 k8s_yaml('namespace.yaml') 5248 k8s_resource('foo', objects=['qu\\u:bar', 'baz:namespace:default']) 5249 `) 5250 5251 f.loadErrString( 5252 `invalid escape sequence '\u' in 'qu\u:b'`, 5253 ) 5254 } 5255 5256 func TestK8sResourceObjectIncludesSelectorThatDoesntExist(t *testing.T) { 5257 f := newFixture(t) 5258 5259 f.setupFoo() 5260 f.yaml("secret.yaml", secret("bar")) 5261 f.yaml("namespace.yaml", namespace("baz")) 5262 5263 f.file("Tiltfile", ` 5264 docker_build('gcr.io/foo', 'foo') 5265 k8s_yaml('foo.yaml') 5266 k8s_yaml('secret.yaml') 5267 k8s_yaml('namespace.yaml') 5268 k8s_resource('foo', objects=['baz:secret:default']) 5269 `) 5270 5271 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\"") 5272 } 5273 5274 func TestK8sResourceObjectsPartialNames(t *testing.T) { 5275 f := newFixture(t) 5276 5277 f.setupFoo() 5278 f.yaml("secret.yaml", secret("bar")) 5279 f.yaml("namespace.yaml", namespace("bar")) 5280 5281 f.file("Tiltfile", ` 5282 docker_build('gcr.io/foo', 'foo') 5283 k8s_yaml('foo.yaml') 5284 k8s_yaml('secret.yaml') 5285 k8s_yaml('namespace.yaml') 5286 k8s_resource('foo', objects=['bar:secret', 'bar:namespace']) 5287 `) 5288 5289 f.load() 5290 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("bar", "Namespace")) 5291 f.assertNoMoreManifests() 5292 } 5293 5294 func TestK8sResourcePrefixesShouldntMatch(t *testing.T) { 5295 f := newFixture(t) 5296 5297 f.setupFoo() 5298 f.yaml("secret.yaml", secret("bar")) 5299 5300 f.file("Tiltfile", ` 5301 docker_build('gcr.io/foo', 'foo') 5302 k8s_yaml('foo.yaml') 5303 k8s_yaml('secret.yaml') 5304 k8s_resource('foo', objects=['ba']) 5305 `) 5306 5307 f.loadErrString("No object identified by the fragment \"ba\" could be found. Possible objects are: \"foo:Deployment:default\", \"bar:Secret:default\"") 5308 } 5309 5310 func TestK8sResourceAmbiguousSelector(t *testing.T) { 5311 f := newFixture(t) 5312 5313 f.setupFoo() 5314 f.yaml("secret.yaml", secret("bar")) 5315 f.yaml("namespace.yaml", namespace("bar")) 5316 5317 f.file("Tiltfile", ` 5318 docker_build('gcr.io/foo', 'foo') 5319 k8s_yaml('foo.yaml') 5320 k8s_yaml('secret.yaml') 5321 k8s_yaml('namespace.yaml') 5322 k8s_resource('foo', objects=['bar']) 5323 `) 5324 5325 f.loadErrString("\"bar\" is not a unique fragment. Objects that match \"bar\" are \"bar:Secret:default\", \"bar:Namespace:default\"") 5326 } 5327 5328 func TestK8sResourceObjectDuplicate(t *testing.T) { 5329 f := newFixture(t) 5330 5331 f.setupFoo() 5332 f.yaml("secret.yaml", secret("bar")) 5333 f.yaml("anotherworkload.yaml", deployment("baz")) 5334 5335 f.file("Tiltfile", ` 5336 docker_build('gcr.io/foo', 'foo') 5337 k8s_yaml('foo.yaml') 5338 k8s_yaml('anotherworkload.yaml') 5339 k8s_yaml('secret.yaml') 5340 k8s_resource('foo', objects=['bar']) 5341 k8s_resource('baz', objects=['bar']) 5342 `) 5343 5344 f.loadErrString("No object identified by the fragment \"bar\" could be found in remaining YAML. Valid remaining fragments are:") 5345 } 5346 5347 func TestK8sResourceObjectMultipleResources(t *testing.T) { 5348 f := newFixture(t) 5349 5350 f.setupFoo() 5351 f.yaml("secret.yaml", secret("bar")) 5352 f.yaml("namespace.yaml", namespace("qux")) 5353 f.yaml("anotherworkload.yaml", deployment("baz")) 5354 5355 f.file("Tiltfile", ` 5356 docker_build('gcr.io/foo', 'foo') 5357 k8s_yaml('foo.yaml') 5358 k8s_yaml('secret.yaml') 5359 k8s_yaml('namespace.yaml') 5360 k8s_yaml('anotherworkload.yaml') 5361 k8s_resource('foo', objects=['bar']) 5362 k8s_resource('baz') 5363 `) 5364 5365 f.load() 5366 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret")) 5367 f.assertNextManifest("baz", deployment("baz")) 5368 f.assertNextManifestUnresourced("qux") 5369 f.assertNoMoreManifests() 5370 } 5371 5372 func TestMultipleResourcesMultipleObjects(t *testing.T) { 5373 f := newFixture(t) 5374 5375 f.setupFoo() 5376 f.yaml("secret.yaml", secret("bar")) 5377 f.yaml("namespace.yaml", namespace("qux")) 5378 f.yaml("anotherworkload.yaml", deployment("baz")) 5379 5380 f.file("Tiltfile", ` 5381 docker_build('gcr.io/foo', 'foo') 5382 k8s_yaml('foo.yaml') 5383 k8s_yaml('secret.yaml') 5384 k8s_yaml('namespace.yaml') 5385 k8s_yaml('anotherworkload.yaml') 5386 k8s_resource('foo', objects=['bar']) 5387 k8s_resource('baz', objects=['qux']) 5388 `) 5389 5390 f.load() 5391 f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret")) 5392 f.assertNextManifest("baz", deployment("baz"), namespace("qux")) 5393 f.assertNoMoreManifests() 5394 } 5395 5396 func TestK8sResourceAmbiguousWorkloadAmbiguousObject(t *testing.T) { 5397 f := newFixture(t) 5398 5399 f.setupFoo() 5400 f.yaml("secret.yaml", secret("foo")) 5401 5402 f.file("Tiltfile", ` 5403 docker_build('gcr.io/foo', 'foo') 5404 k8s_yaml('foo.yaml') 5405 k8s_yaml('secret.yaml') 5406 k8s_resource('foo', objects=['foo']) 5407 `) 5408 5409 f.loadErrString("\"foo\" is not a unique fragment. Objects that match \"foo\" are \"foo:Deployment:default\", \"foo:Secret:default\"") 5410 } 5411 5412 func TestK8sResourceObjectsWithWorkloadToResourceFunction(t *testing.T) { 5413 f := newFixture(t) 5414 5415 f.setupFoo() 5416 f.yaml("secret.yaml", secret("foo")) 5417 5418 f.file("Tiltfile", ` 5419 docker_build('gcr.io/foo', 'foo') 5420 k8s_yaml('foo.yaml') 5421 k8s_yaml('secret.yaml') 5422 def wtrf(id): 5423 return 'hello-' + id.name 5424 workload_to_resource_function(wtrf) 5425 k8s_resource('hello-foo', objects=['foo:secret']) 5426 `) 5427 5428 f.load() 5429 f.assertNumManifests(1) 5430 f.assertNextManifest("hello-foo", k8sObject("foo", "Secret")) 5431 f.assertNoMoreManifests() 5432 } 5433 5434 func TestK8sResourceNewNameWithoutObjects(t *testing.T) { 5435 f := newFixture(t) 5436 5437 f.file("Tiltfile", ` 5438 k8s_resource(new_name='foo') 5439 `) 5440 5441 f.loadErrString("k8s_resource doesn't specify a workload or any objects") 5442 } 5443 5444 func TestK8sResourceObjectsWithGroup(t *testing.T) { 5445 f := newFixture(t) 5446 5447 f.setupFoo() 5448 f.yaml("secret.yaml", secret("bar")) 5449 f.yaml("namespace.yaml", namespace("baz")) 5450 5451 f.file("Tiltfile", ` 5452 docker_build('gcr.io/foo', 'foo') 5453 k8s_yaml('foo.yaml') 5454 k8s_yaml('secret.yaml') 5455 k8s_yaml('namespace.yaml') 5456 k8s_resource('foo', objects=['bar', 'baz:namespace:default:core']) 5457 `) 5458 5459 // TODO(dmiller): see comment on fullNameFromK8sEntity for info on why we don't support specifying group right now 5460 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") 5461 // f.assertNextManifest("foo", deployment("foo"), k8sObject("bar", "Secret"), k8sObject("baz", "Namespace")) 5462 // f.assertNoMoreManifests() 5463 } 5464 5465 func TestK8sResourceObjectClusterScoped(t *testing.T) { 5466 f := newFixture(t) 5467 5468 f.setupFoo() 5469 f.yaml("namespace.yaml", namespace("baz")) 5470 5471 f.file("Tiltfile", ` 5472 docker_build('gcr.io/foo', 'foo') 5473 k8s_yaml('foo.yaml') 5474 k8s_yaml('namespace.yaml') 5475 k8s_resource('foo', objects=['baz:namespace']) 5476 `) 5477 5478 f.load() 5479 5480 f.assertNextManifest("foo", deployment("foo"), k8sObject("baz", "Namespace")) 5481 f.assertNoMoreManifests() 5482 } 5483 5484 // TODO(dmiller): I'm not sure if this makes sense ... cluster scoped things like namespaces _can't_ have 5485 // namespaces, so should we allow you to specify namespaces for them? 5486 // For now we just leave them as "default" 5487 func TestK8sResourceObjectClusterScopedWithNamespace(t *testing.T) { 5488 f := newFixture(t) 5489 5490 f.setupFoo() 5491 f.yaml("namespace.yaml", namespace("baz")) 5492 5493 f.file("Tiltfile", ` 5494 docker_build('gcr.io/foo', 'foo') 5495 k8s_yaml('foo.yaml') 5496 k8s_yaml('namespace.yaml') 5497 k8s_resource('foo', objects=['baz:namespace:qux']) 5498 `) 5499 5500 f.loadErrString("No object identified by the fragment \"baz:namespace:qux\" could be found. Possible objects are: \"foo:Deployment:default\", \"baz:Namespace:default\"") 5501 } 5502 5503 func TestK8sResourceObjectsNonWorkloadOnly(t *testing.T) { 5504 f := newFixture(t) 5505 5506 f.yaml("secret.yaml", secret("bar")) 5507 f.yaml("namespace.yaml", namespace("baz")) 5508 5509 f.file("Tiltfile", ` 5510 k8s_yaml('secret.yaml') 5511 k8s_yaml('namespace.yaml') 5512 k8s_resource(new_name='foo', objects=['bar', 'baz:namespace:default']) 5513 `) 5514 5515 f.load() 5516 5517 f.assertNextManifest("foo", k8sObject("bar", "Secret"), k8sObject("baz", "Namespace"), podReadiness(model.PodReadinessIgnore)) 5518 f.assertNoMoreManifests() 5519 } 5520 5521 func TestK8sResourceNewNameAdditive(t *testing.T) { 5522 f := newFixture(t) 5523 5524 f.yaml("a.yaml", namespace("a")) 5525 f.yaml("b.yaml", namespace("b")) 5526 5527 f.file("Tiltfile", ` 5528 k8s_yaml('a.yaml') 5529 k8s_yaml('b.yaml') 5530 k8s_resource(new_name='namespaces', objects=['a']) 5531 k8s_resource('namespaces', objects=['b']) 5532 `) 5533 5534 f.load() 5535 f.assertNextManifest("namespaces", k8sObject("a", "Namespace"), k8sObject("b", "Namespace")) 5536 } 5537 5538 func TestK8sExistingResourceAdditive(t *testing.T) { 5539 f := newFixture(t) 5540 5541 f.yaml("a.yaml", deployment("a")) 5542 f.yaml("b.yaml", namespace("b")) 5543 f.yaml("c.yaml", namespace("c")) 5544 5545 f.file("Tiltfile", ` 5546 k8s_yaml('a.yaml') 5547 k8s_yaml('b.yaml') 5548 k8s_yaml('c.yaml') 5549 k8s_resource('a', objects=['b']) 5550 k8s_resource('a', objects=['c']) 5551 `) 5552 5553 f.load() 5554 f.assertNextManifest("a", 5555 k8sObject("a", "Deployment"), k8sObject("b", "Namespace"), k8sObject("c", "Namespace")) 5556 } 5557 5558 func TestK8sExistingResourceNewNameAdditive(t *testing.T) { 5559 f := newFixture(t) 5560 5561 // this was working non-deterministically based on hashtable order, so generate a bunch of resources 5562 // to reduce the chance of false positives 5563 // https://github.com/tilt-dev/tilt/issues/4808 5564 for i := 1; i <= 25; i++ { 5565 f.yaml(fmt.Sprintf("deploy%d.yaml", i), deployment(fmt.Sprintf("deploy%d", i))) 5566 } 5567 5568 f.file("Tiltfile", ` 5569 for i in range(1, 26): 5570 k8s_yaml('deploy%d.yaml' % (i)) 5571 k8s_resource('deploy%d' % (i), new_name='deploy%d-renamed' % (i), labels=['a']) 5572 k8s_resource('deploy%d-renamed' % (i), labels=['b']) 5573 `) 5574 5575 f.load() 5576 for i := 1; i <= 25; i++ { 5577 f.assertNextManifest(model.ManifestName(fmt.Sprintf("deploy%d-renamed", i)), 5578 k8sObject(fmt.Sprintf("deploy%d", i), "Deployment"), resourceLabels("a", "b")) 5579 } 5580 } 5581 5582 func TestK8sExistingResourceNewNameAlreadyTaken(t *testing.T) { 5583 f := newFixture(t) 5584 5585 f.yaml("a.yaml", deployment("a")) 5586 f.yaml("b.yaml", namespace("b")) 5587 f.yaml("c.yaml", namespace("c")) 5588 5589 f.file("Tiltfile", ` 5590 k8s_yaml('a.yaml') 5591 k8s_yaml('b.yaml') 5592 k8s_yaml('c.yaml') 5593 k8s_resource('a', objects=['b']) 5594 k8s_resource(new_name='a', objects=['c']) 5595 `) 5596 5597 f.loadErrString(`k8s_resource named "a" already exists`) 5598 } 5599 5600 func TestK8sNonWorkloadOnlyResourceWithAllTheOptions(t *testing.T) { 5601 f := newFixture(t) 5602 5603 f.setupFoo() 5604 f.yaml("secret.yaml", secret("bar")) 5605 f.yaml("namespace.yaml", namespace("baz")) 5606 5607 f.file("Tiltfile", ` 5608 docker_build('gcr.io/foo', 'foo') 5609 k8s_yaml('foo.yaml') 5610 k8s_yaml('secret.yaml') 5611 k8s_yaml('namespace.yaml') 5612 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']) 5613 `) 5614 5615 f.load() 5616 5617 f.assertNextManifest("foo") 5618 f.assertNextManifest("bar", k8sObject("bar", "Secret"), k8sObject("baz", "Namespace")) 5619 f.assertNoMoreManifests() 5620 } 5621 5622 func TestK8sResourceEmptyWorkloadSpecifierAndNoObjects(t *testing.T) { 5623 f := newFixture(t) 5624 5625 f.setupFoo() 5626 5627 f.file("Tiltfile", ` 5628 5629 k8s_yaml('foo.yaml') 5630 k8s_resource('', port_forwards=8000) 5631 `) 5632 5633 f.loadErrString("Resource name missing. Must give a name for an existing resource or a new_name to create a new resource.") 5634 } 5635 5636 func TestK8sResourceNonWorkloadRequiresNewName(t *testing.T) { 5637 f := newFixture(t) 5638 5639 f.yaml("secret.yaml", secret("bar")) 5640 f.yaml("namespace.yaml", namespace("baz")) 5641 5642 f.file("Tiltfile", ` 5643 k8s_yaml('secret.yaml') 5644 k8s_yaml('namespace.yaml') 5645 k8s_resource(objects=['bar', 'baz:namespace:default']) 5646 `) 5647 5648 f.loadErrString("Resource name missing. Must give a name for an existing resource or a new_name to create a new resource.") 5649 } 5650 5651 func TestK8sResourceNewNameCantOverwriteWorkload(t *testing.T) { 5652 f := newFixture(t) 5653 5654 f.setupFoo() 5655 f.yaml("secret.yaml", secret("bar")) 5656 5657 f.file("Tiltfile", ` 5658 k8s_yaml('foo.yaml') 5659 k8s_yaml('secret.yaml') 5660 k8s_resource('foo', new_name='bar') 5661 k8s_resource(new_name='bar', objects=['bar:secret']) 5662 `) 5663 5664 // NOTE(dmiller): because `range`ing over maps is unstable we don't know which error we will encounter: 5665 // 1. Trying to create a non-workload resource when a resource by that name already exists 5666 // 2. Trying to rename a resource to a name that already exists 5667 // so we match a string that appears in both error messages 5668 f.loadErrString("already exists") 5669 } 5670 5671 func TestK8sResourceObjectsNonAmbiguousDefaultNamespace(t *testing.T) { 5672 f := newFixture(t) 5673 5674 f.file("serving-core.yaml", testyaml.KnativeServingCore) 5675 5676 f.file("Tiltfile", ` 5677 k8s_yaml([ 5678 'serving-core.yaml', 5679 ]) 5680 5681 k8s_resource( 5682 objects=[ 5683 'queue-proxy:Image', 5684 ], 5685 new_name='knative-gateways') 5686 `) 5687 5688 f.load() 5689 f.assertNextManifest("knative-gateways") 5690 f.assertNoMoreManifests() 5691 } 5692 5693 func TestK8sResourceObjectsAreNotCaseSensitive(t *testing.T) { 5694 f := newFixture(t) 5695 5696 f.file("serving-core.yaml", testyaml.KnativeServingCore) 5697 5698 f.file("Tiltfile", ` 5699 k8s_yaml([ 5700 'serving-core.yaml', 5701 ]) 5702 5703 k8s_resource( 5704 objects=[ 5705 'queue-proxy:image', 5706 ], 5707 new_name='knative-gateways') 5708 `) 5709 5710 f.load() 5711 f.assertNextManifest("knative-gateways") 5712 f.assertNoMoreManifests() 5713 } 5714 5715 func TestK8sResourceLabels(t *testing.T) { 5716 f := newFixture(t) 5717 5718 f.setupFoo() 5719 5720 f.file("Tiltfile", ` 5721 k8s_yaml('foo.yaml') 5722 k8s_resource('foo', labels="test") 5723 `) 5724 5725 f.load() 5726 f.assertNumManifests(1) 5727 f.assertNextManifest("foo", resourceLabels("test")) 5728 } 5729 5730 func TestK8sResourceLabelsAppend(t *testing.T) { 5731 f := newFixture(t) 5732 5733 f.setupFoo() 5734 5735 f.file("Tiltfile", ` 5736 k8s_yaml('foo.yaml') 5737 k8s_resource('foo', labels="test") 5738 k8s_resource('foo', labels="test2") 5739 `) 5740 5741 f.load() 5742 f.assertNumManifests(1) 5743 f.assertNextManifest("foo", resourceLabels("test", "test2")) 5744 } 5745 5746 func TestLocalResourceLabels(t *testing.T) { 5747 f := newFixture(t) 5748 5749 f.file("Tiltfile", ` 5750 local_resource("test", cmd="echo hi", labels="foo") 5751 local_resource("test2", cmd="echo hi2", labels=["bar", "baz"]) 5752 `) 5753 5754 f.load() 5755 f.assertNumManifests(2) 5756 f.assertNextManifest("test", resourceLabels("foo")) 5757 f.assertNextManifest("test2", resourceLabels("bar", "baz")) 5758 } 5759 5760 // https://github.com/tilt-dev/tilt/issues/5467 5761 func TestLoadErrorWithArgs(t *testing.T) { 5762 f := newFixture(t) 5763 5764 f.file("Tiltfile", "asdf") 5765 f.loadArgsErrString([]string{"foo"}, "undefined: asdf") 5766 } 5767 5768 func TestContentsChangedTag(t *testing.T) { 5769 f := newFixture(t) 5770 5771 f.file("Tiltfile", "print('Hello')") 5772 tiltfile := ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), []string{}) 5773 loader := f.newTiltfileLoader() 5774 5775 // *.changed = false on first load (no previous hash values) 5776 tlr := loader.Load(f.ctx, tiltfile, nil) 5777 assert.Equal(t, "0d4b93146f79968657afdad8b23d423973bf7a7e97690d146e6b6cfcc24e617e", tlr.Hashes.TiltfileSHA256) 5778 assert.Equal(t, "0d4b93146f79968657afdad8b23d423973bf7a7e97690d146e6b6cfcc24e617e", tlr.Hashes.AllFilesSHA256) 5779 5780 event := f.SingleAnalyticsEvent("tiltfile.loaded") 5781 assert.Equal(t, "false", event.Tags["tiltfile.changed"]) 5782 assert.Equal(t, "false", event.Tags["allfiles.changed"]) 5783 5784 // *.changed = true because hash values differ 5785 f.an.Counts = []analytics.CountEvent{} 5786 tlr.Hashes = hasher.Hashes{TiltfileSHA256: "abc123", AllFilesSHA256: "abc123"} 5787 tlr = loader.Load(f.ctx, tiltfile, &tlr) 5788 event = f.SingleAnalyticsEvent("tiltfile.loaded") 5789 assert.Equal(t, "true", event.Tags["tiltfile.changed"]) 5790 assert.Equal(t, "true", event.Tags["allfiles.changed"]) 5791 5792 // *.changed = false because hash values match 5793 f.an.Counts = []analytics.CountEvent{} 5794 tlr = loader.Load(f.ctx, tiltfile, &tlr) 5795 event = f.SingleAnalyticsEvent("tiltfile.loaded") 5796 assert.Equal(t, "false", event.Tags["tiltfile.changed"]) 5797 assert.Equal(t, "false", event.Tags["allfiles.changed"]) 5798 } 5799 5800 type fixture struct { 5801 ctx context.Context 5802 out *bytes.Buffer 5803 t *testing.T 5804 *tempdir.TempDirFixture 5805 k8sContext k8s.KubeContext 5806 k8sNamespace k8s.Namespace 5807 k8sEnv clusterid.Product 5808 webHost model.WebHost 5809 5810 ta *tiltanalytics.TiltAnalytics 5811 an *analytics.MemoryAnalytics 5812 5813 loadResult TiltfileLoadResult 5814 warnings []string 5815 features feature.Defaults 5816 } 5817 5818 func (f *fixture) newTiltfileLoader() TiltfileLoader { 5819 dcc := dockercompose.NewDockerComposeClient(docker.LocalEnv{}) 5820 5821 k8sContextPlugin := k8scontext.NewPlugin(f.k8sContext, f.k8sNamespace, f.k8sEnv) 5822 versionPlugin := version.NewPlugin(model.TiltBuild{Version: "0.5.0"}) 5823 configPlugin := config.NewPlugin("up") 5824 localKubeconfigPath := localexec.KubeconfigPathOnce(func() string { 5825 return "/path/to/kubeconfig.yaml" 5826 }) 5827 localEnv := localexec.DefaultEnv(12345, f.webHost, localKubeconfigPath) 5828 execer := localexec.NewProcessExecer(localEnv) 5829 extr := tiltextension.NewFakeExtReconciler(f.Path()) 5830 extrr := tiltextension.NewFakeExtRepoReconciler(f.Path()) 5831 extPlugin := tiltextension.NewFakePlugin(extrr, extr) 5832 ciSettingsPlugin := cisettings.NewPlugin(0) 5833 return ProvideTiltfileLoader(f.ta, k8sContextPlugin, versionPlugin, configPlugin, 5834 extPlugin, ciSettingsPlugin, dcc, f.webHost, execer, f.features, f.k8sEnv) 5835 } 5836 5837 func newFixture(t *testing.T) *fixture { 5838 out := new(bytes.Buffer) 5839 ctx, ma, ta := testutils.ForkedCtxAndAnalyticsForTest(out) 5840 f := tempdir.NewTempDirFixture(t) 5841 f.Chdir() 5842 5843 // copy the features to avoid unintentional mutation by tests 5844 features := make(feature.Defaults) 5845 for k, v := range feature.MainDefaults { 5846 features[k] = v 5847 } 5848 5849 r := &fixture{ 5850 ctx: ctx, 5851 out: out, 5852 t: t, 5853 TempDirFixture: f, 5854 an: ma, 5855 ta: ta, 5856 k8sContext: "fake-context", 5857 k8sNamespace: "fake-namespace", 5858 k8sEnv: clusterid.ProductDockerDesktop, 5859 features: features, 5860 } 5861 5862 // Collect the warnings 5863 l := logger.NewFuncLogger(false, logger.DebugLvl, func(level logger.Level, fields logger.Fields, msg []byte) error { 5864 if level == logger.WarnLvl { 5865 r.warnings = append(r.warnings, string(msg)) 5866 } 5867 out.Write(msg) 5868 return nil 5869 }) 5870 r.ctx = logger.WithLogger(r.ctx, l) 5871 5872 return r 5873 } 5874 5875 func (f *fixture) file(path string, contents string) { 5876 f.WriteFile(path, contents) 5877 } 5878 5879 type k8sOpts interface{} 5880 5881 func (f *fixture) dockerfile(path string) { 5882 f.file(path, simpleDockerfile) 5883 } 5884 5885 func (f *fixture) dockerignore(path string) { 5886 f.file(path, simpleDockerignore) 5887 } 5888 5889 func (f *fixture) yaml(path string, entities ...k8sOpts) { 5890 var entityObjs []k8s.K8sEntity 5891 5892 for _, e := range entities { 5893 switch e := e.(type) { 5894 case deploymentHelper: 5895 s := testyaml.SnackYaml 5896 if e.image != "" { 5897 s = strings.ReplaceAll(s, testyaml.SnackImage, e.image) 5898 } 5899 s = strings.ReplaceAll(s, testyaml.SnackName, e.name) 5900 objs, err := k8s.ParseYAMLFromString(s) 5901 if err != nil { 5902 f.t.Fatal(err) 5903 } 5904 5905 if len(e.templateLabels) > 0 { 5906 for i, obj := range objs { 5907 withLabels, err := k8s.OverwriteLabels(obj, model.ToLabelPairs(e.templateLabels)) 5908 if err != nil { 5909 f.t.Fatal(err) 5910 } 5911 objs[i] = withLabels 5912 } 5913 } 5914 5915 for i, obj := range objs { 5916 de := obj.Obj.(*appsv1.Deployment) 5917 for i, c := range de.Spec.Template.Spec.Containers { 5918 for _, ev := range e.envVars { 5919 c.Env = append(c.Env, v1.EnvVar{ 5920 Name: ev.name, 5921 Value: ev.value, 5922 }) 5923 } 5924 de.Spec.Template.Spec.Containers[i] = c 5925 } 5926 if e.namespace != "" { 5927 de.Namespace = e.namespace 5928 } 5929 obj.Obj = de 5930 objs[i] = obj 5931 } 5932 5933 entityObjs = append(entityObjs, objs...) 5934 case serviceHelper: 5935 s := testyaml.DoggosServiceYaml 5936 s = strings.ReplaceAll(s, testyaml.DoggosName, e.name) 5937 objs, err := k8s.ParseYAMLFromString(s) 5938 if err != nil { 5939 f.t.Fatal(err) 5940 } 5941 5942 if e.selectorLabels != nil { 5943 for _, obj := range objs { 5944 err := overwriteSelectorsForService(&obj, e.selectorLabels) 5945 if err != nil { 5946 f.t.Fatal(err) 5947 } 5948 } 5949 } 5950 5951 entityObjs = append(entityObjs, objs...) 5952 5953 case secretHelper: 5954 s := testyaml.SecretYaml 5955 s = strings.ReplaceAll(s, testyaml.SecretName, e.name) 5956 objs, err := k8s.ParseYAMLFromString(s) 5957 if err != nil { 5958 f.t.Fatal(err) 5959 } 5960 5961 entityObjs = append(entityObjs, objs...) 5962 case namespaceHelper: 5963 s := testyaml.MyNamespaceYAML 5964 s = strings.ReplaceAll(s, testyaml.MyNamespaceName, e.namespace) 5965 objs, err := k8s.ParseYAMLFromString(s) 5966 if err != nil { 5967 f.t.Fatal(err) 5968 } 5969 entityObjs = append(entityObjs, objs...) 5970 default: 5971 f.t.Fatalf("unexpected entity %T %v", e, e) 5972 } 5973 } 5974 5975 s, err := k8s.SerializeSpecYAML(entityObjs) 5976 if err != nil { 5977 f.t.Fatal(err) 5978 } 5979 f.file(path, s) 5980 } 5981 5982 // Default load. Fails if there are any warnings. 5983 func (f *fixture) load(args ...string) { 5984 f.t.Helper() 5985 f.loadAllowWarnings(args...) 5986 if len(f.warnings) != 0 { 5987 f.t.Fatalf("Unexpected warnings. Actual: %s", f.warnings) 5988 } 5989 } 5990 5991 // Load the manifests, expecting warnings. 5992 // Warnings should be asserted later with assertWarnings 5993 func (f *fixture) loadAllowWarnings(args ...string) { 5994 f.t.Helper() 5995 tlr := f.newTiltfileLoader().Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), args), nil) 5996 err := tlr.Error 5997 if err != nil { 5998 f.t.Fatal(err) 5999 } 6000 f.loadResult = tlr 6001 require.NoError(f.t, model.InferImageProperties(f.loadResult.Manifests)) 6002 } 6003 6004 func unusedImageWarning(unusedImage string, suggestedImages []string, configType string) string { 6005 ret := fmt.Sprintf("Image not used in any %s config:\n ✕ %s", configType, unusedImage) 6006 if len(suggestedImages) > 0 { 6007 ret += "\nDid you mean…" 6008 for _, s := range suggestedImages { 6009 ret += fmt.Sprintf("\n - %s", s) 6010 } 6011 } 6012 ret += "\nSkipping this image build" 6013 ret += fmt.Sprintf("\nIf this is deliberate, suppress this warning with: update_settings(suppress_unused_image_warnings=[%q])", unusedImage) 6014 return ret 6015 } 6016 6017 // Load the manifests, expecting warnings. 6018 func (f *fixture) loadAssertWarnings(warnings ...string) { 6019 f.loadAllowWarnings() 6020 f.assertWarnings(warnings...) 6021 } 6022 6023 func (f *fixture) loadErrString(msgs ...string) { 6024 f.loadArgsErrString(nil, msgs...) 6025 } 6026 6027 func (f *fixture) loadArgsErrString(args []string, msgs ...string) { 6028 f.t.Helper() 6029 tlr := f.newTiltfileLoader().Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), args), nil) 6030 err := tlr.Error 6031 6032 if err == nil { 6033 f.t.Fatalf("expected error but got nil") 6034 } 6035 f.loadResult = tlr 6036 errText := err.Error() 6037 6038 for _, msg := range msgs { 6039 if !strings.Contains(errText, msg) { 6040 f.t.Fatalf("error %q does not contain string %q", errText, msg) 6041 } 6042 } 6043 require.NoError(f.t, model.InferImageProperties(tlr.Manifests)) 6044 } 6045 6046 func (f *fixture) gitInit(path string) { 6047 if err := os.MkdirAll(f.JoinPath(path, ".git"), os.FileMode(0777)); err != nil { 6048 f.t.Fatal(err) 6049 } 6050 } 6051 6052 func (f *fixture) assertNoMoreManifests() { 6053 if len(f.loadResult.Manifests) != 0 { 6054 names := make([]string, len(f.loadResult.Manifests)) 6055 for i, m := range f.loadResult.Manifests { 6056 names[i] = m.Name.String() 6057 } 6058 f.t.Fatalf("expected no more manifests but found %d: %s", 6059 len(names), strings.Join(names, ", ")) 6060 } 6061 } 6062 6063 // Helper func for asserting that the next manifest is Unresourced 6064 // k8s YAML containing the given k8s entities. 6065 func (f *fixture) assertNextManifestUnresourced(expectedEntities ...string) model.Manifest { 6066 lowercaseExpected := []string{} 6067 for _, e := range expectedEntities { 6068 lowercaseExpected = append(lowercaseExpected, strings.ToLower(e)) 6069 } 6070 next := f.assertNextManifest(model.UnresourcedYAMLManifestName) 6071 6072 entities, err := k8s.ParseYAML(bytes.NewBufferString(next.K8sTarget().YAML)) 6073 assert.NoError(f.t, err) 6074 6075 entityNames := make([]string, len(entities)) 6076 for i, e := range entities { 6077 entityNames[i] = strings.ToLower(e.Name()) 6078 } 6079 assert.Equal(f.t, lowercaseExpected, entityNames) 6080 return next 6081 } 6082 6083 type funcOpt func(*testing.T, model.Manifest) bool 6084 6085 // assert functions and helpers 6086 func (f *fixture) assertNextManifest(name model.ManifestName, opts ...interface{}) model.Manifest { 6087 f.t.Helper() 6088 6089 if len(f.loadResult.Manifests) == 0 { 6090 f.t.Fatalf("no more manifests; trying to find %q (did you call `f.load`?)", name) 6091 } 6092 6093 m := f.loadResult.Manifests[0] 6094 if m.Name != name { 6095 f.t.Fatalf("expected next manifest to be '%s' but found '%s'", name, m.Name) 6096 } 6097 6098 f.loadResult.Manifests = f.loadResult.Manifests[1:] 6099 6100 imageIndex := 0 6101 nextImageTarget := func() model.ImageTarget { 6102 ret := m.ImageTargetAt(imageIndex) 6103 imageIndex++ 6104 return ret 6105 } 6106 6107 for _, opt := range opts { 6108 switch opt := opt.(type) { 6109 case dbHelper: 6110 image := nextImageTarget() 6111 6112 refs, err := image.Refs(f.cluster(m)) 6113 require.NoError(f.t, err, "Determining image refs") 6114 ref := refs.ConfigurationRef 6115 if ref.Empty() { 6116 f.t.Fatalf("manifest %v has no more image refs; expected %q", m.Name, opt.image.ref) 6117 } 6118 6119 expectedConfigRef := container.MustParseNamed(opt.image.ref) 6120 if !assert.Equal(f.t, expectedConfigRef.String(), ref.String(), "manifest %v image ref", m.Name) { 6121 f.t.FailNow() 6122 } 6123 6124 expectedLocalRef := container.MustParseNamed(opt.image.localRef) 6125 require.Equal(f.t, expectedLocalRef.String(), refs.LocalRef().String(), "manifest %v localRef", m.Name) 6126 6127 if opt.image.clusterRef != "" { 6128 expectedClusterRef := container.MustParseNamed(opt.image.clusterRef) 6129 require.Equal(f.t, expectedClusterRef.String(), refs.ClusterRef().String(), "manifest %v clusterRef", m.Name) 6130 } 6131 6132 assert.Equal(f.t, opt.image.matchInEnvVars, image.MatchInEnvVars) 6133 6134 if !image.IsDockerBuild() { 6135 f.t.Fatalf("expected docker build but manifest %v has no docker build info", m.Name) 6136 } 6137 6138 for _, matcher := range opt.matchers { 6139 switch matcher := matcher.(type) { 6140 case entrypointHelper: 6141 if !sliceutils.StringSliceEquals(matcher.cmd.Argv, image.OverrideCommand.Command) { 6142 f.t.Fatalf("expected OverrideCommand (aka entrypoint) %v, got %v", 6143 matcher.cmd.Argv, image.OverrideCommand.Command) 6144 } 6145 case v1alpha1.LiveUpdateSpec: 6146 lu := image.LiveUpdateSpec 6147 assert.False(f.t, liveupdate.IsEmptySpec(lu)) 6148 assert.Equal(f.t, matcher, lu) 6149 default: 6150 f.t.Fatalf("unknown dbHelper matcher: %T %v", matcher, matcher) 6151 } 6152 } 6153 case cbHelper: 6154 image := nextImageTarget() 6155 6156 refs, err := image.Refs(f.cluster(m)) 6157 require.NoError(f.t, err, "Determining image refs") 6158 6159 ref := refs.ConfigurationRef 6160 expectedRef := container.MustParseNamed(opt.image.ref) 6161 if !assert.Equal(f.t, expectedRef.String(), ref.String(), "manifest %v image ref", m.Name) { 6162 f.t.FailNow() 6163 } 6164 6165 if !image.IsCustomBuild() { 6166 f.t.Fatalf("Expected custom build but manifest %v has no custom build info", m.Name) 6167 } 6168 cbInfo := image.CustomBuildInfo() 6169 6170 for _, matcher := range opt.matchers { 6171 switch matcher := matcher.(type) { 6172 case depsHelper: 6173 assert.Equal(f.t, matcher.deps, cbInfo.Deps) 6174 case cmdHelper: 6175 assert.Equal(f.t, matcher.cmd.Argv, cbInfo.Args) 6176 case tagHelper: 6177 assert.Equal(f.t, matcher.tag, cbInfo.OutputTag) 6178 case disablePushHelper: 6179 assert.Equal(f.t, matcher.disabled, cbInfo.OutputMode == v1alpha1.CmdImageOutputLocalDockerAndRemote) 6180 case entrypointHelper: 6181 if !sliceutils.StringSliceEquals(matcher.cmd.Argv, image.OverrideCommand.Command) { 6182 f.t.Fatalf("expected OverrideCommand (aka entrypoint) %v, got %v", 6183 matcher.cmd.Argv, image.OverrideCommand.Command) 6184 } 6185 case v1alpha1.LiveUpdateSpec: 6186 lu := image.LiveUpdateSpec 6187 assert.False(f.t, liveupdate.IsEmptySpec(lu)) 6188 assert.Equal(f.t, matcher, lu) 6189 } 6190 } 6191 6192 case deploymentHelper: 6193 yaml := m.K8sTarget().YAML 6194 found := false 6195 for _, e := range f.entities(yaml) { 6196 if e.GVK().Kind == "Deployment" && e.Name() == opt.name { 6197 found = true 6198 break 6199 } 6200 } 6201 if !found { 6202 f.t.Fatalf("deployment %v not found in yaml %q", opt.name, yaml) 6203 } 6204 case v1alpha1.KubernetesDiscoveryStrategy: 6205 assert.Equal(f.t, opt, m.K8sTarget().DiscoveryStrategy) 6206 case podReadinessHelper: 6207 assert.Equal(f.t, opt.podReadiness, m.K8sTarget().PodReadinessMode) 6208 case namespaceHelper: 6209 yaml := m.K8sTarget().YAML 6210 found := false 6211 for _, e := range f.entities(yaml) { 6212 if e.GVK().Kind == "Namespace" && e.Name() == opt.namespace { 6213 found = true 6214 break 6215 } 6216 } 6217 if !found { 6218 f.t.Fatalf("namespace %s not found in yaml %q", opt.namespace, yaml) 6219 } 6220 case serviceHelper: 6221 yaml := m.K8sTarget().YAML 6222 found := false 6223 for _, e := range f.entities(yaml) { 6224 if e.GVK().Kind == "Service" && e.Name() == opt.name { 6225 found = true 6226 break 6227 } 6228 } 6229 if !found { 6230 f.t.Fatalf("service %v not found in yaml %q", opt.name, yaml) 6231 } 6232 case k8sObjectHelper: 6233 yaml := m.K8sTarget().YAML 6234 found := false 6235 for _, e := range f.entities(yaml) { 6236 if e.GVK().Kind == opt.kind && e.Name() == opt.name { 6237 found = true 6238 break 6239 } 6240 } 6241 if !found { 6242 f.t.Fatalf("entity of kind %s with name %s not found in yaml %q", opt.kind, opt.name, yaml) 6243 } 6244 case extraPodSelectorsHelper: 6245 actual := m.K8sTarget().KubernetesApplySpec.KubernetesDiscoveryTemplateSpec.ExtraSelectors 6246 assert.ElementsMatch(f.t, k8s.SetsAsLabelSelectors(opt.labels), actual) 6247 case numEntitiesHelper: 6248 yaml := m.K8sTarget().YAML 6249 entities := f.entities(yaml) 6250 if opt.num != len(f.entities(yaml)) { 6251 f.t.Fatalf("manifest %v has %v entities in %v; expected %v", m.Name, len(entities), yaml, opt.num) 6252 } 6253 6254 case matchPathHelper: 6255 // Make sure the paths matches one of the syncs. 6256 isDep := false 6257 path := f.JoinPath(opt.path) 6258 for _, d := range m.LocalPaths() { 6259 if ospath.IsChild(d, path) { 6260 isDep = true 6261 } 6262 } 6263 6264 if !isDep { 6265 f.t.Errorf("Path %s is not a dependency of manifest %s", path, m.Name) 6266 } 6267 6268 expectedFilter := opt.missing 6269 6270 var filterName string 6271 var filter model.PathMatcher 6272 if opt.fileChange { 6273 filter = ignore.CreateFileChangeFilter(m.ImageTargetAt(0).GetFileWatchIgnores()) 6274 filterName = "FileChangeFilter" 6275 } else { 6276 db, ok := m.ImageTargetAt(0).BuildDetails.(model.DockerBuild) 6277 if !ok { 6278 f.t.Fatalf("BuildContextFilter only applies to docker_build") 6279 } 6280 filter = ignore.CreateBuildContextFilter(db.DockerImageSpec.ContextIgnores) 6281 filterName = "BuildContextFilter" 6282 } 6283 6284 actualFilter, err := filter.Matches(path) 6285 if err != nil { 6286 f.t.Fatalf("Error matching filter (%s): %v", path, err) 6287 } 6288 if actualFilter != expectedFilter { 6289 if expectedFilter { 6290 f.t.Errorf("%s should filter %s", filterName, path) 6291 } else { 6292 f.t.Errorf("%s should not filter %s", filterName, path) 6293 } 6294 } 6295 6296 case []model.PortForward: 6297 if len(opt) == 0 { 6298 assert.Nil(f.t, m.K8sTarget().KubernetesApplySpec.PortForwardTemplateSpec) 6299 } else { 6300 var expectedForwards []v1alpha1.Forward 6301 for _, pf := range opt { 6302 expectedForwards = append(expectedForwards, v1alpha1.Forward{ 6303 LocalPort: int32(pf.LocalPort), 6304 ContainerPort: int32(pf.ContainerPort), 6305 Host: pf.Host, 6306 Name: pf.Name, 6307 Path: pf.PathForAppend(), 6308 }) 6309 } 6310 assert.ElementsMatch(f.t, 6311 expectedForwards, 6312 m.K8sTarget().KubernetesApplySpec.PortForwardTemplateSpec.Forwards) 6313 } 6314 case dcResourceLinks: 6315 f.assertLinks(opt, m.DockerComposeTarget().Links) 6316 case localResourceLinks: 6317 f.assertLinks(opt, m.LocalTarget().Links) 6318 case k8sResourceLinks: 6319 f.assertLinks(opt, m.K8sTarget().Links) 6320 case model.TriggerMode: 6321 assert.Equal(f.t, opt, m.TriggerMode) 6322 case resourceDependenciesHelper: 6323 assert.Equal(f.t, opt.deps, m.ResourceDependencies) 6324 case funcOpt: 6325 assert.True(f.t, opt(f.t, m)) 6326 case localTargetHelper: 6327 lt := m.LocalTarget() 6328 for _, matcher := range opt.matchers { 6329 switch matcher := matcher.(type) { 6330 case updateCmdHelper: 6331 assert.Equal(f.t, matcher.cmd.Argv, lt.UpdateCmdSpec.Args) 6332 assert.Equal(f.t, matcher.cmd.Dir, lt.UpdateCmdSpec.Dir) 6333 assert.Equal(f.t, matcher.cmd.Env, lt.UpdateCmdSpec.Env) 6334 case serveCmdHelper: 6335 assert.Equal(f.t, matcher.cmd, lt.ServeCmd) 6336 case depsHelper: 6337 deps := f.JoinPaths(matcher.deps) 6338 assert.ElementsMatch(f.t, deps, lt.Dependencies()) 6339 case readinessProbeHelper: 6340 assert.EqualValues(f.t, matcher.probeSpec, lt.ReadinessProbe) 6341 default: 6342 f.t.Fatalf("unknown matcher for local target %T", matcher) 6343 } 6344 } 6345 case resourceLabelsHelper: 6346 assert.Equal(f.t, opt.labels, m.Labels) 6347 default: 6348 f.t.Fatalf("unexpected arg to assertNextManifest: %T %v", opt, opt) 6349 } 6350 } 6351 6352 f.assertManifestConsistency(m) 6353 6354 return m 6355 } 6356 6357 // All manifests currently contain redundant information 6358 // such that each Deploy target lists its image ID dependencies. 6359 func (f *fixture) assertManifestConsistency(m model.Manifest) { 6360 iTargetIDs := map[model.TargetID]bool{} 6361 for _, iTarget := range m.ImageTargets { 6362 if iTargetIDs[iTarget.ID()] { 6363 f.t.Fatalf("Image Target %s appears twice in manifest: %s", iTarget.ID(), m.Name) 6364 } 6365 iTargetIDs[iTarget.ID()] = true 6366 } 6367 6368 deployTarget := m.DeployTarget 6369 for _, depID := range deployTarget.DependencyIDs() { 6370 if !iTargetIDs[depID] { 6371 f.t.Fatalf("Image Target needed by deploy target is missing: %s", depID) 6372 } 6373 } 6374 } 6375 6376 func (f *fixture) imageTargetNames(m model.Manifest) []string { 6377 result := []string{} 6378 for _, iTarget := range m.ImageTargets { 6379 result = append(result, iTarget.ID().Name.String()) 6380 } 6381 return result 6382 } 6383 6384 func (f *fixture) idNames(ids []model.TargetID) []string { 6385 result := []string{} 6386 for _, id := range ids { 6387 result = append(result, id.Name.String()) 6388 } 6389 return result 6390 } 6391 6392 func (f *fixture) assertNumManifests(expected int) { 6393 assert.Equal(f.t, expected, len(f.loadResult.Manifests)) 6394 } 6395 6396 func (f *fixture) assertConfigFiles(filenames ...string) { 6397 f.t.Helper() 6398 var expected []string 6399 for _, filename := range filenames { 6400 expected = append(expected, f.JoinPath(filename)) 6401 } 6402 sort.Strings(expected) 6403 sort.Strings(f.loadResult.ConfigFiles) 6404 assert.Equal(f.t, expected, f.loadResult.ConfigFiles) 6405 } 6406 6407 func (f *fixture) assertWarnings(warnings ...string) { 6408 var expected []string 6409 for _, warning := range warnings { 6410 expected = append(expected, warning+"\n") 6411 } 6412 sort.Strings(expected) 6413 sort.Strings(f.warnings) 6414 assert.Equal(f.t, expected, f.warnings) 6415 } 6416 6417 func (f *fixture) entities(y string) []k8s.K8sEntity { 6418 es, err := k8s.ParseYAMLFromString(y) 6419 if err != nil { 6420 f.t.Fatal(err) 6421 } 6422 return es 6423 } 6424 6425 func (f *fixture) assertFeature(key string, enabled bool) { 6426 assert.Equal(f.t, enabled, f.loadResult.FeatureFlags[key]) 6427 } 6428 6429 func (f *fixture) assertLinks(expected, actual []model.Link) { 6430 require.Len(f.t, actual, len(expected), "comparing # of links") 6431 for i, exp := range expected { 6432 require.Equalf(f.t, exp.URLString(), actual[i].URLString(), "link at index %d", i) 6433 require.Equalf(f.t, exp.Name, actual[i].Name, "link at index %d", i) 6434 } 6435 } 6436 6437 func (f *fixture) cluster(m model.Manifest) *v1alpha1.Cluster { 6438 f.t.Helper() 6439 6440 tlr := f.loadResult 6441 6442 if m.IsK8s() { 6443 return &v1alpha1.Cluster{ 6444 ObjectMeta: metav1.ObjectMeta{ 6445 Name: v1alpha1.ClusterNameDefault, 6446 }, 6447 Spec: v1alpha1.ClusterSpec{ 6448 Connection: &v1alpha1.ClusterConnection{ 6449 Kubernetes: &v1alpha1.KubernetesClusterConnection{}, 6450 }, 6451 DefaultRegistry: tlr.DefaultRegistry, 6452 }, 6453 } 6454 } 6455 6456 if m.IsDC() { 6457 return &v1alpha1.Cluster{ 6458 ObjectMeta: metav1.ObjectMeta{ 6459 Name: v1alpha1.ClusterNameDocker, 6460 }, 6461 Spec: v1alpha1.ClusterSpec{ 6462 Connection: &v1alpha1.ClusterConnection{ 6463 Docker: &v1alpha1.DockerClusterConnection{}, 6464 }, 6465 DefaultRegistry: tlr.DefaultRegistry, 6466 }, 6467 } 6468 } 6469 6470 return &v1alpha1.Cluster{} 6471 } 6472 6473 type secretHelper struct { 6474 name string 6475 } 6476 6477 func secret(name string) secretHelper { 6478 return secretHelper{name: name} 6479 } 6480 6481 type namespaceHelper struct { 6482 namespace string 6483 } 6484 6485 func namespace(namespace string) namespaceHelper { 6486 return namespaceHelper{namespace} 6487 } 6488 6489 type deploymentHelper struct { 6490 name string 6491 image string 6492 templateLabels map[string]string 6493 envVars []envVar 6494 namespace string 6495 } 6496 6497 func deployment(name string, opts ...interface{}) deploymentHelper { 6498 r := deploymentHelper{name: name} 6499 for _, opt := range opts { 6500 switch opt := opt.(type) { 6501 case imageHelper: 6502 r.image = opt.ref 6503 case labelsHelper: 6504 r.templateLabels = opt.labels 6505 case envVarHelper: 6506 r.envVars = opt.envVars 6507 case namespaceHelper: 6508 r.namespace = opt.namespace 6509 default: 6510 panic(fmt.Errorf("unexpected arg to deployment: %T %v", opt, opt)) 6511 } 6512 } 6513 return r 6514 } 6515 6516 type podReadinessHelper struct { 6517 podReadiness model.PodReadinessMode 6518 } 6519 6520 func podReadiness(podReadiness model.PodReadinessMode) podReadinessHelper { 6521 return podReadinessHelper{podReadiness: podReadiness} 6522 } 6523 6524 type serviceHelper struct { 6525 name string 6526 selectorLabels map[string]string 6527 } 6528 6529 func service(name string, opts ...interface{}) serviceHelper { 6530 r := serviceHelper{name: name} 6531 for _, opt := range opts { 6532 switch opt := opt.(type) { 6533 case labelsHelper: 6534 r.selectorLabels = opt.labels 6535 default: 6536 panic(fmt.Errorf("unexpected arg to deployment: %T %v", opt, opt)) 6537 } 6538 } 6539 return r 6540 } 6541 6542 type k8sObjectHelper struct { 6543 name string 6544 kind string 6545 } 6546 6547 func k8sObject(name string, kind string) k8sObjectHelper { 6548 return k8sObjectHelper{name: name, kind: kind} 6549 } 6550 6551 type extraPodSelectorsHelper struct { 6552 labels []labels.Set 6553 } 6554 6555 func extraPodSelectors(labelSets ...labels.Set) extraPodSelectorsHelper { 6556 ret := extraPodSelectorsHelper{ 6557 labels: append([]labels.Set(nil), labelSets...), 6558 } 6559 return ret 6560 } 6561 6562 type numEntitiesHelper struct { 6563 num int 6564 } 6565 6566 func numEntities(num int) numEntitiesHelper { 6567 return numEntitiesHelper{num} 6568 } 6569 6570 type matchPathHelper struct { 6571 path string 6572 missing bool 6573 fileChange bool 6574 } 6575 6576 func buildMatches(path string) matchPathHelper { 6577 return matchPathHelper{ 6578 path: path, 6579 } 6580 } 6581 6582 func buildFilters(path string) matchPathHelper { 6583 return matchPathHelper{ 6584 path: path, 6585 missing: true, 6586 } 6587 } 6588 6589 func fileChangeMatches(path string) matchPathHelper { 6590 return matchPathHelper{ 6591 path: path, 6592 fileChange: true, 6593 } 6594 } 6595 6596 func fileChangeFilters(path string) matchPathHelper { 6597 return matchPathHelper{ 6598 path: path, 6599 missing: true, 6600 fileChange: true, 6601 } 6602 } 6603 6604 type resourceDependenciesHelper struct { 6605 deps []model.ManifestName 6606 } 6607 6608 func resourceDeps(deps ...string) resourceDependenciesHelper { 6609 var mns []model.ManifestName 6610 for _, d := range deps { 6611 mns = append(mns, model.ManifestName(d)) 6612 } 6613 return resourceDependenciesHelper{deps: mns} 6614 } 6615 6616 type resourceLabelsHelper struct { 6617 labels map[string]string 6618 } 6619 6620 func resourceLabels(labels ...string) resourceLabelsHelper { 6621 ret := resourceLabelsHelper{ 6622 labels: map[string]string{}, 6623 } 6624 for _, l := range labels { 6625 ret.labels[l] = l 6626 } 6627 return ret 6628 } 6629 6630 type imageHelper struct { 6631 ref string 6632 localRef string 6633 clusterRef string 6634 matchInEnvVars bool 6635 } 6636 6637 func image(ref string) imageHelper { 6638 return imageHelper{ref: ref, localRef: ref} 6639 } 6640 6641 func (ih imageHelper) withLocalRef(localRef string) imageHelper { 6642 ih.localRef = localRef 6643 return ih 6644 } 6645 6646 func (ih imageHelper) withClusterRef(clusterRef string) imageHelper { 6647 ih.clusterRef = clusterRef 6648 return ih 6649 } 6650 6651 func (ih imageHelper) withMatchInEnvVars() imageHelper { 6652 ih.matchInEnvVars = true 6653 return ih 6654 } 6655 6656 type labelsHelper struct { 6657 labels map[string]string 6658 } 6659 6660 func withLabels(labels map[string]string) labelsHelper { 6661 return labelsHelper{labels: labels} 6662 } 6663 6664 type envVar struct { 6665 name string 6666 value string 6667 } 6668 6669 type envVarHelper struct { 6670 envVars []envVar 6671 } 6672 6673 // usage: withEnvVars("key1", "value1", "key2", "value2") 6674 func withEnvVars(envVars ...string) envVarHelper { 6675 var ret envVarHelper 6676 6677 for i := 0; i < len(envVars); i += 2 { 6678 if i+1 >= len(envVars) { 6679 panic("withEnvVars called with odd number of strings") 6680 } 6681 ret.envVars = append(ret.envVars, envVar{envVars[i], envVars[i+1]}) 6682 } 6683 6684 return ret 6685 } 6686 6687 // docker build helper 6688 type dbHelper struct { 6689 image imageHelper 6690 matchers []interface{} 6691 } 6692 6693 func db(img imageHelper, opts ...interface{}) dbHelper { 6694 return dbHelper{image: img, matchers: opts} 6695 } 6696 6697 // custom build helper 6698 type cbHelper struct { 6699 image imageHelper 6700 matchers []interface{} 6701 } 6702 6703 func cb(img imageHelper, opts ...interface{}) cbHelper { 6704 return cbHelper{img, opts} 6705 } 6706 6707 type entrypointHelper struct { 6708 cmd model.Cmd 6709 } 6710 6711 func entrypoint(command model.Cmd) entrypointHelper { 6712 return entrypointHelper{command} 6713 } 6714 6715 type cmdHelper struct { 6716 cmd model.Cmd 6717 } 6718 6719 func cmd(cmd string, dir string) cmdHelper { 6720 return cmdHelper{cmd: model.ToHostCmdInDir(cmd, dir)} 6721 } 6722 6723 type tagHelper struct { 6724 tag string 6725 } 6726 6727 func tag(tag string) tagHelper { 6728 return tagHelper{tag} 6729 } 6730 6731 type depsHelper struct { 6732 deps []string 6733 } 6734 6735 func deps(deps ...string) depsHelper { 6736 return depsHelper{deps} 6737 } 6738 6739 type disablePushHelper struct { 6740 disabled bool 6741 } 6742 6743 func disablePush(disable bool) disablePushHelper { 6744 return disablePushHelper{disable} 6745 } 6746 6747 type updateCmdHelper struct { 6748 cmd model.Cmd 6749 } 6750 6751 func updateCmd(dir string, cmd string, env []string) updateCmdHelper { 6752 return updateCmdHelper{cmd: model.ToHostCmdInDirWithEnv(cmd, dir, env)} 6753 } 6754 6755 func updateCmdArray(dir string, argv []string, env []string) updateCmdHelper { 6756 return updateCmdHelper{cmd: model.Cmd{Argv: argv, Dir: dir, Env: env}} 6757 } 6758 6759 type serveCmdHelper struct { 6760 cmd model.Cmd 6761 } 6762 6763 func serveCmd(dir string, cmd string, env []string) serveCmdHelper { 6764 return serveCmdHelper{cmd: model.ToHostCmdInDirWithEnv(cmd, dir, env)} 6765 } 6766 6767 func serveCmdArray(dir string, argv []string, env []string) serveCmdHelper { 6768 return serveCmdHelper{model.Cmd{Argv: argv, Dir: dir, Env: env}} 6769 } 6770 6771 type readinessProbeHelper struct { 6772 probeSpec *v1alpha1.Probe 6773 } 6774 6775 type localTargetHelper struct { 6776 matchers []interface{} 6777 } 6778 6779 func localTarget(opts ...interface{}) localTargetHelper { 6780 return localTargetHelper{matchers: opts} 6781 } 6782 6783 // useful scenarios to setup 6784 6785 // foo just has one image and one yaml 6786 func (f *fixture) setupFoo() { 6787 f.dockerfile("foo/Dockerfile") 6788 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 6789 f.gitInit("") 6790 } 6791 6792 // bar just has one image and one yaml 6793 func (f *fixture) setupFooAndBar() { 6794 f.dockerfile("foo/Dockerfile") 6795 f.yaml("foo.yaml", deployment("foo", image("gcr.io/foo"))) 6796 6797 f.dockerfile("bar/Dockerfile") 6798 f.yaml("bar.yaml", deployment("bar", image("gcr.io/bar"))) 6799 6800 f.gitInit("") 6801 } 6802 6803 // expand has 4 images, a-d, and a yaml with all of it 6804 func (f *fixture) setupExpand() { 6805 f.dockerfile("a/Dockerfile") 6806 f.dockerfile("b/Dockerfile") 6807 f.dockerfile("c/Dockerfile") 6808 f.dockerfile("d/Dockerfile") 6809 6810 f.yaml("all.yaml", 6811 deployment("a", image("gcr.io/a")), 6812 deployment("b", image("gcr.io/b")), 6813 deployment("c", image("gcr.io/c")), 6814 deployment("d", image("gcr.io/d")), 6815 ) 6816 6817 f.gitInit("") 6818 } 6819 6820 func (f *fixture) setupHelm() { 6821 f.file("helm/Chart.yaml", chartYAML) 6822 f.file("helm/values.yaml", valuesYAML) 6823 f.file("dev/helm/values-dev.yaml", valuesDevYAML) // make sure we can pull in a values.yaml file from outside chart dir 6824 6825 f.file("helm/templates/_helpers.tpl", helpersTPL) 6826 f.file("helm/templates/deployment.yaml", deploymentYAML) 6827 f.file("helm/templates/ingress.yaml", ingressYAML) 6828 f.file("helm/templates/service.yaml", serviceYAML) 6829 f.file("helm/templates/namespace.yaml", namespaceYAML) 6830 } 6831 6832 func (f *fixture) setupHelmWithRequirements() { 6833 f.setupHelm() 6834 6835 nginxIngressChartPath := testdata.NginxIngressChartPath() 6836 f.CopyFile(nginxIngressChartPath, filepath.Join("helm/charts", filepath.Base(nginxIngressChartPath))) 6837 } 6838 6839 func (f *fixture) setupHelmWithTest() { 6840 f.setupHelm() 6841 f.file("helm/templates/tests/test-mariadb-connection.yaml", helmTestYAML) 6842 } 6843 6844 func (f *fixture) setupExtraPodSelectors(s string) { 6845 f.setupFoo() 6846 6847 tiltfile := fmt.Sprintf(` 6848 6849 docker_build('gcr.io/foo', 'foo') 6850 k8s_yaml('foo.yaml') 6851 k8s_resource('foo', extra_pod_selectors=%s) 6852 `, s) 6853 6854 f.file("Tiltfile", tiltfile) 6855 } 6856 6857 func (f *fixture) setupCRD() { 6858 f.file("crd.yaml", `apiVersion: fission.io/v1 6859 kind: Environment 6860 metadata: 6861 name: mycrd 6862 spec: 6863 builder: 6864 command: build 6865 image: test/mycrd-builder 6866 poolsize: 1 6867 runtime: 6868 image: test/mycrd-env`) 6869 } 6870 6871 func overwriteSelectorsForService(entity *k8s.K8sEntity, labels map[string]string) error { 6872 svc, ok := entity.Obj.(*v1.Service) 6873 if !ok { 6874 return fmt.Errorf("don't know how to set selectors for %T", entity.Obj) 6875 } 6876 svc.Spec.Selector = labels 6877 return nil 6878 } 6879 6880 func (f *fixture) SingleAnalyticsEvent(name string) analytics.CountEvent { 6881 var ret analytics.CountEvent 6882 for _, ce := range f.an.Counts { 6883 if ce.Name == name { 6884 require.Equalf(f.t, "", ret.Name, "two count events named %s", name) 6885 ret = ce 6886 } 6887 } 6888 require.NotEqualf(f.t, "", ret.Name, "no count event named %s", name) 6889 6890 return ret 6891 }