Skip to content

Commit

Permalink
Added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
p-strusiewiczsurmacki-mobica committed Oct 9, 2023
1 parent 0d20295 commit 0473288
Show file tree
Hide file tree
Showing 28 changed files with 3,701 additions and 376 deletions.
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/telekom/das-schiff-network-operator/pkg/macvlan"
"github.com/telekom/das-schiff-network-operator/pkg/managerconfig"
"github.com/telekom/das-schiff-network-operator/pkg/monitoring"
"github.com/telekom/das-schiff-network-operator/pkg/nl"
"github.com/telekom/das-schiff-network-operator/pkg/notrack"
"github.com/telekom/das-schiff-network-operator/pkg/reconciler"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -138,7 +139,7 @@ func main() {
os.Exit(1)
}

anycastTracker := &anycast.Tracker{}
anycastTracker := anycast.NewTracker(&nl.Toolkit{})

if err = (&networkv1alpha1.VRFRouteConfiguration{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "VRFRouteConfiguration")
Expand Down
48 changes: 30 additions & 18 deletions pkg/anycast/anycast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package anycast

import (
"bytes"
"fmt"
"net"
"time"

"github.com/go-logr/logr"
"github.com/telekom/das-schiff-network-operator/pkg/nl"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
ctrl "sigs.k8s.io/controller-runtime"
Expand All @@ -19,20 +21,26 @@ var (

type Tracker struct {
TrackedBridges []int
toolkit nl.ToolkitInterface
}

func NewTracker(toolkit nl.ToolkitInterface) *Tracker {
return &Tracker{TrackedBridges: []int{},
toolkit: toolkit}
}

// TODO: Anycast Support is currently highly experimental.

func (t *Tracker) checkTrackedInterfaces() {
logger := ctrl.Log.WithName("anycast")
for _, intfIdx := range t.TrackedBridges {
intf, err := netlink.LinkByIndex(intfIdx)
intf, err := t.toolkit.LinkByIndex(intfIdx)
if err != nil {
logger.Error(err, "couldn't load interface", "index", intfIdx)
continue
}

syncInterface(intf.(*netlink.Bridge), logger)
_ = syncInterface(intf.(*netlink.Bridge), t.toolkit, logger)
}
}

Expand Down Expand Up @@ -81,11 +89,11 @@ func filterNeighbors(neighIn []netlink.Neigh) (neighOut []netlink.Neigh) {
return neighOut
}

func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32, logger logr.Logger) {
bridgeNeighbors, err := netlink.NeighList(intf.Attrs().Index, family)
func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32, toolkit nl.ToolkitInterface, logger logr.Logger) error {
bridgeNeighbors, err := toolkit.NeighList(intf.Attrs().Index, family)
if err != nil {
logger.Error(err, "error getting v4 neighbors of interface", "interface", intf.Attrs().Name)
return
return fmt.Errorf("error getting v4 neighbors of interface %s: %w", intf.Attrs().Name, err)
}
bridgeNeighbors = filterNeighbors(bridgeNeighbors)

Expand All @@ -94,16 +102,16 @@ func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32
Table: int(routingTable),
Protocol: anycastRoutesProt,
}
routes, err := netlink.RouteListFiltered(family, routeFilterV4, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL)
routes, err := toolkit.RouteListFiltered(family, routeFilterV4, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL)
if err != nil {
logger.Error(err, "error getting v4 routes of interface", "interface", intf.Attrs().Name)
return
return fmt.Errorf("error getting v4 routes of interface %s: %w", intf.Attrs().Name, err)
}

alreadyV4Existing := []*net.IPNet{}
for i := range routes {
if !containsIPAddress(bridgeNeighbors, routes[i].Dst) {
if err := netlink.RouteDel(&routes[i]); err != nil {
if err := toolkit.RouteDel(&routes[i]); err != nil {
logger.Error(err, "error deleting route", "route", routes[i])
}
} else {
Expand All @@ -112,33 +120,37 @@ func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32
}

for i := range bridgeNeighbors {
ipnet := netlink.NewIPNet(bridgeNeighbors[i].IP)
ipnet := toolkit.NewIPNet(bridgeNeighbors[i].IP)
if !containsIPNetwork(alreadyV4Existing, ipnet) {
route := buildRoute(family, intf, ipnet, routingTable)
if err := netlink.RouteAdd(route); err != nil {
if err := toolkit.RouteAdd(route); err != nil {
logger.Error(err, "error adding route", "route", routes[i])
}
}
}

return nil
}

func syncInterface(intf *netlink.Bridge, logger logr.Logger) {
func syncInterface(intf *netlink.Bridge, toolkit nl.ToolkitInterface, logger logr.Logger) error {
routingTable := uint32(defaultVrfAnycastTable)
if intf.Attrs().MasterIndex > 0 {
nl, err := netlink.LinkByIndex(intf.Attrs().MasterIndex)
link, err := toolkit.LinkByIndex(intf.Attrs().MasterIndex)
if err != nil {
logger.Error(err, "error getting VRF parent of interface", "interface", intf.Attrs().Name)
return
return fmt.Errorf("error getting VRF parent of interface %s: %w", intf.Attrs().Name, err)
}
if nl.Type() != "vrf" {
if link.Type() != "vrf" {
logger.Info("parent of the interface is not a VRF", "interface", intf.Attrs().Name)
return
return fmt.Errorf("parent interface of %s is not a VRF", intf.Attrs().Name)
}
routingTable = nl.(*netlink.Vrf).Table
routingTable = link.(*netlink.Vrf).Table
}

syncInterfaceByFamily(intf, unix.AF_INET, routingTable, logger)
syncInterfaceByFamily(intf, unix.AF_INET6, routingTable, logger)
syncInterfaceByFamily(intf, unix.AF_INET, routingTable, toolkit, logger)

Check failure on line 150 in pkg/anycast/anycast.go

View workflow job for this annotation

GitHub Actions / lint

Error return value is not checked (errcheck)
syncInterfaceByFamily(intf, unix.AF_INET6, routingTable, toolkit, logger)

Check failure on line 151 in pkg/anycast/anycast.go

View workflow job for this annotation

GitHub Actions / lint

Error return value is not checked (errcheck)

return nil
}

func (t *Tracker) RunAnycastSync() {
Expand Down
186 changes: 186 additions & 0 deletions pkg/anycast/anycast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package anycast

import (
"errors"
"net"
"testing"

"github.com/go-logr/logr"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/telekom/das-schiff-network-operator/pkg/nl"
mock_nl "github.com/telekom/das-schiff-network-operator/pkg/nl/mock"
"github.com/vishvananda/netlink"
"go.uber.org/mock/gomock"
ctrl "sigs.k8s.io/controller-runtime"
)

var (
mockctrl *gomock.Controller
logger logr.Logger
)

func TestAnycast(t *testing.T) {
RegisterFailHandler(Fail)
mockctrl = gomock.NewController(t)
defer mockctrl.Finish()
logger = ctrl.Log.WithName("anycast-test")
RunSpecs(t,
"Anycast Suite")
}

var _ = Describe("buildRoute()", func() {
It("builds route", func() {
route := buildRoute(0, &netlink.Bridge{}, &net.IPNet{}, 0)
Expect(route).ToNot(BeNil())
})
})

var _ = Describe("containsIPAddress()", func() {
It("returns false if list does not contain IP address", func() {
result := containsIPAddress([]netlink.Neigh{{IP: net.IPv4(0, 0, 0, 0)}}, &net.IPNet{IP: net.IPv4(1, 1, 1, 1)})
Expect(result).To(BeFalse())
})
It("returns true if list does contain IP address", func() {
result := containsIPAddress([]netlink.Neigh{{IP: net.IPv4(0, 0, 0, 0)}}, &net.IPNet{IP: net.IPv4(0, 0, 0, 0)})
Expect(result).To(BeTrue())
})
})

var _ = Describe("containsIPNetwork()", func() {
It("returns false if list does not contain IP network", func() {
result := containsIPNetwork([]*net.IPNet{{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
&net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 254)})
Expect(result).To(BeFalse())
})
It("returns true if list does contain IP network", func() {
result := containsIPNetwork([]*net.IPNet{{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
&net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)})
Expect(result).To(BeTrue())
})
})

var _ = Describe("filterNeighbors()", func() {
It("returns empty list if flags not as expected", func() {
neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_LEARNED}}
result := filterNeighbors(neighIn)
Expect(result).To(BeEmpty())
})
It("returns empty list if state not as expected", func() {
neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, State: netlink.NUD_INCOMPLETE}}
result := filterNeighbors(neighIn)
Expect(result).To(BeEmpty())
})
It("returns non empty list", func() {
neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED}}
result := filterNeighbors(neighIn)
Expect(result).ToNot(BeEmpty())
})
})

var _ = Describe("syncInterfaceByFamily()", func() {
netlinkMock := mock_nl.NewMockNetlinkToolkitInterface(mockctrl)
It("returns error if cannot get neighbors", func() {
netlinkMock.EXPECT().NeighList(0, 0).Return(nil, errors.New("fake error"))
err := syncInterfaceByFamily(&netlink.Bridge{}, 0, 0, netlinkMock, logger)
Expect(err).To(HaveOccurred())
})
It("returns error if cannot filter routes", func() {
family := 0
linkIndex := 0
netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED}}, nil)
netlinkMock.EXPECT().RouteListFiltered(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("fake error"))
err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger)
Expect(err).To(HaveOccurred())
})
It("returns no error if cannot delete route", func() {
family := 0
linkIndex := 0
route := netlink.Route{Flags: netlink.NTF_EXT_MANAGED, Dst: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))}
netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, IP: net.IPv4(0, 0, 0, 0)}}, nil)
netlinkMock.EXPECT().RouteListFiltered(0, gomock.Any(), netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL).
Return([]netlink.Route{route}, nil)
netlinkMock.EXPECT().RouteDel(gomock.Any()).Return(errors.New("fake error")) // error is only logged, not returned
netlinkMock.EXPECT().NewIPNet(gomock.Any()).Return(netlink.NewIPNet(net.IPv4(1, 1, 1, 1)))
netlinkMock.EXPECT().RouteAdd(gomock.Any()).Return(errors.New("fake error")) // error is only logged, not returned
err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger)
Expect(err).ToNot(HaveOccurred())
})
It("returns no error if appended route", func() {
family := 0
linkIndex := 0
route := netlink.Route{Flags: netlink.NTF_EXT_MANAGED, Dst: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))}
netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, IP: net.IPv4(1, 1, 1, 1)}}, nil)
netlinkMock.EXPECT().RouteListFiltered(0, gomock.Any(), netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL).
Return([]netlink.Route{route}, nil)
netlinkMock.EXPECT().NewIPNet(gomock.Any()).Return(netlink.NewIPNet(net.IPv4(1, 1, 1, 1)))
err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger)
Expect(err).ToNot(HaveOccurred())
})
})

var _ = Describe("syncInterface()", func() {
netlinkMock := mock_nl.NewMockNetlinkToolkitInterface(mockctrl)
It("returns no error if interface's Master Index <= 0", func() {
intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: 0}}
// returning error just to quit syncInterfaceByFamily call
netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2)
err := syncInterface(intf, netlinkMock, logger)
Expect(err).ToNot(HaveOccurred())
})
It("returns error if cannot get interface by index", func() {
masterIndex := 1
intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}}
netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(nil, errors.New("fake error"))
err := syncInterface(intf, netlinkMock, logger)
Expect(err).To(HaveOccurred())
})
It("returns error if could get interface but it's type is not vrf", func() {
masterIndex := 1
intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}}
netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(&netlink.Gretun{}, nil)
err := syncInterface(intf, netlinkMock, logger)
Expect(err).To(HaveOccurred())
})
It("returns no error", func() {
masterIndex := 1
intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}}
netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(&netlink.Vrf{}, nil)
// returning error just to quit syncInterfaceByFamily call
netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2)
err := syncInterface(intf, netlinkMock, logger)
Expect(err).ToNot(HaveOccurred())
})
})

var _ = Describe("NewTracker()", func() {
It("returns new anycast tracker", func() {
toolkit := &nl.Toolkit{}
tracker := NewTracker(toolkit)
Expect(tracker).ToNot(BeNil())
Expect(tracker.toolkit).To(Equal(toolkit))
})
})

var _ = Describe("checkTrackedInterfaces()", func() {
netlinkMock := mock_nl.NewMockNetlinkToolkitInterface(mockctrl)
It("returns no error if cannot get link by index", func() {
netlinkMock.EXPECT().LinkByIndex(gomock.Any()).Return(nil, errors.New("fake error"))
tracker := NewTracker(netlinkMock)
Expect(tracker).ToNot(BeNil())
Expect(tracker.toolkit).To(Equal(netlinkMock))
tracker.TrackedBridges = []int{0}
tracker.checkTrackedInterfaces()
})
It("returns no error", func() {
netlinkMock.EXPECT().LinkByIndex(gomock.Any()).Return(&netlink.Bridge{}, nil)
// returning error just to quit syncInterface call
netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2)
tracker := NewTracker(netlinkMock)
Expect(tracker).ToNot(BeNil())
Expect(tracker.toolkit).To(Equal(netlinkMock))
tracker.TrackedBridges = []int{0}
tracker.checkTrackedInterfaces()
})
})

0 comments on commit 0473288

Please sign in to comment.