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

     1  package tiltfile
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    13  	"github.com/tilt-dev/tilt/pkg/model"
    14  )
    15  
    16  func TestLiveUpdateStepNotUsed(t *testing.T) {
    17  	f := newFixture(t)
    18  
    19  	f.WriteFile("Tiltfile", "restart_container()")
    20  
    21  	f.loadErrString("steps that were created but not used in a live_update", "restart_container", "Tiltfile:1")
    22  }
    23  
    24  func TestLiveUpdateRestartContainerNotLast(t *testing.T) {
    25  	f := newFixture(t)
    26  
    27  	f.setupFoo()
    28  
    29  	f.file("Tiltfile", `
    30  k8s_yaml('foo.yaml')
    31  docker_build('gcr.io/foo', 'foo',
    32    live_update=[
    33      restart_container(),
    34      sync('foo', '/baz'),
    35    ]
    36  )`)
    37  	f.loadErrString("live_update", "restart container is only valid as the last step")
    38  }
    39  
    40  func TestLiveUpdateSyncRelDest(t *testing.T) {
    41  	f := newFixture(t)
    42  
    43  	f.setupFoo()
    44  
    45  	f.file("Tiltfile", `
    46  k8s_yaml('foo.yaml')
    47  docker_build('gcr.io/foo', 'foo',
    48    live_update=[
    49      sync('foo', 'baz'),
    50    ]
    51  )`)
    52  	f.loadErrString("sync destination", "baz", "is not absolute")
    53  }
    54  
    55  func TestLiveUpdateRunBeforeSync(t *testing.T) {
    56  	f := newFixture(t)
    57  
    58  	f.setupFoo()
    59  
    60  	f.file("Tiltfile", `
    61  k8s_yaml('foo.yaml')
    62  docker_build('gcr.io/foo', 'foo',
    63    live_update=[
    64  	run('quu'),
    65      sync('foo', '/baz'),
    66    ]
    67  )`)
    68  	f.loadErrString("live_update", "all sync steps must precede all run steps")
    69  }
    70  
    71  func TestLiveUpdateNonStepInSteps(t *testing.T) {
    72  	f := newFixture(t)
    73  
    74  	f.setupFoo()
    75  
    76  	f.file("Tiltfile", `
    77  k8s_yaml('foo.yaml')
    78  docker_build('gcr.io/foo', 'foo',
    79    live_update=[
    80      'quu',
    81      sync('bar', '/baz'),
    82    ]
    83  )`)
    84  	f.loadErrString("'steps' must be a list of live update steps - got value '\"quu\"' of type 'string'")
    85  }
    86  
    87  func TestLiveUpdateNonStringInFullBuildTriggers(t *testing.T) {
    88  	f := newFixture(t)
    89  
    90  	f.setupFoo()
    91  
    92  	f.file("Tiltfile", `
    93  k8s_yaml('foo.yaml')
    94  docker_build('gcr.io/foo', 'foo',
    95    live_update=[
    96  	fall_back_on(4),
    97      sync('bar', '/baz'),
    98    ],
    99  )`)
   100  	f.loadErrString("fall_back_on",
   101  		"fall_back_on: for parameter paths: value should be a string or List or Tuple of strings, but is of type int")
   102  }
   103  
   104  func TestLiveUpdateNonStringInRunTriggers(t *testing.T) {
   105  	f := newFixture(t)
   106  
   107  	f.setupFoo()
   108  
   109  	f.file("Tiltfile", `
   110  k8s_yaml('foo.yaml')
   111  docker_build('gcr.io/foo', 'foo',
   112    live_update=[
   113      run('bar', trigger=[4]),
   114    ]
   115  )`)
   116  	f.loadErrString("run", "triggers", "'bar'", "contained value '4' of type 'int'. it may only contain strings")
   117  }
   118  
   119  func TestLiveUpdateDockerBuildUnqualifiedImageName(t *testing.T) {
   120  	f := newLiveUpdateFixture(t)
   121  
   122  	f.tiltfileCode = "docker_build('foo', 'foo', live_update=%s)"
   123  	f.init()
   124  
   125  	f.load("foo")
   126  
   127  	f.assertNextManifest("foo", db(image("foo"), f.expectedLU))
   128  }
   129  
   130  func TestLiveUpdateDockerBuildQualifiedImageName(t *testing.T) {
   131  	f := newLiveUpdateFixture(t)
   132  
   133  	f.expectedImage = "gcr.io/foo"
   134  	f.tiltfileCode = "docker_build('gcr.io/foo', 'foo', live_update=%s)"
   135  	f.init()
   136  
   137  	f.load("foo")
   138  
   139  	f.assertNextManifest("foo", db(image("gcr.io/foo"), f.expectedLU))
   140  }
   141  
   142  func TestLiveUpdateDockerBuildDefaultRegistry(t *testing.T) {
   143  	f := newLiveUpdateFixture(t)
   144  
   145  	f.tiltfileCode = `
   146  default_registry('gcr.io')
   147  docker_build('foo', 'foo', live_update=%s)`
   148  	f.init()
   149  
   150  	f.load("foo")
   151  
   152  	i := image("foo")
   153  	i.localRef = "gcr.io/foo"
   154  	f.assertNextManifest("foo", db(i, f.expectedLU))
   155  }
   156  
   157  func TestLiveUpdateCustomBuild(t *testing.T) {
   158  	f := newLiveUpdateFixture(t)
   159  
   160  	f.tiltfileCode = "custom_build('foo', 'docker build -t $TAG foo', ['foo'], live_update=%s)"
   161  	f.init()
   162  
   163  	f.load("foo")
   164  
   165  	f.assertNextManifest("foo", cb(image("foo"), f.expectedLU))
   166  }
   167  
   168  func TestLiveUpdateOnlyCustomBuild(t *testing.T) {
   169  	f := newLiveUpdateFixture(t)
   170  
   171  	f.tiltfileCode = `
   172  default_registry('gcr.io/myrepo')
   173  custom_build('foo', ':', ['foo'], live_update=%s)
   174  `
   175  	f.init()
   176  
   177  	f.load("foo")
   178  
   179  	m := f.assertNextManifest("foo", cb(image("foo"), f.expectedLU))
   180  	assert.True(t, m.ImageTargets[0].IsLiveUpdateOnly)
   181  
   182  	require.NoError(t, m.InferLiveUpdateSelectors(), "Failed to infer Live Update selectors")
   183  	luSpec := m.ImageTargets[0].LiveUpdateSpec
   184  	require.NotNil(t, luSpec.Selector.Kubernetes)
   185  	assert.Empty(t, luSpec.Selector.Kubernetes.ContainerName)
   186  	// NO registry rewriting should be applied here because Tilt isn't actually building the image
   187  	assert.Equal(t, "foo", luSpec.Selector.Kubernetes.Image)
   188  }
   189  
   190  func TestLiveUpdateSyncFilesOutsideOfDockerBuildContext(t *testing.T) {
   191  	f := newFixture(t)
   192  
   193  	f.setupFoo()
   194  
   195  	f.file("Tiltfile", `
   196  k8s_yaml('foo.yaml')
   197  docker_build('gcr.io/foo', 'foo',
   198    live_update=[
   199      sync('bar', '/baz'),
   200    ]
   201  )`)
   202  	f.loadErrString("sync step source", f.JoinPath("bar"), f.JoinPath("foo"), "child", "any watched filepaths")
   203  }
   204  
   205  func TestLiveUpdateSyncFilesImageDep(t *testing.T) {
   206  	f := newFixture(t)
   207  
   208  	f.gitInit("")
   209  	f.file("a/message.txt", "message")
   210  	f.file("imageA.dockerfile", `FROM golang:1.10
   211  ADD message.txt /src/message.txt
   212  `)
   213  	f.file("imageB.dockerfile", "FROM gcr.io/image-a")
   214  	f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-b")))
   215  	f.file("Tiltfile", `
   216  docker_build('gcr.io/image-b', 'b', dockerfile='imageB.dockerfile',
   217               live_update=[
   218                 sync('a/message.txt', '/src/message.txt'),
   219               ])
   220  docker_build('gcr.io/image-a', 'a', dockerfile='imageA.dockerfile')
   221  k8s_yaml('foo.yaml')
   222  `)
   223  	f.load()
   224  
   225  	lu := v1alpha1.LiveUpdateSpec{
   226  		BasePath: f.Path(),
   227  		Syncs: []v1alpha1.LiveUpdateSync{
   228  			v1alpha1.LiveUpdateSync{
   229  				LocalPath:     filepath.Join("a", "message.txt"),
   230  				ContainerPath: "/src/message.txt",
   231  			},
   232  		},
   233  	}
   234  
   235  	f.assertNextManifest("foo",
   236  		db(image("gcr.io/image-a")),
   237  		db(image("gcr.io/image-b"), lu))
   238  }
   239  
   240  func TestLiveUpdateRun(t *testing.T) {
   241  	for _, tc := range []struct {
   242  		name         string
   243  		tiltfileText string
   244  		expectedArgv []string
   245  	}{
   246  		{"string cmd", `"echo hi"`, []string{"sh", "-c", "echo hi"}},
   247  		{"array cmd", `["echo", "hi"]`, []string{"echo", "hi"}},
   248  	} {
   249  		t.Run(tc.name, func(t *testing.T) {
   250  			f := newFixture(t)
   251  
   252  			f.gitInit("")
   253  			f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-a")))
   254  			f.file("imageA.dockerfile", `FROM golang:1.10`)
   255  			f.file("Tiltfile", fmt.Sprintf(`
   256  docker_build('gcr.io/image-a', 'a', dockerfile='imageA.dockerfile',
   257               live_update=[
   258                 run(%s)
   259               ])
   260  k8s_yaml('foo.yaml')
   261  `, tc.tiltfileText))
   262  			f.load()
   263  
   264  			lu := v1alpha1.LiveUpdateSpec{
   265  				BasePath: f.Path(),
   266  				Execs: []v1alpha1.LiveUpdateExec{
   267  					v1alpha1.LiveUpdateExec{
   268  						Args: tc.expectedArgv,
   269  					},
   270  				},
   271  			}
   272  			f.assertNextManifest("foo",
   273  				db(image("gcr.io/image-a"), lu))
   274  		})
   275  	}
   276  }
   277  
   278  func TestLiveUpdateRunEchoOff(t *testing.T) {
   279  	for _, tc := range []struct {
   280  		name          string
   281  		tiltfileText  string
   282  		expectedValue bool
   283  	}{
   284  		{"echoOff True", `echo_off=True`, true},
   285  		{"echoOff False", `echo_off=False`, false},
   286  		{"echoOff default", ``, false},
   287  		{"echoOff default", `[]`, false},
   288  	} {
   289  		t.Run(tc.name, func(t *testing.T) {
   290  			f := newFixture(t)
   291  
   292  			f.gitInit("")
   293  			f.yaml("foo.yaml", deployment("foo", image("gcr.io/image-a")))
   294  			f.file("imageA.dockerfile", `FROM golang:1.10`)
   295  			f.file("Tiltfile", fmt.Sprintf(`
   296  docker_build('gcr.io/image-a', 'a', dockerfile='imageA.dockerfile',
   297               live_update=[
   298                 run("echo hi", %s)
   299               ])
   300  k8s_yaml('foo.yaml')
   301  `, tc.tiltfileText))
   302  			f.load()
   303  
   304  			lu := v1alpha1.LiveUpdateSpec{
   305  				BasePath: f.Path(),
   306  				Execs: []v1alpha1.LiveUpdateExec{
   307  					{
   308  						Args:    []string{"sh", "-c", "echo hi"},
   309  						EchoOff: tc.expectedValue,
   310  					},
   311  				},
   312  			}
   313  			f.assertNextManifest("foo",
   314  				db(image("gcr.io/image-a"), lu))
   315  		})
   316  	}
   317  }
   318  
   319  func TestLiveUpdateFallBackTriggersOutsideOfDockerBuildContext(t *testing.T) {
   320  	f := newFixture(t)
   321  
   322  	f.setupFoo()
   323  
   324  	f.file("Tiltfile", `
   325  k8s_yaml('foo.yaml')
   326  docker_build('gcr.io/foo', 'foo',
   327    live_update=[
   328      fall_back_on('bar'),
   329      sync('foo/bar', '/baz'),
   330    ]
   331  )`)
   332  	f.loadErrString("fall_back_on", f.JoinPath("bar"), f.JoinPath("foo"), "child", "any watched filepaths")
   333  }
   334  
   335  func TestLiveUpdateSyncFilesOutsideOfCustomBuildDeps(t *testing.T) {
   336  	f := newFixture(t)
   337  
   338  	f.setupFoo()
   339  
   340  	f.file("Tiltfile", `
   341  k8s_yaml('foo.yaml')
   342  custom_build('gcr.io/foo', 'docker build -t $TAG foo', ['./foo'],
   343    live_update=[
   344      sync('bar', '/baz'),
   345    ]
   346  )`)
   347  	f.loadErrString("sync step source", f.JoinPath("bar"), f.JoinPath("foo"), "child", "any watched filepaths")
   348  }
   349  
   350  func TestLiveUpdateFallBackTriggersOutsideOfCustomBuildDeps(t *testing.T) {
   351  	f := newFixture(t)
   352  
   353  	f.setupFoo()
   354  
   355  	f.file("Tiltfile", `
   356  k8s_yaml('foo.yaml')
   357  custom_build('gcr.io/foo', 'docker build -t $TAG foo', ['./foo'],
   358    live_update=[
   359      fall_back_on('bar'),
   360      sync('foo/bar', '/baz'),
   361    ]
   362  )`)
   363  	f.loadErrString("fall_back_on", f.JoinPath("bar"), f.JoinPath("foo"), "child", "any watched filepaths")
   364  }
   365  
   366  func TestLiveUpdateRestartContainerDeprecationErrorK8s(t *testing.T) {
   367  	f := newFixture(t)
   368  
   369  	f.setupFoo()
   370  
   371  	f.file("Tiltfile", `
   372  k8s_yaml('foo.yaml')
   373  docker_build('gcr.io/foo', './foo',
   374    live_update=[
   375      sync('foo/bar', '/baz'),
   376  	restart_container(),
   377    ]
   378  )`)
   379  	f.loadErrString(restartContainerDeprecationError([]model.ManifestName{"foo"}))
   380  }
   381  
   382  func TestLiveUpdateRestartContainerDeprecationErrorK8sCustomBuild(t *testing.T) {
   383  	f := newFixture(t)
   384  
   385  	f.setupFoo()
   386  
   387  	f.file("Tiltfile", `
   388  k8s_yaml('foo.yaml')
   389  custom_build('gcr.io/foo', 'docker build -t $TAG foo', ['./foo'],
   390    live_update=[
   391      sync('foo/bar', '/baz'),
   392  	restart_container(),
   393    ]
   394  )`)
   395  
   396  	f.loadErrString(restartContainerDeprecationError([]model.ManifestName{"foo"}))
   397  }
   398  
   399  func TestLiveUpdateRestartContainerDeprecationErrorMultiple(t *testing.T) {
   400  	f := newFixture(t)
   401  
   402  	f.setupExpand()
   403  
   404  	f.file("Tiltfile", `
   405  k8s_yaml('all.yaml')
   406  docker_build('gcr.io/a', './a',
   407    live_update=[
   408      sync('./a', '/'),
   409  	restart_container(),
   410    ]
   411  )
   412  docker_build('gcr.io/b', './b')
   413  docker_build('gcr.io/c', './c',
   414    live_update=[
   415      sync('./c', '/'),
   416  	restart_container(),
   417    ]
   418  )
   419  docker_build('gcr.io/d', './d',
   420    live_update=[sync('./d', '/')]
   421  )`)
   422  
   423  	f.loadErrString(restartContainerDeprecationError([]model.ManifestName{"a", "c"}))
   424  }
   425  
   426  func TestLiveUpdateNoRestartContainerDeprecationErrorK8sDockerCompose(t *testing.T) {
   427  	f := newFixture(t)
   428  	f.setupFoo()
   429  	f.file("docker-compose.yml", `version: '3'
   430  services:
   431    foo:
   432      image: gcr.io/foo
   433  `)
   434  	f.file("Tiltfile", `
   435  docker_build('gcr.io/foo', 'foo')
   436  docker_compose('docker-compose.yml')
   437  `)
   438  
   439  	// Expect no deprecation error b/c restart_container() is still allowed on Docker Compose resources
   440  	f.load()
   441  	f.assertNextManifest("foo", db(image("gcr.io/foo")))
   442  }
   443  
   444  type liveUpdateFixture struct {
   445  	*fixture
   446  
   447  	tiltfileCode  string
   448  	expectedImage string
   449  	expectedLU    v1alpha1.LiveUpdateSpec
   450  
   451  	skipYAML bool
   452  }
   453  
   454  func (f *liveUpdateFixture) init() {
   455  	f.dockerfile("foo/Dockerfile")
   456  
   457  	if !f.skipYAML {
   458  		f.yaml("foo.yaml", deployment("foo", image(f.expectedImage)))
   459  	}
   460  
   461  	luSteps := `[
   462      fall_back_on(['foo/i', 'foo/j']),
   463  	sync('foo/b', '/c'),
   464  	run('f', trigger=['g', 'h']),
   465  ]`
   466  	codeToInsert := fmt.Sprintf(f.tiltfileCode, luSteps)
   467  
   468  	var tiltfile string
   469  	if !f.skipYAML {
   470  		tiltfile = `k8s_yaml('foo.yaml')`
   471  	}
   472  	tiltfile = strings.Join([]string{tiltfile, codeToInsert}, "\n")
   473  	f.file("Tiltfile", tiltfile)
   474  }
   475  
   476  func newLiveUpdateFixture(t *testing.T) *liveUpdateFixture {
   477  	f := &liveUpdateFixture{
   478  		fixture: newFixture(t),
   479  	}
   480  
   481  	f.expectedLU = v1alpha1.LiveUpdateSpec{
   482  		BasePath:  f.Path(),
   483  		StopPaths: []string{filepath.Join("foo", "i"), filepath.Join("foo", "j")},
   484  		Syncs: []v1alpha1.LiveUpdateSync{
   485  			v1alpha1.LiveUpdateSync{
   486  				LocalPath:     filepath.Join("foo", "b"),
   487  				ContainerPath: "/c",
   488  			},
   489  		},
   490  		Execs: []v1alpha1.LiveUpdateExec{
   491  			v1alpha1.LiveUpdateExec{
   492  				Args:         []string{"sh", "-c", "f"},
   493  				TriggerPaths: []string{"g", "h"},
   494  				EchoOff:      false,
   495  			},
   496  		},
   497  	}
   498  	f.expectedImage = "foo"
   499  
   500  	return f
   501  }