Repository: submariner-io/lighthouse
Pull request #217 (2020-07-22 21:40:24)

Add agent controller unit tests
Added a new constructor, `NewWithDetail`, to allow the UTs too hook in fake clients.

Also renamed `Run` to `Start` and changed it to not wait on the stopCh as that is problematic for UTs.


submariner-bot: 🤖 Created branch: z_pr217/tpantelis/agent_unit_tests
submariner-bot: 🤖 Updated branch: z_pr217/tpantelis/agent_unit_tests
submariner-bot: 🤖 Updated branch: z_pr217/tpantelis/agent_unit_tests

Patch:
diff --git a/go.mod b/go.mod
index 8bd0c90..d15c4b5 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/onsi/ginkgo v1.14.0
 	github.com/onsi/gomega v1.10.1
 	github.com/pkg/errors v0.9.1
-	github.com/submariner-io/admiral v0.5.0
+	github.com/submariner-io/admiral v0.6.0-pre0
 	github.com/submariner-io/shipyard v0.5.0
 	k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2
 	k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d
diff --git a/go.sum b/go.sum
index 99ac73b..009c28f 100644
--- a/go.sum
+++ b/go.sum
@@ -343,8 +343,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/submariner-io/admiral v0.5.0 h1:TaGc0hrB9WOSY2IWXBA3LfZC7c/FDDlTmiEfUah5hIs=
-github.com/submariner-io/admiral v0.5.0/go.mod h1:cBv1Zvfai117idhYv5WPxTZEsgWQfeSqwPpxo0EMZXg=
+github.com/submariner-io/admiral v0.6.0-pre0 h1:/f/cs1qZPNHRQMl4yQxYMEZzOlKeZguGM9HSP84Rt/w=
+github.com/submariner-io/admiral v0.6.0-pre0/go.mod h1:cBv1Zvfai117idhYv5WPxTZEsgWQfeSqwPpxo0EMZXg=
 github.com/submariner-io/shipyard v0.5.0 h1:o4kTCubLFLeh+PaBDac1J7zNY3JKWwj7QrMLNb4g/p8=
 github.com/submariner-io/shipyard v0.5.0/go.mod h1:2OrKoDmJSo1jVurxiVH9FqGFmDwshIU7Vgu9pFLBsYo=
 github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
diff --git a/pkg/agent/controller/agent.go b/pkg/agent/controller/agent.go
index d1fd51e..eff83f4 100644
--- a/pkg/agent/controller/agent.go
+++ b/pkg/agent/controller/agent.go
@@ -3,6 +3,7 @@ package controller
 import (
 	"fmt"
 
+	"github.com/submariner-io/admiral/pkg/log"
 	"github.com/submariner-io/admiral/pkg/syncer"
 	"github.com/submariner-io/admiral/pkg/syncer/broker"
 	"github.com/submariner-io/admiral/pkg/util"
@@ -10,6 +11,7 @@ import (
 	lighthouseClientset "github.com/submariner-io/lighthouse/pkg/client/clientset/versioned"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -25,7 +27,7 @@ const (
 )
 
 func New(spec *AgentSpecification, cfg *rest.Config) (*Controller, error) {
-	clientSet, err := kubernetes.NewForConfig(cfg)
+	kubeClientSet, err := kubernetes.NewForConfig(cfg)
 	if err != nil {
 		return nil, fmt.Errorf("Error building clientset: %s", err.Error())
 	}
@@ -35,10 +37,32 @@ func New(spec *AgentSpecification, cfg *rest.Config) (*Controller, error) {
 		return nil, fmt.Errorf("Error building lighthouseClient %s", err.Error())
 	}
 
+	syncerConf := &broker.SyncerConfig{LocalRestConfig: cfg}
+
+	restMapper, err := util.BuildRestMapper(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	localClient, err := dynamic.NewForConfig(cfg)
+	if err != nil {
+		return nil, fmt.Errorf("error creating dynamic client: %v", err)
+	}
+
+	return NewWithDetail(spec, syncerConf, restMapper, localClient, kubeClientSet, lighthouseClient, nil,
+		func(config *broker.SyncerConfig) (*broker.Syncer, error) {
+			return broker.NewSyncer(*config)
+		})
+}
+
+// Constructor that takes additional detail. This is intended for unit tests.
+func NewWithDetail(spec *AgentSpecification, syncerConf *broker.SyncerConfig, restMapper meta.RESTMapper, localClient dynamic.Interface,
+	kubeClientSet kubernetes.Interface, lighthouseClient lighthouseClientset.Interface, scheme *runtime.Scheme,
+	newSyncer func(*broker.SyncerConfig) (*broker.Syncer, error)) (*Controller, error) {
 	agentController := &Controller{
 		clusterID:        spec.ClusterID,
 		globalnetEnabled: spec.GlobalnetEnabled,
-		kubeClientSet:    clientSet,
+		kubeClientSet:    kubeClientSet,
 		lighthouseClient: lighthouseClient,
 	}
 
@@ -49,38 +73,28 @@ func New(spec *AgentSpecification, cfg *rest.Config) (*Controller, error) {
 		BrokerResourceType:   &lighthousev2a1.ServiceImport{},
 	}
 
-	syncerConf := broker.SyncerConfig{
-		LocalRestConfig: cfg,
-		LocalNamespace:  spec.Namespace,
-		ResourceConfigs: []broker.ResourceConfig{
-			svcExportResourceConfig,
-		},
+	syncerConf.Scheme = scheme
+	syncerConf.LocalNamespace = spec.Namespace
+	syncerConf.ResourceConfigs = []broker.ResourceConfig{
+		svcExportResourceConfig,
 	}
 
-	agentController.serviceExportSyncer, err = broker.NewSyncer(syncerConf)
+	var err error
+	agentController.serviceExportSyncer, err = newSyncer(syncerConf)
 	if err != nil {
 		return nil, err
 	}
 
-	restMapper, err := util.BuildRestMapper(cfg)
-	if err != nil {
-		return nil, err
-	}
-
-	localClient, err := dynamic.NewForConfig(cfg)
-	if err != nil {
-		return nil, fmt.Errorf("error creating dynamic client: %v", err)
-	}
-
 	agentController.serviceSyncer, err = syncer.NewResourceSyncer(&syncer.ResourceSyncerConfig{
 		Name:            "Service deletion",
 		SourceClient:    localClient,
 		SourceNamespace: metav1.NamespaceAll,
 		Direction:       syncer.LocalToRemote,
 		RestMapper:      restMapper,
-		Federator:       agentController.serviceExportSyncer.GetBrokerFederatorFor(svcExportResourceConfig.LocalResourceType),
+		Federator:       agentController.serviceExportSyncer.GetBrokerFederator(),
 		ResourceType:    &corev1.Service{},
 		Transform:       agentController.serviceToRemoteServiceImport,
+		Scheme:          scheme,
 	})
 
 	if err != nil {
@@ -90,7 +104,7 @@ func New(spec *AgentSpecification, cfg *rest.Config) (*Controller, error) {
 	return agentController, nil
 }
 
-func (a *Controller) Run(stopCh <-chan struct{}) error {
+func (a *Controller) Start(stopCh <-chan struct{}) error {
 	defer utilruntime.HandleCrash()
 
 	// Start the informer factories to begin populating the informer caches
@@ -108,8 +122,7 @@ func (a *Controller) Run(stopCh <-chan struct{}) error {
 	}
 
 	klog.Info("Lighthouse agent syncer started")
-	<-stopCh
-	klog.Info("Lighthouse Agent stopping")
+
 	return nil
 }
 
@@ -122,6 +135,7 @@ func (a *Controller) serviceExportToRemoteServiceImport(obj runtime.Object, op s
 	}
 	svc, err := a.kubeClientSet.CoreV1().Services(svcExport.Namespace).Get(svcExport.Name, metav1.GetOptions{})
 	if errors.IsNotFound(err) {
+		klog.V(log.DEBUG).Infof("Service to be exported (%s/%s) doesn't exist", svcExport.Namespace, svcExport.Name)
 		a.updateExportedServiceStatus(svcExport, "Service to be exported doesn't exist", lighthousev2a1.ServiceExportInitialized,
 			corev1.ConditionFalse)
 		return nil, true
@@ -133,8 +147,10 @@ func (a *Controller) serviceExportToRemoteServiceImport(obj runtime.Object, op s
 		return nil, true
 	}
 	if a.globalnetEnabled && getGlobalIpFromService(svc) == "" {
+		klog.V(log.DEBUG).Infof("Service to be exported (%s/%s) doesn't have a global IP yet", svcExport.Namespace, svcExport.Name)
+
 		// Globalnet enabled but service doesn't have globalIp yet, Update the status and requeue
-		a.updateExportedServiceStatus(svcExport, "Service doesn't have global IP yet", lighthousev2a1.ServiceExportInitialized,
+		a.updateExportedServiceStatus(svcExport, "Service doesn't have a global IP yet", lighthousev2a1.ServiceExportInitialized,
 			corev1.ConditionFalse)
 		return nil, true
 	}
@@ -157,6 +173,7 @@ func (a *Controller) serviceToRemoteServiceImport(obj runtime.Object, op syncer.
 		// Ignore create/update
 		return nil, false
 	}
+
 	svc := obj.(*corev1.Service)
 	svcExport, err := a.lighthouseClient.LighthouseV2alpha1().ServiceExports(svc.Namespace).Get(svc.Name, metav1.GetOptions{})
 	if errors.IsNotFound(err) {
@@ -182,7 +199,7 @@ func (a *Controller) updateExportedServiceStatus(export *lighthousev2a1.ServiceE
 	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
 		toUpdate, err := a.lighthouseClient.LighthouseV2alpha1().ServiceExports(export.Namespace).Get(export.Name, metav1.GetOptions{})
 		if errors.IsNotFound(err) {
-			klog.Infof("Export Service not found, hence ignoring %v", export)
+			klog.Infof("ServiceExport (%s/%s) not found - unable to update status", export.Namespace, export.Name)
 			return nil
 		} else if err != nil {
 			return err
diff --git a/pkg/agent/main.go b/pkg/agent/main.go
index a791625..9c9b3f0 100644
--- a/pkg/agent/main.go
+++ b/pkg/agent/main.go
@@ -54,10 +54,12 @@ func main() {
 		klog.Fatalf("Failed to create lighthouse agent: %v", err)
 	}
 
-	if err := lightHouseAgent.Run(stopCh); err != nil {
+	if err := lightHouseAgent.Start(stopCh); err != nil {
 		klog.Fatalf("Failed to start lighthouse agent: %v", err)
 	}
 
+	<-stopCh
+
 	klog.Info("All controllers stopped or exited. Stopping main loop")
 }
 


Test patch:
diff --git a/pkg/agent/controller/agent_test.go b/pkg/agent/controller/agent_test.go
new file mode 100644
index 0000000..b23f3d7
--- /dev/null
+++ b/pkg/agent/controller/agent_test.go
@@ -0,0 +1,302 @@
+package controller_test
+
+import (
+	"time"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	"github.com/submariner-io/admiral/pkg/fake"
+	"github.com/submariner-io/admiral/pkg/syncer/broker"
+	"github.com/submariner-io/admiral/pkg/syncer/test"
+	"github.com/submariner-io/lighthouse/pkg/agent/controller"
+	lighthousev2a1 "github.com/submariner-io/lighthouse/pkg/apis/lighthouse.submariner.io/v2alpha1"
+	lighthouseClientset "github.com/submariner-io/lighthouse/pkg/client/clientset/versioned"
+	fakeLighthouseClientset "github.com/submariner-io/lighthouse/pkg/client/clientset/versioned/fake"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/kubernetes"
+	fakeKubeCLient "k8s.io/client-go/kubernetes/fake"
+	"k8s.io/client-go/kubernetes/scheme"
+)
+
+const clusterID = "east"
+
+var _ = Describe("ServiceImport syncing", func() {
+	t := newTestDiver()
+
+	When("a ServiceExport is created", func() {
+		When("the Service already exists", func() {
+			It("should correctly sync a ServiceImport and update the ServiceExport status", func() {
+				t.createService()
+				t.createServiceExport()
+				t.awaitServiceExported(t.service.Spec.ClusterIP)
+			})
+		})
+
+		When("the Service doesn't initially exist", func() {
+			It("should initially update the ServiceExport status to Initialized and eventually sync a ServiceImport", func() {
+				t.createServiceExport()
+
+				t.awaitServiceExportStatus(&lighthousev2a1.ServiceExportCondition{
+					Type:   lighthousev2a1.ServiceExportInitialized,
+					Status: corev1.ConditionFalse,
+				})
+
+				t.createService()
+				t.awaitServiceExported(t.service.Spec.ClusterIP)
+			})
+		})
+	})
+
+	When("a ServiceExport is deleted after a ServiceImport is synced", func() {
+		It("should delete the ServiceImport", func() {
+			t.createService()
+			t.createServiceExport()
+			t.awaitServiceExported(t.service.Spec.ClusterIP)
+
+			t.deleteServiceExport()
+			t.awaitNoServiceImport(t.brokerServiceImportClient)
+			t.awaitNoServiceImport(t.localServiceImportClient)
+		})
+	})
+
+	When("an exported Service is deleted while the ServiceExport still exists", func() {
+		It("should delete the ServiceImport", func() {
+			t.createService()
+			t.createServiceExport()
+			t.awaitServiceExported(t.service.Spec.ClusterIP)
+
+			t.deleteService()
+			t.awaitNoServiceImport(t.brokerServiceImportClient)
+			t.awaitNoServiceImport(t.localServiceImportClient)
+			t.awaitServiceExportStatus(&lighthousev2a1.ServiceExportCondition{
+				Type:   lighthousev2a1.ServiceExportInitialized,
+				Status: corev1.ConditionFalse,
+			})
+		})
+	})
+})
+
+var _ = Describe("Globalnet enabled", func() {
+	t := newTestDiver()
+
+	globalIP := "192.168.10.34"
+	t.agentSpec.GlobalnetEnabled = true
+
+	JustBeforeEach(func() {
+		t.createService()
+		t.createServiceExport()
+	})
+
+	When("a local ServiceExport is created and the Service has a global IP", func() {
+		BeforeEach(func() {
+			t.service.SetAnnotations(map[string]string{"submariner.io/globalIp": globalIP})
+		})
+
+		It("should sync a ServiceImport with the global IP of the Service", func() {
+			t.awaitServiceExported(globalIP)
+		})
+	})
+
+	When("a local ServiceExport is created and the Service does not initially have a global IP", func() {
+		It("should eventually sync a ServiceImport with the global IP of the Service", func() {
+			t.awaitServiceExportStatus(&lighthousev2a1.ServiceExportCondition{
+				Type:   lighthousev2a1.ServiceExportInitialized,
+				Status: corev1.ConditionFalse,
+			})
+
+			t.service.SetAnnotations(map[string]string{"submariner.io/globalIp": globalIP})
+			_, err := t.localServiceClient.CoreV1().Services(t.service.Namespace).Update(t.service)
+			Expect(err).To(Succeed())
+
+			t.awaitServiceExported(globalIP)
+		})
+	})
+})
+
+type testDriver struct {
+	agentController           *controller.Controller
+	agentSpec                 *controller.AgentSpecification
+	localDynClient            dynamic.Interface
+	localServiceExportClient  dynamic.ResourceInterface
+	localServiceImportClient  dynamic.ResourceInterface
+	brokerServiceImportClient dynamic.ResourceInterface
+	localServiceClient        kubernetes.Interface
+	lighthouseClient          lighthouseClientset.Interface
+	initialResources          []runtime.Object
+	service                   *corev1.Service
+	serviceExport             *lighthousev2a1.ServiceExport
+	stopCh                    chan struct{}
+}
+
+func newTestDiver() *testDriver {
+	t := &testDriver{agentSpec: &controller.AgentSpecification{
+		ClusterID:        clusterID,
+		Namespace:        test.LocalNamespace,
+		GlobalnetEnabled: false,
+	}}
+
+	BeforeEach(func() {
+		t.initialResources = nil
+		t.stopCh = make(chan struct{})
+
+		t.service = &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "nginx",
+				Namespace: test.LocalNamespace,
+			},
+			Spec: corev1.ServiceSpec{
+				ClusterIP: "10.253.9.1",
+			},
+		}
+
+		t.serviceExport = &lighthousev2a1.ServiceExport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      t.service.Name,
+				Namespace: t.service.Namespace,
+			},
+		}
+	})
+
+	JustBeforeEach(func() {
+		restMapper := test.GetRESTMapperFor(&lighthousev2a1.ServiceExport{}, &lighthousev2a1.ServiceImport{}, &corev1.Service{})
+
+		t.localDynClient = fake.NewDynamicClient(test.PrepInitialClientObjs("", "", t.initialResources...)...)
+		brokerDynClient := fake.NewDynamicClient()
+
+		t.localServiceExportClient = t.localDynClient.Resource(*test.GetGroupVersionResourceFor(restMapper,
+			&lighthousev2a1.ServiceExport{})).Namespace(test.LocalNamespace)
+
+		t.localServiceImportClient = t.localDynClient.Resource(*test.GetGroupVersionResourceFor(restMapper,
+			&lighthousev2a1.ServiceImport{})).Namespace(test.LocalNamespace)
+
+		t.brokerServiceImportClient = brokerDynClient.Resource(*test.GetGroupVersionResourceFor(restMapper,
+			&lighthousev2a1.ServiceImport{})).Namespace(test.RemoteNamespace)
+
+		t.localServiceClient = fakeKubeCLient.NewSimpleClientset()
+
+		t.lighthouseClient = fakeLighthouseClientset.NewSimpleClientset(t.serviceExport)
+
+		syncerConfig := &broker.SyncerConfig{
+			BrokerNamespace: test.RemoteNamespace,
+		}
+
+		syncerScheme := runtime.NewScheme()
+		Expect(corev1.AddToScheme(syncerScheme)).To(Succeed())
+		Expect(lighthousev2a1.AddToScheme(syncerScheme)).To(Succeed())
+
+		var err error
+		t.agentController, err = controller.NewWithDetail(t.agentSpec, syncerConfig, restMapper, t.localDynClient,
+			t.localServiceClient, t.lighthouseClient, syncerScheme, func(config *broker.SyncerConfig) (*broker.Syncer, error) {
+				return broker.NewSyncerWithDetail(config, t.localDynClient, brokerDynClient, restMapper)
+			})
+
+		Expect(err).To(Succeed())
+
+		Expect(t.agentController.Start(t.stopCh)).To(Succeed())
+	})
+
+	AfterEach(func() {
+		close(t.stopCh)
+	})
+
+	return t
+}
+
+func (t *testDriver) createService() {
+	_, err := t.localServiceClient.CoreV1().Services(t.service.Namespace).Create(t.service)
+	Expect(err).To(Succeed())
+
+	rawService := &unstructured.Unstructured{}
+	Expect(scheme.Scheme.Convert(t.service, rawService, nil)).To(Succeed())
+
+	_, err = t.dynamicServiceClient().Create(rawService, metav1.CreateOptions{})
+	Expect(err).To(Succeed())
+}
+
+func (t *testDriver) createServiceExport() {
+	test.CreateResource(t.localServiceExportClient, t.serviceExport)
+}
+
+func (t *testDriver) deleteServiceExport() {
+	Expect(t.localServiceExportClient.Delete(t.service.GetName(), nil)).To(Succeed())
+}
+
+func (t *testDriver) deleteService() {
+	Expect(t.dynamicServiceClient().Delete(t.service.Name, nil)).To(Succeed())
+
+	Expect(t.localServiceClient.CoreV1().Services(t.service.Namespace).Delete(t.service.Name, nil)).To(Succeed())
+}
+
+func (t *testDriver) dynamicServiceClient() dynamic.ResourceInterface {
+	return t.localDynClient.Resource(schema.GroupVersionResource{Version: "v1", Resource: "services"}).Namespace(t.service.Namespace)
+}
+
+func (t *testDriver) awaitServiceImport(client dynamic.ResourceInterface, serviceIP string) {
+	obj := test.WaitForResource(client, t.service.Name+"-"+t.service.Namespace+"-"+clusterID)
+
+	serviceImport := &lighthousev2a1.ServiceImport{}
+	Expect(scheme.Scheme.Convert(obj, serviceImport, nil)).To(Succeed())
+
+	Expect(serviceImport.GetAnnotations()["origin-name"]).To(Equal(t.service.Name))
+	Expect(serviceImport.GetAnnotations()["origin-namespace"]).To(Equal(t.service.Namespace))
+	Expect(serviceImport.Spec.Type).To(Equal(lighthousev2a1.SuperclusterIP))
+
+	Expect(serviceImport.Status.Clusters).To(HaveLen(1))
+	Expect(serviceImport.Status.Clusters[0].Cluster).To(Equal(clusterID))
+	Expect(serviceImport.Status.Clusters[0].IPs).To(HaveLen(1))
+	Expect(serviceImport.Status.Clusters[0].IPs[0]).To(Equal(serviceIP))
+}
+
+func (t *testDriver) awaitNoServiceImport(client dynamic.ResourceInterface) {
+	test.WaitForNoResource(client, t.service.Name+"-"+t.service.Namespace+"-"+clusterID)
+}
+
+func (t *testDriver) awaitServiceExportStatus(expCond *lighthousev2a1.ServiceExportCondition) {
+	var found *lighthousev2a1.ServiceExport
+
+	err := wait.PollImmediate(50*time.Millisecond, 5*time.Second, func() (bool, error) {
+		se, err := t.lighthouseClient.LighthouseV2alpha1().ServiceExports(t.service.Namespace).Get(t.service.Name, metav1.GetOptions{})
+		if err != nil {
+			if apierrors.IsNotFound(err) {
+				return false, nil
+			}
+
+			return false, err
+		}
+
+		found = se
+
+		return len(se.Status.Conditions) > 0 && se.Status.Conditions[0].Type == expCond.Type &&
+			se.Status.Conditions[0].Status == expCond.Status, nil
+	})
+
+	if err == wait.ErrWaitTimeout {
+		if found == nil {
+			Fail("ServiceExport not found")
+		}
+	} else {
+		Expect(err).To(Succeed())
+	}
+
+	Expect(found.Status.Conditions).To(HaveLen(1))
+	Expect(found.Status.Conditions[0].Type).To(Equal(expCond.Type))
+	Expect(found.Status.Conditions[0].Status).To(Equal(expCond.Status))
+	Expect(found.Status.Conditions[0].Message).To(Not(BeNil()))
+}
+
+func (t *testDriver) awaitServiceExported(serviceIP string) {
+	t.awaitServiceImport(t.brokerServiceImportClient, serviceIP)
+	t.awaitServiceImport(t.localServiceImportClient, serviceIP)
+
+	t.awaitServiceExportStatus(&lighthousev2a1.ServiceExportCondition{
+		Type:   lighthousev2a1.ServiceExportExported,
+		Status: corev1.ConditionTrue,
+	})
+}
diff --git a/pkg/agent/controller/controller_suite_test.go b/pkg/agent/controller/controller_suite_test.go
new file mode 100644
index 0000000..daa0ca6
--- /dev/null
+++ b/pkg/agent/controller/controller_suite_test.go
@@ -0,0 +1,25 @@
+package controller_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	lighthousev2a1 "github.com/submariner-io/lighthouse/pkg/apis/lighthouse.submariner.io/v2alpha1"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/klog"
+)
+
+func init() {
+	klog.InitFlags(nil)
+
+	err := lighthousev2a1.AddToScheme(scheme.Scheme)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func TestController(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Agent Controller Suite")
+}