github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/liveupdate/reconciler_test.go (about)

     1  package liveupdate
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/types"
    16  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    17  
    18  	"github.com/tilt-dev/tilt/internal/build"
    19  	"github.com/tilt-dev/tilt/internal/containerupdate"
    20  	"github.com/tilt-dev/tilt/internal/controllers/apis/configmap"
    21  	"github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate"
    22  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    23  	"github.com/tilt-dev/tilt/internal/dockercompose"
    24  	"github.com/tilt-dev/tilt/internal/store"
    25  	"github.com/tilt-dev/tilt/internal/store/buildcontrols"
    26  	"github.com/tilt-dev/tilt/pkg/apis"
    27  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    28  	"github.com/tilt-dev/tilt/pkg/logger"
    29  	"github.com/tilt-dev/tilt/pkg/model"
    30  )
    31  
    32  func TestIndexing(t *testing.T) {
    33  	f := newFixture(t)
    34  
    35  	// KubernetesDiscovery + KubernetesApply + ImageMap
    36  	f.Create(&v1alpha1.LiveUpdate{
    37  		ObjectMeta: metav1.ObjectMeta{Name: "all"},
    38  		Spec: v1alpha1.LiveUpdateSpec{
    39  			BasePath: "/tmp",
    40  			Selector: v1alpha1.LiveUpdateSelector{
    41  				Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
    42  					DiscoveryName: "discovery",
    43  					ApplyName:     "apply",
    44  					ImageMapName:  "imagemap",
    45  				},
    46  			},
    47  			Syncs: []v1alpha1.LiveUpdateSync{
    48  				{LocalPath: "in", ContainerPath: "/out/"},
    49  			},
    50  		},
    51  	})
    52  
    53  	// KubernetesDiscovery ONLY [w/o Kubernetes Apply or ImageMap]
    54  	f.Create(&v1alpha1.LiveUpdate{
    55  		ObjectMeta: metav1.ObjectMeta{Name: "kdisco-only"},
    56  		Spec: v1alpha1.LiveUpdateSpec{
    57  			BasePath: "/tmp",
    58  			Selector: v1alpha1.LiveUpdateSelector{
    59  				Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
    60  					DiscoveryName: "discovery",
    61  					ContainerName: "foo",
    62  				},
    63  			},
    64  			Syncs: []v1alpha1.LiveUpdateSync{
    65  				{LocalPath: "in", ContainerPath: "/out/"},
    66  			},
    67  		},
    68  	})
    69  
    70  	ctx := context.Background()
    71  	reqs := f.r.indexer.Enqueue(ctx, &v1alpha1.KubernetesDiscovery{ObjectMeta: metav1.ObjectMeta{Name: "discovery"}})
    72  	require.ElementsMatch(t, []reconcile.Request{
    73  		{NamespacedName: types.NamespacedName{Name: "all"}},
    74  		{NamespacedName: types.NamespacedName{Name: "kdisco-only"}},
    75  	}, reqs, "KubernetesDiscovery indexing")
    76  
    77  	reqs = f.r.indexer.Enqueue(ctx, &v1alpha1.KubernetesApply{ObjectMeta: metav1.ObjectMeta{Name: "apply"}})
    78  	require.ElementsMatch(t, []reconcile.Request{
    79  		{NamespacedName: types.NamespacedName{Name: "all"}},
    80  	}, reqs, "KubernetesApply indexing")
    81  
    82  	reqs = f.r.indexer.Enqueue(ctx, &v1alpha1.ImageMap{ObjectMeta: metav1.ObjectMeta{Name: "imagemap"}})
    83  	require.ElementsMatch(t, []reconcile.Request{
    84  		{NamespacedName: types.NamespacedName{Name: "all"}},
    85  	}, reqs, "ImageMap indexing")
    86  }
    87  
    88  func TestMissingApply(t *testing.T) {
    89  	f := newFixture(t)
    90  
    91  	f.setupFrontend()
    92  	f.Delete(&v1alpha1.KubernetesApply{ObjectMeta: metav1.ObjectMeta{Name: "frontend-apply"}})
    93  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
    94  
    95  	var lu v1alpha1.LiveUpdate
    96  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
    97  	if assert.NotNil(t, lu.Status.Failed) {
    98  		assert.Equal(t, "ObjectNotFound", lu.Status.Failed.Reason)
    99  		assert.NotContains(t, f.Stdout(), "ObjectNotFound")
   100  	}
   101  
   102  	f.assertSteadyState(&lu)
   103  }
   104  
   105  func TestConsumeFileEvents(t *testing.T) {
   106  	f := newFixture(t)
   107  
   108  	p, _ := os.Getwd()
   109  	nowMicro := apis.NowMicro()
   110  	txtPath := filepath.Join(p, "a.txt")
   111  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   112  
   113  	f.setupFrontend()
   114  
   115  	// Verify initial setup.
   116  	m, ok := f.r.monitors["frontend-liveupdate"]
   117  	require.True(t, ok)
   118  	assert.Equal(t, map[string]*monitorSource{}, m.sources)
   119  	assert.Equal(t, "frontend-discovery", m.lastKubernetesDiscovery.Name)
   120  	assert.Nil(t, f.st.lastStartedAction)
   121  
   122  	// Trigger a file event, and make sure that the status reflects the sync.
   123  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   124  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   125  
   126  	var lu v1alpha1.LiveUpdate
   127  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   128  	assert.Nil(t, lu.Status.Failed)
   129  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   130  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   131  	}
   132  
   133  	// Also make sure the sync gets pulled into the monitor.
   134  	assert.Equal(t, map[string]metav1.MicroTime{
   135  		txtPath: txtChangeTime,
   136  	}, m.sources["frontend-fw"].modTimeByPath)
   137  	assert.Equal(t, 1, len(f.cu.Calls))
   138  
   139  	// re-reconcile, and make sure we don't try to resync.
   140  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   141  	assert.Equal(t, 1, len(f.cu.Calls))
   142  
   143  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   144  	assert.Nil(t, lu.Status.Failed)
   145  
   146  	if assert.NotNil(t, f.st.lastStartedAction) {
   147  		assert.Equal(t, []string{txtPath}, f.st.lastStartedAction.FilesChanged)
   148  	}
   149  	assert.NotNil(t, f.st.lastCompletedAction)
   150  }
   151  
   152  func TestConsumeFileEventsDockerCompose(t *testing.T) {
   153  	f := newFixture(t)
   154  
   155  	p, _ := os.Getwd()
   156  	nowMicro := apis.NowMicro()
   157  	txtPath := filepath.Join(p, "a.txt")
   158  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   159  
   160  	f.setupDockerComposeFrontend()
   161  
   162  	// Verify initial setup.
   163  	m, ok := f.r.monitors["frontend-liveupdate"]
   164  	require.True(t, ok)
   165  	assert.Equal(t, map[string]*monitorSource{}, m.sources)
   166  	assert.Equal(t, "frontend-service", m.lastDockerComposeService.Name)
   167  	assert.Nil(t, f.st.lastStartedAction)
   168  
   169  	// Trigger a file event, and make sure that the status reflects the sync.
   170  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   171  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   172  
   173  	var lu v1alpha1.LiveUpdate
   174  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   175  	assert.Nil(t, lu.Status.Failed)
   176  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   177  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   178  	}
   179  
   180  	// Also make sure the sync gets pulled into the monitor.
   181  	assert.Equal(t, map[string]metav1.MicroTime{
   182  		txtPath: txtChangeTime,
   183  	}, m.sources["frontend-fw"].modTimeByPath)
   184  	assert.Equal(t, 1, len(f.cu.Calls))
   185  
   186  	// re-reconcile, and make sure we don't try to resync.
   187  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   188  	assert.Equal(t, 1, len(f.cu.Calls))
   189  
   190  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   191  	assert.Nil(t, lu.Status.Failed)
   192  
   193  	if assert.NotNil(t, f.st.lastStartedAction) {
   194  		assert.Equal(t, []string{txtPath}, f.st.lastStartedAction.FilesChanged)
   195  	}
   196  	assert.NotNil(t, f.st.lastCompletedAction)
   197  
   198  	// Make sure the container was NOT restarted.
   199  	if assert.Equal(t, 1, len(f.cu.Calls)) {
   200  		assert.True(t, f.cu.Calls[0].HotReload)
   201  	}
   202  
   203  	f.assertSteadyState(&lu)
   204  
   205  	// Docker Compose containers can be restarted in-place,
   206  	// preserving their filesystem.
   207  	dc := &v1alpha1.DockerComposeService{}
   208  	f.MustGet(types.NamespacedName{Name: "frontend-service"}, dc)
   209  	dc = dc.DeepCopy()
   210  	dc.Status.ContainerState.StartedAt = apis.NowMicro()
   211  	f.UpdateStatus(dc)
   212  
   213  	f.assertSteadyState(&lu)
   214  }
   215  
   216  func TestConsumeFileEventsUpdateModeManual(t *testing.T) {
   217  	f := newFixture(t)
   218  
   219  	p, _ := os.Getwd()
   220  	nowMicro := apis.NowMicro()
   221  	txtPath := filepath.Join(p, "a.txt")
   222  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   223  
   224  	f.setupFrontend()
   225  
   226  	var lu v1alpha1.LiveUpdate
   227  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   228  	lu.Annotations[liveupdate.AnnotationUpdateMode] = liveupdate.UpdateModeManual
   229  	f.Update(&lu)
   230  
   231  	// Trigger a file event, and make sure that the status reflects the sync.
   232  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   233  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   234  
   235  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   236  	assert.Nil(t, lu.Status.Failed)
   237  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   238  		assert.Equal(t, "Trigger", lu.Status.Containers[0].Waiting.Reason)
   239  	}
   240  
   241  	f.Upsert(&v1alpha1.ConfigMap{
   242  		ObjectMeta: metav1.ObjectMeta{
   243  			Name: configmap.TriggerQueueName,
   244  		},
   245  		Data: map[string]string{
   246  			"0-name": "frontend",
   247  		},
   248  	})
   249  
   250  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   251  
   252  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   253  	assert.Nil(t, lu.Status.Failed)
   254  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   255  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   256  	}
   257  }
   258  
   259  func TestWaitingContainer(t *testing.T) {
   260  	f := newFixture(t)
   261  
   262  	p, _ := os.Getwd()
   263  	nowMicro := apis.NowMicro()
   264  	txtPath := filepath.Join(p, "a.txt")
   265  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   266  
   267  	f.setupFrontend()
   268  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   269  		Pods: []v1alpha1.Pod{
   270  			{
   271  				Name:      "pod-1",
   272  				Namespace: "default",
   273  				Containers: []v1alpha1.Container{
   274  					{
   275  						Name:  "main",
   276  						ID:    "main-id",
   277  						Image: "local-registry:12345/frontend-image:my-tag",
   278  						State: v1alpha1.ContainerState{
   279  							Waiting: &v1alpha1.ContainerStateWaiting{},
   280  						},
   281  					},
   282  				},
   283  			},
   284  		},
   285  	})
   286  
   287  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   288  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   289  
   290  	var lu v1alpha1.LiveUpdate
   291  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   292  	assert.Nil(t, lu.Status.Failed)
   293  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   294  		assert.Equal(t, "ContainerWaiting", lu.Status.Containers[0].Waiting.Reason)
   295  	}
   296  	assert.Equal(t, 0, len(f.cu.Calls))
   297  
   298  	f.assertSteadyState(&lu)
   299  
   300  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   301  		Pods: []v1alpha1.Pod{
   302  			{
   303  				Name:      "pod-1",
   304  				Namespace: "default",
   305  				Containers: []v1alpha1.Container{
   306  					{
   307  						Name:  "main",
   308  						ID:    "main-id",
   309  						Image: "local-registry:12345/frontend-image:my-tag",
   310  						State: v1alpha1.ContainerState{
   311  							Running: &v1alpha1.ContainerStateRunning{},
   312  						},
   313  					},
   314  				},
   315  			},
   316  		},
   317  	})
   318  
   319  	// Re-reconcile, and make sure we pull in the files.
   320  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   321  	assert.Equal(t, 1, len(f.cu.Calls))
   322  }
   323  
   324  func TestWaitingContainerNoID(t *testing.T) {
   325  	f := newFixture(t)
   326  
   327  	p, _ := os.Getwd()
   328  	nowMicro := apis.NowMicro()
   329  	txtPath := filepath.Join(p, "a.txt")
   330  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   331  
   332  	f.setupFrontend()
   333  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   334  		Pods: []v1alpha1.Pod{
   335  			{
   336  				Name:      "pod-1",
   337  				Namespace: "default",
   338  				InitContainers: []v1alpha1.Container{
   339  					{
   340  						Name:  "main-init",
   341  						ID:    "main-id",
   342  						Image: "busybox",
   343  						State: v1alpha1.ContainerState{
   344  							Running: &v1alpha1.ContainerStateRunning{},
   345  						},
   346  					},
   347  				},
   348  				Containers: []v1alpha1.Container{
   349  					{
   350  						Name:  "main",
   351  						Image: "local-registry:12345/frontend-image:my-tag",
   352  						State: v1alpha1.ContainerState{
   353  							Waiting: &v1alpha1.ContainerStateWaiting{Reason: "PodInitializing"},
   354  						},
   355  					},
   356  				},
   357  			},
   358  		},
   359  	})
   360  
   361  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   362  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   363  
   364  	var lu v1alpha1.LiveUpdate
   365  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   366  	assert.Nil(t, lu.Status.Failed)
   367  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   368  		assert.Equal(t, "ContainerWaiting", lu.Status.Containers[0].Waiting.Reason)
   369  	}
   370  	assert.Equal(t, 0, len(f.cu.Calls))
   371  
   372  	f.assertSteadyState(&lu)
   373  }
   374  
   375  func TestOneTerminatedContainer(t *testing.T) {
   376  	f := newFixture(t)
   377  
   378  	p, _ := os.Getwd()
   379  	nowMicro := apis.NowMicro()
   380  	txtPath := filepath.Join(p, "a.txt")
   381  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   382  
   383  	f.setupFrontend()
   384  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   385  		Pods: []v1alpha1.Pod{
   386  			{
   387  				Name:      "pod-1",
   388  				Namespace: "default",
   389  				Containers: []v1alpha1.Container{
   390  					{
   391  						Name:  "main",
   392  						ID:    "main-id",
   393  						Image: "local-registry:12345/frontend-image:my-tag",
   394  						State: v1alpha1.ContainerState{
   395  							Terminated: &v1alpha1.ContainerStateTerminated{},
   396  						},
   397  					},
   398  				},
   399  			},
   400  		},
   401  	})
   402  
   403  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   404  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   405  
   406  	var lu v1alpha1.LiveUpdate
   407  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   408  	if assert.NotNil(t, lu.Status.Failed) {
   409  		assert.Equal(t, "Terminated", lu.Status.Failed.Reason)
   410  		assert.Contains(t, f.Stdout(),
   411  			`LiveUpdate "frontend-liveupdate" Terminated: Container for live update is stopped. Pod name: pod-1`)
   412  	}
   413  
   414  	f.assertSteadyState(&lu)
   415  }
   416  
   417  func TestOneRunningOneTerminatedContainer(t *testing.T) {
   418  	f := newFixture(t)
   419  
   420  	p, _ := os.Getwd()
   421  	nowMicro := apis.NowMicro()
   422  	txtPath := filepath.Join(p, "a.txt")
   423  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   424  
   425  	f.setupFrontend()
   426  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   427  		Pods: []v1alpha1.Pod{
   428  			{
   429  				Name:      "pod-1",
   430  				Namespace: "default",
   431  				Containers: []v1alpha1.Container{
   432  					{
   433  						Name:  "main",
   434  						ID:    "main-id",
   435  						Image: "local-registry:12345/frontend-image:my-tag",
   436  						State: v1alpha1.ContainerState{
   437  							Terminated: &v1alpha1.ContainerStateTerminated{},
   438  						},
   439  					},
   440  				},
   441  			},
   442  			{
   443  				Name:      "pod-2",
   444  				Namespace: "default",
   445  				Containers: []v1alpha1.Container{
   446  					{
   447  						Name:  "main",
   448  						ID:    "main-id",
   449  						Image: "local-registry:12345/frontend-image:my-tag",
   450  						State: v1alpha1.ContainerState{
   451  							Running: &v1alpha1.ContainerStateRunning{},
   452  						},
   453  					},
   454  				},
   455  			},
   456  		},
   457  	})
   458  
   459  	// Trigger a file event, and make sure that the status reflects the sync.
   460  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   461  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   462  
   463  	var lu v1alpha1.LiveUpdate
   464  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   465  	assert.Nil(t, lu.Status.Failed)
   466  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   467  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   468  	}
   469  
   470  	// Also make sure the sync gets pulled into the monitor.
   471  	m, ok := f.r.monitors["frontend-liveupdate"]
   472  	require.True(t, ok)
   473  	assert.Equal(t, map[string]metav1.MicroTime{
   474  		txtPath: txtChangeTime,
   475  	}, m.sources["frontend-fw"].modTimeByPath)
   476  	assert.Equal(t, 1, len(f.cu.Calls))
   477  	assert.Equal(t, "pod-2", f.cu.Calls[0].ContainerInfo.PodID.String())
   478  
   479  	f.assertSteadyState(&lu)
   480  }
   481  
   482  func TestCrashLoopBackoff(t *testing.T) {
   483  	f := newFixture(t)
   484  
   485  	p, _ := os.Getwd()
   486  	nowMicro := apis.NowMicro()
   487  	txtPath := filepath.Join(p, "a.txt")
   488  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   489  
   490  	f.setupFrontend()
   491  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   492  		Pods: []v1alpha1.Pod{
   493  			{
   494  				Name:      "pod-1",
   495  				Namespace: "default",
   496  				Containers: []v1alpha1.Container{
   497  					{
   498  						Name:  "main",
   499  						ID:    "main-id",
   500  						Image: "local-registry:12345/frontend-image:my-tag",
   501  						State: v1alpha1.ContainerState{
   502  							Waiting: &v1alpha1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
   503  						},
   504  					},
   505  				},
   506  			},
   507  		},
   508  	})
   509  
   510  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   511  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   512  
   513  	var lu v1alpha1.LiveUpdate
   514  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   515  	if assert.NotNil(t, lu.Status.Failed) {
   516  		assert.Equal(t, "CrashLoopBackOff", lu.Status.Failed.Reason)
   517  	}
   518  	assert.Equal(t, 0, len(f.cu.Calls))
   519  
   520  	f.assertSteadyState(&lu)
   521  
   522  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   523  		Pods: []v1alpha1.Pod{
   524  			{
   525  				Name:      "pod-1",
   526  				Namespace: "default",
   527  				Containers: []v1alpha1.Container{
   528  					{
   529  						Name:  "main",
   530  						ID:    "main-id",
   531  						Image: "local-registry:12345/frontend-image:my-tag",
   532  						State: v1alpha1.ContainerState{
   533  							Running: &v1alpha1.ContainerStateRunning{},
   534  						},
   535  					},
   536  				},
   537  			},
   538  		},
   539  	})
   540  
   541  	// CrashLoopBackOff is a permanent state. If the container starts running
   542  	// again, we don't "revive" the live-update.
   543  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   544  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   545  	if assert.NotNil(t, lu.Status.Failed) {
   546  		assert.Equal(t, "CrashLoopBackOff", lu.Status.Failed.Reason)
   547  	}
   548  }
   549  
   550  func TestStopPathConsumedByImageBuild(t *testing.T) {
   551  	f := newFixture(t)
   552  
   553  	p, _ := os.Getwd()
   554  	nowMicro := apis.NowMicro()
   555  	stopPath := filepath.Join(p, "stop.txt")
   556  	stopChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   557  
   558  	f.setupFrontend()
   559  
   560  	f.addFileEvent("frontend-fw", stopPath, stopChangeTime)
   561  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   562  
   563  	var lu v1alpha1.LiveUpdate
   564  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   565  	if assert.NotNil(t, lu.Status.Failed) {
   566  		assert.Equal(t, "UpdateStopped", lu.Status.Failed.Reason)
   567  	}
   568  
   569  	f.assertSteadyState(&lu)
   570  
   571  	// Clear the failure with an Image build
   572  	f.Upsert(&v1alpha1.ImageMap{
   573  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-image-map"},
   574  		Status: v1alpha1.ImageMapStatus{
   575  			Image:            "frontend-image:my-tag",
   576  			ImageFromCluster: "local-registry:12345/frontend-image:my-tag",
   577  			BuildStartTime:   &metav1.MicroTime{Time: nowMicro.Add(2 * time.Second)},
   578  		},
   579  	})
   580  
   581  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   582  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   583  	assert.Nil(t, lu.Status.Failed)
   584  
   585  	txtPath := filepath.Join(p, "a.txt")
   586  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(3 * time.Second)}
   587  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   588  
   589  	assert.Equal(t, 0, len(f.cu.Calls))
   590  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   591  	assert.Equal(t, 1, len(f.cu.Calls))
   592  }
   593  
   594  func TestStopPathConsumedByKubernetesApply(t *testing.T) {
   595  	f := newFixture(t)
   596  
   597  	p, _ := os.Getwd()
   598  	nowMicro := apis.NowMicro()
   599  	stopPath := filepath.Join(p, "stop.txt")
   600  	stopChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   601  
   602  	// we are going to delete the ImageMap, so we cannot use it as a selector
   603  	// (the default behavior)
   604  	f.setupFrontendWithSelector(&v1alpha1.LiveUpdateSelector{
   605  		Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
   606  			DiscoveryName: "frontend-discovery",
   607  			ApplyName:     "frontend-apply",
   608  			Image:         "local-registry:12345/frontend-image:some-tag",
   609  		},
   610  	})
   611  	f.Delete(&v1alpha1.ImageMap{
   612  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-image-map"},
   613  	})
   614  
   615  	var lu v1alpha1.LiveUpdate
   616  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   617  	lu.Spec.Sources[0].ImageMap = ""
   618  	f.Update(&lu)
   619  
   620  	f.addFileEvent("frontend-fw", stopPath, stopChangeTime)
   621  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   622  
   623  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   624  	if assert.NotNil(t, lu.Status.Failed) {
   625  		assert.Equal(t, "UpdateStopped", lu.Status.Failed.Reason)
   626  	}
   627  
   628  	f.assertSteadyState(&lu)
   629  
   630  	// Clear the failure with an Apply
   631  	f.Upsert(&v1alpha1.KubernetesApply{
   632  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-apply"},
   633  		Status: v1alpha1.KubernetesApplyStatus{
   634  			LastApplyStartTime: metav1.MicroTime{Time: nowMicro.Add(2 * time.Second)},
   635  		},
   636  	})
   637  
   638  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   639  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   640  	assert.Nil(t, lu.Status.Failed)
   641  
   642  	txtPath := filepath.Join(p, "a.txt")
   643  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(3 * time.Second)}
   644  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   645  
   646  	assert.Equal(t, 0, len(f.cu.Calls))
   647  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   648  	assert.Equal(t, 1, len(f.cu.Calls))
   649  }
   650  
   651  func TestKubernetesContainerNameSelector(t *testing.T) {
   652  	f := newFixture(t)
   653  
   654  	p, _ := os.Getwd()
   655  	nowMicro := apis.NowMicro()
   656  	txtPath := filepath.Join(p, "a.txt")
   657  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   658  
   659  	// change from default ImageMap selector to a container name selector
   660  	f.setupFrontendWithSelector(&v1alpha1.LiveUpdateSelector{
   661  		Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
   662  			DiscoveryName: "frontend-discovery",
   663  			ApplyName:     "frontend-apply",
   664  			ContainerName: "main",
   665  		},
   666  	})
   667  
   668  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   669  		Pods: []v1alpha1.Pod{
   670  			{
   671  				Name:      "pod-1",
   672  				Namespace: "default",
   673  				Containers: []v1alpha1.Container{
   674  					{
   675  						Name:  "main",
   676  						ID:    "main-id",
   677  						Image: "frontend-image",
   678  						State: v1alpha1.ContainerState{
   679  							Running: &v1alpha1.ContainerStateRunning{},
   680  						},
   681  					},
   682  				},
   683  			},
   684  		},
   685  	})
   686  
   687  	// Trigger a file event, and make sure that the status reflects the sync.
   688  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   689  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   690  
   691  	var lu v1alpha1.LiveUpdate
   692  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   693  	assert.Equal(t, "main", lu.Spec.Selector.Kubernetes.ContainerName)
   694  	assert.Nil(t, lu.Status.Failed)
   695  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   696  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   697  	}
   698  
   699  	f.assertSteadyState(&lu)
   700  }
   701  
   702  func TestKubernetesImageSelector(t *testing.T) {
   703  	f := newFixture(t)
   704  
   705  	p, _ := os.Getwd()
   706  	nowMicro := apis.NowMicro()
   707  	txtPath := filepath.Join(p, "a.txt")
   708  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   709  
   710  	// change from default ImageMap selector to an image selector
   711  	f.setupFrontendWithSelector(&v1alpha1.LiveUpdateSelector{
   712  		Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
   713  			DiscoveryName: "frontend-discovery",
   714  			ApplyName:     "frontend-apply",
   715  			Image:         "local-registry:12345/frontend-image:some-tag",
   716  		},
   717  	})
   718  
   719  	f.kdUpdateStatus("frontend-discovery", v1alpha1.KubernetesDiscoveryStatus{
   720  		Pods: []v1alpha1.Pod{
   721  			{
   722  				Name:      "pod-1",
   723  				Namespace: "default",
   724  				Containers: []v1alpha1.Container{
   725  					{
   726  						Name:  "main",
   727  						ID:    "main-id",
   728  						Image: "local-registry:12345/frontend-image:my-tag",
   729  						State: v1alpha1.ContainerState{
   730  							Running: &v1alpha1.ContainerStateRunning{},
   731  						},
   732  					},
   733  				},
   734  			},
   735  		},
   736  	})
   737  
   738  	// Trigger a file event, and make sure that the status reflects the sync.
   739  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   740  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   741  
   742  	var lu v1alpha1.LiveUpdate
   743  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   744  	assert.Equal(t, "local-registry:12345/frontend-image:some-tag",
   745  		lu.Spec.Selector.Kubernetes.Image)
   746  	assert.Nil(t, lu.Status.Failed)
   747  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   748  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   749  	}
   750  
   751  	f.assertSteadyState(&lu)
   752  }
   753  
   754  func TestDockerComposeRestartPolicy(t *testing.T) {
   755  	f := newFixture(t)
   756  
   757  	p, _ := os.Getwd()
   758  	nowMicro := apis.NowMicro()
   759  	txtPath := filepath.Join(p, "a.txt")
   760  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   761  
   762  	f.setupDockerComposeFrontend()
   763  
   764  	var lu v1alpha1.LiveUpdate
   765  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   766  	lu.Spec.Restart = v1alpha1.LiveUpdateRestartStrategyAlways
   767  	f.Upsert(&lu)
   768  
   769  	// Trigger a file event, and make sure that the status reflects the sync.
   770  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   771  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   772  
   773  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   774  	assert.Nil(t, lu.Status.Failed)
   775  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   776  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   777  	}
   778  
   779  	// Make sure the container was restarted.
   780  	if assert.Equal(t, 1, len(f.cu.Calls)) {
   781  		assert.False(t, f.cu.Calls[0].HotReload)
   782  	}
   783  }
   784  
   785  func TestDockerComposeExecs(t *testing.T) {
   786  	f := newFixture(t)
   787  
   788  	p, _ := os.Getwd()
   789  	nowMicro := apis.NowMicro()
   790  	txtPath := filepath.Join(p, "a.txt")
   791  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   792  
   793  	f.setupDockerComposeFrontend()
   794  
   795  	var lu v1alpha1.LiveUpdate
   796  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   797  
   798  	execs := []v1alpha1.LiveUpdateExec{
   799  		{Args: model.ToUnixCmd("./foo.sh bar").Argv},
   800  		{Args: model.ToUnixCmd("yarn install").Argv, TriggerPaths: []string{"a.txt"}},
   801  		{Args: model.ToUnixCmd("pip install").Argv, TriggerPaths: []string{"requirements.txt"}},
   802  	}
   803  	lu.Spec.Execs = execs
   804  	f.Upsert(&lu)
   805  
   806  	// Trigger a file event, and make sure that the status reflects the sync.
   807  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   808  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   809  
   810  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   811  	assert.Nil(t, lu.Status.Failed)
   812  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   813  		assert.Equal(t, txtChangeTime, lu.Status.Containers[0].LastFileTimeSynced)
   814  	}
   815  
   816  	// Make sure there were no exec errors.
   817  	if assert.NotNil(t, f.st.lastCompletedAction) {
   818  		assert.Nil(t, f.st.lastCompletedAction.Error)
   819  	}
   820  
   821  	// Make sure two cmds were executed, and one was skipped.
   822  	if assert.Equal(t, 1, len(f.cu.Calls)) {
   823  		assert.Equal(t, []model.Cmd{
   824  			model.ToUnixCmd("./foo.sh bar"),
   825  			model.ToUnixCmd("yarn install"),
   826  		}, f.cu.Calls[0].Cmds)
   827  	}
   828  }
   829  
   830  func TestDockerComposeExecInfraFailure(t *testing.T) {
   831  	f := newFixture(t)
   832  
   833  	p, _ := os.Getwd()
   834  	nowMicro := apis.NowMicro()
   835  	txtPath := filepath.Join(p, "a.txt")
   836  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   837  
   838  	f.setupDockerComposeFrontend()
   839  
   840  	var lu v1alpha1.LiveUpdate
   841  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   842  
   843  	execs := []v1alpha1.LiveUpdateExec{
   844  		{Args: model.ToUnixCmd("echo error && exit 1").Argv},
   845  	}
   846  	lu.Spec.Execs = execs
   847  	f.Upsert(&lu)
   848  
   849  	f.cu.SetUpdateErr(fmt.Errorf("cluster connection lost"))
   850  
   851  	// Trigger a file event, and make sure that the status reflects the sync.
   852  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   853  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   854  
   855  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   856  	if assert.NotNil(t, lu.Status.Failed) {
   857  		assert.Equal(t, "UpdateFailed", lu.Status.Failed.Reason)
   858  		assert.Equal(t, "Updating container main-id: cluster connection lost",
   859  			lu.Status.Failed.Message)
   860  	}
   861  
   862  	// Make sure there were  exec errors.
   863  	if assert.NotNil(t, f.st.lastCompletedAction) {
   864  		assert.Equal(t, "Updating container main-id: cluster connection lost",
   865  			f.st.lastCompletedAction.Error.Error())
   866  	}
   867  }
   868  
   869  func TestDockerComposeExecRunFailure(t *testing.T) {
   870  	f := newFixture(t)
   871  
   872  	p, _ := os.Getwd()
   873  	nowMicro := apis.NowMicro()
   874  	txtPath := filepath.Join(p, "a.txt")
   875  	txtChangeTime := metav1.MicroTime{Time: nowMicro.Add(time.Second)}
   876  
   877  	f.setupDockerComposeFrontend()
   878  
   879  	var lu v1alpha1.LiveUpdate
   880  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   881  
   882  	execs := []v1alpha1.LiveUpdateExec{
   883  		{Args: model.ToUnixCmd("echo error && exit 1").Argv},
   884  	}
   885  	lu.Spec.Execs = execs
   886  	f.Upsert(&lu)
   887  
   888  	f.cu.SetUpdateErr(build.NewRunStepFailure(errors.New("compilation failed")))
   889  
   890  	// Trigger a file event, and make sure that the status reflects the sync.
   891  	f.addFileEvent("frontend-fw", txtPath, txtChangeTime)
   892  	f.MustReconcile(types.NamespacedName{Name: "frontend-liveupdate"})
   893  
   894  	f.MustGet(types.NamespacedName{Name: "frontend-liveupdate"}, &lu)
   895  	assert.Nil(t, lu.Status.Failed)
   896  	if assert.Equal(t, 1, len(lu.Status.Containers)) {
   897  		assert.Equal(t, "compilation failed", lu.Status.Containers[0].LastExecError)
   898  	}
   899  
   900  	// Make sure there were  exec errors.
   901  	if assert.NotNil(t, f.st.lastCompletedAction) {
   902  		assert.Equal(t, "compilation failed",
   903  			f.st.lastCompletedAction.Error.Error())
   904  	}
   905  }
   906  
   907  type TestingStore struct {
   908  	*store.TestingStore
   909  	ctx                 context.Context
   910  	lastStartedAction   *buildcontrols.BuildStartedAction
   911  	lastCompletedAction *buildcontrols.BuildCompleteAction
   912  }
   913  
   914  func newTestingStore() *TestingStore {
   915  	return &TestingStore{TestingStore: store.NewTestingStore()}
   916  }
   917  
   918  func (s *TestingStore) Dispatch(action store.Action) {
   919  	s.TestingStore.Dispatch(action)
   920  	switch action := action.(type) {
   921  	case buildcontrols.BuildStartedAction:
   922  		s.lastStartedAction = &action
   923  	case buildcontrols.BuildCompleteAction:
   924  		s.lastCompletedAction = &action
   925  	case store.LogAction:
   926  		_, _ = logger.Get(s.ctx).Writer(action.Level()).Write(action.Message())
   927  	}
   928  }
   929  
   930  type fixture struct {
   931  	*fake.ControllerFixture
   932  	r  *Reconciler
   933  	cu *containerupdate.FakeContainerUpdater
   934  	st *TestingStore
   935  }
   936  
   937  func newFixture(t testing.TB) *fixture {
   938  	cfb := fake.NewControllerFixtureBuilder(t)
   939  	cu := &containerupdate.FakeContainerUpdater{}
   940  	st := newTestingStore()
   941  	r := NewFakeReconciler(st, cu, cfb.Client)
   942  	cf := cfb.Build(r)
   943  	st.ctx = cf.Context()
   944  	return &fixture{
   945  		ControllerFixture: cf,
   946  		r:                 r,
   947  		cu:                cu,
   948  		st:                st,
   949  	}
   950  }
   951  
   952  func (f *fixture) addFileEvent(name string, p string, time metav1.MicroTime) {
   953  	var fw v1alpha1.FileWatch
   954  	f.MustGet(types.NamespacedName{Name: name}, &fw)
   955  	update := fw.DeepCopy()
   956  	update.Status.FileEvents = append(update.Status.FileEvents, v1alpha1.FileEvent{
   957  		Time:      time,
   958  		SeenFiles: []string{p},
   959  	})
   960  	f.UpdateStatus(update)
   961  }
   962  
   963  func (f *fixture) setupFrontend() {
   964  	f.setupFrontendWithSelector(nil)
   965  }
   966  
   967  // Create a frontend LiveUpdate with all objects attached.
   968  func (f *fixture) setupFrontendWithSelector(selector *v1alpha1.LiveUpdateSelector) {
   969  	p, _ := os.Getwd()
   970  	now := apis.Now()
   971  	nowMicro := apis.NowMicro()
   972  
   973  	// Create all the objects.
   974  	f.Create(&v1alpha1.FileWatch{
   975  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-fw"},
   976  		Spec: v1alpha1.FileWatchSpec{
   977  			WatchedPaths: []string{p},
   978  		},
   979  		Status: v1alpha1.FileWatchStatus{
   980  			MonitorStartTime: nowMicro,
   981  		},
   982  	})
   983  	f.Create(&v1alpha1.KubernetesApply{
   984  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-apply"},
   985  		Status:     v1alpha1.KubernetesApplyStatus{},
   986  	})
   987  	f.Create(&v1alpha1.ImageMap{
   988  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-image-map"},
   989  		Status: v1alpha1.ImageMapStatus{
   990  			Image:            "frontend-image:my-tag",
   991  			ImageFromCluster: "local-registry:12345/frontend-image:my-tag",
   992  			BuildStartTime:   &nowMicro,
   993  		},
   994  	})
   995  	f.Create(&v1alpha1.KubernetesDiscovery{
   996  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-discovery"},
   997  		Status: v1alpha1.KubernetesDiscoveryStatus{
   998  			MonitorStartTime: nowMicro,
   999  			Pods: []v1alpha1.Pod{
  1000  				{
  1001  					Name:      "pod-1",
  1002  					Namespace: "default",
  1003  					Containers: []v1alpha1.Container{
  1004  						{
  1005  							Name:  "main",
  1006  							ID:    "main-id",
  1007  							Image: "local-registry:12345/frontend-image:my-tag",
  1008  							Ready: true,
  1009  							State: v1alpha1.ContainerState{
  1010  								Running: &v1alpha1.ContainerStateRunning{
  1011  									StartedAt: now,
  1012  								},
  1013  							},
  1014  						},
  1015  					},
  1016  				},
  1017  			},
  1018  		},
  1019  	})
  1020  
  1021  	if selector == nil {
  1022  		// default selector matches the most common Tilt use-case, which has
  1023  		// KDisco + KApply and selects via ImageMap
  1024  		selector = &v1alpha1.LiveUpdateSelector{
  1025  			Kubernetes: &v1alpha1.LiveUpdateKubernetesSelector{
  1026  				ApplyName:     "frontend-apply",
  1027  				DiscoveryName: "frontend-discovery",
  1028  				ImageMapName:  "frontend-image-map",
  1029  			},
  1030  		}
  1031  	}
  1032  
  1033  	f.Create(&v1alpha1.LiveUpdate{
  1034  		ObjectMeta: metav1.ObjectMeta{
  1035  			Name: "frontend-liveupdate",
  1036  			Annotations: map[string]string{
  1037  				v1alpha1.AnnotationManifest:     "frontend",
  1038  				liveupdate.AnnotationUpdateMode: "auto",
  1039  			},
  1040  		},
  1041  		Spec: v1alpha1.LiveUpdateSpec{
  1042  			BasePath: p,
  1043  			Sources: []v1alpha1.LiveUpdateSource{{
  1044  				FileWatch: "frontend-fw",
  1045  				ImageMap:  "frontend-image-map",
  1046  			}},
  1047  			Selector: *selector,
  1048  			Syncs: []v1alpha1.LiveUpdateSync{
  1049  				{LocalPath: ".", ContainerPath: "/app"},
  1050  			},
  1051  			StopPaths: []string{"stop.txt"},
  1052  		},
  1053  	})
  1054  	f.Create(&v1alpha1.ConfigMap{
  1055  		ObjectMeta: metav1.ObjectMeta{
  1056  			Name: configmap.TriggerQueueName,
  1057  		},
  1058  	})
  1059  }
  1060  
  1061  // Create a frontend DockerCompose LiveUpdate with all objects attached.
  1062  func (f *fixture) setupDockerComposeFrontend() {
  1063  	p, _ := os.Getwd()
  1064  	nowMicro := apis.NowMicro()
  1065  
  1066  	// Create all the objects.
  1067  	f.Create(&v1alpha1.FileWatch{
  1068  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-fw"},
  1069  		Spec: v1alpha1.FileWatchSpec{
  1070  			WatchedPaths: []string{p},
  1071  		},
  1072  		Status: v1alpha1.FileWatchStatus{
  1073  			MonitorStartTime: nowMicro,
  1074  		},
  1075  	})
  1076  	f.Create(&v1alpha1.DockerComposeService{
  1077  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-service"},
  1078  		Status: v1alpha1.DockerComposeServiceStatus{
  1079  			ContainerID: "main-id",
  1080  			ContainerState: &v1alpha1.DockerContainerState{
  1081  				Status:    dockercompose.ContainerStatusRunning,
  1082  				StartedAt: nowMicro,
  1083  			},
  1084  		},
  1085  	})
  1086  	f.Create(&v1alpha1.ImageMap{
  1087  		ObjectMeta: metav1.ObjectMeta{Name: "frontend-image-map"},
  1088  		Status: v1alpha1.ImageMapStatus{
  1089  			Image:            "frontend-image:my-tag",
  1090  			ImageFromCluster: "frontend-image:my-tag",
  1091  			BuildStartTime:   &nowMicro,
  1092  		},
  1093  	})
  1094  	f.Create(&v1alpha1.LiveUpdate{
  1095  		ObjectMeta: metav1.ObjectMeta{
  1096  			Name: "frontend-liveupdate",
  1097  			Annotations: map[string]string{
  1098  				v1alpha1.AnnotationManifest:     "frontend",
  1099  				liveupdate.AnnotationUpdateMode: "auto",
  1100  			},
  1101  		},
  1102  		Spec: v1alpha1.LiveUpdateSpec{
  1103  			BasePath: p,
  1104  			Sources: []v1alpha1.LiveUpdateSource{{
  1105  				FileWatch: "frontend-fw",
  1106  				ImageMap:  "frontend-image-map",
  1107  			}},
  1108  			Selector: v1alpha1.LiveUpdateSelector{
  1109  				DockerCompose: &v1alpha1.LiveUpdateDockerComposeSelector{
  1110  					Service: "frontend-service",
  1111  				},
  1112  			},
  1113  			Syncs: []v1alpha1.LiveUpdateSync{
  1114  				{LocalPath: ".", ContainerPath: "/app"},
  1115  			},
  1116  			StopPaths: []string{"stop.txt"},
  1117  		},
  1118  	})
  1119  	f.Create(&v1alpha1.ConfigMap{
  1120  		ObjectMeta: metav1.ObjectMeta{
  1121  			Name: configmap.TriggerQueueName,
  1122  		},
  1123  	})
  1124  }
  1125  
  1126  func (f *fixture) assertSteadyState(lu *v1alpha1.LiveUpdate) {
  1127  	startCalls := len(f.cu.Calls)
  1128  
  1129  	f.T().Helper()
  1130  	f.MustReconcile(types.NamespacedName{Name: lu.Name})
  1131  	var lu2 v1alpha1.LiveUpdate
  1132  	f.MustGet(types.NamespacedName{Name: lu.Name}, &lu2)
  1133  	assert.Equal(f.T(), lu.ResourceVersion, lu2.ResourceVersion)
  1134  
  1135  	assert.Equal(f.T(), startCalls, len(f.cu.Calls))
  1136  }
  1137  
  1138  func (f *fixture) kdUpdateStatus(name string, status v1alpha1.KubernetesDiscoveryStatus) {
  1139  	var kd v1alpha1.KubernetesDiscovery
  1140  	f.MustGet(types.NamespacedName{Name: name}, &kd)
  1141  	update := kd.DeepCopy()
  1142  	update.Status = status
  1143  	f.UpdateStatus(update)
  1144  }