diff --git a/internal/controller/expected_netboxmock_calls_test.go b/internal/controller/expected_netboxmock_calls_test.go index ed7598f4..9d67d8c8 100644 --- a/internal/controller/expected_netboxmock_calls_test.go +++ b/internal/controller/expected_netboxmock_calls_test.go @@ -30,7 +30,7 @@ import ( // IPAM Mock Functions // ----------------------------- -func expectedIpAddressListWithIpAddressFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpAddressListWithIpAddressFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesListOK, error) { got := params.(*ipam.IpamIPAddressesListParams) @@ -45,7 +45,7 @@ func expectedIpAddressListWithIpAddressFilter(ipamMock *mock_interfaces.MockIpam }).MinTimes(1) } -func expectedIpAddressListWithIpAddressFilterEmptyResult(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpAddressListWithIpAddressFilterEmptyResult(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesListOK, error) { got := params.(*ipam.IpamIPAddressesListParams) @@ -57,10 +57,10 @@ func expectedIpAddressListWithIpAddressFilterEmptyResult(ipamMock *mock_interfac } fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesList (empty reslut) was called with expected input,\n") return &ipam.IpamIPAddressesListOK{Payload: mockedResponseEmptyIPAddressList()}, nil - }).Times(1) + }).MinTimes(1) } -func expectedIpAddressListWithHashFilterEmptyResult(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpAddressListWithHashFilterEmptyResult(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesListOK, error) { got := params.(*ipam.IpamIPAddressesListParams) @@ -73,10 +73,10 @@ func expectedIpAddressListWithHashFilterEmptyResult(ipamMock *mock_interfaces.Mo } fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesList (empty reslut) was called with expected input,\n") return &ipam.IpamIPAddressesListOK{Payload: mockedResponseEmptyIPAddressList()}, nil - }).Times(1) + }).MinTimes(1) } -func expectedIpAddressListWithHashFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpAddressListWithHashFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesListOK, error) { got := params.(*ipam.IpamIPAddressesListParams) @@ -89,10 +89,10 @@ func expectedIpAddressListWithHashFilter(ipamMock *mock_interfaces.MockIpamInter } fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesList (empty reslut) was called with expected input,\n") return &ipam.IpamIPAddressesListOK{Payload: mockedResponseIPAddressList()}, nil - }).Times(1) + }).MinTimes(1) } -func expectedPrefixesListWithPrefixFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockPrefixesListWithPrefixFilter(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamPrefixesList(gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamPrefixesListOK, error) { got := params.(*ipam.IpamPrefixesListParams) @@ -105,10 +105,10 @@ func expectedPrefixesListWithPrefixFilter(ipamMock *mock_interfaces.MockIpamInte } fmt.Printf("NETBOXMOCK\t ipam.IpamPrefixesList was called with expected input,\n") return &ipam.IpamPrefixesListOK{Payload: mockedResponsePrefixList()}, nil - }).Times(1) + }).MinTimes(1) } -func expectedPrefixesAvailableIpsList(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockPrefixesAvailableIpsList(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamPrefixesAvailableIpsList(gomock.Any(), gomock.Any()). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamPrefixesAvailableIpsListOK, error) { got := params.(*ipam.IpamPrefixesAvailableIpsListParams) @@ -121,10 +121,10 @@ func expectedPrefixesAvailableIpsList(ipamMock *mock_interfaces.MockIpamInterfac } fmt.Printf("NETBOXMOCK\t ipam.IpamPrefixesAvailableIpsList was called with expected input,\n") return &ipam.IpamPrefixesAvailableIpsListOK{Payload: mockedResponseExpectedAvailableIpAddress()}, nil - }).Times(1) + }).MinTimes(1) } -func expectedIpAddressesDelete(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpAddressesDelete(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesDelete(gomock.Any(), nil). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesDeleteNoContent, error) { got := params.(*ipam.IpamIPAddressesDeleteParams) @@ -134,12 +134,12 @@ func expectedIpAddressesDelete(ipamMock *mock_interfaces.MockIpamInterface, catc catchUnexpectedParams <- err return &ipam.IpamIPAddressesDeleteNoContent{}, err } - fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesDelete was called with expected input\n") + fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesDelete was called with mock input\n") return &ipam.IpamIPAddressesDeleteNoContent{}, nil - }).Times(1) + }).MinTimes(1) } -func expectedIpamIPAddressesUpdate(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpamIPAddressesUpdate(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesUpdate(gomock.Any(), nil). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesUpdateOK, error) { got := params.(*ipam.IpamIPAddressesUpdateParams) @@ -154,7 +154,22 @@ func expectedIpamIPAddressesUpdate(ipamMock *mock_interfaces.MockIpamInterface, }).MinTimes(1) } -func expectedIpamIPAddressesUpdateFail(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpamIPAddressesUpdateWithHash(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { + ipamMock.EXPECT().IpamIPAddressesUpdate(gomock.Any(), nil). + DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesUpdateOK, error) { + got := params.(*ipam.IpamIPAddressesUpdateParams) + diff := deep.Equal(got, ExpectedIpAddressUpdateWithHashParams) + if len(diff) > 0 { + err := fmt.Errorf("netboxmock: unexpected call to ipam.IpamIPAddressesUpdate (with hash), diff to expected params diff: %+v", diff) + catchUnexpectedParams <- err + return &ipam.IpamIPAddressesUpdateOK{}, err + } + fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesUpdate (with hash) was called with mock input\n") + return &ipam.IpamIPAddressesUpdateOK{Payload: mockedResponseIPAddress()}, nil + }).MinTimes(1) +} + +func mockIpamIPAddressesUpdateFail(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesUpdate(gomock.Any(), nil). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesUpdateOK, error) { got := params.(*ipam.IpamIPAddressesUpdateParams) @@ -169,7 +184,7 @@ func expectedIpamIPAddressesUpdateFail(ipamMock *mock_interfaces.MockIpamInterfa }).MinTimes(1) } -func expectedIpamIPAddressesCreate(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { +func mockIpamIPAddressesCreate(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { ipamMock.EXPECT().IpamIPAddressesCreate(gomock.Any(), nil). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesCreateCreated, error) { got := params.(*ipam.IpamIPAddressesCreateParams) @@ -181,14 +196,29 @@ func expectedIpamIPAddressesCreate(ipamMock *mock_interfaces.MockIpamInterface, } fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesCreate was called with expected input\n") return &ipam.IpamIPAddressesCreateCreated{Payload: mockedResponseIPAddress()}, nil - }).Times(1) + }).MinTimes(1) +} + +func mockIpamIPAddressesCreateWithHash(ipamMock *mock_interfaces.MockIpamInterface, catchUnexpectedParams chan error) { + ipamMock.EXPECT().IpamIPAddressesCreate(gomock.Any(), nil). + DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*ipam.IpamIPAddressesCreateCreated, error) { + got := params.(*ipam.IpamIPAddressesCreateParams) + diff := deep.Equal(got, ExpectedIpAddressesCreateWithHashParams) + if len(diff) > 0 { + err := fmt.Errorf("netboxmock: unexpected call to ipam.IpamIPAddressesCreate (with hash), diff to expected params diff: %+v", diff) + catchUnexpectedParams <- err + return &ipam.IpamIPAddressesCreateCreated{}, err + } + fmt.Printf("NETBOXMOCK\t ipam.IpamIPAddressesCreate (with hash) was called with expected input\n") + return &ipam.IpamIPAddressesCreateCreated{Payload: mockedResponseIPAddress()}, nil + }).MinTimes(1) } // ----------------------------- // Tenancy Mock Functions // ----------------------------- -func expectedTenancyTenancyTenantsList(tenancyMock *mock_interfaces.MockTenancyInterface, catchUnexpectedParams chan error) { +func mockTenancyTenancyTenantsList(tenancyMock *mock_interfaces.MockTenancyInterface, catchUnexpectedParams chan error) { tenancyMock.EXPECT().TenancyTenantsList(gomock.Any(), nil). DoAndReturn(func(params interface{}, authInfo interface{}, opts ...interface{}) (*tenancy.TenancyTenantsListOK, error) { got := params.(*tenancy.TenancyTenantsListParams) @@ -202,3 +232,25 @@ func expectedTenancyTenancyTenantsList(tenancyMock *mock_interfaces.MockTenancyI return &tenancy.TenancyTenantsListOK{Payload: mockedResponseTenancyTenantsList()}, nil }).MinTimes(1) } + +// ----------------------------- +// Restet Mock Functions +// ----------------------------- + +func resetMockFunctions(ipamMockA *mock_interfaces.MockIpamInterface, ipamMockB *mock_interfaces.MockIpamInterface, tenancyMock *mock_interfaces.MockTenancyInterface) { + ipamMockA.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any()).Times(0) + ipamMockA.EXPECT().IpamIPAddressesUpdate(gomock.Any(), gomock.Any(), nil).Times(0) + ipamMockA.EXPECT().IpamPrefixesList(gomock.Any(), gomock.Any()).Times(0) + ipamMockA.EXPECT().IpamPrefixesAvailableIpsList(gomock.Any(), gomock.Any()).Times(0) + ipamMockA.EXPECT().IpamIPAddressesDelete(gomock.Any(), nil).Times(0) + ipamMockA.EXPECT().IpamIPAddressesUpdate(gomock.Any(), nil).Times(0) + ipamMockA.EXPECT().IpamIPAddressesCreate(gomock.Any(), nil).Times(0) + ipamMockB.EXPECT().IpamIPAddressesList(gomock.Any(), gomock.Any()).Times(0) + ipamMockB.EXPECT().IpamIPAddressesUpdate(gomock.Any(), gomock.Any(), nil).Times(0) + ipamMockB.EXPECT().IpamPrefixesList(gomock.Any(), gomock.Any()).Times(0) + ipamMockB.EXPECT().IpamPrefixesAvailableIpsList(gomock.Any(), gomock.Any()).Times(0) + ipamMockB.EXPECT().IpamIPAddressesDelete(gomock.Any(), nil).Times(0) + ipamMockB.EXPECT().IpamIPAddressesUpdate(gomock.Any(), nil).Times(0) + ipamMockB.EXPECT().IpamIPAddressesCreate(gomock.Any(), nil).Times(0) + tenancyMock.EXPECT().TenancyTenantsList(gomock.Any(), nil).Times(0) +} diff --git a/internal/controller/ipaddress_controller_test.go b/internal/controller/ipaddress_controller_test.go index fa7227ba..de2d42cb 100644 --- a/internal/controller/ipaddress_controller_test.go +++ b/internal/controller/ipaddress_controller_test.go @@ -18,103 +18,45 @@ package controller import ( "context" - "strings" - "sync" "time" "github.com/netbox-community/netbox-operator/gen/mock_interfaces" - "github.com/netbox-community/netbox-operator/pkg/netbox/api" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/swisscom/leaselocker" - "go.uber.org/mock/gomock" apismeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" netboxv1 "github.com/netbox-community/netbox-operator/api/v1" - ctrl "sigs.k8s.io/controller-runtime" ) -var _ = Describe("IpAddress Controller", func() { +var _ = Describe("IpAddress Controller", Ordered, func() { const timeout = time.Second * 4 const interval = time.Millisecond * 250 - var ctx context.Context - var cancel context.CancelFunc - var ipamMock *mock_interfaces.MockIpamInterface - var tenancyMock *mock_interfaces.MockTenancyInterface var unexpectedCallCh chan error - var managerWG sync.WaitGroup BeforeEach(func() { - mockCtrl = gomock.NewController(GinkgoT()) - - ipamMock = mock_interfaces.NewMockIpamInterface(mockCtrl) - tenancyMock = mock_interfaces.NewMockTenancyInterface(mockCtrl) - - netboxClient := &api.NetboxClient{ - Ipam: ipamMock, - Tenancy: tenancyMock, - } - - k8sManager, err := ctrl.NewManager(cfg, k8sManagerOptions) - Expect(k8sManager.GetConfig()).NotTo(BeNil()) - Expect(err).ToNot(HaveOccurred()) - - err = (&IpAddressReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("ip-address-claim-controller"), - NetboxClient: netboxClient, - OperatorNamespace: OperatorNamespace, - RestConfig: k8sManager.GetConfig(), - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - // Initialize the channel to catch mock calls with unexpected parameters unexpectedCallCh = make(chan error) - managerWG := sync.WaitGroup{} - managerWG.Add(1) - - go func() { - defer GinkgoRecover() - ctx, cancel = context.WithCancel(context.TODO()) - defer func() { cancel() }() - - err := k8sManager.Start(ctx) - defer managerWG.Done() - - // fail the test if the manager stops unexpectedly - isUnexpectedManagerErr := false - if err != nil && ctx.Err() == nil { - isUnexpectedManagerErr = true - } - Expect(isUnexpectedManagerErr).To(BeFalse()) - }() }) AfterEach(func() { - cancel() - mockCtrl.Finish() - - managerWG.Wait() - time.Sleep(2 * time.Second) + By("Resetting the mock controller") + resetMockFunctions(ipamMockIpAddress, ipamMockIpAddressClaim, tenancyMock) }) DescribeTable("Reconciler (ip address CR without owner reference)", func( cr *netboxv1.IpAddress, // our CR as typed object - IpamMocks []func(*mock_interfaces.MockIpamInterface, chan error), + IpamMocksIpAddress []func(*mock_interfaces.MockIpamInterface, chan error), TenancyMocks []func(*mock_interfaces.MockTenancyInterface, chan error), expectedConditionReady bool, // Expected state of the ConditionReady condition expectedCRStatus netboxv1.IpAddressStatus, // Expected status of the CR ) { By("Setting up mocks") - for _, mock := range IpamMocks { - mock(ipamMock, unexpectedCallCh) + for _, mock := range IpamMocksIpAddress { + mock(ipamMockIpAddress, unexpectedCallCh) } for _, mock := range TenancyMocks { mock(tenancyMock, unexpectedCallCh) @@ -170,149 +112,44 @@ var _ = Describe("IpAddress Controller", func() { Entry("Create IpAddress CR, reserve new ip address in NetBox, ", defaultIpAddressCR(false), []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithIpAddressFilterEmptyResult, - expectedIpAddressListWithIpAddressFilter, - expectedIpamIPAddressesCreate, - expectedIpamIPAddressesUpdate, - expectedIpAddressesDelete, + mockIpAddressListWithIpAddressFilterEmptyResult, + mockIpamIPAddressesCreate, + mockIpAddressesDelete, }, []func(*mock_interfaces.MockTenancyInterface, chan error){ - expectedTenancyTenancyTenantsList, + mockTenancyTenancyTenantsList, }, true, ExpectedIpAddressStatus), - Entry("Create IpAddress CR, reserve new ip address in NetBox, preserved in netbox, ", + Entry("Create IpAddress CR, ip address already reserved in NetBox, preserved in netbox, ", defaultIpAddressCR(true), []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithIpAddressFilter, - expectedIpamIPAddressesUpdate, + mockIpAddressListWithIpAddressFilter, + mockIpamIPAddressesUpdate, }, []func(*mock_interfaces.MockTenancyInterface, chan error){ - expectedTenancyTenancyTenantsList, + mockTenancyTenancyTenantsList, }, true, ExpectedIpAddressStatus), Entry("Create IpAddress CR, ip address already reserved in NetBox", defaultIpAddressCR(false), []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithIpAddressFilter, - expectedIpamIPAddressesUpdate, - expectedIpAddressesDelete, + mockIpAddressListWithIpAddressFilter, + mockIpamIPAddressesUpdate, + mockIpAddressesDelete, }, []func(*mock_interfaces.MockTenancyInterface, chan error){ - expectedTenancyTenancyTenantsList, + mockTenancyTenancyTenantsList, }, true, ExpectedIpAddressStatus), Entry("Create IpAddress CR, reserve or update failure", defaultIpAddressCR(false), []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithIpAddressFilter, - expectedIpamIPAddressesUpdateFail, + mockIpAddressListWithIpAddressFilter, + mockIpamIPAddressesUpdateFail, }, []func(*mock_interfaces.MockTenancyInterface, chan error){ - expectedTenancyTenancyTenantsList, + mockTenancyTenancyTenantsList, }, false, ExpectedIpAddressFailedStatus), ) - - DescribeTable("Reconciler (ip address CR with owner reference)", func( - ipcr *netboxv1.IpAddress, // our CR as typed object - ipccr *netboxv1.IpAddressClaim, - IpamMocks []func(*mock_interfaces.MockIpamInterface, chan error), - TenancyMocks []func(*mock_interfaces.MockTenancyInterface, chan error), - expectedConditionReady bool, // Expected state of the ConditionReady condition - expectedCRStatus netboxv1.IpAddressStatus, // Expected status of the CR - ) { - By("Setting up mocks") - for _, mock := range IpamMocks { - mock(ipamMock, unexpectedCallCh) - } - for _, mock := range TenancyMocks { - mock(tenancyMock, unexpectedCallCh) - } - - catchCtx, catchCtxCancel := context.WithCancel(context.Background()) - defer catchCtxCancel() - - // Goroutine to monitor mock calls with unexpected parameters - go func() { - defer GinkgoRecover() - select { - case errMsg := <-unexpectedCallCh: - Fail(errMsg.Error()) - - case <-catchCtx.Done(): - // Context was cancelled - } - }() - - // Lock Parent Prefix - - parentPrefixName := strings.Replace(ipccr.Spec.ParentPrefix, "/", "-", -1) - - leaseLockerNSN := types.NamespacedName{Name: parentPrefixName, Namespace: "default"} - ll, err := leaselocker.NewLeaseLocker(cfg, leaseLockerNSN, "default/ipaddress-test") - Expect(err).To(BeNil()) - - // if the reconciliation of a new ipaddress CR with an owner reference starts - // ipaddressclaim controller should create a leaselock on the prefix where the - // ipaddress controller will reserve the ip address - lockCtx, cancel := context.WithCancel(ctx) - defer cancel() - Expect(ll.TryLock(lockCtx)).To(BeTrue()) - - // Create our CRs - By("Creating IpAddressClaim CR") - Expect(k8sClient.Create(ctx, ipccr)).Should(Succeed()) - - // Add owner reference to ip address CR and crete the resource - Expect(controllerutil.SetOwnerReference(ipccr, ipcr, scheme.Scheme)).Should(Succeed()) - By("Creating IpAddress CR") - Expect(k8sClient.Create(ctx, ipcr)).Should(Succeed()) - - // Check that reconcile loop did run a least once by checking that conditions are set - createdCR := &netboxv1.IpAddress{} - Eventually(func() bool { - err := k8sClient.Get(ctx, types.NamespacedName{Name: ipcr.GetName(), Namespace: ipcr.GetNamespace()}, createdCR) - return err == nil && len(createdCR.Status.Conditions) > 0 - }, timeout, interval).Should(BeTrue()) - - // Now check if conditions are set as expected - createdCR = &netboxv1.IpAddress{} - Eventually(func() bool { - err := k8sClient.Get(ctx, types.NamespacedName{Name: ipcr.GetName(), Namespace: ipcr.GetNamespace()}, createdCR) - return err == nil && - apismeta.IsStatusConditionTrue(createdCR.Status.Conditions, netboxv1.ConditionIpaddressReadyTrue.Type) == expectedConditionReady - }, timeout, interval).Should(BeTrue()) - - // Check that the expected ip address is present in the status - Expect(createdCR.Status.IpAddressId).To(Equal(expectedCRStatus.IpAddressId)) - - // Cleanup the netbox resources - Expect(k8sClient.Delete(ctx, createdCR)).Should(Succeed()) - Expect(k8sClient.Delete(ctx, ipccr)).Should(Succeed()) - - // Wait until the resource is deleted to make sure that it will not interfere with the next test case - Eventually(func() bool { - err := k8sClient.Get(ctx, types.NamespacedName{Name: ipcr.GetName(), Namespace: ipcr.GetNamespace()}, createdCR) - return err != client.IgnoreNotFound(err) - }, timeout, interval).Should(BeTrue()) - - Eventually(func() bool { - err := k8sClient.Get(ctx, types.NamespacedName{Name: ipccr.GetName(), Namespace: ipccr.GetNamespace()}, ipccr) - return err != client.IgnoreNotFound(err) - }, timeout, interval).Should(BeTrue()) - }, - Entry("Create IpAddress CR with owner reference, reserve new ip address in NetBox, ", - defaultIpAddressCR(false), defaultIpAddressClaimCR(), - []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithIpAddressFilterEmptyResult, - expectedIpAddressListWithIpAddressFilter, - expectedIpamIPAddressesCreate, - expectedIpamIPAddressesUpdate, - expectedIpAddressesDelete, - }, - []func(*mock_interfaces.MockTenancyInterface, chan error){ - expectedTenancyTenancyTenantsList, - }, - true, ExpectedIpAddressStatus), - ) }) diff --git a/internal/controller/ipaddressclaim_controller_test.go b/internal/controller/ipaddressclaim_controller_test.go index 6a161f3f..6c4bcda0 100644 --- a/internal/controller/ipaddressclaim_controller_test.go +++ b/internal/controller/ipaddressclaim_controller_test.go @@ -23,92 +23,42 @@ import ( "time" "github.com/netbox-community/netbox-operator/gen/mock_interfaces" - "github.com/netbox-community/netbox-operator/pkg/netbox/api" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/swisscom/leaselocker" - "go.uber.org/mock/gomock" apierrors "k8s.io/apimachinery/pkg/api/errors" apismeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" netboxv1 "github.com/netbox-community/netbox-operator/api/v1" - ctrl "sigs.k8s.io/controller-runtime" ) -var _ = Describe("IpAddress Controller", func() { +var _ = Describe("IpAddressClaim Controller", Ordered, func() { const timeout = time.Second * 4 const interval = time.Millisecond * 250 - var ctx context.Context - var cancel context.CancelFunc - var ipamMock *mock_interfaces.MockIpamInterface - var tenancyMock *mock_interfaces.MockTenancyInterface var unexpectedCallCh chan error - var managerWG sync.WaitGroup BeforeEach(func() { - mockCtrl = gomock.NewController(GinkgoT()) - - ipamMock = mock_interfaces.NewMockIpamInterface(mockCtrl) - tenancyMock = mock_interfaces.NewMockTenancyInterface(mockCtrl) - - netboxClient := &api.NetboxClient{ - Ipam: ipamMock, - Tenancy: tenancyMock, - } - - k8sManager, err := ctrl.NewManager(cfg, k8sManagerOptions) - Expect(k8sManager.GetConfig()).NotTo(BeNil()) - Expect(err).ToNot(HaveOccurred()) - - err = (&IpAddressClaimReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("ip-address-claim-controller"), - NetboxClient: netboxClient, - OperatorNamespace: OperatorNamespace, - RestConfig: k8sManager.GetConfig(), - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - // Initialize the channel to catch mock calls with unexpected parameters unexpectedCallCh = make(chan error) managerWG := sync.WaitGroup{} managerWG.Add(1) - - go func() { - defer GinkgoRecover() - ctx, cancel = context.WithCancel(context.TODO()) - defer func() { cancel() }() - - err := k8sManager.Start(ctx) - defer managerWG.Done() - - // fail the test if the manager stops unexpectedly - isUnexpectedManagerErr := false - if err != nil && ctx.Err() == nil { - isUnexpectedManagerErr = true - } - Expect(isUnexpectedManagerErr).To(BeFalse()) - }() }) AfterEach(func() { - cancel() - mockCtrl.Finish() - - managerWG.Wait() - time.Sleep(2 * time.Second) + By("Resetting the mock controller") + resetMockFunctions(ipamMockIpAddress, ipamMockIpAddressClaim, tenancyMock) }) - DescribeTable("Reconciler (ip address claim CR, ip address CR does not yet exist)", func( + DescribeTable("Reconciler (ip address claim CR)", func( cr *netboxv1.IpAddressClaim, // our CR as typed object ipcr *netboxv1.IpAddress, // ip address CR expected to be created by ip address claim controller ipcrMockStatus netboxv1.IpAddressStatus, // the that will be added to mock the ip address controller - IpamMocks []func(*mock_interfaces.MockIpamInterface, chan error), + IpamMocksIpAddressClaim []func(*mock_interfaces.MockIpamInterface, chan error), + IpamMocksIpAddress []func(*mock_interfaces.MockIpamInterface, chan error), TenancyMocks []func(*mock_interfaces.MockTenancyInterface, chan error), expectedConditionReady bool, // Expected state of the ConditionReady condition expectedConditionIpAssigned bool, // Expected state of the ConditionReady condition @@ -116,8 +66,11 @@ var _ = Describe("IpAddress Controller", func() { prefixLockedByOtherOwner bool, // If prefix is locked by other owner when ipaddress claim CR is created ) { By("Setting up mocks") - for _, mock := range IpamMocks { - mock(ipamMock, unexpectedCallCh) + for _, mock := range IpamMocksIpAddressClaim { + mock(ipamMockIpAddressClaim, unexpectedCallCh) + } + for _, mock := range IpamMocksIpAddress { + mock(ipamMockIpAddress, unexpectedCallCh) } for _, mock := range TenancyMocks { mock(tenancyMock, unexpectedCallCh) @@ -145,8 +98,8 @@ var _ = Describe("IpAddress Controller", func() { ll, err := leaselocker.NewLeaseLocker(cfg, leaseLockerNSN, "default/some-other-owner") Expect(err).To(BeNil()) - lockCtx, cancel := context.WithCancel(ctx) - defer cancel() + lockCtx, lockCancel := context.WithCancel(ctx) + defer lockCancel() locked := ll.TryLock(lockCtx) Expect(locked).To(BeTrue()) @@ -183,11 +136,6 @@ var _ = Describe("IpAddress Controller", func() { // check that the ip address claim controller created the ip address CR with correct spec Expect(createdIpCR.Spec).To(Equal(ipcr.Spec)) - By("Mocking the ip address controller by updating the status of the ip address CR") - createdIpCR.Status = ExpectedIpAddressStatus - apismeta.SetStatusCondition(&createdIpCR.Status.Conditions, netboxv1.ConditionIpaddressReadyTrue) - Eventually(k8sClient.Status().Update(ctx, createdIpCR)).Should(Succeed()) - // Change status of claim to trigger reconciliation (watch on ip address doesn't cause automatic reconciliation with env test) Expect(k8sClient.Get(ctx, types.NamespacedName{Name: cr.GetName(), Namespace: cr.GetNamespace()}, createdCR)).To(Succeed()) apismeta.SetStatusCondition(&cr.Status.Conditions, netboxv1.ConditionIpClaimReadyFalse) @@ -227,23 +175,38 @@ var _ = Describe("IpAddress Controller", func() { Entry("Create IpAddressClaim CR, reserve new ip address in NetBox", defaultIpAddressClaimCR(), defaultIpAddressCreatedByClaim(false), ExpectedIpAddressStatus, []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithHashFilterEmptyResult, - expectedPrefixesListWithPrefixFilter, - expectedPrefixesAvailableIpsList, + mockIpAddressListWithHashFilterEmptyResult, + mockPrefixesListWithPrefixFilter, + mockPrefixesAvailableIpsList, + }, + []func(*mock_interfaces.MockIpamInterface, chan error){ + mockIpAddressListWithIpAddressFilterEmptyResult, + mockIpamIPAddressesCreateWithHash, + mockIpAddressesDelete, + }, + []func(*mock_interfaces.MockTenancyInterface, chan error){ + mockTenancyTenancyTenantsList, }, - nil, true, true, ExpectedIpAddressClaimStatus, false), Entry("Create IpAddressClaim CR, reassign ip from NetBox", defaultIpAddressClaimCR(), defaultIpAddressCreatedByClaim(false), ExpectedIpAddressStatus, []func(*mock_interfaces.MockIpamInterface, chan error){ - expectedIpAddressListWithHashFilter, + mockIpAddressListWithHashFilter, + }, + []func(*mock_interfaces.MockIpamInterface, chan error){ + mockIpAddressListWithIpAddressFilter, + mockIpamIPAddressesUpdateWithHash, + mockIpAddressesDelete, + }, + []func(*mock_interfaces.MockTenancyInterface, chan error){ + mockTenancyTenancyTenantsList, }, - nil, true, true, ExpectedIpAddressClaimStatus, false), Entry("Create IpAddressClaim CR, prefix locked by other resource", defaultIpAddressClaimCR(), defaultIpAddressCreatedByClaim(false), nil, nil, nil, + nil, false, false, netboxv1.IpAddressClaimStatus{}, true), ) }) diff --git a/internal/controller/netbox_testdata_test.go b/internal/controller/netbox_testdata_test.go index 1469aee5..22308da1 100644 --- a/internal/controller/netbox_testdata_test.go +++ b/internal/controller/netbox_testdata_test.go @@ -24,7 +24,6 @@ import ( "github.com/netbox-community/go-netbox/v3/netbox/client/tenancy" netboxModels "github.com/netbox-community/go-netbox/v3/netbox/models" netboxv1 "github.com/netbox-community/netbox-operator/api/v1" - "github.com/netbox-community/netbox-operator/pkg/config" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,8 +42,6 @@ var siteSlug = "mars-ip-claim" var ipAddress = "1.0.0.1/32" var parentPrefix = "1.0.0.0/28" -var customFields = map[string]string{"example_field": "example value"} - var siteId = int64(2) var site = "Mars" @@ -52,6 +49,11 @@ var tenantId = int64(1) var tenant = "test-tenant" var tenantSlug = "test-tenant-slug" +var restorationHash = "6f6c67651f0b43b2969ba2ae35c74fc91815513b" + +var customFields = map[string]string{"example_field": "example value"} +var customFieldsWithHash = map[string]string{"example_field": "example value", "netboxOperatorRestorationHash": restorationHash} + var netboxLabel = "Status" var value = "active" @@ -85,7 +87,7 @@ func defaultIpAddressCreatedByClaim(preserveInNetbox bool) *netboxv1.IpAddress { Spec: netboxv1.IpAddressSpec{ IpAddress: ipAddress, Tenant: tenant, - CustomFields: customFields, + CustomFields: customFieldsWithHash, Comments: comments, Description: description, PreserveInNetbox: preserveInNetbox, @@ -220,7 +222,17 @@ var expectedIpToUpdate = &netboxModels.WritableIPAddress{ Comments: comments + warningComment, CustomFields: map[string]string{ "example_field": "example value", - config.GetOperatorConfig().NetboxRestorationHashFieldName: "a0601ac7e6d196a82c0e61f9be17313113c3043f", + }, + Description: nsn + description + warningComment, + Status: "active", + Tenant: &tenantId} + +var expectedIpToUpdateWithHash = &netboxModels.WritableIPAddress{ + Address: &ipAddress, + Comments: comments + warningComment, + CustomFields: map[string]string{ + "example_field": "example value", + "netboxOperatorRestorationHash": restorationHash, }, Description: nsn + description + warningComment, Status: "active", @@ -229,6 +241,9 @@ var expectedIpToUpdate = &netboxModels.WritableIPAddress{ var ExpectedIpAddressUpdateParams = ipam.NewIpamIPAddressesUpdateParams().WithDefaults(). WithData(expectedIpToUpdate).WithID(expectedIpAddressID) +var ExpectedIpAddressUpdateWithHashParams = ipam.NewIpamIPAddressesUpdateParams().WithDefaults(). + WithData(expectedIpToUpdateWithHash).WithID(expectedIpAddressID) + var ExpectedTenantsListParams = tenancy.NewTenancyTenantsListParams().WithName(&tenant) // expected inputs for ipam.IpamPrefixesList method @@ -247,6 +262,8 @@ var ExpectedIpAddressListParams = ipam.NewIpamIPAddressesListParams() // expected inputs for ipam.IpamIPAddressesCreate method var ExpectedIpAddressesCreateParams = ipam.NewIpamIPAddressesCreateParams().WithDefaults().WithData(expectedIpToUpdate) +var ExpectedIpAddressesCreateWithHashParams = ipam.NewIpamIPAddressesCreateParams().WithDefaults().WithData(expectedIpToUpdateWithHash) + // expected inputs for ipam.IpamIPAddressesDelete method var ExpectedDeleteParams = ipam.NewIpamIPAddressesDeleteParams().WithID(expectedIpAddressID) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 8748e124..82b7db04 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -20,11 +20,11 @@ limitations under the License. package controller import ( + "context" "fmt" "path/filepath" "runtime" "testing" - "time" ctrl "sigs.k8s.io/controller-runtime" @@ -35,6 +35,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/netbox-community/netbox-operator/gen/mock_interfaces" + "github.com/netbox-community/netbox-operator/pkg/netbox/api" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -53,6 +55,12 @@ var k8sClient client.Client var k8sManagerOptions ctrl.Options var testEnv *envtest.Environment var mockCtrl *gomock.Controller +var ipamMockIpAddress *mock_interfaces.MockIpamInterface +var ipamMockIpAddressClaim *mock_interfaces.MockIpamInterface +var tenancyMock *mock_interfaces.MockTenancyInterface +var ctx context.Context +var cancel context.CancelFunc +var netboxClient *api.NetboxClient func TestControllers(t *testing.T) { RegisterFailHandler(Fail) @@ -85,11 +93,9 @@ var _ = BeforeSuite(func() { err = netboxv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - timeout := time.Second * 1 By("defining k8sManager option to disable metrics server") k8sManagerOptions = ctrl.Options{ - Scheme: scheme.Scheme, - GracefulShutdownTimeout: &timeout, + Scheme: scheme.Scheme, Metrics: server.Options{ BindAddress: "0", // Disable the metrics server }, @@ -100,9 +106,56 @@ var _ = BeforeSuite(func() { k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + + mockCtrl = gomock.NewController(GinkgoT(), gomock.WithOverridableExpectations()) + + ipamMockIpAddress = mock_interfaces.NewMockIpamInterface(mockCtrl) + ipamMockIpAddressClaim = mock_interfaces.NewMockIpamInterface(mockCtrl) + tenancyMock = mock_interfaces.NewMockTenancyInterface(mockCtrl) + + k8sManager, err := ctrl.NewManager(cfg, k8sManagerOptions) + Expect(k8sManager.GetConfig()).NotTo(BeNil()) + Expect(err).ToNot(HaveOccurred()) + + err = (&IpAddressReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: k8sManager.GetEventRecorderFor("ip-address-claim-controller"), + NetboxClient: &api.NetboxClient{ + Ipam: ipamMockIpAddress, + Tenancy: tenancyMock, + }, + OperatorNamespace: OperatorNamespace, + RestConfig: k8sManager.GetConfig(), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + err = (&IpAddressClaimReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: k8sManager.GetEventRecorderFor("ip-address-claim-controller"), + NetboxClient: &api.NetboxClient{ + Ipam: ipamMockIpAddressClaim, + Tenancy: tenancyMock, + }, + OperatorNamespace: OperatorNamespace, + RestConfig: k8sManager.GetConfig(), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + ctx, cancel = context.WithCancel(context.TODO()) + defer func() { cancel() }() + + Expect(k8sManager.Start(ctx)).To(Succeed()) + }() }) var _ = AfterSuite(func() { + cancel() + mockCtrl.Finish() + By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred())