github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/session/reconciler_test.go (about) 1 package session 2 3 import ( 4 "errors" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/jonboulle/clockwork" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 v1 "k8s.io/api/core/v1" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/apimachinery/pkg/types" 15 16 "github.com/tilt-dev/tilt/internal/controllers/fake" 17 "github.com/tilt-dev/tilt/internal/k8s" 18 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 19 "github.com/tilt-dev/tilt/internal/store" 20 "github.com/tilt-dev/tilt/internal/store/sessions" 21 "github.com/tilt-dev/tilt/internal/store/tiltfiles" 22 "github.com/tilt-dev/tilt/internal/testutils/manifestbuilder" 23 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 24 "github.com/tilt-dev/tilt/pkg/apis" 25 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 26 "github.com/tilt-dev/tilt/pkg/model" 27 ) 28 29 var sessionKey = types.NamespacedName{Name: sessions.DefaultSessionName} 30 31 func TestExitControlCI_TiltfileFailure(t *testing.T) { 32 f := newFixture(t, store.EngineModeCI) 33 34 // Tiltfile state is stored independent of resource state within engine 35 f.Store.WithState(func(state *store.EngineState) { 36 ms := &store.ManifestState{} 37 ms.AddCompletedBuild(model.BuildRecord{ 38 Error: errors.New("fake Tiltfile error"), 39 }) 40 state.TiltfileStates[model.MainTiltfileManifestName] = ms 41 }) 42 43 f.MustReconcile(sessionKey) 44 f.requireDoneWithError("fake Tiltfile error") 45 } 46 47 func TestExitControlIdempotent(t *testing.T) { 48 f := newFixture(t, store.EngineModeCI) 49 50 f.MustReconcile(sessionKey) 51 52 var s1 v1alpha1.Session 53 f.MustGet(sessionKey, &s1) 54 55 f.MustReconcile(sessionKey) 56 57 var s2 v1alpha1.Session 58 f.MustGet(sessionKey, &s2) 59 60 assert.Equal(t, s1.ObjectMeta, s2.ObjectMeta) 61 } 62 63 func TestExitControlCI_FirstBuildFailure(t *testing.T) { 64 f := newFixture(t, store.EngineModeCI) 65 66 m := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 67 f.upsertManifest(m) 68 m2 := manifestbuilder.New(f, "fe2").WithK8sYAML(testyaml.SanchoYAML).Build() 69 f.upsertManifest(m2) 70 71 f.MustReconcile(sessionKey) 72 f.requireNotDone() 73 74 f.Store.WithState(func(state *store.EngineState) { 75 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 76 StartTime: time.Now(), 77 FinishTime: time.Now(), 78 Error: fmt.Errorf("does not compile"), 79 }) 80 }) 81 82 f.MustReconcile(sessionKey) 83 f.requireDoneWithError("does not compile") 84 } 85 86 func TestExitControlCI_FirstRuntimeFailure(t *testing.T) { 87 f := newFixture(t, store.EngineModeCI) 88 89 m := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 90 f.upsertManifest(m) 91 m2 := manifestbuilder.New(f, "fe2").WithK8sYAML(testyaml.SanchoYAML).Build() 92 f.upsertManifest(m2) 93 f.Store.WithState(func(state *store.EngineState) { 94 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 95 StartTime: time.Now(), 96 FinishTime: time.Now(), 97 }) 98 state.ManifestTargets["fe2"].State.AddCompletedBuild(model.BuildRecord{ 99 StartTime: time.Now(), 100 FinishTime: time.Now(), 101 }) 102 }) 103 104 f.MustReconcile(sessionKey) 105 f.requireNotDone() 106 107 f.Store.WithState(func(state *store.EngineState) { 108 mt := state.ManifestTargets["fe"] 109 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, v1alpha1.Pod{ 110 Name: "pod-a", 111 Status: "ErrImagePull", 112 Containers: []v1alpha1.Container{ 113 { 114 Name: "c1", 115 State: v1alpha1.ContainerState{ 116 Terminated: &v1alpha1.ContainerStateTerminated{ 117 StartedAt: metav1.Now(), 118 FinishedAt: metav1.Now(), 119 Reason: "Error", 120 ExitCode: 127, 121 }, 122 }, 123 }, 124 }, 125 }) 126 }) 127 128 f.MustReconcile(sessionKey) 129 f.requireDoneWithError("Pod pod-a in error state due to container c1: ErrImagePull") 130 } 131 132 func TestExitControlCI_GracePeriod(t *testing.T) { 133 f := newFixture(t, store.EngineModeCI) 134 135 var session v1alpha1.Session 136 f.MustGet(types.NamespacedName{Name: "Tiltfile"}, &session) 137 session.Spec.CI = &v1alpha1.SessionCISpec{K8sGracePeriod: &metav1.Duration{Duration: time.Minute}} 138 f.Update(&session) 139 140 f.upsertFailingPod("fe") 141 142 f.MustReconcile(sessionKey) 143 f.requireNotDone() 144 145 f.clock.Advance(50 * time.Second) 146 147 f.MustReconcile(sessionKey) 148 f.requireNotDone() 149 150 f.clock.Advance(20 * time.Second) 151 f.MustReconcile(sessionKey) 152 f.requireDoneWithError("exceeded grace period: Pod pod-a in error state due to container c1: ErrImagePull") 153 } 154 155 func TestExitControlCI_Timeout(t *testing.T) { 156 f := newFixture(t, store.EngineModeCI) 157 158 var session v1alpha1.Session 159 f.MustGet(types.NamespacedName{Name: "Tiltfile"}, &session) 160 session.Spec.CI = &v1alpha1.SessionCISpec{Timeout: &metav1.Duration{Duration: time.Minute}} 161 f.Update(&session) 162 163 m := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 164 f.upsertManifest(m) 165 166 f.MustReconcile(sessionKey) 167 f.requireNotDone() 168 169 f.clock.Advance(50 * time.Second) 170 171 f.MustReconcile(sessionKey) 172 f.requireNotDone() 173 174 f.clock.Advance(20 * time.Second) 175 f.MustReconcile(sessionKey) 176 f.requireDoneWithError("Timeout after 1m0s: 2 resources waiting (fe:runtime waiting-for-pod,fe:update waiting-for-cluster)") 177 } 178 179 func TestExitControlCI_PodRunningContainerError(t *testing.T) { 180 f := newFixture(t, store.EngineModeCI) 181 182 m := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 183 f.upsertManifest(m) 184 f.Store.WithState(func(state *store.EngineState) { 185 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 186 StartTime: time.Now(), 187 FinishTime: time.Now(), 188 }) 189 }) 190 191 f.MustReconcile(sessionKey) 192 f.requireNotDone() 193 194 f.Store.WithState(func(state *store.EngineState) { 195 mt := state.ManifestTargets["fe"] 196 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, v1alpha1.Pod{ 197 Name: "pod-a", 198 Phase: string(v1.PodRunning), 199 Containers: []v1alpha1.Container{ 200 { 201 Name: "c1", 202 Ready: false, 203 Restarts: 400, 204 State: v1alpha1.ContainerState{ 205 Terminated: &v1alpha1.ContainerStateTerminated{ 206 StartedAt: metav1.Now(), 207 FinishedAt: metav1.Now(), 208 Reason: "Error", 209 ExitCode: 127, 210 }, 211 }, 212 }, 213 { 214 Name: "c2", 215 Ready: true, 216 State: v1alpha1.ContainerState{ 217 Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()}, 218 }, 219 }, 220 }, 221 }) 222 }) 223 224 f.MustReconcile(sessionKey) 225 // even though one of the containers is in an error state, CI shouldn't exit - expectation is that the target for 226 // the pod is in Waiting state 227 f.requireNotDone() 228 229 f.Store.WithState(func(state *store.EngineState) { 230 mt := state.ManifestTargets["fe"] 231 pod := mt.State.K8sRuntimeState().GetPods()[0] 232 c1 := pod.Containers[0] 233 c1.Ready = true 234 c1.Restarts++ 235 c1.State = v1alpha1.ContainerState{ 236 Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()}, 237 } 238 pod.Containers[0] = c1 239 }) 240 241 f.MustReconcile(sessionKey) 242 f.requireDoneWithNoError() 243 } 244 245 func TestExitControlCI_Success(t *testing.T) { 246 f := newFixture(t, store.EngineModeCI) 247 248 m := manifestbuilder.New(f, "fe"). 249 WithK8sYAML(testyaml.SanchoYAML). 250 WithK8sPodReadiness(model.PodReadinessWait). 251 Build() 252 f.upsertManifest(m) 253 254 m2 := manifestbuilder.New(f, "fe2"). 255 WithK8sYAML(testyaml.SanchoYAML). 256 WithK8sPodReadiness(model.PodReadinessWait). 257 Build() 258 f.upsertManifest(m2) 259 260 f.Store.WithState(func(state *store.EngineState) { 261 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 262 StartTime: time.Now(), 263 FinishTime: time.Now(), 264 }) 265 state.ManifestTargets["fe2"].State.AddCompletedBuild(model.BuildRecord{ 266 StartTime: time.Now(), 267 FinishTime: time.Now(), 268 }) 269 // pod-a: ready / pod-b: doesn't exist 270 state.ManifestTargets["fe"].State.RuntimeState = store.NewK8sRuntimeStateWithPods(m, pod("pod-a", true)) 271 }) 272 273 f.MustReconcile(sessionKey) 274 f.requireNotDone() 275 276 // pod-a: ready / pod-b: ready 277 f.Store.WithState(func(state *store.EngineState) { 278 mt := state.ManifestTargets["fe2"] 279 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, pod("pod-b", true)) 280 }) 281 282 f.MustReconcile(sessionKey) 283 f.requireDoneWithNoError() 284 } 285 286 func TestExitControlCI_PodReadinessMode_Wait(t *testing.T) { 287 f := newFixture(t, store.EngineModeCI) 288 289 m := manifestbuilder.New(f, "fe"). 290 WithK8sYAML(testyaml.SanchoYAML). 291 WithK8sPodReadiness(model.PodReadinessWait). 292 Build() 293 f.upsertManifest(m) 294 f.Store.WithState(func(state *store.EngineState) { 295 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 296 StartTime: time.Now(), 297 FinishTime: time.Now(), 298 }) 299 300 state.ManifestTargets["fe"].State.RuntimeState = store.NewK8sRuntimeStateWithPods(m, 301 pod("pod-a", false)) 302 }) 303 304 f.MustReconcile(sessionKey) 305 f.requireNotDone() 306 307 f.Store.WithState(func(state *store.EngineState) { 308 mt := state.ManifestTargets["fe"] 309 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, 310 pod("pod-a", true), 311 ) 312 }) 313 314 f.MustReconcile(sessionKey) 315 f.requireDoneWithNoError() 316 } 317 318 // TestExitControlCI_PodReadinessMode_Ignore_Pods covers the case where you don't care about a Pod's readiness state 319 func TestExitControlCI_PodReadinessMode_Ignore_Pods(t *testing.T) { 320 f := newFixture(t, store.EngineModeCI) 321 322 m := manifestbuilder.New(f, "fe"). 323 WithK8sYAML(testyaml.SecretYaml). 324 WithK8sPodReadiness(model.PodReadinessIgnore). 325 Build() 326 f.upsertManifest(m) 327 f.Store.WithState(func(state *store.EngineState) { 328 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 329 StartTime: time.Now(), 330 FinishTime: time.Now(), 331 }) 332 333 // created but no pods yet 334 state.ManifestTargets["fe"].State.RuntimeState = store.NewK8sRuntimeState(m) 335 }) 336 337 f.MustReconcile(sessionKey) 338 f.requireNotDone() 339 340 f.Store.WithState(func(state *store.EngineState) { 341 mt := state.ManifestTargets["fe"] 342 // pod deployed, but explicitly not ready - we should not care and exit anyway 343 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, pod("pod-a", false)) 344 }) 345 346 f.MustReconcile(sessionKey) 347 f.requireDoneWithNoError() 348 } 349 350 // TestExitControlCI_PodReadinessMode_Ignore_NoPods covers the case where there are K8s resources that have no 351 // runtime component (i.e. no pods) - this most commonly happens with "uncategorized" 352 func TestExitControlCI_PodReadinessMode_Ignore_NoPods(t *testing.T) { 353 f := newFixture(t, store.EngineModeCI) 354 355 m := manifestbuilder.New(f, "fe"). 356 WithK8sYAML(testyaml.SecretYaml). 357 WithK8sPodReadiness(model.PodReadinessIgnore). 358 Build() 359 f.upsertManifest(m) 360 f.Store.WithState(func(state *store.EngineState) { 361 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 362 StartTime: time.Now(), 363 FinishTime: time.Now(), 364 }) 365 366 state.ManifestTargets["fe"].State.RuntimeState = store.NewK8sRuntimeState(m) 367 }) 368 369 f.MustReconcile(sessionKey) 370 f.requireNotDone() 371 372 f.Store.WithState(func(state *store.EngineState) { 373 mt := state.ManifestTargets["fe"] 374 krs := store.NewK8sRuntimeState(mt.Manifest) 375 // entities were created, but there's no pods in sight! 376 krs.HasEverDeployedSuccessfully = true 377 mt.State.RuntimeState = krs 378 }) 379 380 f.MustReconcile(sessionKey) 381 f.requireDoneWithNoError() 382 } 383 384 func TestExitControlCI_JobSuccess(t *testing.T) { 385 f := newFixture(t, store.EngineModeCI) 386 387 m := manifestbuilder.New(f, "fe"). 388 WithK8sYAML(testyaml.JobYAML). 389 WithK8sPodReadiness(model.PodReadinessSucceeded). 390 Build() 391 f.upsertManifest(m) 392 f.Store.WithState(func(state *store.EngineState) { 393 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 394 StartTime: time.Now(), 395 FinishTime: time.Now(), 396 }) 397 krs := store.NewK8sRuntimeStateWithPods(m, pod("pod-a", true)) 398 state.ManifestTargets["fe"].State.RuntimeState = krs 399 }) 400 401 f.MustReconcile(sessionKey) 402 f.requireNotDone() 403 404 f.Store.WithState(func(state *store.EngineState) { 405 mt := state.ManifestTargets["fe"] 406 krs := store.NewK8sRuntimeStateWithPods(mt.Manifest, successPod("pod-a")) 407 mt.State.RuntimeState = krs 408 }) 409 410 f.MustReconcile(sessionKey) 411 f.requireDoneWithNoError() 412 } 413 414 func TestExitControlCI_JobSuccessWithNoPods(t *testing.T) { 415 f := newFixture(t, store.EngineModeCI) 416 417 m := manifestbuilder.New(f, "fe"). 418 WithK8sYAML(testyaml.JobYAML). 419 WithK8sPodReadiness(model.PodReadinessSucceeded). 420 Build() 421 f.upsertManifest(m) 422 f.Store.WithState(func(state *store.EngineState) { 423 state.ManifestTargets["fe"].State.AddCompletedBuild(model.BuildRecord{ 424 StartTime: time.Now(), 425 FinishTime: time.Now(), 426 }) 427 }) 428 429 f.MustReconcile(sessionKey) 430 f.requireNotDone() 431 432 f.Store.WithState(func(state *store.EngineState) { 433 mt := state.ManifestTargets["fe"] 434 krs := store.NewK8sRuntimeState(mt.Manifest) 435 krs.HasEverDeployedSuccessfully = true 436 // There are no pods but the job completed successfully. 437 krs.Conditions = []metav1.Condition{ 438 { 439 Type: v1alpha1.ApplyConditionJobComplete, 440 Status: metav1.ConditionTrue, 441 }, 442 } 443 mt.State.RuntimeState = krs 444 }) 445 446 f.MustReconcile(sessionKey) 447 f.requireDoneWithNoError() 448 } 449 450 func TestExitControlCI_TriggerMode_Local(t *testing.T) { 451 type tc struct { 452 triggerMode model.TriggerMode 453 serveCmd bool 454 } 455 var tcs []tc 456 for triggerMode := range model.TriggerModes { 457 for _, hasServeCmd := range []bool{false, true} { 458 tcs = append(tcs, tc{ 459 triggerMode: triggerMode, 460 serveCmd: hasServeCmd, 461 }) 462 } 463 } 464 465 for _, tc := range tcs { 466 name := triggerModeString(tc.triggerMode) 467 if !tc.serveCmd { 468 name += "_EmptyServeCmd" 469 } 470 t.Run(name, func(t *testing.T) { 471 f := newFixture(t, store.EngineModeCI) 472 473 mb := manifestbuilder.New(f, "fe"). 474 WithLocalResource("echo hi", nil). 475 WithTriggerMode(tc.triggerMode) 476 477 if tc.serveCmd { 478 mb = mb.WithLocalServeCmd("while true; echo hi; done") 479 } 480 481 f.upsertManifest(mb.Build()) 482 483 if tc.triggerMode.AutoInitial() { 484 // because this resource SHOULD start automatically, no exit signal should be received before 485 // a build has completed 486 f.MustReconcile(sessionKey) 487 f.requireNotDone() 488 489 // N.B. a build is triggered regardless of if there is an update_cmd! it's a fake build produced 490 // by the engine in this case, which is why this test doesn't have cases for empty update_cmd 491 f.Store.WithState(func(state *store.EngineState) { 492 mt := state.ManifestTargets["fe"] 493 mt.State.AddCompletedBuild(model.BuildRecord{ 494 StartTime: time.Now(), 495 FinishTime: time.Now(), 496 }) 497 }) 498 499 if tc.serveCmd { 500 // the serve_cmd hasn't started yet, so no exit signal should be received still even though 501 // a build occurred 502 f.MustReconcile(sessionKey) 503 f.requireNotDone() 504 505 // only mimic a runtime state if there is a serve_cmd since this won't be populated 506 // otherwise 507 f.Store.WithState(func(state *store.EngineState) { 508 mt := state.ManifestTargets["fe"] 509 mt.State.RuntimeState = store.LocalRuntimeState{ 510 CmdName: "echo hi", 511 Status: v1alpha1.RuntimeStatusOK, 512 PID: 1234, 513 StartTime: time.Now(), 514 LastReadyOrSucceededTime: time.Now(), 515 Ready: true, 516 } 517 }) 518 } 519 } 520 521 // for auto_init=True, it's now ready, so can exit 522 // for auto_init=False, it should NOT block on it, so can exit 523 f.MustReconcile(sessionKey) 524 f.requireDoneWithNoError() 525 }) 526 } 527 } 528 529 func TestExitControlCI_TriggerMode_K8s(t *testing.T) { 530 for triggerMode := range model.TriggerModes { 531 t.Run(triggerModeString(triggerMode), func(t *testing.T) { 532 f := newFixture(t, store.EngineModeCI) 533 534 manifest := manifestbuilder.New(f, "fe"). 535 WithK8sYAML(testyaml.JobYAML). 536 WithTriggerMode(triggerMode). 537 Build() 538 f.upsertManifest(manifest) 539 540 if triggerMode.AutoInitial() { 541 // because this resource SHOULD start automatically, no exit signal should be received until it's ready 542 f.MustReconcile(sessionKey) 543 f.requireNotDone() 544 545 f.Store.WithState(func(state *store.EngineState) { 546 mt := state.ManifestTargets["fe"] 547 mt.State.AddCompletedBuild(model.BuildRecord{ 548 StartTime: time.Now(), 549 FinishTime: time.Now(), 550 }) 551 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, successPod("pod-a")) 552 }) 553 } 554 555 // for auto_init=True, it's now ready, so can exit 556 // for auto_init=False, it should NOT block on it, so can exit 557 f.MustReconcile(sessionKey) 558 f.requireDoneWithNoError() 559 }) 560 } 561 } 562 563 func TestExitControlCI_Disabled(t *testing.T) { 564 f := newFixture(t, store.EngineModeCI) 565 566 f.Store.WithState(func(state *store.EngineState) { 567 m1 := manifestbuilder.New(f, "m1").WithLocalServeCmd("m1").Build() 568 mt1 := store.NewManifestTarget(m1) 569 mt1.State.DisableState = v1alpha1.DisableStateDisabled 570 state.UpsertManifestTarget(mt1) 571 572 m2 := manifestbuilder.New(f, "m2").WithLocalResource("m2", nil).Build() 573 mt2 := store.NewManifestTarget(m2) 574 mt2.State.AddCompletedBuild(model.BuildRecord{ 575 StartTime: time.Now(), 576 FinishTime: time.Now(), 577 }) 578 mt2.State.DisableState = v1alpha1.DisableStateEnabled 579 state.UpsertManifestTarget(mt2) 580 }) 581 582 // the manifest is disabled, so we should be ready to exit 583 f.MustReconcile(sessionKey) 584 f.requireDoneWithNoError() 585 } 586 587 func TestStatusDisabled(t *testing.T) { 588 f := newFixture(t, store.EngineModeCI) 589 590 f.Store.WithState(func(state *store.EngineState) { 591 m1 := manifestbuilder.New(f, "local_update").WithLocalResource("a", nil).Build() 592 m2 := manifestbuilder.New(f, "local_serve").WithLocalServeCmd("a").Build() 593 m3 := manifestbuilder.New(f, "k8s").WithK8sYAML(testyaml.JobYAML).Build() 594 m4 := manifestbuilder.New(f, "dc").WithDockerCompose().Build() 595 for _, m := range []model.Manifest{m1, m2, m3, m4} { 596 mt := store.NewManifestTarget(m) 597 mt.State.DisableState = v1alpha1.DisableStateDisabled 598 state.UpsertManifestTarget(mt) 599 } 600 }) 601 602 f.MustReconcile(sessionKey) 603 status := f.sessionStatus() 604 targetbyName := make(map[string]v1alpha1.Target) 605 for _, target := range status.Targets { 606 targetbyName[target.Name] = target 607 } 608 609 expectedTargets := []string{ 610 "dc:runtime", 611 "dc:update", 612 "k8s:runtime", 613 "k8s:update", 614 "local_update:update", 615 "local_serve:serve", 616 } 617 // + 1 for Tiltfile 618 require.Len(t, targetbyName, len(expectedTargets)+1) 619 for _, name := range expectedTargets { 620 target, ok := targetbyName[name] 621 require.Truef(t, ok, "no target named %q", name) 622 require.NotNil(t, target.State.Disabled) 623 } 624 } 625 626 func TestRequeueLongGracePeriod(t *testing.T) { 627 f := newFixture(t, store.EngineModeCI) 628 629 var session v1alpha1.Session 630 f.MustGet(types.NamespacedName{Name: "Tiltfile"}, &session) 631 session.Spec.CI = &v1alpha1.SessionCISpec{ 632 Timeout: &metav1.Duration{Duration: time.Minute}, 633 K8sGracePeriod: &metav1.Duration{Duration: 10 * time.Minute}, 634 } 635 f.Update(&session) 636 637 f.upsertFailingPod("fe") 638 639 result, err := f.Reconcile(sessionKey) 640 require.NoError(t, err) 641 assert.Equal(t, time.Minute, result.RequeueAfter) 642 643 f.clock.Advance(50 * time.Second) 644 645 result, err = f.Reconcile(sessionKey) 646 require.NoError(t, err) 647 assert.Equal(t, 10*time.Second, result.RequeueAfter) 648 } 649 650 func TestRequeueLongTimeout(t *testing.T) { 651 f := newFixture(t, store.EngineModeCI) 652 653 var session v1alpha1.Session 654 f.MustGet(types.NamespacedName{Name: "Tiltfile"}, &session) 655 session.Spec.CI = &v1alpha1.SessionCISpec{ 656 Timeout: &metav1.Duration{Duration: 10 * time.Minute}, 657 K8sGracePeriod: &metav1.Duration{Duration: time.Minute}, 658 } 659 f.Update(&session) 660 661 f.upsertFailingPod("fe") 662 663 result, err := f.Reconcile(sessionKey) 664 require.NoError(t, err) 665 assert.Equal(t, time.Minute, result.RequeueAfter) 666 667 f.clock.Advance(50 * time.Second) 668 669 result, err = f.Reconcile(sessionKey) 670 require.NoError(t, err) 671 assert.Equal(t, 10*time.Second, result.RequeueAfter) 672 } 673 674 type fixture struct { 675 *fake.ControllerFixture 676 tf *tempdir.TempDirFixture 677 r *Reconciler 678 clock clockwork.FakeClock 679 } 680 681 func newFixture(t testing.TB, engineMode store.EngineMode) *fixture { 682 cfb := fake.NewControllerFixtureBuilder(t) 683 tdf := tempdir.NewTempDirFixture(t) 684 st := cfb.Store 685 mn := model.MainTiltfileManifestName 686 tf := &v1alpha1.Tiltfile{ 687 ObjectMeta: metav1.ObjectMeta{Name: mn.String()}, 688 Spec: v1alpha1.TiltfileSpec{Path: tdf.JoinPath("Tiltfile")}, 689 } 690 st.WithState(func(state *store.EngineState) { 691 tiltfiles.HandleTiltfileUpsertAction(state, tiltfiles.TiltfileUpsertAction{ 692 Tiltfile: tf, 693 }) 694 state.TiltfileStates[mn].AddCompletedBuild(model.BuildRecord{ 695 StartTime: time.Now(), 696 FinishTime: time.Now(), 697 Reason: model.BuildReasonFlagInit, 698 }) 699 }) 700 701 clock := clockwork.NewFakeClock() 702 r := NewReconciler(cfb.Client, st, clock) 703 cf := cfb.Build(r) 704 705 session := sessions.FromTiltfile(tf, nil, model.CITimeoutFlag(model.CITimeoutDefault), engineMode) 706 session.Status.StartTime = apis.NewMicroTime(clock.Now()) 707 cf.Create(session) 708 709 return &fixture{ 710 ControllerFixture: cf, 711 tf: tdf, 712 r: r, 713 clock: clock, 714 } 715 } 716 717 func (f *fixture) upsertManifest(m model.Manifest) { 718 f.Store.WithState(func(state *store.EngineState) { 719 mt := store.NewManifestTarget(m) 720 mt.State.DisableState = v1alpha1.DisableStateEnabled 721 state.UpsertManifestTarget(mt) 722 }) 723 } 724 725 func (f *fixture) sessionStatus() v1alpha1.SessionStatus { 726 f.T().Helper() 727 var session v1alpha1.Session 728 f.MustGet(types.NamespacedName{Name: "Tiltfile"}, &session) 729 return session.Status 730 } 731 732 func (f *fixture) requireNotDone() { 733 f.T().Helper() 734 require.False(f.T(), f.sessionStatus().Done) 735 } 736 737 func (f *fixture) requireDoneWithError(errString string) { 738 f.T().Helper() 739 status := f.sessionStatus() 740 assert.True(f.T(), status.Done) 741 require.Equal(f.T(), status.Error, errString) 742 } 743 744 func (f *fixture) requireDoneWithNoError() { 745 f.T().Helper() 746 status := f.sessionStatus() 747 assert.True(f.T(), status.Done) 748 require.Equal(f.T(), status.Error, "") 749 } 750 751 func (f *fixture) JoinPath(path ...string) string { 752 return f.tf.JoinPath(path...) 753 } 754 func (f *fixture) MkdirAll(path string) { 755 f.tf.MkdirAll(path) 756 } 757 func (f *fixture) Path() string { 758 return f.tf.Path() 759 } 760 761 func (f *fixture) upsertFailingPod(mn model.ManifestName) { 762 m := manifestbuilder.New(f, mn).WithK8sYAML(testyaml.SanchoYAML).Build() 763 f.upsertManifest(m) 764 f.Store.WithState(func(state *store.EngineState) { 765 mt := state.ManifestTargets[mn] 766 mt.State.AddCompletedBuild(model.BuildRecord{ 767 StartTime: f.clock.Now(), 768 FinishTime: f.clock.Now(), 769 }) 770 mt.State.LastSuccessfulDeployTime = f.clock.Now() 771 mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(mt.Manifest, v1alpha1.Pod{ 772 Name: "pod-a", 773 Status: "ErrImagePull", 774 Containers: []v1alpha1.Container{ 775 { 776 Name: "c1", 777 State: v1alpha1.ContainerState{ 778 Terminated: &v1alpha1.ContainerStateTerminated{ 779 StartedAt: apis.NewTime(f.clock.Now()), 780 FinishedAt: apis.NewTime(f.clock.Now()), 781 Reason: "Error", 782 ExitCode: 127, 783 }, 784 }, 785 }, 786 }, 787 }) 788 }) 789 } 790 791 func pod(podID k8s.PodID, ready bool) v1alpha1.Pod { 792 return v1alpha1.Pod{ 793 Name: podID.String(), 794 Phase: string(v1.PodRunning), 795 Containers: []v1alpha1.Container{ 796 { 797 ID: string(podID + "-container"), 798 Ready: ready, 799 State: v1alpha1.ContainerState{ 800 Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()}, 801 }, 802 }, 803 }, 804 } 805 } 806 807 func successPod(podID k8s.PodID) v1alpha1.Pod { 808 return v1alpha1.Pod{ 809 Name: podID.String(), 810 Phase: string(v1.PodSucceeded), 811 Status: "Completed", 812 Containers: []v1alpha1.Container{ 813 { 814 ID: string(podID + "-container"), 815 State: v1alpha1.ContainerState{ 816 Terminated: &v1alpha1.ContainerStateTerminated{ 817 StartedAt: metav1.Now(), 818 FinishedAt: metav1.Now(), 819 ExitCode: 0, 820 }, 821 }, 822 }, 823 }, 824 } 825 } 826 827 func triggerModeString(v model.TriggerMode) string { 828 switch v { 829 case model.TriggerModeAuto: 830 return "TriggerModeAuto" 831 case model.TriggerModeAutoWithManualInit: 832 return "TriggerModeAutoWithManualInit" 833 case model.TriggerModeManual: 834 return "TriggerModeManual" 835 case model.TriggerModeManualWithAutoInit: 836 return "TriggerModeManualWithAutoInit" 837 default: 838 panic(fmt.Errorf("unknown trigger mode value: %v", v)) 839 } 840 }