package controller import ( "reflect" "testing" routev1 "github.com/openshift/api/route/v1" routefake "github.com/openshift/client-go/route/clientset/versioned/fake" routeinformers "github.com/openshift/client-go/route/informers/externalversions" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/diff" kubeinformers "k8s.io/client-go/informers" k8sfake "k8s.io/client-go/kubernetes/fake" core "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1alpha1" "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned/fake" informers "kubevirt.io/containerized-data-importer/pkg/client/informers/externalversions" ) type configFixture struct { t *testing.T client *fake.Clientset kubeclient *k8sfake.Clientset routeClient *routefake.Clientset // Objects to put in the store. configLister []*cdiv1.CDIConfig ingressLister []*extensionsv1beta1.Ingress routeLister []*routev1.Route // Actions expected to happen on the client. kubeactions []core.Action actions []core.Action routeactions []core.Action // Objects from here preloaded into NewSimpleFake. kubeobjects []runtime.Object objects []runtime.Object routeobjects []runtime.Object } func newConfigFixture(t *testing.T) *configFixture { f := &configFixture{} f.t = t f.objects = []runtime.Object{} f.kubeobjects = []runtime.Object{} return f } func getConfigKey(config *cdiv1.CDIConfig, t *testing.T) string { key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(config) if err != nil { t.Errorf("Unexpected error getting key for config %v: %v", config.Name, err) return "" } return key } func createRoute(name, ns, service string) *routev1.Route { route := &routev1.Route{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, }, Spec: routev1.RouteSpec{ To: routev1.RouteTargetReference{ Kind: "Service", Name: service, }, }, Status: routev1.RouteStatus{ Ingress: []routev1.RouteIngress{ {Host: "cdi-uploadproxy.example.com"}, }, }, } return route } func createIngress(name, ns, service, url string) *extensionsv1beta1.Ingress { return &extensionsv1beta1.Ingress{ TypeMeta: metav1.TypeMeta{ APIVersion: "extensions/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, }, Spec: extensionsv1beta1.IngressSpec{ Backend: &extensionsv1beta1.IngressBackend{ ServiceName: service, }, Rules: []extensionsv1beta1.IngressRule{ {Host: url}, }, }, } } func (f *configFixture) newController() (*ConfigController, informers.SharedInformerFactory, routeinformers.SharedInformerFactory, kubeinformers.SharedInformerFactory) { f.client = fake.NewSimpleClientset(f.objects...) f.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...) f.routeClient = routefake.NewSimpleClientset(f.routeobjects...) i := informers.NewSharedInformerFactory(f.client, noResyncPeriodFunc()) k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc()) r := routeinformers.NewSharedInformerFactory(f.routeClient, noResyncPeriodFunc()) for _, conf := range f.configLister { i.Cdi().V1alpha1().CDIConfigs().Informer().GetIndexer().Add(conf) } for _, ing := range f.ingressLister { k8sI.Extensions().V1beta1().Ingresses().Informer().GetIndexer().Add(ing) } for _, route := range f.routeLister { r.Route().V1().Routes().Informer().GetIndexer().Add(route) } c := NewConfigController(f.kubeclient, f.client, k8sI.Extensions().V1beta1().Ingresses(), r.Route().V1().Routes(), i.Cdi().V1alpha1().CDIConfigs(), "cdi-uploadproxy", "testConfig", "Always", "5") c.ingressesSynced = alwaysReady c.routesSynced = alwaysReady c.configsSynced = alwaysReady return c, i, r, k8sI } func (f *configFixture) run(configName string) { f.runController(configName, true, false) } func (f *configFixture) runController(configName string, startInformers bool, expectError bool) { c, i, r, k8sI := f.newController() if startInformers { stopCh := make(chan struct{}) defer close(stopCh) i.Start(stopCh) r.Start(stopCh) k8sI.Start(stopCh) } err := c.syncHandler(configName) if !expectError && err != nil { f.t.Errorf("error syncing foo: %v", err) } else if expectError && err == nil { f.t.Error("expected error syncing foo, got nil") } actions := filterInformerActions(f.client.Actions()) for i, action := range actions { if len(f.actions) < i+1 { f.t.Errorf("%d unexpected actions: %+v", len(actions)-len(f.actions), actions[i:]) break } expectedAction := f.actions[i] checkConfigAction(expectedAction, action, f.t) } if len(f.actions) > len(actions) { f.t.Errorf("%d additional expected actions:%+v", len(f.actions)-len(actions), f.actions[len(actions):]) } routeactions := filterInformerActions(f.routeClient.Actions()) for i, action := range routeactions { if len(f.routeactions) < i+1 { f.t.Errorf("%d unexpected actions: %+v", len(routeactions)-len(f.routeactions), routeactions[i:]) break } expectedAction := f.routeactions[i] checkConfigAction(expectedAction, action, f.t) } if len(f.routeactions) > len(routeactions) { f.t.Errorf("%d additional expected actions:%+v", len(f.routeactions)-len(routeactions), f.routeactions[len(routeactions):]) } k8sActions := filterInformerActions(f.kubeclient.Actions()) for i, action := range k8sActions { if len(f.kubeactions) < i+1 { f.t.Errorf("%d unexpected actions: %+v", len(k8sActions)-len(f.kubeactions), k8sActions[i:]) break } expectedAction := f.kubeactions[i] checkConfigAction(expectedAction, action, f.t) } if len(f.kubeactions) > len(k8sActions) { f.t.Errorf("%d additional expected actions:%+v", len(f.kubeactions)-len(k8sActions), f.kubeactions[len(k8sActions):]) } } func filterConfigInformerActions(actions []core.Action) []core.Action { ret := []core.Action{} for _, action := range actions { if len(action.GetNamespace()) == 0 && (action.Matches("list", "ingresses") || action.Matches("watch", "ingresses") || action.Matches("list", "routes") || action.Matches("watch", "routes") || action.Matches("list", "cdiconfigs") || action.Matches("watch", "cdiconfigs")) { continue } ret = append(ret, action) } return ret } func checkConfigAction(expected, actual core.Action, t *testing.T) { if !(expected.Matches(actual.GetVerb(), actual.GetResource().Resource) && actual.GetSubresource() == expected.GetSubresource()) { t.Errorf("Expected\n\t%#v\ngot\n\t%#v", expected, actual) return } if reflect.TypeOf(actual) != reflect.TypeOf(expected) { t.Errorf("Action has wrong type. Expected: %t. Got: %t", expected, actual) return } switch a := actual.(type) { case core.CreateAction: e, _ := expected.(core.CreateAction) expObject := e.GetObject() object := a.GetObject() if !reflect.DeepEqual(expObject, object) { t.Errorf("Action %s %s has wrong object\nDiff:\n %s", a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expObject, object)) } case core.UpdateAction: e, _ := expected.(core.UpdateAction) expObject := e.GetObject() object := a.GetObject() if !reflect.DeepEqual(expObject, object) { t.Errorf("Action %s %s has wrong object\nDiff:\n %s", a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expObject, object)) } case core.PatchAction: e, _ := expected.(core.PatchAction) expPatch := e.GetPatch() patch := a.GetPatch() if !reflect.DeepEqual(expPatch, expPatch) { t.Errorf("Action %s %s has wrong patch\nDiff:\n %s", a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expPatch, patch)) } } } func (f *configFixture) expectUpdateConfigAction(config *cdiv1.CDIConfig) { action := core.NewUpdateAction(schema.GroupVersionResource{Group: "cdi.kubevirt.io", Resource: "cdiconfigs", Version: "v1alpha1"}, config.Namespace, config) f.actions = append(f.actions, action) } func (f *configFixture) expectListStorageClass() { f.kubeactions = append(f.kubeactions, core.NewRootListAction(schema.GroupVersionResource{Resource: "storageclasses", Version: "v1"}, schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}, metav1.ListOptions{})) } // very flaky /* func TestCreatesCDIConfig(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) f.expectListStorageClass() f.run(getConfigKey(config, t)) } */ // another flaky test /* func TestCDIConfigStatusChanged(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") url := "www.example.com" config.Spec.UploadProxyURLOverride = &url f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) result := config.DeepCopy() result.Status.UploadProxyURL = &url f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } */ func TestCreatesRoute(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) route := createRoute("testRoute", "default", "cdi-uploadproxy") f.routeLister = append(f.routeLister, route) url := route.Status.Ingress[0].Host result := config.DeepCopy() result.Status.UploadProxyURL = &url f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } // another flaky one /* func TestCreatesRouteOverrideExists(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") newURL := "www.override.com" config.Spec.UploadProxyURLOverride = &newURL f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) route := createRoute("testRoute", "default", "cdi-uploadproxy") f.routeLister = append(f.routeLister, route) result := config.DeepCopy() result.Status.UploadProxyURL = &newURL f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } */ func TestCreatesRouteDifferentService(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) route := createRoute("testRoute", "default", "other-service") f.routeLister = append(f.routeLister, route) f.expectListStorageClass() f.run(getConfigKey(config, t)) } func TestCreatesIngress(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) url := "www.example.com" ing := createIngress("ing", "default", "cdi-uploadproxy", url) f.ingressLister = append(f.ingressLister, ing) f.kubeobjects = append(f.kubeobjects, ing) result := config.DeepCopy() result.Status.UploadProxyURL = &url f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } func TestCreatesIngressOverrideExists(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") newURL := "www.override.com" config.Spec.UploadProxyURLOverride = &newURL f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) url := "www.example.com" ing := createIngress("ing", "default", "cdi-uploadproxy", url) f.ingressLister = append(f.ingressLister, ing) f.kubeobjects = append(f.kubeobjects, ing) result := config.DeepCopy() result.Status.UploadProxyURL = &newURL f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } func TestCreatesIngressDifferentService(t *testing.T) { f := newConfigFixture(t) config := createCDIConfig("testConfig") f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) url := "www.example.com" ing := createIngress("ing", "default", "other-service", url) f.ingressLister = append(f.ingressLister, ing) f.kubeobjects = append(f.kubeobjects, ing) f.expectListStorageClass() f.run(getConfigKey(config, t)) } func TestCreatesScratchStorageClassOverrideExists(t *testing.T) { f := newConfigFixture(t) f.kubeobjects = append(f.kubeobjects, createStorageClass("test1", nil)) f.kubeobjects = append(f.kubeobjects, createStorageClass("test2", nil)) f.kubeobjects = append(f.kubeobjects, createStorageClass("test3", map[string]string{ AnnDefaultStorageClass: "true", })) config := createCDIConfig("testConfig") scratchStorageClass := "test2" config.Spec.ScratchSpaceStorageClass = &scratchStorageClass f.configLister = append(f.configLister, config) f.objects = append(f.objects, config) result := config.DeepCopy() result.Status.ScratchSpaceStorageClass = scratchStorageClass f.expectListStorageClass() f.expectUpdateConfigAction(result) f.run(getConfigKey(config, t)) } // TODO Enable me when we refactor the controller. //func TestCreatesScratchStorageClassOverrideMissing(t *testing.T) { // f := newConfigFixture(t) // // f.kubeobjects = append(f.kubeobjects, createStorageClass("test1", nil)) // f.kubeobjects = append(f.kubeobjects, createStorageClass("test2", nil)) // f.kubeobjects = append(f.kubeobjects, createStorageClass("test3", map[string]string{ // AnnDefaultStorageClass: "true", // })) // // config := createCDIConfig("testConfig") // scratchStorageClass := "test3" // // f.configLister = append(f.configLister, config) // f.objects = append(f.objects, config) // // result := config.DeepCopy() // result.Status.ScratchSpaceStorageClass = scratchStorageClass // f.expectListStorageClass() // f.expectUpdateConfigAction(result) // // f.run(getConfigKey(config, t)) //}