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 }