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  }