github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/tiltfile_test.go (about)

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