github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/build_control_test.go (about)

     1  package buildcontrol
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"testing"
     7  	"time"
     8  
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  
    11  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	v1 "k8s.io/api/core/v1"
    16  
    17  	"github.com/tilt-dev/tilt/internal/container"
    18  	"github.com/tilt-dev/tilt/internal/k8s"
    19  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    20  	"github.com/tilt-dev/tilt/internal/store"
    21  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    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/model"
    25  )
    26  
    27  func TestNextTargetToBuildDoesntReturnCurrentlyBuildingTarget(t *testing.T) {
    28  	f := newTestFixture(t)
    29  
    30  	mt := f.upsertK8sManifest("k8s1")
    31  	f.st.UpsertManifestTarget(mt)
    32  
    33  	// Verify this target is normally next-to-build
    34  	f.assertNextTargetToBuild(mt.Manifest.Name)
    35  
    36  	// If target is currently building, should NOT be next-to-build
    37  	mt.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    38  	f.assertNoTargetNextToBuild()
    39  }
    40  
    41  func TestCurrentlyBuildingK8sResourceDisablesLocalScheduling(t *testing.T) {
    42  	f := newTestFixture(t)
    43  
    44  	k8s1 := f.upsertK8sManifest("k8s1")
    45  	k8s2 := f.upsertK8sManifest("k8s2")
    46  	f.upsertLocalManifest("local1")
    47  
    48  	f.assertNextTargetToBuild("local1")
    49  
    50  	k8s1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    51  	f.assertNextTargetToBuild("k8s2")
    52  	f.assertHold("local1", store.HoldReasonIsUnparallelizableTarget)
    53  
    54  	k8s2.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    55  	f.assertNoTargetNextToBuild()
    56  }
    57  
    58  func TestCurrentlyBuildingK8sResourceDoesNotCreateHoldIfResourceNotPending(t *testing.T) {
    59  	f := newTestFixture(t)
    60  
    61  	k8s1 := f.upsertK8sManifest("k8s1")
    62  	k8s2 := f.upsertK8sManifest("k8s2")
    63  	f.upsertLocalManifest("local1", func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
    64  		return m.WithTriggerMode(model.TriggerModeManual)
    65  	})
    66  
    67  	f.assertHold("local1", store.HoldReasonNone)
    68  
    69  	k8s1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    70  	f.assertNextTargetToBuild("k8s2")
    71  	f.assertHold("local1", store.HoldReasonNone)
    72  
    73  	k8s2.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    74  	f.assertNoTargetNextToBuild()
    75  	f.assertHold("local1", store.HoldReasonNone)
    76  }
    77  
    78  func TestCurrentlyBuildingUncategorizedDisablesOtherK8sTargets(t *testing.T) {
    79  	f := newTestFixture(t)
    80  
    81  	_ = f.upsertK8sManifest("k8s1")
    82  	k8sUnresourced := f.upsertK8sManifest(model.UnresourcedYAMLManifestName)
    83  	_ = f.upsertK8sManifest("k8s2")
    84  
    85  	f.assertNextTargetToBuild(model.UnresourcedYAMLManifestName)
    86  	k8sUnresourced.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
    87  	f.assertNoTargetNextToBuild()
    88  	for _, mn := range []model.ManifestName{"k8s1", "k8s2"} {
    89  		f.assertHold(mn, store.HoldReasonWaitingForUncategorized, model.ManifestName("uncategorized").TargetID())
    90  	}
    91  }
    92  
    93  func TestK8sDependsOnLocal(t *testing.T) {
    94  	f := newTestFixture(t)
    95  
    96  	k8s1 := f.upsertK8sManifest("k8s1", withResourceDeps("local1"))
    97  	k8s2 := f.upsertK8sManifest("k8s2")
    98  	local1 := f.upsertLocalManifest("local1")
    99  
   100  	f.assertNextTargetToBuild("local1")
   101  
   102  	f.assertHold("k8s1", store.HoldReasonWaitingForDep, model.ManifestName("local1").TargetID())
   103  	f.assertHold("k8s2", store.HoldReasonNone)
   104  
   105  	local1.State.AddCompletedBuild(model.BuildRecord{
   106  		StartTime:  time.Now(),
   107  		FinishTime: time.Now(),
   108  	})
   109  	lrs := local1.State.LocalRuntimeState()
   110  	lrs.LastReadyOrSucceededTime = time.Now()
   111  	local1.State.RuntimeState = lrs
   112  
   113  	f.assertNextTargetToBuild("k8s1")
   114  	k8s1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   115  	f.assertNextTargetToBuild("k8s2")
   116  
   117  	_ = k8s2
   118  }
   119  
   120  func TestLocalDependsOnNonWorkloadK8s(t *testing.T) {
   121  	f := newTestFixture(t)
   122  
   123  	local1 := f.upsertLocalManifest("local1", withResourceDeps("k8s1"))
   124  	k8s1 := f.upsertK8sManifest("k8s1", withK8sPodReadiness(model.PodReadinessIgnore))
   125  	k8s2 := f.upsertK8sManifest("k8s2", withK8sPodReadiness(model.PodReadinessIgnore))
   126  
   127  	f.assertNextTargetToBuild("k8s1")
   128  	f.assertHold("local1", store.HoldReasonWaitingForDep, model.ManifestName("k8s1").TargetID())
   129  	f.assertHold("k8s1", store.HoldReasonNone)
   130  	f.assertHold("k8s2", store.HoldReasonNone)
   131  
   132  	k8s1.State.AddCompletedBuild(model.BuildRecord{
   133  		StartTime:  time.Now(),
   134  		FinishTime: time.Now(),
   135  	})
   136  	k8s1.State.RuntimeState = store.K8sRuntimeState{
   137  		PodReadinessMode:            model.PodReadinessIgnore,
   138  		HasEverDeployedSuccessfully: true,
   139  	}
   140  
   141  	f.assertNextTargetToBuild("local1")
   142  	local1.State.AddCompletedBuild(model.BuildRecord{
   143  		StartTime:  time.Now(),
   144  		FinishTime: time.Now(),
   145  	})
   146  	f.assertNextTargetToBuild("k8s2")
   147  
   148  	_ = k8s2
   149  }
   150  
   151  func TestK8sDependsOnCluster(t *testing.T) {
   152  	f := newTestFixture(t)
   153  
   154  	f.st.Clusters["default"].Status.Error = "connection error"
   155  
   156  	_ = f.upsertK8sManifest("k8s1")
   157  	f.assertNoTargetNextToBuild()
   158  	f.assertHoldOnRefs("k8s1", store.HoldReasonCluster, v1alpha1.UIResourceStateWaitingOnRef{
   159  		Group:      "tilt.dev",
   160  		APIVersion: "v1alpha1",
   161  		Kind:       "Cluster",
   162  		Name:       "default",
   163  	})
   164  
   165  	f.st.Clusters["default"].Status.Error = ""
   166  	f.assertNextTargetToBuild("k8s1")
   167  }
   168  
   169  func TestCurrentlyBuildingLocalResourceDisablesK8sScheduling(t *testing.T) {
   170  	f := newTestFixture(t)
   171  
   172  	f.upsertK8sManifest("k8s1")
   173  	f.upsertK8sManifest("k8s2")
   174  	local1 := f.upsertLocalManifest("local1")
   175  	f.upsertLocalManifest("local2")
   176  
   177  	f.assertNextTargetToBuild("local1")
   178  	local1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   179  	f.assertNoTargetNextToBuild()
   180  	for _, mn := range []model.ManifestName{"k8s1", "k8s2", "local2"} {
   181  		f.assertHold(mn, store.HoldReasonWaitingForUnparallelizableTarget, model.ManifestName("local1").TargetID())
   182  	}
   183  }
   184  
   185  func TestCurrentlyBuildingParallelLocalResource(t *testing.T) {
   186  	f := newTestFixture(t)
   187  
   188  	f.upsertK8sManifest("k8s1")
   189  	local1 := f.upsertLocalManifest("local1", func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
   190  		return m.WithLocalAllowParallel(true)
   191  	})
   192  	local2 := f.upsertLocalManifest("local2", func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
   193  		return m.WithLocalAllowParallel(true)
   194  	})
   195  
   196  	f.assertNextTargetToBuild("local1")
   197  
   198  	local1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   199  	f.assertNextTargetToBuild("local2")
   200  
   201  	local2.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   202  	f.assertNextTargetToBuild("k8s1")
   203  }
   204  
   205  func TestTriggerIneligibleResource(t *testing.T) {
   206  	f := newTestFixture(t)
   207  
   208  	// local1 has a build in progress
   209  	local1 := f.upsertLocalManifest("local1", func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
   210  		return m.WithLocalAllowParallel(true)
   211  	})
   212  	local1.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   213  
   214  	// local2 is not parallelizable
   215  	local2 := f.upsertLocalManifest("local2")
   216  
   217  	f.st.AppendToTriggerQueue(local1.Manifest.Name, model.BuildReasonFlagTriggerCLI)
   218  	f.st.AppendToTriggerQueue(local2.Manifest.Name, model.BuildReasonFlagTriggerCLI)
   219  	f.assertNoTargetNextToBuild()
   220  }
   221  
   222  func TestTwoK8sTargetsWithBaseImage(t *testing.T) {
   223  	f := newTestFixture(t)
   224  
   225  	baseImage := newDockerImageTarget("sancho-base")
   226  	sanchoOneImage := newDockerImageTarget("sancho-one").
   227  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   228  	sanchoTwoImage := newDockerImageTarget("sancho-two").
   229  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   230  
   231  	sanchoOne := f.upsertManifest(manifestbuilder.New(f, "sancho-one").
   232  		WithImageTargets(baseImage, sanchoOneImage).
   233  		WithK8sYAML(testyaml.SanchoYAML).
   234  		Build())
   235  	f.upsertManifest(manifestbuilder.New(f, "sancho-two").
   236  		WithImageTargets(baseImage, sanchoTwoImage).
   237  		WithK8sYAML(testyaml.SanchoYAML).
   238  		Build())
   239  
   240  	f.assertNextTargetToBuild("sancho-one")
   241  
   242  	sanchoOne.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   243  
   244  	f.assertNoTargetNextToBuild()
   245  	f.assertHold("sancho-two", store.HoldReasonBuildingComponent, baseImage.ID())
   246  
   247  	delete(sanchoOne.State.CurrentBuilds, "buildcontrol")
   248  	sanchoOne.State.AddCompletedBuild(model.BuildRecord{
   249  		StartTime:  time.Now(),
   250  		FinishTime: time.Now(),
   251  	})
   252  
   253  	f.assertNextTargetToBuild("sancho-two")
   254  }
   255  
   256  func TestLiveUpdateMainImageHold(t *testing.T) {
   257  	f := newTestFixture(t)
   258  
   259  	srcFile := f.JoinPath("src", "a.txt")
   260  	f.WriteFile(srcFile, "hello")
   261  	luSpec := v1alpha1.LiveUpdateSpec{
   262  		BasePath: f.Path(),
   263  		Syncs: []v1alpha1.LiveUpdateSync{
   264  			{LocalPath: "src", ContainerPath: "/src"},
   265  		},
   266  		Sources: []v1alpha1.LiveUpdateSource{
   267  			{FileWatch: "image:sancho"},
   268  		},
   269  		Selector: v1alpha1.LiveUpdateSelector{
   270  			Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
   271  				ContainerName: "c",
   272  			},
   273  		},
   274  	}
   275  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{Spec: luSpec}
   276  
   277  	baseImage := newDockerImageTarget("sancho-base")
   278  	sanchoImage := newDockerImageTarget("sancho").
   279  		WithLiveUpdateSpec("sancho", luSpec).
   280  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   281  
   282  	sancho := f.upsertManifest(manifestbuilder.New(f, "sancho").
   283  		WithImageTargets(baseImage, sanchoImage).
   284  		WithK8sYAML(testyaml.SanchoYAML).
   285  		Build())
   286  
   287  	f.assertNextTargetToBuild("sancho")
   288  	sancho.State.AddCompletedBuild(model.BuildRecord{
   289  		StartTime:  time.Now(),
   290  		FinishTime: time.Now(),
   291  	})
   292  
   293  	resource := &k8sconv.KubernetesResource{
   294  		FilteredPods: []v1alpha1.Pod{
   295  			*readyPod("pod-1", sanchoImage.ImageMapSpec.Selector),
   296  		},
   297  	}
   298  	f.st.KubernetesResources["sancho"] = resource
   299  
   300  	sancho.State.MutableBuildStatus(sanchoImage.ID()).PendingFileChanges[srcFile] = time.Now()
   301  	f.assertNoTargetNextToBuild()
   302  	f.assertHold("sancho", store.HoldReasonReconciling)
   303  
   304  	// If the live update is failing, we have to rebuild.
   305  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{
   306  		Spec:   luSpec,
   307  		Status: v1alpha1.LiveUpdateStatus{Failed: &v1alpha1.LiveUpdateStateFailed{Reason: "fake-reason"}},
   308  	}
   309  	f.assertNextTargetToBuild("sancho")
   310  
   311  	// reset to a good state.
   312  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{Spec: luSpec}
   313  	f.assertNoTargetNextToBuild()
   314  
   315  	// If the base image has a change, we have to rebuild.
   316  	sancho.State.MutableBuildStatus(baseImage.ID()).PendingFileChanges[srcFile] = time.Now()
   317  	f.assertNextTargetToBuild("sancho")
   318  }
   319  
   320  // Test to make sure the buildcontroller does the translation
   321  // correctly between the image target with the file watch
   322  // and the image target matching the deployed container.
   323  func TestLiveUpdateBaseImageHold(t *testing.T) {
   324  	f := newTestFixture(t)
   325  
   326  	srcFile := f.JoinPath("base", "a.txt")
   327  	f.WriteFile(srcFile, "hello")
   328  
   329  	luSpec := v1alpha1.LiveUpdateSpec{
   330  		BasePath: f.Path(),
   331  		Syncs: []v1alpha1.LiveUpdateSync{
   332  			{LocalPath: "base", ContainerPath: "/base"},
   333  		},
   334  		Sources: []v1alpha1.LiveUpdateSource{
   335  			{FileWatch: "image:sancho-base"},
   336  		},
   337  	}
   338  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{Spec: luSpec}
   339  
   340  	baseImage := newDockerImageTarget("sancho-base")
   341  	sanchoImage := newDockerImageTarget("sancho").
   342  		WithLiveUpdateSpec("sancho", luSpec).
   343  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   344  
   345  	sancho := f.upsertManifest(manifestbuilder.New(f, "sancho").
   346  		WithImageTargets(baseImage, sanchoImage).
   347  		WithK8sYAML(testyaml.SanchoYAML).
   348  		Build())
   349  
   350  	f.assertNextTargetToBuild("sancho")
   351  	sancho.State.AddCompletedBuild(model.BuildRecord{
   352  		StartTime:  time.Now(),
   353  		FinishTime: time.Now(),
   354  	})
   355  
   356  	resource := &k8sconv.KubernetesResource{
   357  		FilteredPods: []v1alpha1.Pod{
   358  			*readyPod("pod-1", sanchoImage.Selector),
   359  		},
   360  	}
   361  	f.st.KubernetesResources["sancho"] = resource
   362  
   363  	sancho.State.MutableBuildStatus(baseImage.ID()).PendingFileChanges[srcFile] = time.Now()
   364  	f.assertNoTargetNextToBuild()
   365  	f.assertHold("sancho", store.HoldReasonReconciling)
   366  
   367  	// If the live update is failing, we have to rebuild.
   368  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{
   369  		Spec:   luSpec,
   370  		Status: v1alpha1.LiveUpdateStatus{Failed: &v1alpha1.LiveUpdateStateFailed{Reason: "fake-reason"}},
   371  	}
   372  	f.assertNextTargetToBuild("sancho")
   373  
   374  	// reset to a good state.
   375  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{Spec: luSpec}
   376  	f.assertNoTargetNextToBuild()
   377  
   378  	// If the deploy image has a change, we have to rebuild.
   379  	sancho.State.MutableBuildStatus(sanchoImage.ID()).PendingFileChanges[srcFile] = time.Now()
   380  	f.assertNextTargetToBuild("sancho")
   381  }
   382  
   383  func TestTwoK8sTargetsWithBaseImagePrebuilt(t *testing.T) {
   384  	f := newTestFixture(t)
   385  
   386  	baseImage := newDockerImageTarget("sancho-base")
   387  	sanchoOneImage := newDockerImageTarget("sancho-one").
   388  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   389  	sanchoTwoImage := newDockerImageTarget("sancho-two").
   390  		WithImageMapDeps([]string{baseImage.ImageMapName()})
   391  
   392  	sanchoOne := f.upsertManifest(manifestbuilder.New(f, "sancho-one").
   393  		WithImageTargets(baseImage, sanchoOneImage).
   394  		WithK8sYAML(testyaml.SanchoYAML).
   395  		Build())
   396  	sanchoTwo := f.upsertManifest(manifestbuilder.New(f, "sancho-two").
   397  		WithImageTargets(baseImage, sanchoTwoImage).
   398  		WithK8sYAML(testyaml.SanchoYAML).
   399  		Build())
   400  
   401  	sanchoOne.State.MutableBuildStatus(baseImage.ID()).LastResult = store.ImageBuildResult{}
   402  	sanchoTwo.State.MutableBuildStatus(baseImage.ID()).LastResult = store.ImageBuildResult{}
   403  
   404  	f.assertNextTargetToBuild("sancho-one")
   405  
   406  	sanchoOne.State.CurrentBuilds["buildcontrol"] = model.BuildRecord{StartTime: time.Now()}
   407  
   408  	// Make sure sancho-two can start while sanchoOne is still pending.
   409  	f.assertNextTargetToBuild("sancho-two")
   410  }
   411  
   412  func TestHoldForDeploy(t *testing.T) {
   413  	f := newTestFixture(t)
   414  
   415  	srcFile := f.JoinPath("src", "a.txt")
   416  	objFile := f.JoinPath("obj", "a.out")
   417  	fallbackFile := f.JoinPath("src", "package.json")
   418  	f.WriteFile(srcFile, "hello")
   419  	f.WriteFile(objFile, "hello")
   420  	f.WriteFile(fallbackFile, "hello")
   421  
   422  	luSpec := v1alpha1.LiveUpdateSpec{
   423  		BasePath:  f.Path(),
   424  		StopPaths: []string{filepath.Join("src", "package.json")},
   425  		Syncs:     []v1alpha1.LiveUpdateSync{{LocalPath: "src", ContainerPath: "/src"}},
   426  		Selector: v1alpha1.LiveUpdateSelector{
   427  			Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
   428  				ContainerName: "c",
   429  			},
   430  		},
   431  	}
   432  	sanchoImage := newDockerImageTarget("sancho").
   433  		WithLiveUpdateSpec("sancho", luSpec).
   434  		WithDockerImage(v1alpha1.DockerImageSpec{Context: f.Path()})
   435  	sancho := f.upsertManifest(manifestbuilder.New(f, "sancho").
   436  		WithImageTargets(sanchoImage).
   437  		WithK8sYAML(testyaml.SanchoYAML).
   438  		Build())
   439  
   440  	f.assertNextTargetToBuild("sancho")
   441  
   442  	sancho.State.AddCompletedBuild(model.BuildRecord{
   443  		StartTime:  time.Now(),
   444  		FinishTime: time.Now(),
   445  	})
   446  	f.assertNoTargetNextToBuild()
   447  
   448  	status := sancho.State.MutableBuildStatus(sanchoImage.ID())
   449  
   450  	status.PendingFileChanges[objFile] = time.Now()
   451  	f.assertNextTargetToBuild("sancho")
   452  	delete(status.PendingFileChanges, objFile)
   453  
   454  	status.PendingFileChanges[fallbackFile] = time.Now()
   455  	f.assertNextTargetToBuild("sancho")
   456  	delete(status.PendingFileChanges, fallbackFile)
   457  
   458  	status.PendingFileChanges[srcFile] = time.Now()
   459  	f.assertNoTargetNextToBuild()
   460  	f.assertHold("sancho", store.HoldReasonWaitingForDeploy)
   461  
   462  	resource := &k8sconv.KubernetesResource{
   463  		FilteredPods: []v1alpha1.Pod{},
   464  	}
   465  	f.st.KubernetesResources["sancho"] = resource
   466  
   467  	resource.FilteredPods = append(resource.FilteredPods, *readyPod("pod-1", sanchoImage.Selector))
   468  	f.assertNextTargetToBuild("sancho")
   469  
   470  	resource.FilteredPods[0] = *crashingPod("pod-1", sanchoImage.Selector)
   471  	f.assertNextTargetToBuild("sancho")
   472  
   473  	resource.FilteredPods[0] = *crashedInThePastPod("pod-1", sanchoImage.Selector)
   474  	f.assertNextTargetToBuild("sancho")
   475  
   476  	resource.FilteredPods[0] = *sidecarCrashedPod("pod-1", sanchoImage.Selector)
   477  	f.assertNextTargetToBuild("sancho")
   478  
   479  	resource.FilteredPods[0] = *completedPod("pod-1", sanchoImage.Selector)
   480  	f.assertNextTargetToBuild("sancho")
   481  }
   482  
   483  func TestHoldForManualLiveUpdate(t *testing.T) {
   484  	f := newTestFixture(t)
   485  
   486  	srcFile := f.JoinPath("src", "a.txt")
   487  	f.WriteFile(srcFile, "hello")
   488  
   489  	luSpec := v1alpha1.LiveUpdateSpec{
   490  		BasePath: f.Path(),
   491  		Syncs:    []v1alpha1.LiveUpdateSync{{LocalPath: "src", ContainerPath: "/src"}},
   492  		Sources: []v1alpha1.LiveUpdateSource{
   493  			{FileWatch: "image:sancho"},
   494  		},
   495  	}
   496  	sanchoImage := newDockerImageTarget("sancho").
   497  		WithLiveUpdateSpec("sancho", luSpec).
   498  		WithDockerImage(v1alpha1.DockerImageSpec{Context: f.Path()})
   499  	sancho := f.upsertManifest(manifestbuilder.New(f, "sancho").
   500  		WithImageTargets(sanchoImage).
   501  		WithK8sYAML(testyaml.SanchoYAML).
   502  		WithTriggerMode(model.TriggerModeManualWithAutoInit).
   503  		Build())
   504  
   505  	f.assertNextTargetToBuild("sancho")
   506  
   507  	// Set the live-update state to healthy.
   508  	sancho.State.AddCompletedBuild(model.BuildRecord{
   509  		StartTime:  time.Now(),
   510  		FinishTime: time.Now(),
   511  	})
   512  	resource := &k8sconv.KubernetesResource{
   513  		FilteredPods: []v1alpha1.Pod{*completedPod("pod-1", sanchoImage.Selector)},
   514  	}
   515  	f.st.KubernetesResources["sancho"] = resource
   516  	f.st.LiveUpdates["sancho"] = &v1alpha1.LiveUpdate{Spec: luSpec}
   517  	f.assertNoTargetNextToBuild()
   518  
   519  	// This shouldn't trigger a full-build, because it will be handled by the live-updater.
   520  	status := sancho.State.MutableBuildStatus(sanchoImage.ID())
   521  	f.st.AppendToTriggerQueue(sancho.Manifest.Name, model.BuildReasonFlagTriggerCLI)
   522  	status.PendingFileChanges[srcFile] = time.Now()
   523  	f.assertNoTargetNextToBuild()
   524  
   525  	// This should trigger a full-rebuild, because we have a trigger without pending changes.
   526  	delete(status.PendingFileChanges, srcFile)
   527  	f.assertNextTargetToBuild("sancho")
   528  }
   529  
   530  func TestHoldDisabled(t *testing.T) {
   531  	f := newTestFixture(t)
   532  
   533  	f.upsertLocalManifest("local")
   534  	f.st.ManifestTargets["local"].State.DisableState = v1alpha1.DisableStateDisabled
   535  	f.assertNoTargetNextToBuild()
   536  }
   537  
   538  func TestHoldIfAnyDisableStatusPending(t *testing.T) {
   539  	f := newTestFixture(t)
   540  
   541  	f.upsertLocalManifest("local1")
   542  	f.upsertLocalManifest("local2")
   543  	f.upsertLocalManifest("local3")
   544  	f.st.ManifestTargets["local2"].State.DisableState = v1alpha1.DisableStatePending
   545  
   546  	f.assertHold("local1", store.HoldReasonTiltfileReload, model.TargetID{Type: "manifest", Name: "local2"})
   547  	f.assertHold("local2", store.HoldReasonTiltfileReload, model.TargetID{Type: "manifest", Name: "local2"})
   548  	f.assertHold("local3", store.HoldReasonTiltfileReload, model.TargetID{Type: "manifest", Name: "local2"})
   549  	f.assertNoTargetNextToBuild()
   550  }
   551  
   552  func readyPod(podID k8s.PodID, ref string) *v1alpha1.Pod {
   553  	return &v1alpha1.Pod{
   554  		Name:   podID.String(),
   555  		Phase:  string(v1.PodRunning),
   556  		Status: "Running",
   557  		Containers: []v1alpha1.Container{
   558  			{
   559  				ID:    string(podID + "-container"),
   560  				Name:  "c",
   561  				Ready: true,
   562  				Image: ref,
   563  				State: v1alpha1.ContainerState{
   564  					Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()},
   565  				},
   566  			},
   567  		},
   568  	}
   569  }
   570  
   571  func crashingPod(podID k8s.PodID, ref string) *v1alpha1.Pod {
   572  	return &v1alpha1.Pod{
   573  		Name:   podID.String(),
   574  		Phase:  string(v1.PodRunning),
   575  		Status: "CrashLoopBackOff",
   576  		Containers: []v1alpha1.Container{
   577  			{
   578  				ID:       string(podID + "-container"),
   579  				Name:     "c",
   580  				Ready:    false,
   581  				Image:    ref,
   582  				Restarts: 1,
   583  				State: v1alpha1.ContainerState{
   584  					Terminated: &v1alpha1.ContainerStateTerminated{
   585  						StartedAt:  metav1.Now(),
   586  						FinishedAt: metav1.Now(),
   587  						Reason:     "Error",
   588  						ExitCode:   127,
   589  					}},
   590  			},
   591  		},
   592  	}
   593  }
   594  
   595  func crashedInThePastPod(podID k8s.PodID, ref string) *v1alpha1.Pod {
   596  	return &v1alpha1.Pod{
   597  		Name:   podID.String(),
   598  		Phase:  string(v1.PodRunning),
   599  		Status: "Ready",
   600  		Containers: []v1alpha1.Container{
   601  			{
   602  				ID:       string(podID + "-container"),
   603  				Name:     "c",
   604  				Ready:    true,
   605  				Image:    ref,
   606  				Restarts: 1,
   607  				State: v1alpha1.ContainerState{
   608  					Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()},
   609  				},
   610  			},
   611  		},
   612  	}
   613  }
   614  
   615  func sidecarCrashedPod(podID k8s.PodID, ref string) *v1alpha1.Pod {
   616  	return &v1alpha1.Pod{
   617  		Name:   podID.String(),
   618  		Phase:  string(v1.PodRunning),
   619  		Status: "Ready",
   620  		Containers: []v1alpha1.Container{
   621  			{
   622  				ID:       string(podID + "-container"),
   623  				Name:     "c",
   624  				Ready:    true,
   625  				Image:    ref,
   626  				Restarts: 0,
   627  				State: v1alpha1.ContainerState{
   628  					Running: &v1alpha1.ContainerStateRunning{StartedAt: metav1.Now()},
   629  				},
   630  			},
   631  			{
   632  				ID:       string(podID + "-sidecar"),
   633  				Name:     "s",
   634  				Ready:    false,
   635  				Image:    container.MustParseNamed("sidecar").String(),
   636  				Restarts: 1,
   637  				State: v1alpha1.ContainerState{
   638  					Terminated: &v1alpha1.ContainerStateTerminated{
   639  						StartedAt:  metav1.Now(),
   640  						FinishedAt: metav1.Now(),
   641  						Reason:     "Error",
   642  						ExitCode:   127,
   643  					}},
   644  			},
   645  		},
   646  	}
   647  }
   648  
   649  func completedPod(podID k8s.PodID, ref string) *v1alpha1.Pod {
   650  	return &v1alpha1.Pod{
   651  		Name:   podID.String(),
   652  		Phase:  string(v1.PodSucceeded),
   653  		Status: "Completed",
   654  		Containers: []v1alpha1.Container{
   655  			{
   656  				ID:       string(podID + "-container"),
   657  				Name:     "c",
   658  				Ready:    false,
   659  				Image:    ref,
   660  				Restarts: 0,
   661  				State: v1alpha1.ContainerState{
   662  					Terminated: &v1alpha1.ContainerStateTerminated{
   663  						StartedAt:  metav1.Now(),
   664  						FinishedAt: metav1.Now(),
   665  						Reason:     "Success!",
   666  						ExitCode:   0,
   667  					}},
   668  			},
   669  		},
   670  	}
   671  }
   672  
   673  type testFixture struct {
   674  	*tempdir.TempDirFixture
   675  	t  *testing.T
   676  	st *store.EngineState
   677  }
   678  
   679  func newTestFixture(t *testing.T) testFixture {
   680  	f := tempdir.NewTempDirFixture(t)
   681  	st := store.NewState()
   682  	st.Clusters["default"] = &v1alpha1.Cluster{
   683  		Status: v1alpha1.ClusterStatus{
   684  			Arch: "amd64",
   685  		},
   686  	}
   687  	return testFixture{
   688  		TempDirFixture: f,
   689  		t:              t,
   690  		st:             st,
   691  	}
   692  }
   693  
   694  func (f *testFixture) assertHold(m model.ManifestName, reason store.HoldReason, holdOn ...model.TargetID) {
   695  	f.T().Helper()
   696  	_, hs := NextTargetToBuild(*f.st)
   697  	hold := store.Hold{
   698  		Reason: reason,
   699  		HoldOn: holdOn,
   700  	}
   701  	assert.Equal(f.t, hold, hs[m])
   702  }
   703  
   704  func (f *testFixture) assertHoldOnRefs(m model.ManifestName, reason store.HoldReason, onRefs ...v1alpha1.UIResourceStateWaitingOnRef) {
   705  	f.T().Helper()
   706  	_, hs := NextTargetToBuild(*f.st)
   707  	hold := store.Hold{
   708  		Reason: reason,
   709  		OnRefs: onRefs,
   710  	}
   711  	assert.Equal(f.t, hold, hs[m])
   712  }
   713  
   714  func (f *testFixture) assertNextTargetToBuild(expected model.ManifestName) {
   715  	f.T().Helper()
   716  	next, holds := NextTargetToBuild(*f.st)
   717  	require.NotNil(f.t, next, "expected next target %s but got: nil. holds: %v", expected, holds)
   718  	actual := next.Manifest.Name
   719  	assert.Equal(f.t, expected, actual, "expected next target to be %s but got %s", expected, actual)
   720  }
   721  
   722  func (f *testFixture) assertNoTargetNextToBuild() {
   723  	f.T().Helper()
   724  	next, _ := NextTargetToBuild(*f.st)
   725  	if next != nil {
   726  		f.t.Fatalf("expected no next target to build, but got %s", next.Manifest.Name)
   727  	}
   728  }
   729  
   730  func (f *testFixture) upsertManifest(m model.Manifest) *store.ManifestTarget {
   731  	mt := store.NewManifestTarget(m)
   732  	mt.State.DisableState = v1alpha1.DisableStateEnabled
   733  	f.st.UpsertManifestTarget(mt)
   734  	return mt
   735  }
   736  
   737  func (f *testFixture) upsertK8sManifest(name model.ManifestName, opts ...manifestOption) *store.ManifestTarget {
   738  	b := manifestbuilder.New(f, name)
   739  	for _, o := range opts {
   740  		b = o(b)
   741  	}
   742  	return f.upsertManifest(b.WithK8sYAML(testyaml.SanchoYAML).Build())
   743  }
   744  
   745  func (f *testFixture) upsertLocalManifest(name model.ManifestName, opts ...manifestOption) *store.ManifestTarget {
   746  	b := manifestbuilder.New(f, name)
   747  	for _, o := range opts {
   748  		b = o(b)
   749  	}
   750  	return f.upsertManifest(b.WithLocalResource(fmt.Sprintf("exec-%s", name), nil).Build())
   751  }
   752  
   753  type manifestOption func(manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder
   754  
   755  func withResourceDeps(deps ...string) manifestOption {
   756  	return manifestOption(func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
   757  		return m.WithResourceDeps(deps...)
   758  	})
   759  }
   760  func withK8sPodReadiness(pr model.PodReadinessMode) manifestOption {
   761  	return manifestOption(func(m manifestbuilder.ManifestBuilder) manifestbuilder.ManifestBuilder {
   762  		return m.WithK8sPodReadiness(pr)
   763  	})
   764  }