github.com/tilt-dev/tilt@v0.36.0/internal/tiltfile/tiltfile_test.go (about)

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