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