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  }