Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests #46

Merged
merged 2 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ clab-containerlab-test-setup/
tls/*
testbin/*
testdata/.containerlab.yaml.bak
pkg/*/testdata*

# Test binary, build with `go test -c`
*.test
Expand Down
3 changes: 2 additions & 1 deletion cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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"

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)
_ = syncInterfaceByFamily(intf, unix.AF_INET6, routingTable, toolkit, logger)

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.NewMockToolkitInterface(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.NewMockToolkitInterface(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.NewMockToolkitInterface(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()
})
})
Loading