github.com/tilt-dev/tilt@v0.36.0/internal/hud/webview/convert_test.go (about)

     1  package webview
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	v1 "k8s.io/api/core/v1"
     8  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     9  	"k8s.io/apimachinery/pkg/util/uuid"
    10  
    11  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    12  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/core/tiltfile"
    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/testutils/tempdir"
    22  	"github.com/tilt-dev/tilt/internal/timecmp"
    23  	"github.com/tilt-dev/tilt/pkg/logger"
    24  	"github.com/tilt-dev/tilt/pkg/model"
    25  	proto_webview "github.com/tilt-dev/tilt/pkg/webview"
    26  )
    27  
    28  var fooManifest = model.Manifest{Name: "foo"}.WithDeployTarget(model.K8sTarget{})
    29  
    30  func completeProtoView(t *testing.T, s store.EngineState) *proto_webview.View {
    31  	st := store.NewTestingStore()
    32  	st.SetState(s)
    33  
    34  	view, err := LogUpdate(st, 0)
    35  	require.NoError(t, err)
    36  
    37  	view.UiSession = ToUISession(s)
    38  
    39  	resources, err := ToUIResourceList(s, make(map[string][]v1alpha1.DisableSource))
    40  	require.NoError(t, err)
    41  	view.UiResources = resources
    42  
    43  	sortUIResources(view.UiResources, s.ManifestDefinitionOrder)
    44  
    45  	return view
    46  }
    47  
    48  func TestStateToWebViewRelativeEditPaths(t *testing.T) {
    49  	f := tempdir.NewTempDirFixture(t)
    50  
    51  	m := model.Manifest{
    52  		Name: "foo",
    53  	}.WithDeployTarget(model.K8sTarget{}).WithImageTarget(model.ImageTarget{}.
    54  		WithDockerImage(v1alpha1.DockerImageSpec{Context: f.JoinPath("a", "b", "c")}))
    55  
    56  	state := newState([]model.Manifest{m})
    57  	ms := state.ManifestTargets[m.Name].State
    58  	ms.BuildHistory = []model.BuildRecord{
    59  		{},
    60  	}
    61  	bs, ok := ms.BuildStatus(m.ImageTargets[0].ID())
    62  	require.True(t, ok)
    63  	bs.FileChanges =
    64  		map[string]time.Time{
    65  			f.JoinPath("a", "b", "c", "foo"):    time.Now(),
    66  			f.JoinPath("a", "b", "c", "d", "e"): time.Now(),
    67  		}
    68  	v := completeProtoView(t, *state)
    69  
    70  	require.Len(t, v.UiResources, 2)
    71  }
    72  
    73  func TestStateToWebViewPortForwards(t *testing.T) {
    74  	m := model.Manifest{
    75  		Name: "foo",
    76  	}.WithDeployTarget(model.K8sTarget{
    77  		KubernetesApplySpec: v1alpha1.KubernetesApplySpec{
    78  			PortForwardTemplateSpec: &v1alpha1.PortForwardTemplateSpec{
    79  				Forwards: []v1alpha1.Forward{
    80  					{LocalPort: 8000, ContainerPort: 5000},
    81  					{LocalPort: 7000, ContainerPort: 5001},
    82  					{LocalPort: 5000, ContainerPort: 5002, Host: "127.0.0.2", Name: "dashboard"},
    83  					{LocalPort: 6000, ContainerPort: 5003, Name: "debugger"},
    84  				},
    85  			},
    86  		},
    87  	})
    88  	state := newState([]model.Manifest{m})
    89  	v := completeProtoView(t, *state)
    90  
    91  	expected := []v1alpha1.UIResourceLink{
    92  		v1alpha1.UIResourceLink{URL: "http://localhost:8000/"},
    93  		v1alpha1.UIResourceLink{URL: "http://localhost:7000/"},
    94  		v1alpha1.UIResourceLink{URL: "http://127.0.0.2:5000/", Name: "dashboard"},
    95  		v1alpha1.UIResourceLink{URL: "http://localhost:6000/", Name: "debugger"},
    96  	}
    97  	res, _ := findResource(m.Name, v)
    98  	assert.Equal(t, expected, res.EndpointLinks)
    99  }
   100  
   101  func TestStateToWebViewLinksAndPortForwards(t *testing.T) {
   102  	m := model.Manifest{
   103  		Name: "foo",
   104  	}.WithDeployTarget(model.K8sTarget{
   105  		KubernetesApplySpec: v1alpha1.KubernetesApplySpec{
   106  			PortForwardTemplateSpec: &v1alpha1.PortForwardTemplateSpec{
   107  				Forwards: []v1alpha1.Forward{
   108  					{LocalPort: 8000, ContainerPort: 5000},
   109  					{LocalPort: 8001, ContainerPort: 5001, Name: "debugger"},
   110  				},
   111  			},
   112  		},
   113  		Links: []model.Link{
   114  			model.MustNewLink("www.apple.edu", "apple"),
   115  			model.MustNewLink("www.zombo.com", "zombo"),
   116  		},
   117  	})
   118  	state := newState([]model.Manifest{m})
   119  	v := completeProtoView(t, *state)
   120  
   121  	expected := []v1alpha1.UIResourceLink{
   122  		v1alpha1.UIResourceLink{URL: "www.apple.edu", Name: "apple"},
   123  		v1alpha1.UIResourceLink{URL: "www.zombo.com", Name: "zombo"},
   124  		v1alpha1.UIResourceLink{URL: "http://localhost:8000/"},
   125  		v1alpha1.UIResourceLink{URL: "http://localhost:8001/", Name: "debugger"},
   126  	}
   127  	res, _ := findResource(m.Name, v)
   128  	assert.Equal(t, expected, res.EndpointLinks)
   129  }
   130  
   131  func TestStateToWebViewLocalResourceLink(t *testing.T) {
   132  	m := model.Manifest{
   133  		Name: "foo",
   134  	}.WithDeployTarget(model.LocalTarget{
   135  		Links: []model.Link{
   136  			model.MustNewLink("www.apple.edu", "apple"),
   137  			model.MustNewLink("www.zombo.com", "zombo"),
   138  		},
   139  	})
   140  	state := newState([]model.Manifest{m})
   141  	v := completeProtoView(t, *state)
   142  
   143  	expected := []v1alpha1.UIResourceLink{
   144  		v1alpha1.UIResourceLink{URL: "www.apple.edu", Name: "apple"},
   145  		v1alpha1.UIResourceLink{URL: "www.zombo.com", Name: "zombo"},
   146  	}
   147  	res, _ := findResource(m.Name, v)
   148  	assert.Equal(t, expected, res.EndpointLinks)
   149  }
   150  
   151  func TestStateToViewUnresourcedYAMLManifest(t *testing.T) {
   152  	mn := model.UnresourcedYAMLManifestName
   153  	m := model.Manifest{Name: mn}.WithDeployTarget(k8s.MustTarget(mn.TargetName(), testyaml.SanchoYAML))
   154  	state := newState([]model.Manifest{m})
   155  	v := completeProtoView(t, *state)
   156  
   157  	assert.Equal(t, 2, len(v.UiResources))
   158  
   159  	r, _ := findResource(m.Name, v)
   160  	assert.Equal(t, "", lastBuild(r).Error)
   161  }
   162  
   163  func TestStateToViewK8sTargetsIncludeDisplayNames(t *testing.T) {
   164  	m := model.Manifest{Name: "foo"}.WithDeployTarget(model.K8sTarget{})
   165  	state := newState([]model.Manifest{m})
   166  	krs := state.ManifestTargets["foo"].State.K8sRuntimeState()
   167  	krs.ApplyFilter = &k8sconv.KubernetesApplyFilter{
   168  		DeployedRefs: []v1.ObjectReference{
   169  			{Kind: "Namespace", Name: "foo"},
   170  			{Kind: "Secret", Name: "foo"},
   171  		},
   172  	}
   173  	state.ManifestTargets["foo"].State.RuntimeState = krs
   174  
   175  	v := completeProtoView(t, *state)
   176  
   177  	assert.Equal(t, 2, len(v.UiResources))
   178  
   179  	r, _ := findResource(m.Name, v)
   180  
   181  	assert.Equal(t, []string{"foo:namespace", "foo:secret"}, r.K8sResourceInfo.DisplayNames)
   182  }
   183  
   184  func TestStateToViewTiltfileLog(t *testing.T) {
   185  	es := newState([]model.Manifest{})
   186  	spanID := ctrltiltfile.SpanIDForLoadCount("(Tiltfile)", 1)
   187  	es.LogStore.Append(
   188  		store.NewLogAction(store.MainTiltfileManifestName, spanID, logger.InfoLvl, nil, []byte("hello")),
   189  		nil)
   190  	v := completeProtoView(t, *es)
   191  	_, ok := findResource("(Tiltfile)", v)
   192  	require.True(t, ok, "no resource named (Tiltfile) found")
   193  	assert.Equal(t, "hello", v.LogList.Segments[0].Text)
   194  	assert.Equal(t, "(Tiltfile)", v.LogList.Spans[string(spanID)].ManifestName)
   195  }
   196  
   197  func TestNeedsNudgeSet(t *testing.T) {
   198  	state := newState(nil)
   199  
   200  	m := fooManifest
   201  	targ := store.NewManifestTarget(m)
   202  	targ.State = &store.ManifestState{}
   203  	state.UpsertManifestTarget(targ)
   204  
   205  	v := completeProtoView(t, *state)
   206  
   207  	assert.False(t, v.UiSession.Status.NeedsAnalyticsNudge,
   208  		"LastSuccessfulDeployTime not set, so NeedsNudge should not be set")
   209  
   210  	targ.State = &store.ManifestState{LastSuccessfulDeployTime: time.Now()}
   211  	state.UpsertManifestTarget(targ)
   212  
   213  	v = completeProtoView(t, *state)
   214  	assert.True(t, v.UiSession.Status.NeedsAnalyticsNudge)
   215  }
   216  
   217  func TestTriggerMode(t *testing.T) {
   218  	state := newState(nil)
   219  	m := fooManifest
   220  	targ := store.NewManifestTarget(m)
   221  	targ.Manifest.TriggerMode = model.TriggerModeManualWithAutoInit
   222  	targ.State = &store.ManifestState{}
   223  	state.UpsertManifestTarget(targ)
   224  
   225  	v := completeProtoView(t, *state)
   226  	assert.Equal(t, 2, len(v.UiResources))
   227  
   228  	newM, _ := findResource(model.ManifestName("foo"), v)
   229  	assert.Equal(t, model.TriggerModeManualWithAutoInit, model.TriggerMode(newM.TriggerMode))
   230  }
   231  
   232  func TestFeatureFlags(t *testing.T) {
   233  	state := newState(nil)
   234  	state.Features = map[string]bool{"foo_feature": true}
   235  
   236  	v := completeProtoView(t, *state)
   237  	assert.Equal(t, v.UiSession.Status.FeatureFlags, []v1alpha1.UIFeatureFlag{
   238  		v1alpha1.UIFeatureFlag{Name: "foo_feature", Value: true},
   239  	})
   240  }
   241  
   242  func TestReadinessCheckFailing(t *testing.T) {
   243  	m := model.Manifest{
   244  		Name: "foo",
   245  	}.WithDeployTarget(model.K8sTarget{})
   246  	state := newState([]model.Manifest{m})
   247  	state.ManifestTargets[m.Name].State.RuntimeState = store.NewK8sRuntimeStateWithPods(m, v1alpha1.Pod{
   248  		Name:   "pod-id",
   249  		Status: "Running",
   250  		Phase:  "Running",
   251  		Containers: []v1alpha1.Container{
   252  			{
   253  				Ready: false,
   254  			},
   255  		},
   256  	})
   257  
   258  	v := completeProtoView(t, *state)
   259  	rv, ok := findResource(m.Name, v)
   260  	require.True(t, ok)
   261  	require.Equal(t, v1alpha1.RuntimeStatusPending, rv.RuntimeStatus)
   262  	require.Equal(t, "False", string(readyCondition(rv).Status))
   263  }
   264  
   265  func TestRuntimeErrorAndDisabled(t *testing.T) {
   266  	m := model.Manifest{
   267  		Name: "foo",
   268  	}.WithDeployTarget(model.K8sTarget{})
   269  	state := newState([]model.Manifest{m})
   270  	mt := state.ManifestTargets[m.Name]
   271  	mt.State.RuntimeState = store.NewK8sRuntimeStateWithPods(m, v1alpha1.Pod{
   272  		Name:   "pod-id",
   273  		Status: "Error",
   274  		Phase:  string(v1.PodFailed),
   275  		Containers: []v1alpha1.Container{
   276  			{
   277  				Ready: false,
   278  			},
   279  		},
   280  	})
   281  
   282  	state.ConfigMaps["foo-disable"] = &v1alpha1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "disabled"}, Data: map[string]string{"isDisabled": "true"}}
   283  	disableSources := map[string][]v1alpha1.DisableSource{
   284  		"foo": {
   285  			{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "foo-disable", Key: "isDisabled"}},
   286  		},
   287  	}
   288  	uiResources, err := ToUIResourceList(*state, disableSources)
   289  	require.NoError(t, err)
   290  	require.Equal(t, m.Name.String(), uiResources[1].Name)
   291  
   292  	rv := uiResources[1].Status
   293  	rc := readyCondition(rv)
   294  	require.Equal(t, "False", string(rc.Status))
   295  	require.Equal(t, "Disabled", rc.Reason)
   296  	uc := upToDateCondition(rv)
   297  	require.Equal(t, "False", string(uc.Status))
   298  	require.Equal(t, "Disabled", uc.Reason)
   299  	require.Equal(t, v1alpha1.RuntimeStatusNone, rv.RuntimeStatus)
   300  	require.Equal(t, v1alpha1.UpdateStatusNone, rv.UpdateStatus)
   301  }
   302  
   303  func TestLocalResource(t *testing.T) {
   304  	cmd := model.Cmd{
   305  		Argv: []string{"make", "test"},
   306  		Dir:  "path/to/tiltfile",
   307  	}
   308  	lt := model.NewLocalTarget("my-local", cmd, model.Cmd{}, []string{"/foo/bar", "/baz/qux"})
   309  	m := model.Manifest{
   310  		Name: "test",
   311  	}.WithDeployTarget(lt)
   312  
   313  	state := newState([]model.Manifest{m})
   314  	lrs := store.LocalRuntimeState{Status: v1alpha1.RuntimeStatusNotApplicable}
   315  	state.ManifestTargets[m.Name].State.RuntimeState = lrs
   316  	v := completeProtoView(t, *state)
   317  
   318  	assert.Equal(t, 2, len(v.UiResources))
   319  	r := v.UiResources[1]
   320  	rs := r.Status
   321  	assert.Equal(t, "test", r.Name)
   322  	assert.Equal(t, v1alpha1.RuntimeStatusNotApplicable, rs.RuntimeStatus)
   323  	require.Equal(t, "False", string(readyCondition(rs).Status))
   324  	require.Len(t, rs.Specs, 1)
   325  	spec := rs.Specs[0]
   326  	require.Equal(t, v1alpha1.UIResourceTargetTypeLocal, spec.Type)
   327  	require.False(t, spec.HasLiveUpdate)
   328  }
   329  
   330  func TestBuildHistory(t *testing.T) {
   331  	br1 := model.BuildRecord{
   332  		StartTime:  time.Now().Add(-1 * time.Hour),
   333  		FinishTime: time.Now().Add(-50 * time.Minute),
   334  		Reason:     model.BuildReasonFlagInit,
   335  		BuildTypes: []model.BuildType{model.BuildTypeImage, model.BuildTypeK8s},
   336  	}
   337  	br2 := model.BuildRecord{
   338  		StartTime:  time.Now().Add(-45 * time.Minute),
   339  		FinishTime: time.Now().Add(-44 * time.Minute),
   340  		Reason:     model.BuildReasonFlagChangedFiles,
   341  		BuildTypes: []model.BuildType{model.BuildTypeLiveUpdate},
   342  	}
   343  	br3 := model.BuildRecord{
   344  		StartTime:  time.Now().Add(-20 * time.Minute),
   345  		FinishTime: time.Now().Add(-19 * time.Minute),
   346  		Reason:     model.BuildReasonFlagChangedFiles,
   347  		BuildTypes: []model.BuildType{model.BuildTypeImage, model.BuildTypeK8s},
   348  	}
   349  	buildRecords := []model.BuildRecord{br1, br2, br3}
   350  
   351  	m := model.Manifest{Name: "foo"}.WithDeployTarget(model.K8sTarget{})
   352  	state := newState([]model.Manifest{m})
   353  	state.ManifestTargets[m.Name].State.BuildHistory = buildRecords
   354  
   355  	v := completeProtoView(t, *state)
   356  	require.Equal(t, 2, len(v.UiResources))
   357  	r := v.UiResources[1]
   358  	require.Equal(t, "foo", r.Name)
   359  
   360  	rs := r.Status
   361  	require.Len(t, rs.BuildHistory, 3)
   362  
   363  	for i, actual := range rs.BuildHistory {
   364  		expected := buildRecords[i]
   365  		timecmp.AssertTimeEqual(t, expected.StartTime, actual.StartTime)
   366  		timecmp.AssertTimeEqual(t, expected.FinishTime, actual.FinishTime)
   367  		require.False(t, actual.IsCrashRebuild)
   368  	}
   369  }
   370  
   371  func TestSpecs(t *testing.T) {
   372  	luSpec := v1alpha1.LiveUpdateSpec{
   373  		BasePath: ".",
   374  		Syncs:    []v1alpha1.LiveUpdateSync{{LocalPath: "foo", ContainerPath: "bar"}},
   375  	}
   376  	luTarg := model.ImageTarget{}.WithLiveUpdateSpec("sancho", luSpec).WithBuildDetails(model.DockerBuild{})
   377  
   378  	mNoLiveUpd := model.Manifest{Name: "noLiveUpd"}.WithImageTarget(model.ImageTarget{}).WithDeployTarget(model.K8sTarget{})
   379  	mLiveUpd := model.Manifest{Name: "liveUpd"}.WithImageTarget(luTarg).WithDeployTarget(model.K8sTarget{})
   380  	mLocal := model.Manifest{Name: "local"}.WithDeployTarget(model.LocalTarget{})
   381  
   382  	expected := []struct {
   383  		name          string
   384  		targetTypes   []v1alpha1.UIResourceTargetType
   385  		hasLiveUpdate bool
   386  	}{
   387  		{"noLiveUpd", []v1alpha1.UIResourceTargetType{v1alpha1.UIResourceTargetTypeImage, v1alpha1.UIResourceTargetTypeKubernetes}, false},
   388  		{"liveUpd", []v1alpha1.UIResourceTargetType{v1alpha1.UIResourceTargetTypeImage, v1alpha1.UIResourceTargetTypeKubernetes}, true},
   389  		{"local", []v1alpha1.UIResourceTargetType{v1alpha1.UIResourceTargetTypeLocal}, false},
   390  	}
   391  	state := newState([]model.Manifest{mNoLiveUpd, mLiveUpd, mLocal})
   392  	v := completeProtoView(t, *state)
   393  
   394  	require.Equal(t, 4, len(v.UiResources))
   395  	for i, r := range v.UiResources {
   396  		if i == 0 {
   397  			continue // skip Tiltfile
   398  		}
   399  		expected := expected[i-1]
   400  		require.Equal(t, expected.name, r.Name, "name mismatch for resource at index %d", i)
   401  		observedTypes := []v1alpha1.UIResourceTargetType{}
   402  		var iTargHasLU bool
   403  		rs := r.Status
   404  		for _, spec := range rs.Specs {
   405  			observedTypes = append(observedTypes, spec.Type)
   406  			if spec.Type == v1alpha1.UIResourceTargetTypeImage {
   407  				iTargHasLU = spec.HasLiveUpdate
   408  			}
   409  		}
   410  		require.ElementsMatch(t, expected.targetTypes, observedTypes, "for resource %q", r.Name)
   411  		require.Equal(t, expected.hasLiveUpdate, iTargHasLU, "for resource %q", r.Name)
   412  	}
   413  }
   414  
   415  func TestDisableResourceStatus(t *testing.T) {
   416  	for _, tc := range []struct {
   417  		name           string
   418  		configMaps     []*v1alpha1.ConfigMap
   419  		disableSources []v1alpha1.DisableSource
   420  		// `expected.Sources` will be automatically set to `disableSources`'s value
   421  		expected v1alpha1.DisableResourceStatus
   422  	}{
   423  		{
   424  			"disabled",
   425  			[]*v1alpha1.ConfigMap{{ObjectMeta: metav1.ObjectMeta{Name: "disable-m1"}, Data: map[string]string{"isDisabled": "true"}}},
   426  			[]v1alpha1.DisableSource{{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disable-m1", Key: "isDisabled"}}},
   427  			v1alpha1.DisableResourceStatus{
   428  				EnabledCount:  0,
   429  				DisabledCount: 1,
   430  				State:         v1alpha1.DisableStateDisabled,
   431  			},
   432  		},
   433  		{
   434  			"some disabled",
   435  			[]*v1alpha1.ConfigMap{
   436  				{ObjectMeta: metav1.ObjectMeta{Name: "disable-m1a"}, Data: map[string]string{"isDisabled": "true"}},
   437  				{ObjectMeta: metav1.ObjectMeta{Name: "disable-m1b"}, Data: map[string]string{"isDisabled": "true"}},
   438  				{ObjectMeta: metav1.ObjectMeta{Name: "disable-m1c"}, Data: map[string]string{"isDisabled": "false"}},
   439  			},
   440  			[]v1alpha1.DisableSource{
   441  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disable-m1a", Key: "isDisabled"}},
   442  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disable-m1b", Key: "isDisabled"}},
   443  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disable-m1c", Key: "isDisabled"}},
   444  			},
   445  			v1alpha1.DisableResourceStatus{
   446  				EnabledCount:  1,
   447  				DisabledCount: 2,
   448  				State:         v1alpha1.DisableStateDisabled,
   449  			},
   450  		},
   451  		{
   452  			"no sources - enabled",
   453  			nil,
   454  			nil,
   455  			v1alpha1.DisableResourceStatus{
   456  				EnabledCount:  0,
   457  				DisabledCount: 0,
   458  				Sources:       nil,
   459  				State:         v1alpha1.DisableStateEnabled,
   460  			},
   461  		},
   462  		{
   463  			"missing ConfigMap - pending",
   464  			nil,
   465  			[]v1alpha1.DisableSource{{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disable-m1", Key: "isDisabled"}}},
   466  			v1alpha1.DisableResourceStatus{
   467  				EnabledCount:  0,
   468  				DisabledCount: 0,
   469  				State:         v1alpha1.DisableStatePending,
   470  			},
   471  		},
   472  		{
   473  			"error trumps all",
   474  			[]*v1alpha1.ConfigMap{
   475  				{ObjectMeta: metav1.ObjectMeta{Name: "enabled"}, Data: map[string]string{"isDisabled": "false"}},
   476  				{ObjectMeta: metav1.ObjectMeta{Name: "disabled"}, Data: map[string]string{"isDisabled": "true"}},
   477  				{ObjectMeta: metav1.ObjectMeta{Name: "error"}, Data: map[string]string{}},
   478  			},
   479  			[]v1alpha1.DisableSource{
   480  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "enabled", Key: "isDisabled"}},
   481  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disabled", Key: "isDisabled"}},
   482  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "error", Key: "isDisabled"}},
   483  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "pending", Key: "isDisabled"}},
   484  			},
   485  			v1alpha1.DisableResourceStatus{
   486  				EnabledCount:  1,
   487  				DisabledCount: 2,
   488  				State:         v1alpha1.DisableStateError,
   489  			},
   490  		},
   491  		{
   492  			"pending trumps enabled/disabled",
   493  			[]*v1alpha1.ConfigMap{
   494  				{ObjectMeta: metav1.ObjectMeta{Name: "enabled"}, Data: map[string]string{"isDisabled": "false"}},
   495  				{ObjectMeta: metav1.ObjectMeta{Name: "disabled"}, Data: map[string]string{"isDisabled": "true"}},
   496  			},
   497  			[]v1alpha1.DisableSource{
   498  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "enabled", Key: "isDisabled"}},
   499  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "pending", Key: "isDisabled"}},
   500  				{ConfigMap: &v1alpha1.ConfigMapDisableSource{Name: "disabled", Key: "isDisabled"}},
   501  			},
   502  			v1alpha1.DisableResourceStatus{
   503  				EnabledCount:  1,
   504  				DisabledCount: 1,
   505  				State:         v1alpha1.DisableStatePending,
   506  			},
   507  		},
   508  	} {
   509  		t.Run(tc.name, func(t *testing.T) {
   510  			m := model.Manifest{Name: "testmanifest"}.WithDeployTarget(model.LocalTarget{})
   511  			state := newState([]model.Manifest{m})
   512  			state.ConfigMaps = make(map[string]*v1alpha1.ConfigMap)
   513  			for _, cm := range tc.configMaps {
   514  				state.ConfigMaps[cm.Name] = cm
   515  			}
   516  			disableSources := map[string][]v1alpha1.DisableSource{
   517  				m.Name.String(): tc.disableSources,
   518  			}
   519  			uiResources, err := ToUIResourceList(*state, disableSources)
   520  			require.NoError(t, err)
   521  
   522  			require.Equal(t, 2, len(uiResources))
   523  			require.Equal(t, "(Tiltfile)", uiResources[0].Name)
   524  			require.Equal(t, m.Name.String(), uiResources[1].Name)
   525  			tc.expected.Sources = tc.disableSources
   526  			require.Equal(t, tc.expected, uiResources[1].Status.DisableStatus)
   527  		})
   528  	}
   529  }
   530  
   531  func TestTiltfileNameCollision(t *testing.T) {
   532  	m := model.Manifest{Name: "collision"}.WithDeployTarget(model.LocalTarget{})
   533  	state := newState([]model.Manifest{m})
   534  	state.Tiltfiles["collision"] = &v1alpha1.Tiltfile{}
   535  	state.TiltfileDefinitionOrder = append(state.TiltfileDefinitionOrder, "collision")
   536  	state.TiltfileStates["collision"] = &store.ManifestState{
   537  		Name:          model.MainTiltfileManifestName,
   538  		BuildStatuses: make(map[model.TargetID]*store.BuildStatus),
   539  		DisableState:  v1alpha1.DisableStateEnabled,
   540  		CurrentBuilds: make(map[string]model.BuildRecord),
   541  	}
   542  
   543  	_, err := ToUIResourceList(*state, nil)
   544  	require.EqualError(t, err, `Tiltfile "collision" has the same name as a local resource`)
   545  }
   546  
   547  func TestExtensionNameCollision(t *testing.T) {
   548  	m := model.Manifest{Name: "collision"}.WithDeployTarget(model.K8sTarget{})
   549  	state := newState([]model.Manifest{m})
   550  
   551  	extensionGVK := v1alpha1.SchemeGroupVersion.WithKind("Extension")
   552  	controller := true
   553  	state.Tiltfiles["collision"] = &v1alpha1.Tiltfile{
   554  		ObjectMeta: metav1.ObjectMeta{
   555  			OwnerReferences: []metav1.OwnerReference{
   556  				{
   557  					APIVersion: extensionGVK.GroupVersion().String(),
   558  					Kind:       extensionGVK.Kind,
   559  					UID:        uuid.NewUUID(),
   560  					Controller: &controller,
   561  				},
   562  			},
   563  		},
   564  	}
   565  	state.TiltfileDefinitionOrder = append(state.TiltfileDefinitionOrder, "collision")
   566  	state.TiltfileStates["collision"] = &store.ManifestState{
   567  		Name:          model.MainTiltfileManifestName,
   568  		BuildStatuses: make(map[model.TargetID]*store.BuildStatus),
   569  		DisableState:  v1alpha1.DisableStateEnabled,
   570  		CurrentBuilds: make(map[string]model.BuildRecord),
   571  	}
   572  
   573  	_, err := ToUIResourceList(*state, nil)
   574  	require.EqualError(t, err, `Extension "collision" has the same name as a Kubernetes resource`)
   575  }
   576  
   577  func findResource(n model.ManifestName, view *proto_webview.View) (v1alpha1.UIResourceStatus, bool) {
   578  	for _, r := range view.UiResources {
   579  		if r.Name == n.String() {
   580  			return r.Status, true
   581  		}
   582  	}
   583  	return v1alpha1.UIResourceStatus{}, false
   584  }
   585  
   586  func lastBuild(r v1alpha1.UIResourceStatus) v1alpha1.UIBuildTerminated {
   587  	if len(r.BuildHistory) == 0 {
   588  		return v1alpha1.UIBuildTerminated{}
   589  	}
   590  
   591  	return r.BuildHistory[0]
   592  }
   593  
   594  func readyCondition(rs v1alpha1.UIResourceStatus) *v1alpha1.UIResourceCondition {
   595  	for _, c := range rs.Conditions {
   596  		if c.Type == v1alpha1.UIResourceReady {
   597  			return &c
   598  		}
   599  	}
   600  	return nil
   601  }
   602  
   603  func upToDateCondition(rs v1alpha1.UIResourceStatus) *v1alpha1.UIResourceCondition {
   604  	for _, c := range rs.Conditions {
   605  		if c.Type == v1alpha1.UIResourceUpToDate {
   606  			return &c
   607  		}
   608  	}
   609  	return nil
   610  }