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

     1  package k8swatch
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"k8s.io/apimachinery/pkg/types"
    11  
    12  	"github.com/tilt-dev/tilt/internal/controllers/apis/cluster"
    13  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    14  	"github.com/tilt-dev/tilt/pkg/apis"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  
    17  	"github.com/jonboulle/clockwork"
    18  	"github.com/pkg/errors"
    19  	"github.com/stretchr/testify/assert"
    20  	v1 "k8s.io/api/core/v1"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  
    23  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    24  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    25  	"github.com/tilt-dev/tilt/internal/testutils"
    26  	"github.com/tilt-dev/tilt/internal/testutils/manifestbuilder"
    27  	"github.com/tilt-dev/tilt/internal/testutils/podbuilder"
    28  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    29  
    30  	"github.com/tilt-dev/tilt/internal/k8s"
    31  	"github.com/tilt-dev/tilt/internal/store"
    32  	"github.com/tilt-dev/tilt/pkg/model"
    33  )
    34  
    35  func TestEventWatchManager_dispatchesEvent(t *testing.T) {
    36  	f := newEWMFixture(t)
    37  
    38  	mn := model.ManifestName("someK8sManifest")
    39  
    40  	// Seed the k8s client with a pod and its owner tree
    41  	manifest := f.addManifest(mn)
    42  	pb := podbuilder.New(t, manifest)
    43  	entities := pb.ObjectTreeEntities()
    44  	f.addDeployedEntity(manifest, entities.Deployment())
    45  	f.kClient.Inject(entities...)
    46  
    47  	evt := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
    48  
    49  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
    50  	f.kClient.UpsertEvent(evt)
    51  	expected := store.K8sEventAction{Event: evt, ManifestName: mn}
    52  	f.assertActions(expected)
    53  }
    54  
    55  func TestEventWatchManager_dispatchesNamespaceEvent(t *testing.T) {
    56  	f := newEWMFixture(t)
    57  
    58  	mn := model.ManifestName("someK8sManifest")
    59  
    60  	// Seed the k8s client with a pod and its owner tree
    61  	manifest := f.addManifest(mn)
    62  	pb := podbuilder.New(t, manifest)
    63  	entities := pb.ObjectTreeEntities()
    64  	f.addDeployedEntity(manifest, entities.Deployment())
    65  	f.kClient.Inject(entities...)
    66  
    67  	evt1 := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
    68  	evt1.ObjectMeta.Namespace = "kube-system"
    69  
    70  	evt2 := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
    71  
    72  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
    73  	f.kClient.UpsertEvent(evt1)
    74  	f.kClient.UpsertEvent(evt2)
    75  
    76  	expected := store.K8sEventAction{Event: evt2, ManifestName: mn}
    77  	f.assertActions(expected)
    78  }
    79  
    80  func TestEventWatchManager_duplicateDeployIDs(t *testing.T) {
    81  	f := newEWMFixture(t)
    82  
    83  	fe1 := model.ManifestName("fe1")
    84  	m1 := f.addManifest(fe1)
    85  	fe2 := model.ManifestName("fe2")
    86  	m2 := f.addManifest(fe2)
    87  
    88  	// Seed the k8s client with a pod and its owner tree
    89  	pb := podbuilder.New(t, m1)
    90  	entities := pb.ObjectTreeEntities()
    91  	f.addDeployedEntity(m1, entities.Deployment())
    92  	f.addDeployedEntity(m2, entities.Deployment())
    93  	f.kClient.Inject(entities...)
    94  
    95  	evt := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
    96  
    97  	f.kClient.UpsertEvent(evt)
    98  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
    99  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   100  	expected := store.K8sEventAction{Event: evt, ManifestName: fe1}
   101  	f.assertActions(expected)
   102  }
   103  
   104  type eventTestCase struct {
   105  	Reason   string
   106  	Type     string
   107  	Expected bool
   108  }
   109  
   110  func TestEventWatchManagerDifferentEvents(t *testing.T) {
   111  	cases := []eventTestCase{
   112  		eventTestCase{Reason: "Bumble", Type: v1.EventTypeNormal, Expected: false},
   113  		eventTestCase{Reason: "Bumble", Type: v1.EventTypeWarning, Expected: true},
   114  		eventTestCase{Reason: ImagePulledReason, Type: v1.EventTypeNormal, Expected: true},
   115  		eventTestCase{Reason: ImagePullingReason, Type: v1.EventTypeNormal, Expected: true},
   116  	}
   117  
   118  	for i, c := range cases {
   119  		t.Run(fmt.Sprintf("Case%d", i), func(t *testing.T) {
   120  			f := newEWMFixture(t)
   121  
   122  			mn := model.ManifestName("someK8sManifest")
   123  
   124  			// Seed the k8s client with a pod and its owner tree
   125  			manifest := f.addManifest(mn)
   126  			pb := podbuilder.New(t, manifest)
   127  			entities := pb.ObjectTreeEntities()
   128  			f.addDeployedEntity(manifest, entities.Deployment())
   129  			f.kClient.Inject(entities...)
   130  
   131  			evt := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
   132  			evt.Reason = c.Reason
   133  			evt.Type = c.Type
   134  
   135  			require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   136  			f.kClient.UpsertEvent(evt)
   137  			if c.Expected {
   138  				expected := store.K8sEventAction{Event: evt, ManifestName: mn}
   139  				f.assertActions(expected)
   140  			} else {
   141  				f.assertNoActions()
   142  			}
   143  		})
   144  	}
   145  }
   146  
   147  func TestEventWatchManager_listensOnce(t *testing.T) {
   148  	f := newEWMFixture(t)
   149  
   150  	m := f.addManifest("fe")
   151  	entities := podbuilder.New(t, m).ObjectTreeEntities()
   152  	f.addDeployedEntity(m, entities.Deployment())
   153  	f.kClient.Inject(entities...)
   154  
   155  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   156  
   157  	f.kClient.EventsWatchErr = fmt.Errorf("Multiple watches forbidden")
   158  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   159  
   160  	f.assertNoActions()
   161  }
   162  
   163  func TestEventWatchManager_watchError(t *testing.T) {
   164  	f := newEWMFixture(t)
   165  
   166  	err := fmt.Errorf("oh noes")
   167  	f.kClient.EventsWatchErr = err
   168  
   169  	m := f.addManifest("someK8sManifest")
   170  	entities := podbuilder.New(t, m).ObjectTreeEntities()
   171  	f.addDeployedEntity(m, entities.Deployment())
   172  	f.kClient.Inject(entities...)
   173  
   174  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   175  
   176  	expectedErr := errors.Wrap(err, "Error watching events. Are you connected to kubernetes?\nTry running `kubectl get events -n \"default\"`")
   177  	expected := store.ErrorAction{Error: expectedErr}
   178  	f.assertActions(expected)
   179  	f.store.ClearActions()
   180  }
   181  
   182  func TestEventWatchManager_eventBeforeUID(t *testing.T) {
   183  	f := newEWMFixture(t)
   184  
   185  	mn := model.ManifestName("someK8sManifest")
   186  
   187  	// Seed the k8s client with a pod and its owner tree
   188  	manifest := f.addManifest(mn)
   189  	require.NoError(t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   190  
   191  	pb := podbuilder.New(t, manifest)
   192  	entities := pb.ObjectTreeEntities()
   193  	f.kClient.Inject(entities...)
   194  
   195  	evt := f.makeEvent(k8s.NewK8sEntity(pb.Build()))
   196  
   197  	// The UIDs haven't shown up in the engine state yet, so
   198  	// we shouldn't emit the events.
   199  	f.kClient.UpsertEvent(evt)
   200  	f.assertNoActions()
   201  
   202  	// When the UIDs of the deployed objects show up, then
   203  	// we need to go back and emit the events we saw earlier.
   204  	f.addDeployedEntity(manifest, entities.Deployment())
   205  	expected := store.K8sEventAction{Event: evt, ManifestName: mn}
   206  	f.assertActions(expected)
   207  }
   208  
   209  func TestEventWatchManager_ignoresPreStartEvents(t *testing.T) {
   210  	f := newEWMFixture(t)
   211  
   212  	mn := model.ManifestName("someK8sManifest")
   213  
   214  	// Seed the k8s client with a pod and its owner tree
   215  	manifest := f.addManifest(mn)
   216  	pb := podbuilder.New(t, manifest)
   217  	entities := pb.ObjectTreeEntities()
   218  	f.addDeployedEntity(manifest, entities.Deployment())
   219  	f.kClient.Inject(entities...)
   220  
   221  	entity := k8s.NewK8sEntity(pb.Build())
   222  	evt1 := f.makeEvent(entity)
   223  	evt1.CreationTimestamp = apis.NewTime(f.clock.Now().Add(-time.Minute))
   224  
   225  	f.kClient.UpsertEvent(evt1)
   226  
   227  	evt2 := f.makeEvent(entity)
   228  
   229  	f.kClient.UpsertEvent(evt2)
   230  
   231  	// first event predates tilt start time, so should be ignored
   232  	expected := store.K8sEventAction{Event: evt2, ManifestName: mn}
   233  
   234  	f.assertActions(expected)
   235  }
   236  
   237  func (f *ewmFixture) makeEvent(obj k8s.K8sEntity) *v1.Event {
   238  	return &v1.Event{
   239  		ObjectMeta: metav1.ObjectMeta{
   240  			CreationTimestamp: apis.NewTime(f.clock.Now()),
   241  			Namespace:         k8s.DefaultNamespace.String(),
   242  		},
   243  		Reason:         "test event reason",
   244  		Message:        "test event message",
   245  		InvolvedObject: v1.ObjectReference{UID: obj.UID(), Name: obj.Name()},
   246  		Type:           v1.EventTypeWarning,
   247  	}
   248  }
   249  
   250  type ewmFixture struct {
   251  	*tempdir.TempDirFixture
   252  	t       *testing.T
   253  	kClient *k8s.FakeK8sClient
   254  	ewm     *EventWatchManager
   255  	ctx     context.Context
   256  	cancel  func()
   257  	store   *store.TestingStore
   258  	clock   clockwork.FakeClock
   259  }
   260  
   261  func newEWMFixture(t *testing.T) *ewmFixture {
   262  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   263  	ctx, cancel := context.WithCancel(ctx)
   264  
   265  	clock := clockwork.NewFakeClock()
   266  	st := store.NewTestingStore()
   267  
   268  	cc := cluster.NewFakeClientProvider(t, fake.NewFakeTiltClient())
   269  	kClient := cc.EnsureDefaultK8sCluster(ctx)
   270  
   271  	ret := &ewmFixture{
   272  		TempDirFixture: tempdir.NewTempDirFixture(t),
   273  		kClient:        kClient,
   274  		ewm:            NewEventWatchManager(cc, k8s.DefaultNamespace),
   275  		ctx:            ctx,
   276  		cancel:         cancel,
   277  		t:              t,
   278  		clock:          clock,
   279  		store:          st,
   280  	}
   281  
   282  	state := ret.store.LockMutableStateForTesting()
   283  	state.TiltStartTime = clock.Now()
   284  	_, createdAt, err := cc.GetK8sClient(types.NamespacedName{Name: "default"})
   285  	require.NoError(t, err, "Failed to get default cluster client hash")
   286  	state.Clusters["default"] = &v1alpha1.Cluster{
   287  		ObjectMeta: metav1.ObjectMeta{
   288  			Name: "default",
   289  		},
   290  		Spec: v1alpha1.ClusterSpec{
   291  			Connection: &v1alpha1.ClusterConnection{
   292  				Kubernetes: &v1alpha1.KubernetesClusterConnection{},
   293  			},
   294  		},
   295  		Status: v1alpha1.ClusterStatus{
   296  			Arch:        "fake-arch",
   297  			ConnectedAt: createdAt.DeepCopy(),
   298  		},
   299  	}
   300  	ret.store.UnlockMutableState()
   301  
   302  	t.Cleanup(ret.TearDown)
   303  	return ret
   304  }
   305  
   306  func (f *ewmFixture) TearDown() {
   307  	f.cancel()
   308  	f.store.AssertNoErrorActions(f.t)
   309  }
   310  
   311  func (f *ewmFixture) addManifest(manifestName model.ManifestName) model.Manifest {
   312  	state := f.store.LockMutableStateForTesting()
   313  
   314  	m := manifestbuilder.New(f, manifestName).
   315  		WithK8sYAML(testyaml.SanchoYAML).
   316  		Build()
   317  	state.UpsertManifestTarget(store.NewManifestTarget(m))
   318  	f.store.UnlockMutableState()
   319  	return m
   320  }
   321  
   322  func (f *ewmFixture) addDeployedEntity(m model.Manifest, entity k8s.K8sEntity) {
   323  	defer func() {
   324  		require.NoError(f.t, f.ewm.OnChange(f.ctx, f.store, store.LegacyChangeSummary()))
   325  	}()
   326  
   327  	state := f.store.LockMutableStateForTesting()
   328  	defer f.store.UnlockMutableState()
   329  	mState, ok := state.ManifestState(m.Name)
   330  	if !ok {
   331  		f.t.Fatalf("Unknown manifest: %s", m.Name)
   332  	}
   333  	runtimeState := mState.K8sRuntimeState()
   334  	runtimeState.ApplyFilter = &k8sconv.KubernetesApplyFilter{
   335  		DeployedRefs: k8s.ObjRefList{entity.ToObjectReference()},
   336  	}
   337  	mState.RuntimeState = runtimeState
   338  }
   339  
   340  func (f *ewmFixture) assertNoActions() {
   341  	f.assertActions()
   342  }
   343  
   344  func (f *ewmFixture) assertActions(expected ...store.Action) {
   345  	f.t.Helper()
   346  
   347  	start := time.Now()
   348  	for time.Since(start) < time.Second {
   349  		actions := f.store.Actions()
   350  		if len(actions) >= len(expected) {
   351  			break
   352  		}
   353  	}
   354  
   355  	// Make extra sure we didn't get any extra actions
   356  	time.Sleep(10 * time.Millisecond)
   357  
   358  	// NOTE(maia): this test will break if this the code ever returns other
   359  	// correct-but-incidental-to-this-test actions, but for now it's fine.
   360  	actual := f.store.Actions()
   361  	if !assert.Len(f.t, actual, len(expected)) {
   362  		f.t.FailNow()
   363  	}
   364  
   365  	for i, a := range actual {
   366  		switch exp := expected[i].(type) {
   367  		case store.ErrorAction:
   368  			// Special case -- we can't just assert.Equal b/c pointer equality stuff
   369  			act, ok := a.(store.ErrorAction)
   370  			if !ok {
   371  				f.t.Fatalf("got non-%T: %v", store.ErrorAction{}, a)
   372  			}
   373  			assert.Equal(f.t, exp.Error.Error(), act.Error.Error())
   374  		default:
   375  			assert.Equal(f.t, expected[i], a)
   376  		}
   377  	}
   378  }