github.com/grahambrereton-form3/tilt@v0.10.18/internal/k8s/watch_test.go (about) 1 package k8s 2 3 import ( 4 "context" 5 "net/http" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 appsv1 "k8s.io/api/apps/v1" 11 v1 "k8s.io/api/core/v1" 12 apiErrors "k8s.io/apimachinery/pkg/api/errors" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/apimachinery/pkg/fields" 15 "k8s.io/apimachinery/pkg/labels" 16 "k8s.io/apimachinery/pkg/runtime" 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 "k8s.io/apimachinery/pkg/watch" 19 dfake "k8s.io/client-go/dynamic/fake" 20 kfake "k8s.io/client-go/kubernetes/fake" 21 "k8s.io/client-go/kubernetes/scheme" 22 ktesting "k8s.io/client-go/testing" 23 24 "github.com/windmilleng/tilt/internal/testutils" 25 ) 26 27 func TestK8sClient_WatchPods(t *testing.T) { 28 tf := newWatchTestFixture(t) 29 defer tf.TearDown() 30 31 pod1 := fakePod(PodID("abcd"), "efgh") 32 pod2 := fakePod(PodID("1234"), "hieruyge") 33 pod3 := fakePod(PodID("754"), "efgh") 34 pods := []runtime.Object{pod1, pod2, pod3} 35 tf.runPods(pods, pods) 36 } 37 38 func TestK8sClient_WatchPodsFilterNonPods(t *testing.T) { 39 tf := newWatchTestFixture(t) 40 defer tf.TearDown() 41 42 pod := fakePod(PodID("abcd"), "efgh") 43 pods := []runtime.Object{pod} 44 45 deployment := &appsv1.Deployment{} 46 input := []runtime.Object{deployment, pod} 47 tf.runPods(input, pods) 48 } 49 50 func TestK8sClient_WatchPodsLabelsPassed(t *testing.T) { 51 tf := newWatchTestFixture(t) 52 defer tf.TearDown() 53 54 ls := labels.Set{"foo": "bar", "baz": "quu"} 55 tf.testPodLabels(ls, ls) 56 } 57 58 func TestK8sClient_WatchServices(t *testing.T) { 59 tf := newWatchTestFixture(t) 60 defer tf.TearDown() 61 62 svc1 := fakeService("svc1") 63 svc2 := fakeService("svc2") 64 svc3 := fakeService("svc3") 65 svcs := []runtime.Object{svc1, svc2, svc3} 66 tf.runServices(svcs, svcs) 67 } 68 69 func TestK8sClient_WatchServicesFilterNonServices(t *testing.T) { 70 tf := newWatchTestFixture(t) 71 defer tf.TearDown() 72 73 svc := fakeService("svc1") 74 svcs := []runtime.Object{svc} 75 76 deployment := &appsv1.Deployment{} 77 input := []runtime.Object{deployment, svc} 78 tf.runServices(input, svcs) 79 } 80 81 func TestK8sClient_WatchServicesLabelsPassed(t *testing.T) { 82 tf := newWatchTestFixture(t) 83 defer tf.TearDown() 84 85 lps := labels.Set{"foo": "bar", "baz": "quu"} 86 tf.testServiceLabels(lps, lps) 87 } 88 89 func TestK8sClient_WatchPodsError(t *testing.T) { 90 tf := newWatchTestFixture(t) 91 defer tf.TearDown() 92 93 tf.watchErr = newForbiddenError() 94 _, err := tf.kCli.WatchPods(tf.ctx, labels.Everything()) 95 if assert.Error(t, err) { 96 assert.Contains(t, err.Error(), "Forbidden") 97 } 98 } 99 100 func TestK8sClient_WatchPodsWithNamespaceRestriction(t *testing.T) { 101 tf := newWatchTestFixture(t) 102 defer tf.TearDown() 103 104 tf.nsRestriction = "sandbox" 105 tf.kCli.configNamespace = "sandbox" 106 107 pod1 := fakePod(PodID("pod1"), "image1") 108 pod1.Namespace = "sandbox" 109 110 input := []runtime.Object{pod1} 111 expected := []runtime.Object{pod1} 112 tf.runPods(input, expected) 113 } 114 115 func TestK8sClient_WatchPodsBlockedByNamespaceRestriction(t *testing.T) { 116 tf := newWatchTestFixture(t) 117 defer tf.TearDown() 118 119 tf.nsRestriction = "sandbox" 120 tf.kCli.configNamespace = "" 121 122 _, err := tf.kCli.WatchPods(tf.ctx, labels.Everything()) 123 if assert.Error(t, err) { 124 assert.Contains(t, err.Error(), "Code: 403") 125 } 126 } 127 128 func TestK8sClient_WatchServicesWithNamespaceRestriction(t *testing.T) { 129 tf := newWatchTestFixture(t) 130 defer tf.TearDown() 131 132 tf.nsRestriction = "sandbox" 133 tf.kCli.configNamespace = "sandbox" 134 135 svc1 := fakeService("svc1") 136 svc1.Namespace = "sandbox" 137 138 input := []runtime.Object{svc1} 139 expected := []runtime.Object{svc1} 140 tf.runServices(input, expected) 141 } 142 143 func TestK8sClient_WatchServicesBlockedByNamespaceRestriction(t *testing.T) { 144 tf := newWatchTestFixture(t) 145 defer tf.TearDown() 146 147 tf.nsRestriction = "sandbox" 148 tf.kCli.configNamespace = "" 149 150 _, err := tf.kCli.WatchServices(tf.ctx, labels.Everything()) 151 if assert.Error(t, err) { 152 assert.Contains(t, err.Error(), "Code: 403") 153 } 154 } 155 156 func TestK8sClient_WatchEvents(t *testing.T) { 157 tf := newWatchTestFixture(t) 158 defer tf.TearDown() 159 160 event1 := fakeEvent("event1", "hello1", 1) 161 event2 := fakeEvent("event2", "hello2", 2) 162 event3 := fakeEvent("event3", "hello3", 3) 163 events := []runtime.Object{event1, event2, event3} 164 tf.runEvents(events, events) 165 } 166 167 func TestK8sClient_WatchEventsNamespaced(t *testing.T) { 168 tf := newWatchTestFixture(t) 169 defer tf.TearDown() 170 171 tf.kCli.configNamespace = "default" 172 173 event1 := fakeEvent("event1", "hello1", 1) 174 event1.Namespace = "sandbox" 175 176 events := []runtime.Object{event1} 177 tf.runEvents(events, events) 178 } 179 180 func TestK8sClient_WatchEventsUpdate(t *testing.T) { 181 tf := newWatchTestFixture(t) 182 defer tf.TearDown() 183 184 event1 := fakeEvent("event1", "hello1", 1) 185 event2 := fakeEvent("event2", "hello2", 1) 186 event1b := fakeEvent("event1", "hello1", 1) 187 event3 := fakeEvent("event3", "hello3", 1) 188 event2b := fakeEvent("event2", "hello2", 2) 189 190 ch := tf.watchEvents() 191 192 gvr := schema.GroupVersionResource{Version: "v1", Resource: "events"} 193 tf.addObjects(event1, event2) 194 tf.assertEvents([]runtime.Object{event1, event2}, ch) 195 196 tf.tracker.Update(gvr, event1b, "") 197 tf.assertEvents([]runtime.Object{}, ch) 198 199 tf.tracker.Add(event3) 200 tf.tracker.Update(gvr, event2b, "") 201 tf.assertEvents([]runtime.Object{event3, event2b}, ch) 202 } 203 204 func TestWatchPodsAfterAdding(t *testing.T) { 205 tf := newWatchTestFixture(t) 206 defer tf.TearDown() 207 208 pod1 := fakePod(PodID("abcd"), "efgh") 209 tf.addObjects(pod1) 210 ch := tf.watchPods() 211 tf.assertPods([]runtime.Object{pod1}, ch) 212 } 213 214 func TestWatchServicesAfterAdding(t *testing.T) { 215 tf := newWatchTestFixture(t) 216 defer tf.TearDown() 217 218 svc := fakeService("svc1") 219 tf.addObjects(svc) 220 ch := tf.watchServices() 221 tf.assertServices([]runtime.Object{svc}, ch) 222 } 223 224 func TestWatchEventsAfterAdding(t *testing.T) { 225 tf := newWatchTestFixture(t) 226 defer tf.TearDown() 227 228 event := fakeEvent("event1", "hello1", 1) 229 tf.addObjects(event) 230 ch := tf.watchEvents() 231 tf.assertEvents([]runtime.Object{event}, ch) 232 } 233 234 type watchTestFixture struct { 235 t *testing.T 236 kCli K8sClient 237 238 tracker ktesting.ObjectTracker 239 watchRestrictions ktesting.WatchRestrictions 240 ctx context.Context 241 watchErr error 242 nsRestriction Namespace 243 cancel context.CancelFunc 244 } 245 246 func newWatchTestFixture(t *testing.T) *watchTestFixture { 247 ret := &watchTestFixture{t: t} 248 249 ctx, _, _ := testutils.CtxAndAnalyticsForTest() 250 ret.ctx, ret.cancel = context.WithCancel(ctx) 251 252 tracker := ktesting.NewObjectTracker(scheme.Scheme, scheme.Codecs.UniversalDecoder()) 253 ret.tracker = tracker 254 255 wr := func(action ktesting.Action) (handled bool, wi watch.Interface, err error) { 256 wa := action.(ktesting.WatchAction) 257 nsRestriction := ret.nsRestriction 258 if !nsRestriction.Empty() && wa.GetNamespace() != nsRestriction.String() { 259 return true, nil, &apiErrors.StatusError{ 260 ErrStatus: metav1.Status{Code: http.StatusForbidden}, 261 } 262 } 263 264 ret.watchRestrictions = wa.GetWatchRestrictions() 265 if ret.watchErr != nil { 266 return true, nil, ret.watchErr 267 } 268 269 // Fake watcher implementation based on objects added to the tracker. 270 gvr := action.GetResource() 271 ns := action.GetNamespace() 272 watch, err := tracker.Watch(gvr, ns) 273 if err != nil { 274 return false, nil, err 275 } 276 277 return true, watch, nil 278 } 279 280 cs := kfake.NewSimpleClientset() 281 cs.PrependReactor("*", "*", ktesting.ObjectReaction(tracker)) 282 cs.PrependWatchReactor("*", wr) 283 284 dcs := dfake.NewSimpleDynamicClient(scheme.Scheme) 285 dcs.PrependReactor("*", "*", ktesting.ObjectReaction(tracker)) 286 dcs.PrependWatchReactor("*", wr) 287 288 ret.kCli = K8sClient{ 289 env: EnvUnknown, 290 dynamic: dcs, 291 clientset: cs, 292 core: cs.CoreV1(), 293 } 294 295 return ret 296 } 297 298 func (tf *watchTestFixture) TearDown() { 299 tf.cancel() 300 } 301 302 func (tf *watchTestFixture) watchPods() <-chan *v1.Pod { 303 ch, err := tf.kCli.WatchPods(tf.ctx, labels.Everything()) 304 if err != nil { 305 tf.t.Fatalf("watchPods: %v", err) 306 } 307 return ch 308 } 309 310 func (tf *watchTestFixture) watchServices() <-chan *v1.Service { 311 ch, err := tf.kCli.WatchServices(tf.ctx, labels.Everything()) 312 if err != nil { 313 tf.t.Fatalf("watchServices: %v", err) 314 } 315 return ch 316 } 317 318 func (tf *watchTestFixture) watchEvents() <-chan *v1.Event { 319 ch, err := tf.kCli.WatchEvents(tf.ctx) 320 if err != nil { 321 tf.t.Fatalf("watchEvents: %v", err) 322 } 323 return ch 324 } 325 326 func (tf *watchTestFixture) addObjects(inputs ...runtime.Object) { 327 for _, o := range inputs { 328 err := tf.tracker.Add(o) 329 if err != nil { 330 tf.t.Fatalf("addObjects: %v", err) 331 } 332 } 333 } 334 335 func (tf *watchTestFixture) runPods(input []runtime.Object, expected []runtime.Object) { 336 ch := tf.watchPods() 337 tf.addObjects(input...) 338 tf.assertPods(expected, ch) 339 } 340 341 func (tf *watchTestFixture) assertPods(expectedOutput []runtime.Object, ch <-chan *v1.Pod) { 342 var observedPods []runtime.Object 343 344 done := false 345 for !done { 346 select { 347 case pod, ok := <-ch: 348 if !ok { 349 done = true 350 } else { 351 observedPods = append(observedPods, pod) 352 } 353 case <-time.After(10 * time.Millisecond): 354 // if we haven't seen any events for 10ms, assume we're done 355 done = true 356 } 357 } 358 359 assert.Equal(tf.t, expectedOutput, observedPods) 360 } 361 362 func (tf *watchTestFixture) runServices(input []runtime.Object, expected []runtime.Object) { 363 ch := tf.watchServices() 364 tf.addObjects(input...) 365 tf.assertServices(expected, ch) 366 } 367 368 func (tf *watchTestFixture) assertServices(expectedOutput []runtime.Object, ch <-chan *v1.Service) { 369 var observedServices []runtime.Object 370 371 done := false 372 for !done { 373 select { 374 case pod, ok := <-ch: 375 if !ok { 376 done = true 377 } else { 378 observedServices = append(observedServices, pod) 379 } 380 case <-time.After(10 * time.Millisecond): 381 // if we haven't seen any events for 10ms, assume we're done 382 done = true 383 } 384 } 385 386 assert.Equal(tf.t, expectedOutput, observedServices) 387 } 388 389 func (tf *watchTestFixture) runEvents(input []runtime.Object, expectedOutput []runtime.Object) { 390 ch := tf.watchEvents() 391 tf.addObjects(input...) 392 tf.assertEvents(expectedOutput, ch) 393 } 394 395 func (tf *watchTestFixture) assertEvents(expectedOutput []runtime.Object, ch <-chan *v1.Event) { 396 var observedEvents []runtime.Object 397 398 done := false 399 for !done { 400 select { 401 case event, ok := <-ch: 402 if !ok { 403 done = true 404 } else { 405 observedEvents = append(observedEvents, event) 406 } 407 case <-time.After(10 * time.Millisecond): 408 // if we haven't seen any events for 10ms, assume we're done 409 // ideally we'd always be exiting from ch closing, but it's not currently clear how to do that via informer 410 done = true 411 } 412 } 413 414 // TODO(matt) - using ElementsMatch instead of Equal because, for some reason, events do not always come out in the 415 // same order we feed them in. I'm punting on figuring out why for now. 416 assert.ElementsMatch(tf.t, expectedOutput, observedEvents) 417 } 418 419 func (tf *watchTestFixture) testPodLabels(input labels.Set, expectedLabels labels.Set) { 420 _, err := tf.kCli.WatchPods(tf.ctx, input.AsSelector()) 421 if !assert.NoError(tf.t, err) { 422 return 423 } 424 425 assert.Equal(tf.t, expectedLabels.String(), input.String()) 426 } 427 428 func (tf *watchTestFixture) testServiceLabels(input labels.Set, expectedLabels labels.Set) { 429 _, err := tf.kCli.WatchServices(tf.ctx, input.AsSelector()) 430 if !assert.NoError(tf.t, err) { 431 return 432 } 433 434 assert.Equal(tf.t, fields.Everything(), tf.watchRestrictions.Fields) 435 436 expectedLabelSelector := expectedLabels.AsSelector() 437 assert.Equal(tf.t, expectedLabelSelector, tf.watchRestrictions.Labels) 438 }