package instances import ( "context" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/cache/informertest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" "edge-infra.dev/pkg/sds/interlock/internal/config" "edge-infra.dev/pkg/sds/interlock/topic" "edge-infra.dev/pkg/sds/interlock/websocket" "edge-infra.dev/pkg/sds/lib/k8s/retryclient" ) // CreateScheme creates a new scheme, adds all types of the automatically generated // clientset, and returns it. func createScheme() *kruntime.Scheme { scheme := kruntime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) return scheme } // GetFakeKubeClient returns a fake client initialised with a slice of // Kubernets objects. To be used for testing purposes. func GetFakeKubeClient(initObjs ...client.Object) client.Client { return fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(initObjs...).Build() } // getTestPod returns a test representation of a pod. func getTestPod(name, namespace, nodeName, ip string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: corev1.PodSpec{ NodeName: nodeName, }, Status: corev1.PodStatus{ PodIP: ip, }, } } func TestIsInterlockPod(t *testing.T) { tests := map[string]struct { pod interface{} expected bool }{ "Invalid": { pod: struct{}{}, expected: false, }, "Incorrect_Prefix": { pod: getTestPod("notinterlock-hsj45", "interlock", "", ""), expected: false, }, "Incorrect_Suffix": { pod: getTestPod("interlock-hsj4", "interlock", "", ""), expected: false, }, "Incorrect_Namespace": { pod: getTestPod("interlock-hsj45", "notinterlock", "", ""), expected: false, }, "Correct": { pod: getTestPod("interlock-hsj45", "interlock", "", ""), expected: true, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { got := IsInterlockPod(tc.pod) assert.Equal(t, tc.expected, got) }) } } func TestNewState(t *testing.T) { pod1 := getTestPod("interlock-11111", "interlock", "node1", "1.1.1.1") pod2 := getTestPod("interlock-22222", "interlock", "node2", "2.2.2.2") cli := GetFakeKubeClient(pod1, pod2) cfg := &config.Config{ Fs: afero.NewMemMapFs(), KubeRetryClient: retryclient.New(cli, cli, retryclient.Config{}), Cache: &informertest.FakeInformers{}, } state, err := newState(context.Background(), cfg) require.NoError(t, err) assert.Equal(t, "http://1.1.1.1:80", state.Instances["node1"].URL) assert.Equal(t, "http://2.2.2.2:80", state.Instances["node2"].URL) } type mockCache struct { *informertest.FakeInformers } func newTestInstancesWithFakeInformer(t *testing.T, initState *State) (*Instances, *controllertest.FakeInformer) { testCfg := config.Config{ Cache: mockCache{&informertest.FakeInformers{}}, } testInstances := Instances{ topic: topic.NewTopic( TopicName, initState, nil, websocket.NewManager(), ), } err := testInstances.SetupAPIInformers(context.Background(), &testCfg) require.NoError(t, err) informer, err := testCfg.Cache.GetInformer(context.Background(), &corev1.Pod{}) require.NoError(t, err) fakeInformer, ok := informer.(*controllertest.FakeInformer) require.True(t, ok) return &testInstances, fakeInformer } func TestOnAdd(t *testing.T) { testInstances, fakeInformer := newTestInstancesWithFakeInformer(t, &State{Instances: map[string]Instance{}}) newNode := getTestPod("interlock-12345", "interlock", "node1", "1.2.3.4") fakeInformer.Add(newNode) instancesState := testInstances.topic.State() state, ok := instancesState.(*State) require.True(t, ok) assert.Equal(t, "http://1.2.3.4:80", state.Instances["node1"].URL) } func TestOnUpdate(t *testing.T) { initState := &State{ Instances: map[string]Instance{ "node1": {URL: "http://1.2.3.4:80"}, }, } testInstances, fakeInformer := newTestInstancesWithFakeInformer(t, initState) oldPod := getTestPod("interlock-12345", "interlock", "node1", "1.2.3.4") newPod := getTestPod("interlock-12345", "interlock", "node1", "4.3.2.1") fakeInformer.Update(oldPod, newPod) instancesState := testInstances.topic.State() state, ok := instancesState.(*State) require.True(t, ok) assert.Equal(t, "http://4.3.2.1:80", state.Instances["node1"].URL) } func TestOnDelete(t *testing.T) { testInstances, fakeInformer := newTestInstancesWithFakeInformer(t, &State{ Instances: map[string]Instance{ "node1": {URL: "http://1.2.3.4:80"}, }, }) pod := getTestPod("interlock-12345", "interlock", "node1", "1.2.3.4") fakeInformer.Delete(pod) instancesState := testInstances.topic.State() state, ok := instancesState.(*State) require.True(t, ok) _, ok = state.Instances["node1"] require.False(t, ok) } type testStruct struct { state *State } func (t *testStruct) updateState(fn topic.UpdateFunc) error { return fn(t.state) } func TestUpdateInstance(t *testing.T) { tests := map[string]struct { previousIP string delete bool want string }{ "NotPreviouslySet": { previousIP: "", delete: false, want: "1.2.3.4", }, "PreviouslySet": { previousIP: "1.2.3.4", delete: false, want: "1.2.3.4", }, "Delete": { previousIP: "1.2.3.4", delete: true, want: "", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { nodeName := "node1" ts := testStruct{ &State{ Instances: map[string]Instance{ nodeName: {URL: "http://" + tc.previousIP + ":80"}, }, }, } pod := getTestPod("interlock-11111", "interlock", nodeName, tc.want) updateInstance(ts.updateState, pod, tc.delete) if tc.delete { _, ok := ts.state.Instances[nodeName] require.False(t, ok) return } want := "http://" + tc.want + ":80" assert.Equal(t, want, ts.state.Instances[nodeName].URL) }) } }