package routing import ( "context" "errors" "strings" "testing" "time" "github.com/go-logr/logr" "github.com/kedacore/http-add-on/pkg/k8s" "github.com/kedacore/http-add-on/pkg/queue" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/fake" clgotesting "k8s.io/client-go/testing" ) // fake adapters for the k8s.GetterWatcher interface. // // Note that there is another way to fake the k8s getter and // watcher types. // // we could use the "fake" package in k8s.io/client-go // (https://pkg.go.dev/k8s.io/client-go@v0.22.0/kubernetes/fake) // instead of creating and using these structs, but doing so // requires internal knowledge of several layers of the client-go // module, since it's not well documented (even if it were, // you would need to touch a few different packages to get it // working). // // I've (arschles) chosen to create these structs and sidestep // the entire process, since this approach is explicit and only // requires knowledge of the k8s.GetterWatcher interface in this // codebase, the standard k8s/client-go package (which you // already need to know to understand this codebase), and the // fake watcher, which you would need to understand using either // approach. The fake watcher documentation is linked below: // // (https://pkg.go.dev/k8s.io/apimachinery@v0.21.3/pkg/watch#NewFake), type fakeConfigMapWatcher struct { watchIface watch.Interface } func (c fakeConfigMapWatcher) Watch( ctx context.Context, opts metav1.ListOptions, ) (watch.Interface, error) { return c.watchIface, nil } func TestStartUpdateLoop(t *testing.T) { r := require.New(t) a := assert.New(t) lggr := logr.Discard() ctx, done := context.WithCancel(context.Background()) // ensure that we call done so that we clean // up running test resources like the update loop, etc... defer done() const ( interval = 10 * time.Millisecond ns = "testns" ) q := queue.NewFakeCounter() table := NewTable() table.AddTarget("host1", NewTarget( "svc1", 8080, "depl1", 100, )) cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: ConfigMapRoutingTableName, Namespace: ns, }, Data: map[string]string{}, } r.NoError(SaveTableToConfigMap(table, cm)) fakeGetter := fake.NewSimpleClientset(cm) configMapInformer := k8s.NewInformerConfigMapUpdater( lggr, fakeGetter, time.Second*1, ) grp, ctx := errgroup.WithContext(ctx) grp.Go(func() error { err := StartConfigMapRoutingTableUpdater( ctx, lggr, configMapInformer, ns, table, q, nil, ) // we purposefully cancel the context below, // so we need to ignore that error. if !errors.Is(err, context.Canceled) { return err } return nil }) // send a watch event in parallel. we'll ensure that it // made it through in the below loop grp.Go(func() error { if _, err := fakeGetter. CoreV1(). ConfigMaps(ns). Create(ctx, cm, metav1.CreateOptions{}); err != nil && strings.Contains( err.Error(), "already exists", ) { if err := fakeGetter. CoreV1(). ConfigMaps(ns). Delete(ctx, cm.Name, metav1.DeleteOptions{}); err != nil { return err } if _, err := fakeGetter. CoreV1(). ConfigMaps(ns). Create(ctx, cm, metav1.CreateOptions{}); err != nil { return err } } return nil }) cmGetActions := []clgotesting.Action{} otherGetActions := []clgotesting.Action{} const waitDur = interval * 5 time.Sleep(waitDur) _, err := fakeGetter. CoreV1(). ConfigMaps(ns). Get(ctx, ConfigMapRoutingTableName, metav1.GetOptions{}) r.NoError(err) for _, action := range fakeGetter.Actions() { verb := action.GetVerb() resource := action.GetResource().Resource // record, then ignore all actions that were not for // ConfigMaps. // the loop should not do anything with other resources if resource != "configmaps" { otherGetActions = append(otherGetActions, action) continue } else if verb == "get" { cmGetActions = append(cmGetActions, action) } } // assert (don't require) these conditions so that // we can check them, fail if necessary, but continue onward // to check the result of the error group afterward a.Equal( 0, len(otherGetActions), "unexpected actions on non-ConfigMap resources: %s", otherGetActions, ) a.Greater( len(cmGetActions), 0, "no get actions for ConfigMaps", ) done() // if this test returns without timing out, // then we can be sure that the fakeWatcher was // able to send a watch event. if that times out // or otherwise fails, the update loop was not properly // listening for these events. r.NoError(grp.Wait()) // ensure that the queue and table host lists matches // exactly table.l.RLock() curTable := table.m curQCounts, err := q.Current() r.NoError(err) // check that the queue has every host in the table for tableHost := range curTable { _, ok := curQCounts.Counts[tableHost] r.True( ok, "host %s not found in queue", tableHost, ) } // check that the table has every host in the queue for qHost := range curQCounts.Counts { _, ok := curTable[qHost] r.True( ok, "host %s not found in table", qHost, ) } }