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 }