From c730c77dd7f71f8689db01b801950ddafc28cc31 Mon Sep 17 00:00:00 2001 From: Rajnish Kumar Date: Thu, 17 Aug 2023 19:21:15 +0530 Subject: [PATCH] UT ipassigner linux Signed-off-by: Rajnish Kumar --- hack/update-codegen-dockerized.sh | 1 + pkg/agent/ipassigner/ip_assigner_linux.go | 23 +- .../ipassigner/ip_assigner_linux_test.go | 362 ++++++++++++++++++ .../responder/testing/mock_responder.go | 108 ++++++ 4 files changed, 487 insertions(+), 7 deletions(-) create mode 100644 pkg/agent/ipassigner/ip_assigner_linux_test.go create mode 100644 pkg/agent/ipassigner/responder/testing/mock_responder.go diff --git a/hack/update-codegen-dockerized.sh b/hack/update-codegen-dockerized.sh index 6c4fa2d6fd4..ba870790718 100755 --- a/hack/update-codegen-dockerized.sh +++ b/hack/update-codegen-dockerized.sh @@ -52,6 +52,7 @@ MOCKGEN_TARGETS=( "pkg/agent/querier AgentQuerier testing" "pkg/agent/route Interface testing" "pkg/agent/ipassigner IPAssigner testing" + "pkg/agent/ipassigner/responder Responder testing" "pkg/agent/secondarynetwork/podwatch InterfaceConfigurator testing" "pkg/agent/secondarynetwork/ipam IPAMDelegator testing" "pkg/agent/servicecidr Interface testing" diff --git a/pkg/agent/ipassigner/ip_assigner_linux.go b/pkg/agent/ipassigner/ip_assigner_linux.go index fa6946338a0..4e6b6a2b128 100644 --- a/pkg/agent/ipassigner/ip_assigner_linux.go +++ b/pkg/agent/ipassigner/ip_assigner_linux.go @@ -33,6 +33,15 @@ import ( "antrea.io/antrea/pkg/agent/util/sysctl" ) +var ( + netlinkAddrAdd = netlink.AddrAdd + netlinkAddrDel = netlink.AddrDel +) + +type advertiseArpNdp func(a *ipAssigner, ip net.IP) + +var advertiseResponder advertiseArpNdp = (*ipAssigner).advertise + // ipAssigner creates a dummy device and assigns IPs to it. // It's supposed to be used in the cases that external IPs should be configured on the system so that they can be used // for SNAT (egress scenario) or DNAT (ingress scenario). A dummy device is used because the IPs just need to be present @@ -154,14 +163,14 @@ func (a *ipAssigner) AssignIP(ip string, forceAdvertise bool) error { if a.assignedIPs.Has(ip) { klog.V(2).InfoS("The IP is already assigned", "ip", ip) if forceAdvertise { - a.advertise(parsedIP) + advertiseResponder(a, parsedIP) } return nil } if a.dummyDevice != nil { addr := util.NewIPNet(parsedIP) - if err := netlink.AddrAdd(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { + if err := netlinkAddrAdd(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { if !errors.Is(err, unix.EEXIST) { return fmt.Errorf("failed to add IP %v to interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } else { @@ -183,7 +192,7 @@ func (a *ipAssigner) AssignIP(ip string, forceAdvertise bool) error { } } // Always advertise the IP when the IP is newly assigned to this Node. - a.advertise(parsedIP) + advertiseResponder(a, parsedIP) a.assignedIPs.Insert(ip) return nil } @@ -218,7 +227,7 @@ func (a *ipAssigner) UnassignIP(ip string) error { if a.dummyDevice != nil { addr := util.NewIPNet(parsedIP) - if err := netlink.AddrDel(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { + if err := netlinkAddrDel(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { if !errors.Is(err, unix.EADDRNOTAVAIL) { return fmt.Errorf("failed to delete IP %v from interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } else { @@ -264,7 +273,7 @@ func (a *ipAssigner) InitIPs(ips sets.Set[string]) error { } for ip := range ips.Difference(assigned) { addr := util.NewIPNet(net.ParseIP(ip)) - if err := netlink.AddrAdd(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { + if err := netlinkAddrAdd(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { if !errors.Is(err, unix.EEXIST) { return fmt.Errorf("failed to add IP %v to interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } @@ -272,7 +281,7 @@ func (a *ipAssigner) InitIPs(ips sets.Set[string]) error { } for ip := range assigned.Difference(ips) { addr := util.NewIPNet(net.ParseIP(ip)) - if err := netlink.AddrDel(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { + if err := netlinkAddrDel(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { if !errors.Is(err, unix.EADDRNOTAVAIL) { return fmt.Errorf("failed to delete IP %v from interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } @@ -291,7 +300,7 @@ func (a *ipAssigner) InitIPs(ips sets.Set[string]) error { if err != nil { return err } - a.advertise(ip) + advertiseResponder(a, ip) } a.assignedIPs = ips.Union(nil) return nil diff --git a/pkg/agent/ipassigner/ip_assigner_linux_test.go b/pkg/agent/ipassigner/ip_assigner_linux_test.go new file mode 100644 index 00000000000..4f2edfec349 --- /dev/null +++ b/pkg/agent/ipassigner/ip_assigner_linux_test.go @@ -0,0 +1,362 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipassigner + +import ( + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vishvananda/netlink" + gomock "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/util/sets" + + respondertest "antrea.io/antrea/pkg/agent/ipassigner/responder/testing" +) + +type AddrRecord struct { + Link netlink.Link + Address []*netlink.Addr +} + +var addedAddresses []*AddrRecord + +func newFakeNetworkInterface() *net.Interface { + return &net.Interface{ + Index: 0, + MTU: 1500, + Name: "eth0", + HardwareAddr: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + } +} + +type dummyDeviceMock struct { + addedIPs []net.IPNet +} + +func (d *dummyDeviceMock) Attrs() *netlink.LinkAttrs { + return &netlink.LinkAttrs{Name: "antrea-dummy0"} +} + +func (d *dummyDeviceMock) AddrAdd(addr *netlink.Addr) error { + ipNet := net.IPNet{ + IP: addr.IPNet.IP, + Mask: net.CIDRMask(32, 32), + } + d.addedIPs = append(d.addedIPs, ipNet) + return nil +} + +func (d *dummyDeviceMock) Type() string { + return "dummy" +} + +func AddrAddDel(link netlink.Link, addr *netlink.Addr) error { + // Find the existing record for the link + var linkRecord *AddrRecord + for _, record := range addedAddresses { + if record.Link == link { + linkRecord = record + break + } + } + + // If the link record doesn't exist, create a new one + if linkRecord == nil { + linkRecord = &AddrRecord{ + Link: link, + Address: []*netlink.Addr{addr}, + } + addedAddresses = append(addedAddresses, linkRecord) + } else { + // Check if the address already exists in the record + var addrIndex = -1 + for i, existingAddr := range linkRecord.Address { + if existingAddr.IPNet.IP.Equal(addr.IPNet.IP) { + addrIndex = i + break + } + } + + if addrIndex != -1 { + // Address already exists, remove it + linkRecord.Address = append(linkRecord.Address[:addrIndex], linkRecord.Address[addrIndex+1:]...) + } else { + // Address doesn't exist, add it + linkRecord.Address = append(linkRecord.Address, addr) + } + } + + return nil +} + +func (a *ipAssigner) advertiseArpNdpResponder(ip net.IP) { +} + +func TestIPAssigner_AssignIP(t *testing.T) { + var err error + + tests := []struct { + name string + ip string + assignedIPs sets.Set[string] + expectedError bool + expectedAssignedIPs sets.Set[string] + }{ + { + name: "Invalid IP", + ip: "abc", + assignedIPs: sets.New[string](), + expectedError: true, + expectedAssignedIPs: sets.New[string](), + }, + { + name: "Assign new IP", + ip: "2.1.1.1", + assignedIPs: sets.New[string](), + expectedAssignedIPs: sets.New[string]("2.1.1.1"), + }, + { + name: "Assign existing IP", + ip: "2.1.1.1", + assignedIPs: sets.New[string]("2.1.1.1"), + expectedAssignedIPs: sets.New[string]("2.1.1.1"), + }, + { + name: "Add more IP", + ip: "2.2.2.1", + assignedIPs: sets.New[string]("2.1.1.1"), + expectedAssignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1"), + }, + { + name: "Assign IPv6", + ip: "2001:db8::1", + assignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1"), + expectedAssignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1", "2001:db8::1"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + controller := gomock.NewController(t) + mockResponder := respondertest.NewMockResponder(controller) + + a := &ipAssigner{ + externalInterface: newFakeNetworkInterface(), + dummyDevice: &dummyDeviceMock{}, + assignedIPs: tt.assignedIPs, + arpResponder: mockResponder, + ndpResponder: mockResponder, + mutex: sync.RWMutex{}, + } + if tt.name != "Invalid IP" && tt.name != "Assign existing IP" { + mockResponder.EXPECT().AddIP(net.ParseIP(tt.ip)).Return(nil) + } + + advertiseArpNdp := advertiseResponder + defer func() { advertiseResponder = advertiseArpNdp }() + advertiseResponder = (*ipAssigner).advertiseArpNdpResponder + + netlinkAddrAddFunc := netlinkAddrAdd + defer func() { netlinkAddrAdd = netlinkAddrAddFunc }() + netlinkAddrAdd = AddrAddDel + + err = a.AssignIP(tt.ip, false) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.expectedAssignedIPs, a.assignedIPs, "Assigned IPs don't match") + }) + } +} + +func TestIPAssigner_UnAssignIP(t *testing.T) { + + tests := []struct { + name string + ip string + assignedIPs sets.Set[string] + expectedError bool + expectedAssignedIPs sets.Set[string] + }{ + { + name: "Invalid IP", + ip: "abc", + assignedIPs: sets.New[string](), + expectedError: true, + expectedAssignedIPs: sets.New[string](), + }, + { + name: "UnassignIP not assigned", + ip: "3.3.3.2", + assignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1"), + expectedAssignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1"), + }, + { + name: "Unassign IPv4", + ip: "2.1.1.1", + assignedIPs: sets.New[string]("2.1.1.1", "2.2.2.1"), + expectedAssignedIPs: sets.New[string]("2.2.2.1"), + }, + { + name: "Unassign IPv6", + ip: "2001:db8::1", + assignedIPs: sets.New[string]("2.2.2.1", "2001:db8::1"), + expectedAssignedIPs: sets.New[string]("2.2.2.1"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + controller := gomock.NewController(t) + mockResponder := respondertest.NewMockResponder(controller) + + a := &ipAssigner{ + externalInterface: newFakeNetworkInterface(), + dummyDevice: &dummyDeviceMock{}, + assignedIPs: tt.assignedIPs, + arpResponder: mockResponder, + ndpResponder: mockResponder, + mutex: sync.RWMutex{}, + } + if tt.name != "Invalid IP" && tt.name != "UnassignIP not assigned" { + mockResponder.EXPECT().RemoveIP(net.ParseIP(tt.ip)).Return(nil) + } + + netlinkAddrAddFunc := netlinkAddrDel + defer func() { netlinkAddrDel = netlinkAddrAddFunc }() + netlinkAddrDel = AddrAddDel + + err := a.UnassignIP(tt.ip) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.expectedAssignedIPs, a.assignedIPs, "Unassigned IPs don't match") + + }) + + } + +} + +func TestIPAssigner_AssignedIPs(t *testing.T) { + + controller := gomock.NewController(t) + mockResponder := respondertest.NewMockResponder(controller) + + a := &ipAssigner{ + externalInterface: newFakeNetworkInterface(), + dummyDevice: &dummyDeviceMock{}, + assignedIPs: sets.New[string]("2.2.2.1", "3.3.3.1"), + arpResponder: mockResponder, + ndpResponder: mockResponder, + mutex: sync.RWMutex{}, + } + + ips := a.AssignedIPs() + + expectedIPs := sets.New[string]("3.3.3.1", "2.2.2.1") + if !ips.Equal(expectedIPs) { + t.Errorf("expected IPs: %v, but got: %v", expectedIPs, ips) + } +} + +func TestIPAssigner_InitIPs(t *testing.T) { + var err error + + tests := []struct { + name string + ips sets.Set[string] + assignedIPs sets.Set[string] + expectedError bool + expectedAssignedIPs sets.Set[string] + }{ + { + name: "InitIPs with new IP", + ips: sets.New[string]("192.168.1.100"), + assignedIPs: sets.New[string]("192.168.1.5", "2.1.1.3"), + expectedAssignedIPs: sets.New[string]("192.168.1.100"), + }, + { + name: "InitIPs with new and old ip", + ips: sets.New[string]("192.168.1.101", "192.168.1.100"), + assignedIPs: sets.New[string]("192.168.1.105", "192.168.1.5", "2.1.1.3"), + expectedAssignedIPs: sets.New[string]("192.168.1.100", "192.168.1.101"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + controller := gomock.NewController(t) + mockResponder := respondertest.NewMockResponder(controller) + + a := &ipAssigner{ + externalInterface: newFakeNetworkInterface(), + dummyDevice: &dummyDeviceMock{}, + assignedIPs: tt.assignedIPs, + arpResponder: mockResponder, + ndpResponder: mockResponder, + } + + for ipStr := range tt.ips { + mockResponder.EXPECT().AddIP(net.ParseIP(ipStr)).Return(nil) + } + + advertiseArpNdp := advertiseResponder + defer func() { advertiseResponder = advertiseArpNdp }() + advertiseResponder = (*ipAssigner).advertiseArpNdpResponder + + netlinkAddrDelFunc := netlinkAddrDel + defer func() { netlinkAddrDel = netlinkAddrDelFunc }() + netlinkAddrDel = AddrAddDel + + err = a.InitIPs(tt.ips) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestIPAssigner_Run(t *testing.T) { + done := make(chan struct{}) + + controller := gomock.NewController(t) + defer controller.Finish() + + mockARPResponder := respondertest.NewMockResponder(controller) + mockNDPResponder := respondertest.NewMockResponder(controller) + + a := &ipAssigner{ + arpResponder: mockARPResponder, + ndpResponder: mockNDPResponder, + } + + mockARPResponder.EXPECT().Run(gomock.Any()) + mockNDPResponder.EXPECT().Run(gomock.Any()) + + go a.Run(done) + close(done) + time.Sleep(100 * time.Millisecond) +} diff --git a/pkg/agent/ipassigner/responder/testing/mock_responder.go b/pkg/agent/ipassigner/responder/testing/mock_responder.go new file mode 100644 index 00000000000..09a9110bf5a --- /dev/null +++ b/pkg/agent/ipassigner/responder/testing/mock_responder.go @@ -0,0 +1,108 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: antrea.io/antrea/pkg/agent/ipassigner/responder (interfaces: Responder) +// +// Generated by this command: +// +// mockgen -copyright_file hack/boilerplate/license_header.raw.txt -destination pkg/agent/ipassigner/responder/testing/mock_responder.go -package testing antrea.io/antrea/pkg/agent/ipassigner/responder Responder +// +// Package testing is a generated GoMock package. +package testing + +import ( + net "net" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockResponder is a mock of Responder interface. +type MockResponder struct { + ctrl *gomock.Controller + recorder *MockResponderMockRecorder +} + +// MockResponderMockRecorder is the mock recorder for MockResponder. +type MockResponderMockRecorder struct { + mock *MockResponder +} + +// NewMockResponder creates a new mock instance. +func NewMockResponder(ctrl *gomock.Controller) *MockResponder { + mock := &MockResponder{ctrl: ctrl} + mock.recorder = &MockResponderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockResponder) EXPECT() *MockResponderMockRecorder { + return m.recorder +} + +// AddIP mocks base method. +func (m *MockResponder) AddIP(arg0 net.IP) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddIP", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddIP indicates an expected call of AddIP. +func (mr *MockResponderMockRecorder) AddIP(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIP", reflect.TypeOf((*MockResponder)(nil).AddIP), arg0) +} + +// InterfaceName mocks base method. +func (m *MockResponder) InterfaceName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterfaceName") + ret0, _ := ret[0].(string) + return ret0 +} + +// InterfaceName indicates an expected call of InterfaceName. +func (mr *MockResponderMockRecorder) InterfaceName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterfaceName", reflect.TypeOf((*MockResponder)(nil).InterfaceName)) +} + +// RemoveIP mocks base method. +func (m *MockResponder) RemoveIP(arg0 net.IP) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveIP", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveIP indicates an expected call of RemoveIP. +func (mr *MockResponderMockRecorder) RemoveIP(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveIP", reflect.TypeOf((*MockResponder)(nil).RemoveIP), arg0) +} + +// Run mocks base method. +func (m *MockResponder) Run(arg0 <-chan struct{}) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Run", arg0) +} + +// Run indicates an expected call of Run. +func (mr *MockResponderMockRecorder) Run(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockResponder)(nil).Run), arg0) +}