diff --git a/pkg/netlink/fake/netlink.go b/pkg/netlink/fake/netlink.go index 0e31c4d281..421942c2b6 100644 --- a/pkg/netlink/fake/netlink.go +++ b/pkg/netlink/fake/netlink.go @@ -302,9 +302,26 @@ func (n *basicType) RuleAdd(rule *netlink.Rule) error { n.mutex.Lock() defer n.mutex.Unlock() + family := func(ip net.IP) int { + if ip.To4() != nil { + return netlink.FAMILY_V4 + } else if ip.To16() != nil { + return netlink.FAMILY_V6 + } + + return 0 + } + + r := *rule + if r.Src != nil { + r.Family = family(r.Src.IP) + } else if r.Dst != nil { + r.Family = family(r.Dst.IP) + } + var added bool - n.rules[rule.Table], added = slices.AppendIfNotPresent(n.rules[rule.Table], *rule, ruleKey) + n.rules[rule.Table], added = slices.AppendIfNotPresent(n.rules[rule.Table], r, ruleKey) if !added { return os.ErrExist } diff --git a/pkg/routeagent_driver/handlers/ovn/fake/ovsdb_client.go b/pkg/routeagent_driver/handlers/ovn/fake/ovsdb_client.go new file mode 100644 index 0000000000..2b1e9e3c23 --- /dev/null +++ b/pkg/routeagent_driver/handlers/ovn/fake/ovsdb_client.go @@ -0,0 +1,249 @@ +/* +SPDX-License-Identifier: Apache-2.0 + +Copyright Contributors to the Submariner project. + +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 fake + +import ( + "context" + "reflect" + "strings" + "sync" + "sync/atomic" + + . "github.com/onsi/gomega" + "github.com/ovn-org/libovsdb/cache" + libovsdbclient "github.com/ovn-org/libovsdb/client" + "github.com/ovn-org/libovsdb/model" + "github.com/ovn-org/libovsdb/ovsdb" + "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/nbdb" + "github.com/submariner-io/admiral/pkg/resource" + "github.com/submariner-io/admiral/pkg/slices" +) + +type OVSDBClient struct { + mutex sync.Mutex + connected atomic.Bool + models map[reflect.Type][]any +} + +func NewOVSDBClient() *OVSDBClient { + return &OVSDBClient{ + models: map[reflect.Type][]any{}, + } +} + +func (c *OVSDBClient) Connect(_ context.Context) error { + c.connected.Store(true) + return nil +} + +func (c *OVSDBClient) Disconnect() { + c.connected.Store(false) +} + +func (c *OVSDBClient) Close() { +} + +func (c *OVSDBClient) Schema() ovsdb.DatabaseSchema { + return ovsdb.DatabaseSchema{} +} + +func (c *OVSDBClient) Cache() *cache.TableCache { + return nil +} + +func (c *OVSDBClient) SetOption(_ libovsdbclient.Option) error { + return nil +} + +func (c *OVSDBClient) Connected() bool { + return c.connected.Load() +} + +func (c *OVSDBClient) DisconnectNotify() chan struct{} { + return make(chan struct{}) +} + +func (c *OVSDBClient) Echo(_ context.Context) error { + return nil +} + +func (c *OVSDBClient) Transact(_ context.Context, _ ...ovsdb.Operation) ([]ovsdb.OperationResult, error) { + return []ovsdb.OperationResult{}, nil +} + +func (c *OVSDBClient) Monitor(_ context.Context, _ *libovsdbclient.Monitor) (libovsdbclient.MonitorCookie, error) { + return libovsdbclient.MonitorCookie{}, nil +} + +func (c *OVSDBClient) MonitorAll(_ context.Context) (libovsdbclient.MonitorCookie, error) { + return libovsdbclient.MonitorCookie{}, nil +} + +func (c *OVSDBClient) MonitorCancel(_ context.Context, _ libovsdbclient.MonitorCookie) error { + return nil +} + +func (c *OVSDBClient) NewMonitor(_ ...libovsdbclient.MonitorOption) *libovsdbclient.Monitor { + return &libovsdbclient.Monitor{ + Method: ovsdb.ConditionalMonitorSinceRPC, + Errors: make([]error, 0), + LastTransactionID: "00000000-0000-0000-0000-000000000000", + } +} + +func (c *OVSDBClient) CurrentEndpoint() string { + return "" +} + +func (c *OVSDBClient) List(_ context.Context, _ interface{}) error { + return nil +} + +func (c *OVSDBClient) WhereCache(predicate interface{}) libovsdbclient.ConditionalAPI { + return &predicateConditionalAPI{client: c, predicate: predicate} +} + +func (c *OVSDBClient) Where(m model.Model, _ ...model.Condition) libovsdbclient.ConditionalAPI { + return &modelConditionalAPI{client: c, model: m} +} + +func (c *OVSDBClient) WhereAll(_ model.Model, _ ...model.Condition) libovsdbclient.ConditionalAPI { + return &noopConditionalAPI{} +} + +func (c *OVSDBClient) Get(_ context.Context, _ model.Model) error { + return nil +} + +func (c *OVSDBClient) Create(models ...model.Model) ([]ovsdb.Operation, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + for _, m := range models { + c.models[reflect.TypeOf(m)] = append(c.models[reflect.TypeOf(m)], m) + } + + return []ovsdb.Operation{}, nil +} + +func (c *OVSDBClient) hasModel(m any) bool { + c.mutex.Lock() + defer c.mutex.Unlock() + + for _, o := range c.models[reflect.TypeOf(m)] { + switch t := m.(type) { + case *nbdb.LogicalRouterPolicy: + if strings.Contains(o.(*nbdb.LogicalRouterPolicy).Match, t.Match) && + reflect.DeepEqual(o.(*nbdb.LogicalRouterPolicy).Nexthop, t.Nexthop) { + return true + } + case *nbdb.LogicalRouterStaticRoute: + if o.(*nbdb.LogicalRouterStaticRoute).IPPrefix == t.IPPrefix { + return true + } + } + } + + return false +} + +func (c *OVSDBClient) AwaitModel(m any) { + Eventually(func() bool { + return c.hasModel(m) + }).Should(BeTrue(), "OVSBD model not found: %s", resource.ToJSON(m)) +} + +func (c *OVSDBClient) AwaitNoModel(m any) { + Eventually(func() bool { + return c.hasModel(m) + }).Should(BeFalse(), "OVSBD model exists: %s", resource.ToJSON(m)) +} + +type noopConditionalAPI struct{} + +func (c noopConditionalAPI) List(_ context.Context, _ interface{}) error { + return nil +} + +func (c noopConditionalAPI) Mutate(_ model.Model, _ ...model.Mutation) ([]ovsdb.Operation, error) { + return []ovsdb.Operation{}, nil +} + +func (c noopConditionalAPI) Update(_ model.Model, _ ...interface{}) ([]ovsdb.Operation, error) { + return []ovsdb.Operation{}, nil +} + +func (c noopConditionalAPI) Delete() ([]ovsdb.Operation, error) { + return []ovsdb.Operation{}, nil +} + +func (c noopConditionalAPI) Wait(_ ovsdb.WaitCondition, _ *int, _ model.Model, _ ...interface{}) ([]ovsdb.Operation, error) { + return []ovsdb.Operation{}, nil +} + +type predicateConditionalAPI struct { + noopConditionalAPI + client *OVSDBClient + predicate any +} + +func list[T any](client *OVSDBClient, t T, p any, result *[]T) { + client.mutex.Lock() + defer client.mutex.Unlock() + + r := []T{} + fn := reflect.ValueOf(p) + + for _, o := range client.models[reflect.TypeOf(t)] { + v := fn.Call([]reflect.Value{reflect.ValueOf(o)}) + if v[0].Bool() { + r = append(r, o.(T)) + } + } + + *result = r +} + +func (c *predicateConditionalAPI) List(_ context.Context, result interface{}) error { + switch r := result.(type) { + case *[]*nbdb.LogicalRouter: + list(c.client, &nbdb.LogicalRouter{}, c.predicate, r) + case *[]*nbdb.LogicalRouterPolicy: + list(c.client, &nbdb.LogicalRouterPolicy{}, c.predicate, r) + case *[]*nbdb.LogicalRouterStaticRoute: + list(c.client, &nbdb.LogicalRouterStaticRoute{}, c.predicate, r) + } + + return nil +} + +type modelConditionalAPI struct { + noopConditionalAPI + client *OVSDBClient + model any +} + +func (c *modelConditionalAPI) Delete() ([]ovsdb.Operation, error) { + c.client.mutex.Lock() + defer c.client.mutex.Unlock() + + c.client.models[reflect.TypeOf(c.model)], _ = slices.Remove(c.client.models[reflect.TypeOf(c.model)], c.model, resource.ToJSON) + + return []ovsdb.Operation{}, nil +} diff --git a/pkg/routeagent_driver/handlers/ovn/gateway_route_handler_test.go b/pkg/routeagent_driver/handlers/ovn/gateway_route_handler_test.go index 3633c6e65d..dc3967632f 100644 --- a/pkg/routeagent_driver/handlers/ovn/gateway_route_handler_test.go +++ b/pkg/routeagent_driver/handlers/ovn/gateway_route_handler_test.go @@ -20,7 +20,6 @@ package ovn_test import ( "errors" - "net" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -29,41 +28,19 @@ import ( submarinerv1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1" "github.com/submariner-io/submariner/pkg/event/testing" "github.com/submariner-io/submariner/pkg/routeagent_driver/handlers/ovn" - "github.com/vishvananda/netlink" ) var _ = Describe("GatewayRouteHandler", func() { t := newTestDriver() - var nextHopIP *net.IPNet - - BeforeEach(func() { - nextHopIP = &net.IPNet{ - IP: []byte{128, 1, 20, 2}, - } - }) - JustBeforeEach(func() { - link := &netlink.GenericLink{ - LinkAttrs: netlink.LinkAttrs{ - Index: 99, - Name: ovn.OVNK8sMgmntIntfName, - }, - } - - t.netLink.SetLinkIndex(ovn.OVNK8sMgmntIntfName, link.Index) - Expect(t.netLink.LinkAdd(link)).To(Succeed()) - Expect(t.netLink.AddrAdd(link, &netlink.Addr{ - IPNet: nextHopIP, - })).To(Succeed()) - t.Start(ovn.NewGatewayRouteHandler(t.submClient)) }) awaitGatewayRoute := func(ep *submarinerv1.Endpoint) { gwRoute := test.AwaitResource(ovn.GatewayResourceInterface(t.submClient, testing.Namespace), ep.Name).(*submarinerv1.GatewayRoute) Expect(gwRoute.RoutePolicySpec.RemoteCIDRs).To(Equal(ep.Spec.Subnets)) - Expect(gwRoute.RoutePolicySpec.NextHops).To(Equal([]string{nextHopIP.IP.String()})) + Expect(gwRoute.RoutePolicySpec.NextHops).To(Equal([]string{t.mgmntIntfIP})) } When("a remote Endpoint is created and deleted on the gateway", func() { diff --git a/pkg/routeagent_driver/handlers/ovn/handler_test.go b/pkg/routeagent_driver/handlers/ovn/handler_test.go new file mode 100644 index 0000000000..5121e956a6 --- /dev/null +++ b/pkg/routeagent_driver/handlers/ovn/handler_test.go @@ -0,0 +1,342 @@ +/* +SPDX-License-Identifier: Apache-2.0 + +Copyright Contributors to the Submariner project. + +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 ovn_test + +import ( + "context" + "net" + "syscall" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + libovsdbclient "github.com/ovn-org/libovsdb/client" + "github.com/ovn-org/libovsdb/model" + "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/nbdb" + "github.com/submariner-io/admiral/pkg/syncer/test" + "github.com/submariner-io/admiral/pkg/watcher" + submarinerv1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1" + "github.com/submariner-io/submariner/pkg/event/testing" + "github.com/submariner-io/submariner/pkg/routeagent_driver/constants" + "github.com/submariner-io/submariner/pkg/routeagent_driver/handlers/ovn" + fakeovn "github.com/submariner-io/submariner/pkg/routeagent_driver/handlers/ovn/fake" + "github.com/vishvananda/netlink" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +const ( + clusterCIDR = "171.0.1.0/24" + serviceCIDR = "181.0.1.0/24" + OVNK8sMgmntIntGw = "100.1.1.1" +) + +var _ = Describe("Handler", func() { + t := newTestDriver() + + var ovsdbClient *fakeovn.OVSDBClient + + BeforeEach(func() { + ovsdbClient = fakeovn.NewOVSDBClient() + + _, _ = ovsdbClient.Create(&nbdb.LogicalRouter{ + Name: ovn.OVNClusterRouter, + }) + }) + + JustBeforeEach(func() { + _, err := t.k8sClient.CoreV1().Pods(testing.Namespace).Create(context.Background(), &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ovn-pod", + Labels: map[string]string{"app": "ovnkube-node"}, + }, + }, metav1.CreateOptions{}) + Expect(err).To(Succeed()) + + Expect(t.netLink.RouteAdd(&netlink.Route{ + LinkIndex: OVNK8sMgmntIntIndex, + Family: syscall.AF_INET, + Dst: toIPNet(clusterCIDR), + Gw: net.ParseIP(OVNK8sMgmntIntGw), + })).To(Succeed()) + + restMapper := test.GetRESTMapperFor(&submarinerv1.GatewayRoute{}, &submarinerv1.NonGatewayRoute{}) + + t.Start(ovn.NewHandler(&ovn.HandlerConfig{ + Namespace: testing.Namespace, + ClusterCIDR: []string{clusterCIDR}, + ServiceCIDR: []string{serviceCIDR}, + SubmClient: t.submClient, + K8sClient: t.k8sClient, + DynClient: t.dynClient, + WatcherConfig: &watcher.Config{ + RestMapper: restMapper, + Client: t.dynClient, + }, + NewOVSDBClient: func(_ model.ClientDBModel, opts ...libovsdbclient.Option) (libovsdbclient.Client, error) { + return ovsdbClient, nil + }, + })) + + Expect(ovsdbClient.Connected()).To(BeTrue()) + }) + + When("a remote Endpoint is created, updated, and deleted", func() { + JustBeforeEach(func() { + // There needs to be some local endpoint before remote endpoints are processed. This may be a remnant and needs to be revisited. + t.CreateEndpoint(testing.NewEndpoint(testing.LocalClusterID, "")) + }) + + It("should correctly update the host network dataplane", func() { + By("Creating remote Endpoint") + + endpoint := t.CreateEndpoint(testing.NewEndpoint("remote-cluster", "host", "192.0.1.0/24", "192.0.2.0/24")) + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitRule(constants.RouteAgentHostNetworkTableID, "", s) + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + + t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, OVNK8sMgmntIntGw) + + By("Updating remote Endpoint") + + oldSubnets := endpoint.Spec.Subnets + endpoint.Spec.Subnets = []string{"192.0.3.0/24"} + t.UpdateEndpoint(endpoint) + + for _, s := range oldSubnets { + t.netLink.AwaitNoRule(constants.RouteAgentHostNetworkTableID, "", s) + } + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitRule(constants.RouteAgentHostNetworkTableID, "", s) + } + + By("Deleting remote Endpoint") + + t.DeleteEndpoint(endpoint.Name) + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitNoRule(constants.RouteAgentHostNetworkTableID, "", s) + } + }) + + Context("on the gateway", func() { + JustBeforeEach(func() { + t.CreateLocalHostEndpoint() + }) + + It("should correctly update the gateway dataplane", func() { + By("Creating remote Endpoint") + + endpoint := t.CreateEndpoint(testing.NewEndpoint("remote-cluster", "host", "192.0.1.0/24", "192.0.2.0/24")) + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + + By("Updating remote Endpoint") + + oldSubnets := endpoint.Spec.Subnets + endpoint.Spec.Subnets = []string{oldSubnets[0], "192.0.3.0/24"} + t.UpdateEndpoint(endpoint) + + for i := 1; i < len(oldSubnets); i++ { + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, oldSubnets[i], clusterCIDR) + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, oldSubnets[i], serviceCIDR) + } + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + + By("Deleting remote Endpoint") + + t.DeleteEndpoint(endpoint.Name) + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + }) + }) + }) + + Context("on gateway transitions", func() { + It("should correctly update the gateway dataplane", func() { + endpoint := t.CreateEndpoint(testing.NewEndpoint("remote-cluster", "host", "192.0.1.0/24", "192.0.2.0/24")) + + By("Creating local gateway Endpoint") + + localEP := t.CreateLocalHostEndpoint() + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + + t.netLink.AwaitGwRoutes(0, constants.RouteAgentInterClusterNetworkTableID, OVNK8sMgmntIntGw) + + By("Deleting local gateway Endpoint") + + t.DeleteEndpoint(localEP.Name) + + for _, s := range endpoint.Spec.Subnets { + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, clusterCIDR) + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, s, serviceCIDR) + } + + t.netLink.AwaitNoGwRoutes(0, constants.RouteAgentInterClusterNetworkTableID, OVNK8sMgmntIntGw) + }) + }) + + When("a GatewayRoute is created and deleted", func() { + It("should correctly reconcile OVN router policies", func() { + client := t.dynClient.Resource(submarinerv1.SchemeGroupVersion.WithResource("gatewayroutes")).Namespace(testing.Namespace) + + gwRoute := &submarinerv1.GatewayRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gateway-route", + }, + RoutePolicySpec: submarinerv1.RoutePolicySpec{ + NextHops: []string{OVNK8sMgmntIntCIDR.IP.String()}, + RemoteCIDRs: []string{"192.0.1.0/24", "192.0.2.0/24"}, + }, + } + + test.CreateResource(client, gwRoute) + + for _, cidr := range gwRoute.RoutePolicySpec.RemoteCIDRs { + ovsdbClient.AwaitModel(&nbdb.LogicalRouterPolicy{ + Match: cidr, + Nexthop: ptr.To(gwRoute.RoutePolicySpec.NextHops[0]), + }) + + ovsdbClient.AwaitModel(&nbdb.LogicalRouterStaticRoute{ + IPPrefix: cidr, + }) + } + + Expect(client.Delete(context.Background(), gwRoute.Name, metav1.DeleteOptions{})).To(Succeed()) + + for _, cidr := range gwRoute.RoutePolicySpec.RemoteCIDRs { + ovsdbClient.AwaitNoModel(&nbdb.LogicalRouterPolicy{ + Match: cidr, + Nexthop: ptr.To(gwRoute.RoutePolicySpec.NextHops[0]), + }) + + ovsdbClient.AwaitNoModel(&nbdb.LogicalRouterStaticRoute{ + IPPrefix: cidr, + }) + } + }) + }) + + When("a NonGatewayRoute is created and deleted", func() { + It("should correctly reconcile OVN router policies", func() { + client := t.dynClient.Resource(submarinerv1.SchemeGroupVersion.WithResource("nongatewayroutes")).Namespace(testing.Namespace) + + nonGWRoute := &submarinerv1.NonGatewayRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nongateway-route", + }, + RoutePolicySpec: submarinerv1.RoutePolicySpec{ + NextHops: []string{"111.1.1.1"}, + RemoteCIDRs: []string{"192.0.1.0/24", "192.0.2.0/24"}, + }, + } + + test.CreateResource(client, nonGWRoute) + + for _, cidr := range nonGWRoute.RoutePolicySpec.RemoteCIDRs { + ovsdbClient.AwaitModel(&nbdb.LogicalRouterPolicy{ + Match: cidr, + Nexthop: ptr.To(nonGWRoute.RoutePolicySpec.NextHops[0]), + }) + } + + Expect(client.Delete(context.Background(), nonGWRoute.Name, metav1.DeleteOptions{})).To(Succeed()) + + for _, cidr := range nonGWRoute.RoutePolicySpec.RemoteCIDRs { + ovsdbClient.AwaitNoModel(&nbdb.LogicalRouterPolicy{ + Match: cidr, + Nexthop: ptr.To(nonGWRoute.RoutePolicySpec.NextHops[0]), + }) + } + }) + }) + + When("the OVN management interface address changes", func() { + JustBeforeEach(func() { + t.CreateLocalHostEndpoint() + t.netLink.AwaitGwRoutes(0, constants.RouteAgentInterClusterNetworkTableID, OVNK8sMgmntIntGw) + + t.CreateEndpoint(testing.NewEndpoint("remote-cluster", "host", "192.0.1.0/24")) + t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, OVNK8sMgmntIntGw) + }) + + It("should update the gateway and host network dataplanes", func() { + Expect(t.netLink.FlushRouteTable(constants.RouteAgentInterClusterNetworkTableID)).To(Succeed()) + Expect(t.netLink.FlushRouteTable(constants.RouteAgentHostNetworkTableID)).To(Succeed()) + + link, err := t.netLink.LinkByName(ovn.OVNK8sMgmntIntfName) + Expect(err).To(Succeed()) + + Expect(t.netLink.AddrDel(link, &netlink.Addr{ + IPNet: OVNK8sMgmntIntCIDR, + })).To(Succeed()) + + newMgmtIPNet := toIPNet("128.2.30.3/24") + Expect(t.netLink.AddrAdd(link, &netlink.Addr{ + IPNet: newMgmtIPNet, + })).To(Succeed()) + + t.netLink.AwaitGwRoutes(0, constants.RouteAgentInterClusterNetworkTableID, OVNK8sMgmntIntGw) + t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, OVNK8sMgmntIntGw) + }) + }) + + Context("on Uninstall", func() { + It("should delete the table rules", func() { + Expect(t.ipTables.ChainExists(constants.FilterTable, ovn.ForwardingSubmarinerFWDChain)).To(BeTrue()) + Expect(t.ipTables.ChainExists(constants.FilterTable, ovn.ForwardingSubmarinerMSSClampChain)).To(BeTrue()) + + _ = t.netLink.RuleAdd(&netlink.Rule{ + Table: constants.RouteAgentHostNetworkTableID, + Family: netlink.FAMILY_V4, + }) + + _ = t.netLink.RuleAdd(&netlink.Rule{ + Table: constants.RouteAgentInterClusterNetworkTableID, + Family: netlink.FAMILY_V4, + }) + + Expect(t.handler.Uninstall()).To(Succeed()) + + t.netLink.AwaitNoRule(constants.RouteAgentHostNetworkTableID, "", "") + t.netLink.AwaitNoRule(constants.RouteAgentInterClusterNetworkTableID, "", "") + + Expect(t.ipTables.ChainExists(constants.FilterTable, ovn.ForwardingSubmarinerFWDChain)).To(BeFalse()) + }) + }) +}) diff --git a/pkg/routeagent_driver/handlers/ovn/ovn_suite_test.go b/pkg/routeagent_driver/handlers/ovn/ovn_suite_test.go index 52f04517fd..ec9992f7e7 100644 --- a/pkg/routeagent_driver/handlers/ovn/ovn_suite_test.go +++ b/pkg/routeagent_driver/handlers/ovn/ovn_suite_test.go @@ -21,29 +21,45 @@ package ovn_test import ( "context" "encoding/json" + "net" "os" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/submariner-io/admiral/pkg/log/kzerolog" + submV1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1" fakesubm "github.com/submariner-io/submariner/pkg/client/clientset/versioned/fake" "github.com/submariner-io/submariner/pkg/event" eventtesting "github.com/submariner-io/submariner/pkg/event/testing" + "github.com/submariner-io/submariner/pkg/iptables" + fakeIPT "github.com/submariner-io/submariner/pkg/iptables/fake" netlinkAPI "github.com/submariner-io/submariner/pkg/netlink" fakenetlink "github.com/submariner-io/submariner/pkg/netlink/fake" "github.com/submariner-io/submariner/pkg/routeagent_driver/constants" + "github.com/submariner-io/submariner/pkg/routeagent_driver/handlers/ovn" + "github.com/vishvananda/netlink" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + fakedynamic "k8s.io/client-go/dynamic/fake" fakek8s "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" ) +const ( + OVNK8sMgmntIntIndex = 99 +) + +var OVNK8sMgmntIntCIDR = toIPNet("128.1.20.2/24") + func init() { kzerolog.AddFlags(nil) } var _ = BeforeSuite(func() { kzerolog.InitK8sLogging() + Expect(submV1.AddToScheme(scheme.Scheme)).To(Succeed()) }) func TestOvn(t *testing.T) { @@ -55,8 +71,12 @@ type testDriver struct { *eventtesting.ControllerSupport submClient *fakesubm.Clientset k8sClient *fakek8s.Clientset + dynClient *fakedynamic.FakeDynamicClient netLink *fakenetlink.NetLink + ipTables *fakeIPT.IPTables + handler event.Handler transitSwitchIP string + mgmntIntfIP string } func newTestDriver() *testDriver { @@ -65,14 +85,37 @@ func newTestDriver() *testDriver { } BeforeEach(func() { - t.transitSwitchIP = "192.0.2.0" + t.transitSwitchIP = "190.1.2.0" t.submClient = fakesubm.NewSimpleClientset() t.k8sClient = fakek8s.NewSimpleClientset() + t.dynClient = fakedynamic.NewSimpleDynamicClient(scheme.Scheme) t.netLink = fakenetlink.New() netlinkAPI.NewFunc = func() netlinkAPI.Interface { return t.netLink } + + t.ipTables = fakeIPT.New() + iptables.NewFunc = func() (iptables.Interface, error) { + return t.ipTables, nil + } + + link := &netlink.GenericLink{ + LinkAttrs: netlink.LinkAttrs{ + Index: OVNK8sMgmntIntIndex, + Name: ovn.OVNK8sMgmntIntfName, + }, + } + + t.netLink.SetLinkIndex(ovn.OVNK8sMgmntIntfName, link.Index) + Expect(t.netLink.LinkAdd(link)).To(Succeed()) + + addr := &netlink.Addr{ + IPNet: OVNK8sMgmntIntCIDR, + } + Expect(t.netLink.AddrAdd(link, addr)).To(Succeed()) + + t.mgmntIntfIP = addr.IPNet.IP.String() }) return t @@ -80,6 +123,7 @@ func newTestDriver() *testDriver { func (t *testDriver) Start(handler event.Handler) { t.createNode() + t.handler = handler t.ControllerSupport.Start(handler) } @@ -103,3 +147,10 @@ func (t *testDriver) createNode() { os.Setenv("NODE_NAME", node.Name) } + +func toIPNet(s string) *net.IPNet { + _, n, err := net.ParseCIDR(s) + utilruntime.Must(err) + + return n +}