package controllers import ( "context" "fmt" cloudmapMock "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/mocks/pkg/cloudmap" aboutv1alpha1 "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/apis/about/v1alpha1" multiclusterv1alpha1 "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/apis/multicluster/v1alpha1" "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/common" "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/model" "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/test" "github.com/go-logr/logr/testr" "github.com/golang/mock/gomock" "testing" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestServiceExportReconciler_Reconcile_NewServiceExport(t *testing.T) { // create a fake controller client and add some objects fakeClient := fake.NewClientBuilder(). WithScheme(getServiceExportScheme()). WithObjects(k8sServiceForTest(), serviceExportForTest(), test.ClusterIdForTest(), test.ClusterSetIdForTest()). WithLists(&discovery.EndpointSliceList{ Items: []discovery.EndpointSlice{*endpointSliceForTest()}, }). Build() // create a mock cloudmap service discovery client mockController := gomock.NewController(t) defer mockController.Finish() mock := cloudmapMock.NewMockServiceDiscoveryClient(mockController) // expected interactions with the Cloud Map client // The first get call is expected to return nil, then second call after the creation of service is // supposed to return the value first := mock.EXPECT().GetService(gomock.Any(), test.HttpNsName, test.SvcName). Return(nil, common.NotFoundError("")) second := mock.EXPECT().GetService(gomock.Any(), test.HttpNsName, test.SvcName). Return(&model.Service{Namespace: test.HttpNsName, Name: test.SvcName}, nil) gomock.InOrder(first, second) mock.EXPECT().CreateService(gomock.Any(), test.HttpNsName, test.SvcName).Return(nil).Times(1) mock.EXPECT().RegisterEndpoints(gomock.Any(), test.HttpNsName, test.SvcName, []*model.Endpoint{test.GetTestEndpoint1()}).Return(nil).Times(1) reconciler := getServiceExportReconciler(t, mock, fakeClient) request := ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: test.HttpNsName, Name: test.SvcName, }, } got, err := reconciler.Reconcile(context.Background(), request) if err != nil { t.Errorf("Reconcile() error = %v", err) return } assert.Equal(t, ctrl.Result{}, got, "Result should be empty") serviceExport := &multiclusterv1alpha1.ServiceExport{} err = fakeClient.Get(context.TODO(), types.NamespacedName{Namespace: test.HttpNsName, Name: test.SvcName}, serviceExport) assert.NoError(t, err) assert.Contains(t, serviceExport.Finalizers, ServiceExportFinalizer, "Finalizer added to the service export") } func TestServiceExportReconciler_Reconcile_ExistingServiceExport(t *testing.T) { // create a fake controller client and add some objects fakeClient := fake.NewClientBuilder(). WithScheme(getServiceExportScheme()). WithObjects(k8sServiceForTest(), serviceExportForTest(), test.ClusterIdForTest(), test.ClusterSetIdForTest()). WithLists(&discovery.EndpointSliceList{ Items: []discovery.EndpointSlice{*endpointSliceForTest()}, }). Build() mockController := gomock.NewController(t) defer mockController.Finish() mock := cloudmapMock.NewMockServiceDiscoveryClient(mockController) // GetService from Cloudmap returns endpoint1 and endpoint2 mock.EXPECT().GetService(gomock.Any(), test.HttpNsName, test.SvcName). Return(test.GetTestService(), nil) // call to delete the endpoint not present in the k8s cluster mock.EXPECT().DeleteEndpoints(gomock.Any(), test.HttpNsName, test.SvcName, []*model.Endpoint{test.GetTestEndpoint2()}).Return(nil).Times(1) request := ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: test.HttpNsName, Name: test.SvcName, }, } reconciler := getServiceExportReconciler(t, mock, fakeClient) got, err := reconciler.Reconcile(context.Background(), request) if err != nil { t.Errorf("Reconcile() error = %v", err) return } assert.Equal(t, ctrl.Result{}, got, "Result should be empty") serviceExport := &multiclusterv1alpha1.ServiceExport{} err = fakeClient.Get(context.TODO(), types.NamespacedName{Namespace: test.HttpNsName, Name: test.SvcName}, serviceExport) assert.NoError(t, err) assert.Contains(t, serviceExport.Finalizers, ServiceExportFinalizer, "Finalizer added to the service export") } func TestServiceExportReconciler_Reconcile_DeleteExistingService(t *testing.T) { // create a fake controller client and add some objects serviceExportObj := serviceExportForTest() // Add finalizer string to the service serviceExportObj.Finalizers = []string{ServiceExportFinalizer} fakeClient := fake.NewClientBuilder(). WithScheme(getServiceExportScheme()). WithObjects(serviceExportObj, test.ClusterIdForTest(), test.ClusterSetIdForTest()). WithLists(&discovery.EndpointSliceList{ Items: []discovery.EndpointSlice{*endpointSliceForTest()}, }). Build() mockController := gomock.NewController(t) defer mockController.Finish() mock := cloudmapMock.NewMockServiceDiscoveryClient(mockController) // GetService from Cloudmap returns endpoint1 and endpoint2 mock.EXPECT().GetService(gomock.Any(), test.HttpNsName, test.SvcName). Return(test.GetTestService(), nil) // call to delete the endpoint in the cloudmap mock.EXPECT().DeleteEndpoints(gomock.Any(), test.HttpNsName, test.SvcName, test.GetTestService().GetEndpoints(test.ClusterId1)).Return(nil).Times(1) request := ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: test.HttpNsName, Name: test.SvcName, }, } reconciler := getServiceExportReconciler(t, mock, fakeClient) got, err := reconciler.Reconcile(context.Background(), request) assert.NoError(t, err) assert.Equal(t, ctrl.Result{}, got, "Result should be empty") serviceExport := &multiclusterv1alpha1.ServiceExport{} err = fakeClient.Get(context.TODO(), types.NamespacedName{Namespace: test.HttpNsName, Name: test.SvcName}, serviceExport) assert.NoError(t, err) assert.Empty(t, serviceExport.Finalizers, "Finalizer removed from the service export") } func TestServiceExportReconciler_Reconcile_NoClusterProperty(t *testing.T) { // create a fake controller client and add some objects fakeClient := fake.NewClientBuilder(). WithScheme(getServiceExportScheme()). // do-not add clusterId WithObjects(k8sServiceForTest(), serviceExportForTest(), test.ClusterSetIdForTest()). WithLists(&discovery.EndpointSliceList{ Items: []discovery.EndpointSlice{*endpointSliceForTest()}, }).Build() // create a mock cloudmap service discovery client mockController := gomock.NewController(t) defer mockController.Finish() mockSDClient := cloudmapMock.NewMockServiceDiscoveryClient(mockController) reconciler := getServiceExportReconciler(t, mockSDClient, fakeClient) request := ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: test.HttpNsName, Name: test.SvcName, }, } // Reconciling should throw an error got, err := reconciler.Reconcile(context.Background(), request) expectedError := fmt.Errorf("ClusterProperty not found") assert.ErrorContains(t, err, expectedError.Error()) assert.Equal(t, ctrl.Result{}, got, "Result should be empty") } func getServiceExportScheme() *runtime.Scheme { scheme := runtime.NewScheme() scheme.AddKnownTypes(aboutv1alpha1.GroupVersion, &aboutv1alpha1.ClusterProperty{}, &aboutv1alpha1.ClusterPropertyList{}) scheme.AddKnownTypes(multiclusterv1alpha1.GroupVersion, &multiclusterv1alpha1.ServiceExport{}) scheme.AddKnownTypes(v1.SchemeGroupVersion, &v1.Service{}) scheme.AddKnownTypes(discovery.SchemeGroupVersion, &discovery.EndpointSlice{}, &discovery.EndpointSliceList{}) return scheme } func getServiceExportReconciler(t *testing.T, mockClient *cloudmapMock.MockServiceDiscoveryClient, client client.Client) *ServiceExportReconciler { test.SetTestVersion() return &ServiceExportReconciler{ Client: client, Log: common.NewLoggerWithLogr(testr.New(t)), Scheme: client.Scheme(), CloudMap: mockClient, ClusterUtils: model.NewClusterUtils(client), } }