github.com/tilt-dev/tilt@v0.36.0/internal/k8s/watch_test.go (about) 1 package k8s 2 3 import ( 4 "context" 5 "net/http" 6 goRuntime "runtime" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/require" 11 "go.uber.org/atomic" 12 "k8s.io/client-go/rest" 13 14 "github.com/stretchr/testify/assert" 15 appsv1 "k8s.io/api/apps/v1" 16 v1 "k8s.io/api/core/v1" 17 apiErrors "k8s.io/apimachinery/pkg/api/errors" 18 apierrors "k8s.io/apimachinery/pkg/api/errors" 19 "k8s.io/apimachinery/pkg/api/meta" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/apimachinery/pkg/runtime" 22 "k8s.io/apimachinery/pkg/runtime/schema" 23 "k8s.io/apimachinery/pkg/types" 24 "k8s.io/apimachinery/pkg/version" 25 "k8s.io/apimachinery/pkg/watch" 26 difake "k8s.io/client-go/discovery/fake" 27 dfake "k8s.io/client-go/dynamic/fake" 28 kfake "k8s.io/client-go/kubernetes/fake" 29 "k8s.io/client-go/kubernetes/scheme" 30 mfake "k8s.io/client-go/metadata/fake" 31 ktesting "k8s.io/client-go/testing" 32 33 "github.com/tilt-dev/clusterid" 34 "github.com/tilt-dev/tilt/internal/testutils" 35 ) 36 37 func TestK8sClient_WatchPods(t *testing.T) { 38 tf := newWatchTestFixture(t) 39 40 pod1 := fakePod(PodID("abcd"), "efgh") 41 pod2 := fakePod(PodID("1234"), "hieruyge") 42 pod3 := fakePod(PodID("754"), "efgh") 43 pods := []runtime.Object{pod1, pod2, pod3} 44 tf.runPods(pods, pods) 45 } 46 47 func TestPodFromInformerCacheAfterWatch(t *testing.T) { 48 tf := newWatchTestFixture(t) 49 50 pod1 := fakePod(PodID("abcd"), "efgh") 51 pods := []runtime.Object{pod1} 52 ch := tf.watchPods() 53 tf.addObjects(pods...) 54 tf.assertPods(pods, ch) 55 56 pod1Cache, err := tf.kCli.PodFromInformerCache(tf.ctx, types.NamespacedName{Name: "abcd", Namespace: "default"}) 57 require.NoError(t, err) 58 assert.Equal(t, "abcd", pod1Cache.Name) 59 60 _, err = tf.kCli.PodFromInformerCache(tf.ctx, types.NamespacedName{Name: "missing", Namespace: "default"}) 61 if assert.Error(t, err) { 62 assert.True(t, apierrors.IsNotFound(err)) 63 } 64 } 65 66 func TestPodFromInformerCacheBeforeWatch(t *testing.T) { 67 tf := newWatchTestFixture(t) 68 69 pod1 := fakePod(PodID("abcd"), "efgh") 70 pods := []runtime.Object{pod1} 71 tf.addObjects(pods...) 72 73 nn := types.NamespacedName{Name: "abcd", Namespace: "default"} 74 assert.Eventually(t, func() bool { 75 _, err := tf.kCli.PodFromInformerCache(tf.ctx, nn) 76 return err == nil 77 }, time.Second, 5*time.Millisecond) 78 79 pod1Cache, err := tf.kCli.PodFromInformerCache(tf.ctx, nn) 80 require.NoError(t, err) 81 assert.Equal(t, "abcd", pod1Cache.Name) 82 83 // This uses a pooled informer, so don't use the helper function 84 // (which waits for the informer to finish setup). 85 ch, err := tf.kCli.WatchPods(tf.ctx, Namespace(nn.Namespace)) 86 require.NoError(tf.t, err) 87 tf.assertPods(pods, ch) 88 } 89 90 func TestK8sClient_WatchPodsNamespaces(t *testing.T) { 91 tf := newWatchTestFixture(t) 92 93 pod1 := fakePod(PodID("pod1"), "pod1") 94 pod2 := fakePod(PodID("pod2-system"), "pod2-system") 95 pod2.Namespace = "kube-system" 96 pod3 := fakePod(PodID("pod3"), "pod3") 97 98 ch := tf.watchPodsNS("default") 99 tf.addObjects(pod1, pod2, pod3) 100 tf.assertPods([]runtime.Object{pod1, pod3}, ch) 101 } 102 103 func TestK8sClient_WatchPodDeletion(t *testing.T) { 104 tf := newWatchTestFixture(t) 105 106 podID := PodID("pod1") 107 pod := fakePod(podID, "image1") 108 ch := tf.watchPods() 109 tf.addObjects(pod) 110 111 select { 112 case <-time.After(time.Second): 113 t.Fatalf("Timed out waiting for pod update") 114 case obj := <-ch: 115 asPod, _ := obj.AsPod() 116 assert.Equal(t, podID, PodIDFromPod(asPod)) 117 } 118 119 err := tf.tracker.Delete(PodGVR, "default", "pod1") 120 assert.NoError(t, err) 121 122 select { 123 case <-time.After(time.Second): 124 t.Fatalf("Timed out waiting for pod delete") 125 case obj := <-ch: 126 ns, name, _ := obj.AsDeletedKey() 127 assert.Equal(t, "pod1", name) 128 assert.Equal(t, Namespace("default"), ns) 129 } 130 } 131 132 func TestK8sClient_WatchPodsFilterNonPods(t *testing.T) { 133 tf := newWatchTestFixture(t) 134 135 pod := fakePod(PodID("abcd"), "efgh") 136 pods := []runtime.Object{pod} 137 138 deployment := &appsv1.Deployment{} 139 input := []runtime.Object{deployment, pod} 140 tf.runPods(input, pods) 141 } 142 143 func TestK8sClient_WatchServices(t *testing.T) { 144 if goRuntime.GOOS == "windows" { 145 t.Skip("TODO(nick): investigate") 146 } 147 tf := newWatchTestFixture(t) 148 149 svc1 := fakeService("svc1") 150 svc2 := fakeService("svc2") 151 svc3 := fakeService("svc3") 152 svcs := []runtime.Object{svc1, svc2, svc3} 153 tf.runServices(svcs, svcs) 154 } 155 156 func TestK8sClient_WatchServicesFilterNonServices(t *testing.T) { 157 tf := newWatchTestFixture(t) 158 159 svc := fakeService("svc1") 160 svcs := []runtime.Object{svc} 161 162 deployment := &appsv1.Deployment{} 163 input := []runtime.Object{deployment, svc} 164 tf.runServices(input, svcs) 165 } 166 167 func TestK8sClient_WatchPodsError(t *testing.T) { 168 tf := newWatchTestFixture(t) 169 170 tf.watchErr = newForbiddenError() 171 _, err := tf.kCli.WatchPods(tf.ctx, "default") 172 if assert.Error(t, err) { 173 assert.Contains(t, err.Error(), "Forbidden") 174 } 175 } 176 177 func TestK8sClient_WatchPodsWithNamespaceRestriction(t *testing.T) { 178 tf := newWatchTestFixture(t) 179 180 tf.nsRestriction = "sandbox" 181 tf.kCli.configNamespace = "sandbox" 182 183 pod1 := fakePod(PodID("pod1"), "image1") 184 pod1.Namespace = "sandbox" 185 186 input := []runtime.Object{pod1} 187 expected := []runtime.Object{pod1} 188 tf.runPods(input, expected) 189 } 190 191 func TestK8sClient_WatchPodsBlockedByNamespaceRestriction(t *testing.T) { 192 tf := newWatchTestFixture(t) 193 194 tf.nsRestriction = "sandbox" 195 tf.kCli.configNamespace = "" 196 197 _, err := tf.kCli.WatchPods(tf.ctx, "default") 198 if assert.Error(t, err) { 199 assert.Contains(t, err.Error(), "Code: 403") 200 } 201 } 202 203 func TestK8sClient_WatchServicesWithNamespaceRestriction(t *testing.T) { 204 tf := newWatchTestFixture(t) 205 206 tf.nsRestriction = "sandbox" 207 tf.kCli.configNamespace = "sandbox" 208 209 svc1 := fakeService("svc1") 210 svc1.Namespace = "sandbox" 211 212 input := []runtime.Object{svc1} 213 expected := []runtime.Object{svc1} 214 tf.runServices(input, expected) 215 } 216 217 func TestK8sClient_WatchServicesBlockedByNamespaceRestriction(t *testing.T) { 218 tf := newWatchTestFixture(t) 219 220 tf.nsRestriction = "sandbox" 221 tf.kCli.configNamespace = "" 222 223 _, err := tf.kCli.WatchServices(tf.ctx, "default") 224 if assert.Error(t, err) { 225 assert.Contains(t, err.Error(), "Code: 403") 226 } 227 } 228 229 func TestK8sClient_WatchEvents(t *testing.T) { 230 tf := newWatchTestFixture(t) 231 232 event1 := fakeEvent("event1", "hello1", 1) 233 event2 := fakeEvent("event2", "hello2", 2) 234 event3 := fakeEvent("event3", "hello3", 3) 235 events := []runtime.Object{event1, event2, event3} 236 tf.runEvents(events, events) 237 } 238 239 func TestK8sClient_WatchEventsNamespaced(t *testing.T) { 240 tf := newWatchTestFixture(t) 241 242 tf.kCli.configNamespace = "sandbox" 243 244 event1 := fakeEvent("event1", "hello1", 1) 245 event1.Namespace = "sandbox" 246 247 events := []runtime.Object{event1} 248 tf.runEvents(events, events) 249 } 250 251 func TestK8sClient_WatchEventsUpdate(t *testing.T) { 252 tf := newWatchTestFixture(t) 253 254 event1 := fakeEvent("event1", "hello1", 1) 255 event2 := fakeEvent("event2", "hello2", 1) 256 event1b := fakeEvent("event1", "hello1", 1) 257 event3 := fakeEvent("event3", "hello3", 1) 258 event2b := fakeEvent("event2", "hello2", 2) 259 260 ch := tf.watchEvents() 261 262 gvr := schema.GroupVersionResource{Version: "v1", Resource: "events"} 263 tf.addObjects(event1, event2) 264 tf.assertEvents([]runtime.Object{event1, event2}, ch) 265 266 err := tf.tracker.Update(gvr, event1b, "default") 267 require.NoError(t, err) 268 tf.assertEvents([]runtime.Object{}, ch) 269 270 err = tf.tracker.Add(event3) 271 require.NoError(t, err) 272 err = tf.tracker.Update(gvr, event2b, "default") 273 require.NoError(t, err) 274 tf.assertEvents([]runtime.Object{event3, event2b}, ch) 275 } 276 277 func TestWatchPodsAfterAdding(t *testing.T) { 278 tf := newWatchTestFixture(t) 279 280 pod1 := fakePod(PodID("abcd"), "efgh") 281 tf.addObjects(pod1) 282 ch := tf.watchPods() 283 tf.assertPods([]runtime.Object{pod1}, ch) 284 } 285 286 func TestWatchServicesAfterAdding(t *testing.T) { 287 tf := newWatchTestFixture(t) 288 289 svc := fakeService("svc1") 290 tf.addObjects(svc) 291 ch := tf.watchServices() 292 tf.assertServices([]runtime.Object{svc}, ch) 293 } 294 295 func TestWatchEventsAfterAdding(t *testing.T) { 296 tf := newWatchTestFixture(t) 297 298 event := fakeEvent("event1", "hello1", 1) 299 tf.addObjects(event) 300 ch := tf.watchEvents() 301 tf.assertEvents([]runtime.Object{event}, ch) 302 } 303 304 func TestK8sClient_WatchMeta(t *testing.T) { 305 tf := newWatchTestFixture(t) 306 307 pod1 := fakePod(PodID("abcd"), "efgh") 308 pod2 := fakePod(PodID("1234"), "hieruyge") 309 ch := tf.watchMeta(schema.GroupVersionKind{Version: "v1", Kind: "Pod"}) 310 311 _, _ = tf.metadata.Resource(PodGVR).Namespace("default").(mfake.MetadataClient).CreateFake( 312 &metav1.PartialObjectMetadata{TypeMeta: pod1.TypeMeta, ObjectMeta: pod1.ObjectMeta}, 313 metav1.CreateOptions{}) 314 _, _ = tf.metadata.Resource(PodGVR).Namespace("default").(mfake.MetadataClient).CreateFake( 315 &metav1.PartialObjectMetadata{TypeMeta: pod2.TypeMeta, ObjectMeta: pod2.ObjectMeta}, 316 metav1.CreateOptions{}) 317 318 expected := []metav1.Object{&pod1.ObjectMeta, &pod2.ObjectMeta} 319 tf.assertMeta(expected, ch) 320 } 321 322 func TestK8sClient_WatchMetaBackfillK8s14(t *testing.T) { 323 tf := newWatchTestFixture(t) 324 325 tf.version.GitVersion = "v1.14.1" 326 327 pod1 := fakePod(PodID("abcd"), "efgh") 328 pod2 := fakePod(PodID("1234"), "hieruyge") 329 ch := tf.watchMeta(schema.GroupVersionKind{Version: "v1", Kind: "Pod"}) 330 331 tf.addObjects(pod1, pod2) 332 333 expected := []metav1.Object{pod1, pod2} 334 tf.assertMeta(expected, ch) 335 } 336 337 type partialMetaTestCase struct { 338 v string 339 expected bool 340 } 341 342 func TestSupportsPartialMeta(t *testing.T) { 343 cases := []partialMetaTestCase{ 344 // minikube 345 partialMetaTestCase{"v1.19.1", true}, 346 partialMetaTestCase{"v1.15.0", true}, 347 partialMetaTestCase{"v1.14.0", false}, 348 349 // gke 350 partialMetaTestCase{"v1.18.10-gke.601", true}, 351 partialMetaTestCase{"v1.15.10-gke.601", true}, 352 partialMetaTestCase{"v1.14.10-gke.601", false}, 353 354 // microk8s 355 partialMetaTestCase{"v1.19.3-34+fa32ff1c160058", true}, 356 partialMetaTestCase{"v1.15.3-34+fa32ff1c160058", true}, 357 partialMetaTestCase{"v1.14.3-34+fa32ff1c160058", false}, 358 359 partialMetaTestCase{"garbage", false}, 360 } 361 for _, c := range cases { 362 t.Run(c.v, func(t *testing.T) { 363 assert.Equal(t, c.expected, supportsPartialMetadata(&version.Info{GitVersion: c.v})) 364 }) 365 } 366 } 367 368 type fakeDiscovery struct { 369 *difake.FakeDiscovery 370 restClient rest.Interface 371 } 372 373 func (fakeDiscovery) Fresh() bool { return true } 374 func (fakeDiscovery) Invalidate() {} 375 376 func (f fakeDiscovery) RESTClient() rest.Interface { 377 return f.restClient 378 } 379 380 type watchTestFixture struct { 381 t *testing.T 382 kCli *K8sClient 383 384 numWatches atomic.Int32 385 tracker ktesting.ObjectTracker 386 watchRestrictions ktesting.WatchRestrictions 387 metadata *mfake.FakeMetadataClient 388 ctx context.Context 389 watchErr error 390 nsRestriction Namespace 391 cancel context.CancelFunc 392 version *version.Info 393 } 394 395 func newWatchTestFixture(t *testing.T) *watchTestFixture { 396 ret := &watchTestFixture{t: t} 397 t.Cleanup(ret.TearDown) 398 399 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 400 ret.ctx, ret.cancel = context.WithCancel(ctx) 401 402 tracker := ktesting.NewObjectTracker(scheme.Scheme, scheme.Codecs.UniversalDecoder()) 403 ret.tracker = tracker 404 405 wr := func(action ktesting.Action) (handled bool, wi watch.Interface, err error) { 406 wa := action.(ktesting.WatchAction) 407 nsRestriction := ret.nsRestriction 408 if !nsRestriction.Empty() && wa.GetNamespace() != nsRestriction.String() { 409 return true, nil, &apiErrors.StatusError{ 410 ErrStatus: metav1.Status{Code: http.StatusForbidden}, 411 } 412 } 413 414 ret.watchRestrictions = wa.GetWatchRestrictions() 415 if ret.watchErr != nil { 416 return true, nil, ret.watchErr 417 } 418 419 // Fake watcher implementation based on objects added to the tracker. 420 gvr := action.GetResource() 421 ns := action.GetNamespace() 422 watch, err := tracker.Watch(gvr, ns) 423 if err != nil { 424 return false, nil, err 425 } 426 427 ret.numWatches.Add(1) 428 return true, watch, nil 429 } 430 431 cs := kfake.NewSimpleClientset() 432 cs.PrependReactor("*", "*", ktesting.ObjectReaction(tracker)) 433 cs.PrependWatchReactor("*", wr) 434 435 dcs := dfake.NewSimpleDynamicClient(scheme.Scheme) 436 dcs.PrependReactor("*", "*", ktesting.ObjectReaction(tracker)) 437 dcs.PrependWatchReactor("*", wr) 438 439 mcs := mfake.NewSimpleMetadataClient(scheme.Scheme) 440 mcs.PrependReactor("*", "*", ktesting.ObjectReaction(tracker)) 441 mcs.PrependWatchReactor("*", wr) 442 ret.metadata = mcs 443 444 version := &version.Info{Major: "1", Minor: "19", GitVersion: "v1.19.1"} 445 di := fakeDiscovery{ 446 FakeDiscovery: &difake.FakeDiscovery{ 447 Fake: &ktesting.Fake{}, 448 FakedServerVersion: version, 449 }, 450 } 451 452 ret.kCli = &K8sClient{ 453 InformerSet: newInformerSet(cs, dcs), 454 product: clusterid.ProductUnknown, 455 drm: fakeRESTMapper{}, 456 dynamic: dcs, 457 clientset: cs, 458 metadata: mcs, 459 core: cs.CoreV1(), 460 discovery: di, 461 configNamespace: "default", 462 } 463 ret.version = version 464 465 return ret 466 } 467 468 func (tf *watchTestFixture) TearDown() { 469 tf.cancel() 470 } 471 472 func (tf *watchTestFixture) watchPods() <-chan ObjectUpdate { 473 return tf.watchPodsNS(tf.kCli.configNamespace) 474 } 475 476 // the fake watcher has race conditions, so wait until the shared informer 477 // sets up all its watchers 478 func (tf *watchTestFixture) waitForInformerSetup(originalWatches int32) { 479 require.Eventually(tf.t, func() bool { 480 return tf.numWatches.Load() == originalWatches+2 481 }, time.Second, time.Millisecond) 482 } 483 484 func (tf *watchTestFixture) watchPodsNS(ns Namespace) <-chan ObjectUpdate { 485 originalWatches := tf.numWatches.Load() 486 ch, err := tf.kCli.WatchPods(tf.ctx, ns) 487 require.NoError(tf.t, err) 488 tf.waitForInformerSetup(originalWatches) 489 return ch 490 } 491 492 func (tf *watchTestFixture) watchServices() <-chan *v1.Service { 493 originalWatches := tf.numWatches.Load() 494 ch, err := tf.kCli.WatchServices(tf.ctx, tf.kCli.configNamespace) 495 require.NoError(tf.t, err) 496 tf.waitForInformerSetup(originalWatches) 497 return ch 498 } 499 500 func (tf *watchTestFixture) watchEvents() <-chan *v1.Event { 501 originalWatches := tf.numWatches.Load() 502 ch, err := tf.kCli.WatchEvents(tf.ctx, tf.kCli.configNamespace) 503 require.NoError(tf.t, err) 504 tf.waitForInformerSetup(originalWatches) 505 return ch 506 } 507 508 func (tf *watchTestFixture) watchMeta(gvr schema.GroupVersionKind) <-chan metav1.Object { 509 originalWatches := tf.numWatches.Load() 510 ch, err := tf.kCli.WatchMeta(tf.ctx, gvr, tf.kCli.configNamespace) 511 require.NoError(tf.t, err) 512 require.Eventually(tf.t, func() bool { 513 return tf.numWatches.Load() == originalWatches+1 514 }, time.Second, time.Millisecond) 515 return ch 516 } 517 518 func (tf *watchTestFixture) addObjects(inputs ...runtime.Object) { 519 for _, o := range inputs { 520 err := tf.tracker.Add(o) 521 if err != nil { 522 tf.t.Fatalf("addObjects: %v", err) 523 } 524 } 525 } 526 527 func (tf *watchTestFixture) runPods(input []runtime.Object, expected []runtime.Object) { 528 ch := tf.watchPods() 529 tf.addObjects(input...) 530 tf.assertPods(expected, ch) 531 } 532 533 func take[T interface{}](ch <-chan T, expected int) []T { 534 result := []T{} 535 done := false 536 for !done { 537 wait := time.Second 538 if len(result) >= expected { 539 // No need to wait as long if we already have N objects. 540 wait = 200 * time.Millisecond 541 } 542 select { 543 case obj, ok := <-ch: 544 if !ok { 545 done = true 546 continue 547 } 548 549 result = append(result, obj) 550 case <-time.After(wait): 551 // if we haven't seen any events for 200ms, assume we're done 552 done = true 553 } 554 } 555 return result 556 } 557 558 func (tf *watchTestFixture) assertPods(expectedOutput []runtime.Object, ch <-chan ObjectUpdate) { 559 var observedPods []runtime.Object 560 updates := take(ch, len(expectedOutput)) 561 for _, update := range updates { 562 pod, ok := update.AsPod() 563 if ok { 564 observedPods = append(observedPods, pod) 565 } 566 } 567 568 // Our k8s simulation library does not guarantee event order. 569 assert.ElementsMatch(tf.t, expectedOutput, observedPods) 570 } 571 572 func (tf *watchTestFixture) runServices(input []runtime.Object, expected []runtime.Object) { 573 ch := tf.watchServices() 574 tf.addObjects(input...) 575 tf.assertServices(expected, ch) 576 } 577 578 func (tf *watchTestFixture) assertServices(expectedOutput []runtime.Object, ch <-chan *v1.Service) { 579 observedServices := take(ch, len(expectedOutput)) 580 // Our k8s simulation library does not guarantee event order. 581 assert.ElementsMatch(tf.t, expectedOutput, observedServices) 582 } 583 584 func (tf *watchTestFixture) runEvents(input []runtime.Object, expectedOutput []runtime.Object) { 585 ch := tf.watchEvents() 586 tf.addObjects(input...) 587 tf.assertEvents(expectedOutput, ch) 588 } 589 590 func (tf *watchTestFixture) assertEvents(expectedOutput []runtime.Object, ch <-chan *v1.Event) { 591 observedEvents := take(ch, len(expectedOutput)) 592 // Our k8s simulation library does not guarantee event order. 593 assert.ElementsMatch(tf.t, expectedOutput, observedEvents) 594 } 595 596 func (tf *watchTestFixture) assertMeta(expected []metav1.Object, ch <-chan metav1.Object) { 597 observed := take(ch, len(expected)) 598 // Our k8s simulation library does not guarantee event order. 599 assert.ElementsMatch(tf.t, expected, observed) 600 } 601 602 type fakeRESTMapper struct { 603 *meta.DefaultRESTMapper 604 } 605 606 func (f fakeRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { 607 return &meta.RESTMapping{ 608 Resource: PodGVR, 609 Scope: meta.RESTScopeNamespace, 610 }, nil 611 } 612 613 func (f fakeRESTMapper) Reset() { 614 }