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

     1  package podlogstream
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/jonboulle/clockwork"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/tilt-dev/tilt/internal/controllers/apicmp"
    15  	"github.com/tilt-dev/tilt/internal/timecmp"
    16  	"github.com/tilt-dev/tilt/pkg/apis"
    17  
    18  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    19  
    20  	"github.com/stretchr/testify/assert"
    21  	v1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/apimachinery/pkg/types"
    24  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    25  
    26  	"github.com/tilt-dev/tilt/internal/container"
    27  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    28  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    29  	"github.com/tilt-dev/tilt/internal/k8s"
    30  	"github.com/tilt-dev/tilt/internal/store"
    31  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    32  	"github.com/tilt-dev/tilt/internal/testutils/bufsync"
    33  	"github.com/tilt-dev/tilt/pkg/logger"
    34  	"github.com/tilt-dev/tilt/pkg/model"
    35  )
    36  
    37  var podID = k8s.PodID("pod-id")
    38  var cName = container.Name("cname")
    39  var cID = container.ID("cid")
    40  
    41  func TestLogs(t *testing.T) {
    42  	f := newPLMFixture(t)
    43  
    44  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!")
    45  
    46  	start := f.clock.Now()
    47  
    48  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
    49  	f.kClient.UpsertPod(pb.toPod())
    50  
    51  	pls := plsFromPod("server", pb, start)
    52  	f.Create(pls)
    53  
    54  	f.triggerPodEvent(podID)
    55  	f.AssertOutputContains("hello world!")
    56  	f.AssertLogStartTime(start)
    57  
    58  	// Check to make sure that we're enqueuing pod changes as Reconcile() calls.
    59  	podNN := types.NamespacedName{Name: string(podID), Namespace: "default"}
    60  	streamNN := types.NamespacedName{Name: fmt.Sprintf("default-%s", podID)}
    61  	assert.Equal(t, []reconcile.Request{
    62  		{NamespacedName: streamNN},
    63  	}, f.plsc.podSource.indexer.EnqueueKey(indexer.Key{Name: podNN, GVK: podGVK}))
    64  }
    65  
    66  func TestLogCleanup(t *testing.T) {
    67  	f := newPLMFixture(t)
    68  
    69  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!")
    70  
    71  	start := f.clock.Now()
    72  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
    73  	f.kClient.UpsertPod(pb.toPod())
    74  
    75  	pls := plsFromPod("server", pb, start)
    76  	f.Create(pls)
    77  
    78  	f.triggerPodEvent(podID)
    79  	f.AssertOutputContains("hello world!")
    80  
    81  	f.Delete(pls)
    82  	assert.Len(t, f.plsc.watches, 0)
    83  
    84  	// TODO(nick): Currently, namespace watches are never cleanedup,
    85  	// because the user might restart them again.
    86  	assert.Len(t, f.plsc.podSource.watchesByNamespace, 1)
    87  }
    88  
    89  func TestLogActions(t *testing.T) {
    90  	f := newPLMFixture(t)
    91  
    92  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!\ngoodbye world!\n")
    93  
    94  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
    95  	f.kClient.UpsertPod(pb.toPod())
    96  
    97  	f.Create(plsFromPod("server", pb, time.Time{}))
    98  
    99  	f.triggerPodEvent(podID)
   100  	f.ConsumeLogActionsUntil("hello world!")
   101  }
   102  
   103  func TestLogsFailed(t *testing.T) {
   104  	f := newPLMFixture(t)
   105  
   106  	f.kClient.ContainerLogsError = fmt.Errorf("my-error")
   107  
   108  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
   109  	f.kClient.UpsertPod(pb.toPod())
   110  
   111  	pls := plsFromPod("server", pb, time.Time{})
   112  	f.Create(pls)
   113  
   114  	f.AssertOutputContains("Error streaming pod-id logs")
   115  	assert.Contains(t, f.out.String(), "my-error")
   116  
   117  	require.Eventually(t,
   118  		func() bool {
   119  			// Check to make sure the status has an error.
   120  			f.MustGet(f.KeyForObject(pls), pls)
   121  			return apicmp.DeepEqual(pls.Status,
   122  				PodLogStreamStatus{
   123  					ContainerStatuses: []ContainerLogStreamStatus{
   124  						{
   125  							Name:  "cname",
   126  							Error: "my-error",
   127  						},
   128  					},
   129  				})
   130  		},
   131  		time.Second, 10*time.Millisecond,
   132  		"Expected error not present on PodLogStreamStatus: %v", pls,
   133  	)
   134  
   135  	result := f.MustReconcile(f.KeyForObject(pls))
   136  	assert.Equal(t, 2*time.Second, result.RequeueAfter)
   137  
   138  	f.clock.Advance(2 * time.Second)
   139  
   140  	assert.Eventually(f.t, func() bool {
   141  		result = f.MustReconcile(f.KeyForObject(pls))
   142  		return result.RequeueAfter == 4*time.Second
   143  	}, time.Second, 5*time.Millisecond, "should re-stream and backoff again")
   144  }
   145  
   146  func TestLogsCanceledUnexpectedly(t *testing.T) {
   147  	f := newPLMFixture(t)
   148  
   149  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!\n")
   150  
   151  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
   152  	f.kClient.UpsertPod(pb.toPod())
   153  	pls := plsFromPod("server", pb, time.Time{})
   154  	f.Create(pls)
   155  
   156  	f.AssertOutputContains("hello world!\n")
   157  
   158  	// Wait until the previous log stream finishes.
   159  	assert.Eventually(f.t, func() bool {
   160  		f.MustGet(f.KeyForObject(pls), pls)
   161  		statuses := pls.Status.ContainerStatuses
   162  		if len(statuses) != 1 {
   163  			return false
   164  		}
   165  		return !statuses[0].Active
   166  	}, time.Second, 5*time.Millisecond)
   167  
   168  	// Set new logs, as if the pod restarted.
   169  	f.kClient.SetLogsForPodContainer(podID, cName, "goodbye world!\n")
   170  	f.triggerPodEvent(podID)
   171  	f.clock.Advance(10 * time.Second)
   172  	f.MustReconcile(types.NamespacedName{Name: pls.Name})
   173  	f.AssertOutputContains("goodbye world!\n")
   174  }
   175  
   176  func TestMultiContainerLogs(t *testing.T) {
   177  	f := newPLMFixture(t)
   178  
   179  	f.kClient.SetLogsForPodContainer(podID, "cont1", "hello world!")
   180  	f.kClient.SetLogsForPodContainer(podID, "cont2", "goodbye world!")
   181  
   182  	pb := newPodBuilder(podID).
   183  		addRunningContainer("cont1", "cid1").
   184  		addRunningContainer("cont2", "cid2")
   185  	f.kClient.UpsertPod(pb.toPod())
   186  	f.Create(plsFromPod("server", pb, time.Time{}))
   187  
   188  	f.AssertOutputContains("hello world!")
   189  	f.AssertOutputContains("goodbye world!")
   190  }
   191  
   192  func TestContainerPrefixes(t *testing.T) {
   193  	f := newPLMFixture(t)
   194  
   195  	pID1 := k8s.PodID("pod1")
   196  	cNamePrefix1 := container.Name("yes-prefix-1")
   197  	cNamePrefix2 := container.Name("yes-prefix-2")
   198  	f.kClient.SetLogsForPodContainer(pID1, cNamePrefix1, "hello world!")
   199  	f.kClient.SetLogsForPodContainer(pID1, cNamePrefix2, "goodbye world!")
   200  
   201  	pID2 := k8s.PodID("pod2")
   202  	cNameNoPrefix := container.Name("no-prefix")
   203  	f.kClient.SetLogsForPodContainer(pID2, cNameNoPrefix, "hello jupiter!")
   204  
   205  	pbMultiC := newPodBuilder(pID1).
   206  		// Pod with multiple containers -- logs should be prefixed with container name
   207  		addRunningContainer(cNamePrefix1, "cid1").
   208  		addRunningContainer(cNamePrefix2, "cid2")
   209  	f.kClient.UpsertPod(pbMultiC.toPod())
   210  
   211  	f.Create(plsFromPod("multiContainer", pbMultiC, time.Time{}))
   212  
   213  	pbSingleC := newPodBuilder(pID2).
   214  		// Pod with just one container -- logs should NOT be prefixed with container name
   215  		addRunningContainer(cNameNoPrefix, "cid3")
   216  	f.kClient.UpsertPod(pbSingleC.toPod())
   217  
   218  	f.Create(plsFromPod("singleContainer", pbSingleC, time.Time{}))
   219  
   220  	// Make sure we have expected logs
   221  	f.AssertOutputContains("hello world!")
   222  	f.AssertOutputContains("goodbye world!")
   223  	f.AssertOutputContains("hello jupiter!")
   224  
   225  	// Check for un/expected prefixes
   226  	f.AssertOutputContains(cNamePrefix1.String())
   227  	f.AssertOutputContains(cNamePrefix2.String())
   228  	f.AssertOutputDoesNotContain(cNameNoPrefix.String())
   229  }
   230  
   231  func TestTerminatedContainerLogs(t *testing.T) {
   232  	f := newPLMFixture(t)
   233  
   234  	cName := container.Name("cName")
   235  	pb := newPodBuilder(podID).addTerminatedContainer(cName, "cID")
   236  	f.kClient.UpsertPod(pb.toPod())
   237  
   238  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!")
   239  
   240  	f.Create(plsFromPod("server", pb, time.Time{}))
   241  
   242  	// Fire OnChange twice, because we used to have a bug where
   243  	// we'd immediately teardown the log watch on the terminated container.
   244  	f.triggerPodEvent(podID)
   245  	f.triggerPodEvent(podID)
   246  
   247  	f.AssertOutputContains("hello world!")
   248  
   249  	// Make sure that we don't try to re-stream after the terminated container
   250  	// closes the log stream.
   251  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!\ngoodbye world!\n")
   252  
   253  	f.triggerPodEvent(podID)
   254  	f.AssertOutputContains("hello world!")
   255  	f.AssertOutputDoesNotContain("goodbye world!")
   256  }
   257  
   258  // https://github.com/tilt-dev/tilt/issues/3908
   259  func TestLogReconnection(t *testing.T) {
   260  	f := newPLMFixture(t)
   261  	cName := container.Name("cName")
   262  	pb := newPodBuilder(podID).addRunningContainer(cName, "cID")
   263  	f.kClient.UpsertPod(pb.toPod())
   264  
   265  	reader, writer := io.Pipe()
   266  	defer func() {
   267  		require.NoError(t, writer.Close())
   268  	}()
   269  	f.kClient.SetLogReaderForPodContainer(podID, cName, reader)
   270  
   271  	// Set up fake time
   272  	startTime := f.clock.Now()
   273  	f.Create(plsFromPod("server", pb, startTime))
   274  
   275  	_, err := writer.Write([]byte("hello world!"))
   276  	require.NoError(t, err)
   277  	f.AssertOutputContains("hello world!")
   278  	f.AssertLogStartTime(startTime)
   279  
   280  	f.clock.Advance(20 * time.Second)
   281  	lastRead := f.clock.Now()
   282  	_, _ = writer.Write([]byte("hello world2!"))
   283  	f.AssertOutputContains("hello world2!")
   284  
   285  	// Simulate Kubernetes rotating the logs by creating a new pipe.
   286  	reader2, writer2 := io.Pipe()
   287  	defer func() {
   288  		require.NoError(t, writer2.Close())
   289  	}()
   290  	f.kClient.SetLogReaderForPodContainer(podID, cName, reader2)
   291  	go func() {
   292  		_, _ = writer2.Write([]byte("goodbye world!"))
   293  	}()
   294  	f.AssertOutputDoesNotContain("goodbye world!")
   295  
   296  	f.clock.Advance(5 * time.Second)
   297  	f.AssertOutputDoesNotContain("goodbye world!")
   298  
   299  	f.clock.Advance(5 * time.Second)
   300  	f.AssertOutputDoesNotContain("goodbye world!")
   301  	f.AssertLogStartTime(startTime)
   302  
   303  	// simulate 15s since we last read a log; this triggers a reconnect
   304  	f.clock.Advance(15 * time.Second)
   305  	time.Sleep(20 * time.Millisecond)
   306  	assert.Error(t, f.kClient.LastPodLogContext.Err())
   307  	require.NoError(t, writer.Close())
   308  
   309  	f.AssertOutputContains("goodbye world!")
   310  
   311  	// Make sure the start time was adjusted for when the last read happened.
   312  	f.AssertLogStartTime(lastRead.Add(podLogReconnectGap))
   313  }
   314  
   315  func TestInitContainerLogs(t *testing.T) {
   316  	f := newPLMFixture(t)
   317  
   318  	f.kClient.SetLogsForPodContainer(podID, "cont1", "hello world!")
   319  
   320  	cNameInit := container.Name("cNameInit")
   321  	cNameNormal := container.Name("cNameNormal")
   322  	pb := newPodBuilder(podID).
   323  		addTerminatedInitContainer(cNameInit, "cID-init").
   324  		addRunningContainer(cNameNormal, "cID-normal")
   325  	f.kClient.UpsertPod(pb.toPod())
   326  
   327  	f.kClient.SetLogsForPodContainer(podID, cNameInit, "init world!")
   328  	f.kClient.SetLogsForPodContainer(podID, cNameNormal, "hello world!")
   329  
   330  	f.Create(plsFromPod("server", pb, time.Time{}))
   331  
   332  	f.AssertOutputContains(cNameInit.String())
   333  	f.AssertOutputContains("init world!")
   334  	f.AssertOutputDoesNotContain(cNameNormal.String())
   335  	f.AssertOutputContains("hello world!")
   336  }
   337  
   338  func TestIgnoredContainerLogs(t *testing.T) {
   339  	f := newPLMFixture(t)
   340  
   341  	f.kClient.SetLogsForPodContainer(podID, "cont1", "hello world!")
   342  
   343  	istioInit := container.IstioInitContainerName
   344  	istioSidecar := container.IstioSidecarContainerName
   345  	cNormal := container.Name("cNameNormal")
   346  	pb := newPodBuilder(podID).
   347  		addTerminatedInitContainer(istioInit, "cID-init").
   348  		addRunningContainer(istioSidecar, "cID-sidecar").
   349  		addRunningContainer(cNormal, "cID-normal")
   350  	f.kClient.UpsertPod(pb.toPod())
   351  
   352  	f.kClient.SetLogsForPodContainer(podID, istioInit, "init istio!")
   353  	f.kClient.SetLogsForPodContainer(podID, istioSidecar, "hello istio!")
   354  	f.kClient.SetLogsForPodContainer(podID, cNormal, "hello world!")
   355  
   356  	pls := plsFromPod("server", pb, time.Time{})
   357  	pls.Spec.IgnoreContainers = []string{string(istioInit), string(istioSidecar)}
   358  	f.Create(pls)
   359  
   360  	f.AssertOutputDoesNotContain("istio")
   361  	f.AssertOutputContains("hello world!")
   362  }
   363  
   364  // Our old Fake Kubernetes client used to interact badly
   365  // with the pod log stream reconciler, leading to an infinite
   366  // loop in tests.
   367  func TestInfiniteLoop(t *testing.T) {
   368  	f := newPLMFixture(t)
   369  
   370  	f.kClient.SetLogsForPodContainer(podID, "cont1", "hello world!")
   371  
   372  	pb := newPodBuilder(podID).
   373  		addRunningContainer("cNameNormal", "cID-normal")
   374  	f.kClient.UpsertPod(pb.toPod())
   375  
   376  	pls := plsFromPod("server", pb, time.Time{})
   377  	f.Create(pls)
   378  
   379  	nn := types.NamespacedName{Name: pls.Name}
   380  	f.MustReconcile(nn)
   381  
   382  	// Make sure this goes into an active state and stays there.
   383  	assert.Eventually(t, func() bool {
   384  		var pls v1alpha1.PodLogStream
   385  		f.MustGet(nn, &pls)
   386  		return len(pls.Status.ContainerStatuses) > 0 && pls.Status.ContainerStatuses[0].Active
   387  	}, 200*time.Millisecond, 10*time.Millisecond)
   388  
   389  	assert.Never(t, func() bool {
   390  		var pls v1alpha1.PodLogStream
   391  		f.MustGet(nn, &pls)
   392  		return len(pls.Status.ContainerStatuses) == 0 || !pls.Status.ContainerStatuses[0].Active
   393  	}, 200*time.Millisecond, 10*time.Millisecond)
   394  
   395  	_ = f.kClient.LastPodLogPipeWriter.CloseWithError(fmt.Errorf("manually closed"))
   396  
   397  	assert.Eventually(t, func() bool {
   398  		var pls v1alpha1.PodLogStream
   399  		f.MustGet(nn, &pls)
   400  		if len(pls.Status.ContainerStatuses) == 0 {
   401  			return false
   402  		}
   403  		cst := pls.Status.ContainerStatuses[0]
   404  		return !cst.Active && strings.Contains(cst.Error, "manually closed")
   405  	}, 200*time.Millisecond, 10*time.Millisecond)
   406  }
   407  
   408  func TestReconcilerIndexing(t *testing.T) {
   409  	f := newPLMFixture(t)
   410  
   411  	pls := plsFromPod("server", newPodBuilder(podID), f.clock.Now())
   412  	pls.Namespace = "some-ns"
   413  	pls.Spec.Cluster = "my-cluster"
   414  	f.Create(pls)
   415  
   416  	ctx := context.Background()
   417  	reqs := f.plsc.indexer.Enqueue(ctx, &v1alpha1.Cluster{
   418  		ObjectMeta: metav1.ObjectMeta{Namespace: "some-ns", Name: "my-cluster"},
   419  	})
   420  	assert.ElementsMatch(t, []reconcile.Request{
   421  		{NamespacedName: types.NamespacedName{Namespace: "some-ns", Name: "default-pod-id"}},
   422  	}, reqs)
   423  }
   424  
   425  func TestDeletionTimestamp(t *testing.T) {
   426  	f := newPLMFixture(t)
   427  
   428  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!")
   429  
   430  	start := f.clock.Now()
   431  
   432  	pb := newPodBuilder(podID).addRunningContainer(cName, cID).addDeletionTimestamp()
   433  	f.kClient.UpsertPod(pb.toPod())
   434  
   435  	pls := plsFromPod("server", pb, start)
   436  	f.Create(pls)
   437  
   438  	f.triggerPodEvent(podID)
   439  
   440  	nn := types.NamespacedName{Name: pls.Name}
   441  	f.MustReconcile(nn)
   442  
   443  	f.AssertOutputContains("hello world!")
   444  
   445  	assert.Eventually(f.t, func() bool {
   446  		f.Get(nn, pls)
   447  		return len(pls.Status.ContainerStatuses) == 1 && !pls.Status.ContainerStatuses[0].Active
   448  	}, time.Second, 5*time.Millisecond, "should stream then stop")
   449  
   450  	// No log streams should be active.
   451  	assert.Equal(t, pls.Status, v1alpha1.PodLogStreamStatus{
   452  		ContainerStatuses: []v1alpha1.ContainerLogStreamStatus{
   453  			v1alpha1.ContainerLogStreamStatus{Name: "cname"},
   454  		},
   455  	})
   456  
   457  	// The cname stream is closed forever.
   458  	assert.Len(t, f.plsc.hasClosedStream, 1)
   459  }
   460  
   461  func TestMissingPod(t *testing.T) {
   462  	f := newPLMFixture(t)
   463  
   464  	f.kClient.SetLogsForPodContainer(podID, cName, "hello world!")
   465  
   466  	start := f.clock.Now()
   467  
   468  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
   469  	pls := plsFromPod("server", pb, start)
   470  	nn := types.NamespacedName{Name: pls.Name}
   471  	result := f.Create(pls)
   472  	assert.Equal(t, time.Second, result.RequeueAfter)
   473  
   474  	result = f.MustReconcile(nn)
   475  	assert.Equal(t, 2*time.Second, result.RequeueAfter)
   476  
   477  	f.Get(nn, pls)
   478  	assert.Equal(t, "pod not found: default/pod-id", pls.Status.Error)
   479  
   480  	f.kClient.UpsertPod(pb.toPod())
   481  
   482  	result = f.MustReconcile(nn)
   483  	assert.Equal(t, time.Duration(0), result.RequeueAfter)
   484  
   485  	f.AssertOutputContains("hello world!")
   486  	f.AssertLogStartTime(start)
   487  }
   488  
   489  func TestFailedToCreateLogWatcher(t *testing.T) {
   490  	f := newPLMFixture(t)
   491  
   492  	f.kClient.SetLogsForPodContainer(podID, cName,
   493  		"listening on 8080\nfailed to create fsnotify watcher: too many open files")
   494  
   495  	start := f.clock.Now()
   496  
   497  	pb := newPodBuilder(podID).addRunningContainer(cName, cID)
   498  	f.kClient.UpsertPod(pb.toPod())
   499  
   500  	pls := plsFromPod("server", pb, start)
   501  	f.Create(pls)
   502  
   503  	f.triggerPodEvent(podID)
   504  	f.AssertOutputContains(`listening on 8080
   505  failed to create fsnotify watcher: too many open files
   506  Error streaming pod-id logs: failed to create fsnotify watcher: too many open files. Consider adjusting inotify limits: https://kind.sigs.k8s.io/docs/user/known-issues/#pod-errors-due-to-too-many-open-files
   507  `)
   508  }
   509  
   510  type plmStore struct {
   511  	t testing.TB
   512  	*store.TestingStore
   513  	out *bufsync.ThreadSafeBuffer
   514  }
   515  
   516  func newPLMStore(t testing.TB, out *bufsync.ThreadSafeBuffer) *plmStore {
   517  	return &plmStore{
   518  		t:            t,
   519  		TestingStore: store.NewTestingStore(),
   520  		out:          out,
   521  	}
   522  }
   523  
   524  func (s *plmStore) Dispatch(action store.Action) {
   525  	event, ok := action.(store.LogAction)
   526  	if !ok {
   527  		s.t.Errorf("Expected action type LogAction. Actual: %T", action)
   528  	}
   529  
   530  	_, err := s.out.Write(event.Message())
   531  	if err != nil {
   532  		fmt.Printf("error writing event: %v\n", err)
   533  	}
   534  }
   535  
   536  type plmFixture struct {
   537  	*fake.ControllerFixture
   538  	t       testing.TB
   539  	ctx     context.Context
   540  	kClient *k8s.FakeK8sClient
   541  	plsc    *Controller
   542  	out     *bufsync.ThreadSafeBuffer
   543  	store   *plmStore
   544  	clock   clockwork.FakeClock
   545  }
   546  
   547  func newPLMFixture(t testing.TB) *plmFixture {
   548  	kClient := k8s.NewFakeK8sClient(t)
   549  
   550  	out := bufsync.NewThreadSafeBuffer()
   551  	ctx, cancel := context.WithCancel(context.Background())
   552  	t.Cleanup(cancel)
   553  	ctx = logger.WithLogger(ctx, logger.NewTestLogger(out))
   554  
   555  	cfb := fake.NewControllerFixtureBuilder(t)
   556  
   557  	clock := clockwork.NewFakeClock()
   558  	st := newPLMStore(t, out)
   559  	podSource := NewPodSource(ctx, kClient, cfb.Client.Scheme(), clock)
   560  	plsc := NewController(ctx, cfb.Client, cfb.Scheme(), st, kClient, podSource, clock)
   561  
   562  	return &plmFixture{
   563  		t:                 t,
   564  		ControllerFixture: cfb.WithRequeuer(plsc.podSource).Build(plsc),
   565  		kClient:           kClient,
   566  		plsc:              plsc,
   567  		ctx:               ctx,
   568  		out:               out,
   569  		store:             st,
   570  		clock:             clock,
   571  	}
   572  }
   573  
   574  func (f *plmFixture) triggerPodEvent(podID k8s.PodID) {
   575  	podNN := types.NamespacedName{Name: string(podID), Namespace: "default"}
   576  	reqs := f.plsc.podSource.indexer.EnqueueKey(indexer.Key{Name: podNN, GVK: podGVK})
   577  	for _, req := range reqs {
   578  		_, err := f.plsc.Reconcile(f.ctx, req)
   579  		assert.NoError(f.t, err)
   580  	}
   581  }
   582  
   583  func (f *plmFixture) ConsumeLogActionsUntil(expected string) {
   584  	start := time.Now()
   585  	for time.Since(start) < time.Second {
   586  		f.store.RLockState()
   587  		done := strings.Contains(f.out.String(), expected)
   588  		f.store.RUnlockState()
   589  
   590  		if done {
   591  			return
   592  		}
   593  
   594  		time.Sleep(10 * time.Millisecond)
   595  	}
   596  
   597  	f.t.Fatalf("Timeout. Collected output: %s", f.out.String())
   598  }
   599  
   600  func (f *plmFixture) AssertOutputContains(s string) {
   601  	f.t.Helper()
   602  	f.out.AssertEventuallyContains(f.t, s, time.Second)
   603  }
   604  
   605  func (f *plmFixture) AssertOutputDoesNotContain(s string) {
   606  	time.Sleep(10 * time.Millisecond)
   607  	assert.NotContains(f.t, f.out.String(), s)
   608  }
   609  
   610  func (f *plmFixture) AssertLogStartTime(t time.Time) {
   611  	f.t.Helper()
   612  
   613  	// Truncate the time to match the behavior of metav1.Time
   614  	timecmp.AssertTimeEqual(f.t, t.Truncate(time.Second), f.kClient.LastPodLogStartTime)
   615  }
   616  
   617  type podBuilder v1.Pod
   618  
   619  func newPodBuilder(id k8s.PodID) *podBuilder {
   620  	return (*podBuilder)(&v1.Pod{
   621  		ObjectMeta: metav1.ObjectMeta{
   622  			Name:      string(id),
   623  			Namespace: "default",
   624  		},
   625  	})
   626  }
   627  
   628  func (pb *podBuilder) addDeletionTimestamp() *podBuilder {
   629  	now := metav1.Now()
   630  	pb.ObjectMeta.DeletionTimestamp = &now
   631  	return pb
   632  }
   633  
   634  func (pb *podBuilder) addRunningContainer(name container.Name, id container.ID) *podBuilder {
   635  	pb.Spec.Containers = append(pb.Spec.Containers, v1.Container{
   636  		Name: string(name),
   637  	})
   638  	pb.Status.ContainerStatuses = append(pb.Status.ContainerStatuses, v1.ContainerStatus{
   639  		Name:        string(name),
   640  		ContainerID: fmt.Sprintf("containerd://%s", id),
   641  		Image:       fmt.Sprintf("image-%s", strings.ToLower(string(name))),
   642  		ImageID:     fmt.Sprintf("image-%s", strings.ToLower(string(name))),
   643  		Ready:       true,
   644  		State: v1.ContainerState{
   645  			Running: &v1.ContainerStateRunning{
   646  				StartedAt: metav1.Now(),
   647  			},
   648  		},
   649  	})
   650  	return pb
   651  }
   652  
   653  func (pb *podBuilder) addRunningInitContainer(name container.Name, id container.ID) *podBuilder {
   654  	pb.Spec.InitContainers = append(pb.Spec.InitContainers, v1.Container{
   655  		Name: string(name),
   656  	})
   657  	pb.Status.InitContainerStatuses = append(pb.Status.InitContainerStatuses, v1.ContainerStatus{
   658  		Name:        string(name),
   659  		ContainerID: fmt.Sprintf("containerd://%s", id),
   660  		Image:       fmt.Sprintf("image-%s", strings.ToLower(string(name))),
   661  		ImageID:     fmt.Sprintf("image-%s", strings.ToLower(string(name))),
   662  		Ready:       true,
   663  		State: v1.ContainerState{
   664  			Running: &v1.ContainerStateRunning{
   665  				StartedAt: metav1.Now(),
   666  			},
   667  		},
   668  	})
   669  	return pb
   670  }
   671  
   672  func (pb *podBuilder) addTerminatedContainer(name container.Name, id container.ID) *podBuilder {
   673  	pb.addRunningContainer(name, id)
   674  	statuses := pb.Status.ContainerStatuses
   675  	statuses[len(statuses)-1].State.Running = nil
   676  	statuses[len(statuses)-1].State.Terminated = &v1.ContainerStateTerminated{
   677  		StartedAt: metav1.Now(),
   678  	}
   679  	return pb
   680  }
   681  
   682  func (pb *podBuilder) addTerminatedInitContainer(name container.Name, id container.ID) *podBuilder {
   683  	pb.addRunningInitContainer(name, id)
   684  	statuses := pb.Status.InitContainerStatuses
   685  	statuses[len(statuses)-1].State.Running = nil
   686  	statuses[len(statuses)-1].State.Terminated = &v1.ContainerStateTerminated{
   687  		StartedAt: metav1.Now(),
   688  	}
   689  	return pb
   690  }
   691  
   692  func (pb *podBuilder) toPod() *v1.Pod {
   693  	return (*v1.Pod)(pb)
   694  }
   695  
   696  func plsFromPod(mn model.ManifestName, pb *podBuilder, start time.Time) *v1alpha1.PodLogStream {
   697  	var sinceTime *metav1.Time
   698  	if !start.IsZero() {
   699  		t := apis.NewTime(start)
   700  		sinceTime = &t
   701  	}
   702  	return &v1alpha1.PodLogStream{
   703  		ObjectMeta: metav1.ObjectMeta{
   704  			Name: fmt.Sprintf("%s-%s", pb.Namespace, pb.Name),
   705  			Annotations: map[string]string{
   706  				v1alpha1.AnnotationManifest: string(mn),
   707  				v1alpha1.AnnotationSpanID:   string(k8sconv.SpanIDForPod(mn, k8s.PodID(pb.Name))),
   708  			},
   709  		},
   710  		Spec: PodLogStreamSpec{
   711  			Namespace: pb.Namespace,
   712  			Pod:       pb.Name,
   713  			SinceTime: sinceTime,
   714  		},
   715  	}
   716  }