diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9804264d..32a78107 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -173,6 +173,7 @@ jobs: if [ "${{ matrix.ip-family }}" == "ipv4" ]; then SKIP="$SKIP\|IPV6\|DUALSTACK"; fi if [ "${{ matrix.ip-family }}" == "dual" ]; then SKIP="$SKIP\|IPV6"; fi if [ "${{ matrix.ip-family }}" == "ipv6" ]; then SKIP="$SKIP\|IPV4\|DUALSTACK"; fi + SKIP="$SKIP\|Leaked.*advertising" # TODO fixed by frr 10.0, remove this when https://github.com/metallb/frr-k8s/issues/165 is fixed GINKGO_ARGS="--skip $SKIP" TEST_ARGS="--report-path=/tmp/kind_logs" make e2etests - name: Export kind logs diff --git a/API-DOCS.md b/API-DOCS.md index 8bfa86ae..96cdab0c 100644 --- a/API-DOCS.md +++ b/API-DOCS.md @@ -222,6 +222,22 @@ _Appears in:_ | `lastReloadResult` _string_ | LastReloadResult represents the status of the last configuration update operation by FRR, contains "success" or an error. | | | +#### Import + + + +Import represents the possible imported VRFs to a given router. + + + +_Appears in:_ +- [Router](#router) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `vrf` _string_ | Vrf is the vrf we want to import from | | | + + #### LocalPrefPrefixes @@ -339,6 +355,7 @@ _Appears in:_ | `vrf` _string_ | VRF is the host vrf used to establish sessions from this router. | | | | `neighbors` _[Neighbor](#neighbor) array_ | Neighbors is the list of neighbors we want to establish BGP sessions with. | | | | `prefixes` _string array_ | Prefixes is the list of prefixes we want to advertise from this router instance. | | | +| `imports` _[Import](#import) array_ | Imports is the list of imported VRFs we want for this router / vrf. | | | #### SecretReference diff --git a/api/v1beta1/frrconfiguration_types.go b/api/v1beta1/frrconfiguration_types.go index b6639d21..cb99bfc8 100644 --- a/api/v1beta1/frrconfiguration_types.go +++ b/api/v1beta1/frrconfiguration_types.go @@ -79,6 +79,17 @@ type Router struct { // Prefixes is the list of prefixes we want to advertise from this router instance. // +optional Prefixes []string `json:"prefixes,omitempty"` + + // Imports is the list of imported VRFs we want for this router / vrf. + // +optional + Imports []Import `json:"imports,omitempty"` +} + +// Import represents the possible imported VRFs to a given router. +type Import struct { + // Vrf is the vrf we want to import from + // +optional + VRF string `json:"vrf,omitempty"` } // Neighbor represents a BGP Neighbor we want FRR to connect to. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index e2b484ea..982781e7 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -361,6 +361,21 @@ func (in *FRRNodeStateStatus) DeepCopy() *FRRNodeStateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Import) DeepCopyInto(out *Import) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Import. +func (in *Import) DeepCopy() *Import { + if in == nil { + return nil + } + out := new(Import) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalPrefPrefixes) DeepCopyInto(out *LocalPrefPrefixes) { *out = *in @@ -480,6 +495,11 @@ func (in *Router) DeepCopyInto(out *Router) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Imports != nil { + in, out := &in.Imports, &out.Imports + *out = make([]Import, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Router. diff --git a/charts/frr-k8s/charts/crds/templates/frrk8s.metallb.io_frrconfigurations.yaml b/charts/frr-k8s/charts/crds/templates/frrk8s.metallb.io_frrconfigurations.yaml index ddc09f3a..b6867651 100644 --- a/charts/frr-k8s/charts/crds/templates/frrk8s.metallb.io_frrconfigurations.yaml +++ b/charts/frr-k8s/charts/crds/templates/frrk8s.metallb.io_frrconfigurations.yaml @@ -132,6 +132,18 @@ spec: id: description: ID is the BGP router ID type: string + imports: + description: Imports is the list of imported VRFs we want + for this router / vrf. + items: + description: Import represents the possible imported VRFs + to a given router. + properties: + vrf: + description: Vrf is the vrf we want to import from + type: string + type: object + type: array neighbors: description: Neighbors is the list of neighbors we want to establish BGP sessions with. diff --git a/config/all-in-one/frr-k8s-prometheus.yaml b/config/all-in-one/frr-k8s-prometheus.yaml index 9addabaf..e413c48c 100644 --- a/config/all-in-one/frr-k8s-prometheus.yaml +++ b/config/all-in-one/frr-k8s-prometheus.yaml @@ -147,6 +147,18 @@ spec: id: description: ID is the BGP router ID type: string + imports: + description: Imports is the list of imported VRFs we want + for this router / vrf. + items: + description: Import represents the possible imported VRFs + to a given router. + properties: + vrf: + description: Vrf is the vrf we want to import from + type: string + type: object + type: array neighbors: description: Neighbors is the list of neighbors we want to establish BGP sessions with. diff --git a/config/all-in-one/frr-k8s.yaml b/config/all-in-one/frr-k8s.yaml index 47f0bf03..2885da9f 100644 --- a/config/all-in-one/frr-k8s.yaml +++ b/config/all-in-one/frr-k8s.yaml @@ -147,6 +147,18 @@ spec: id: description: ID is the BGP router ID type: string + imports: + description: Imports is the list of imported VRFs we want + for this router / vrf. + items: + description: Import represents the possible imported VRFs + to a given router. + properties: + vrf: + description: Vrf is the vrf we want to import from + type: string + type: object + type: array neighbors: description: Neighbors is the list of neighbors we want to establish BGP sessions with. diff --git a/config/crd/bases/frrk8s.metallb.io_frrconfigurations.yaml b/config/crd/bases/frrk8s.metallb.io_frrconfigurations.yaml index ddc09f3a..b6867651 100644 --- a/config/crd/bases/frrk8s.metallb.io_frrconfigurations.yaml +++ b/config/crd/bases/frrk8s.metallb.io_frrconfigurations.yaml @@ -132,6 +132,18 @@ spec: id: description: ID is the BGP router ID type: string + imports: + description: Imports is the list of imported VRFs we want + for this router / vrf. + items: + description: Import represents the possible imported VRFs + to a given router. + properties: + vrf: + description: Vrf is the vrf we want to import from + type: string + type: object + type: array neighbors: description: Neighbors is the list of neighbors we want to establish BGP sessions with. diff --git a/e2etests/pkg/address/address.go b/e2etests/pkg/address/address.go new file mode 100644 index 00000000..617ac66d --- /dev/null +++ b/e2etests/pkg/address/address.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier:Apache-2.0 + +package address + +import ( + "net" + + "go.universe.tf/e2etest/pkg/ipfamily" +) + +func FilterForFamily(ips []string, family ipfamily.Family) []string { + if family == ipfamily.DualStack { + return ips + } + var res []string + for _, ip := range ips { + parsedIP, _, _ := net.ParseCIDR(ip) + if parsedIP == nil { + panic("invalid ip") // it's a test after all, should never happen + } + isV4 := (parsedIP.To4() != nil) + if family == ipfamily.IPv4 && isV4 { + res = append(res, ip) + continue + } + if family == ipfamily.IPv6 && !isV4 { + res = append(res, ip) + continue + } + } + return res +} diff --git a/e2etests/pkg/routes/routes.go b/e2etests/pkg/routes/routes.go index cb20c9bc..437d5bb0 100644 --- a/e2etests/pkg/routes/routes.go +++ b/e2etests/pkg/routes/routes.go @@ -4,7 +4,6 @@ package routes import ( "bytes" - "errors" "fmt" "net" @@ -18,55 +17,52 @@ import ( // PodHasPrefixFromContainer tells if the given frr-k8s pod has recevied a route for // the given prefix from the given container. -func PodHasPrefixFromContainer(pod *v1.Pod, frr frrcontainer.FRR, prefix string) bool { +func PodHasPrefixFromContainer(pod *v1.Pod, frr frrcontainer.FRR, vrf, prefix string) bool { _, cidr, _ := net.ParseCIDR(prefix) ipFamily := ipfamily.ForCIDR(cidr) nextHop := frr.Ipv4 if ipFamily == ipfamily.IPv6 { nextHop = frr.Ipv6 } - vrf := frr.RouterConfig.VRF return hasPrefix(pod, ipFamily, cidr, nextHop, vrf) } // CheckNeighborHasPrefix tells if the given frr container has a route toward the given prefix // via the set of node passed to this function. -func CheckNeighborHasPrefix(neighbor frrcontainer.FRR, prefix string, nodes []v1.Node) (bool, error) { +func CheckNeighborHasPrefix(neighbor frrcontainer.FRR, vrf, prefix string, nodes []v1.Node) error { routesV4, routesV6, err := frr.Routes(neighbor) if err != nil { - return false, err + return err } _, cidr, err := net.ParseCIDR(prefix) if err != nil { - return false, err + return err } route, err := routeForCIDR(cidr, routesV4, routesV6) - var notFound RouteNotFoundError - if errors.As(err, ¬Found) { - return false, nil - } if err != nil { - return false, err + return err } cidrFamily := ipfamily.ForCIDR(cidr) - err = frr.RoutesMatchNodes(nodes, route, cidrFamily, neighbor.RouterConfig.VRF) + err = frr.RoutesMatchNodes(nodes, route, cidrFamily, vrf) if err != nil { - return false, nil + return err } - return true, nil + return nil } func cidrsAreEqual(a, b *net.IPNet) bool { return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask) } -type RouteNotFoundError string +type RouteNotFoundError struct { + Route string +} func (e RouteNotFoundError) Error() string { - return string(e) + return fmt.Sprintf("route not found %s", e.Route) } func routeForCIDR(cidr *net.IPNet, routesV4 map[string]frr.Route, routesV6 map[string]frr.Route) (frr.Route, error) { @@ -80,7 +76,7 @@ func routeForCIDR(cidr *net.IPNet, routesV4 map[string]frr.Route, routesV6 map[s return route, nil } } - return frr.Route{}, RouteNotFoundError(fmt.Sprintf("route %s not found", cidr)) + return frr.Route{}, RouteNotFoundError{Route: cidr.String()} } func hasPrefix(pod *v1.Pod, pairingFamily ipfamily.Family, prefix *net.IPNet, nextHop, vrf string) bool { diff --git a/e2etests/tests/graceful_restart.go b/e2etests/tests/graceful_restart.go index 5d064e18..5e59c507 100644 --- a/e2etests/tests/graceful_restart.go +++ b/e2etests/tests/graceful_restart.go @@ -105,10 +105,9 @@ var _ = ginkgo.Describe("Establish BGP session with EnableGracefulRestart", func check := func() error { for _, p := range peersConfig.Peers() { - found, err := routes.CheckNeighborHasPrefix(p.FRR, prefix, nodes) - Expect(err).NotTo(HaveOccurred()) - if !found { - return fmt.Errorf("Neigh %s does not have prefix %s", p.FRR.Name, prefix) + err := routes.CheckNeighborHasPrefix(p.FRR, p.FRR.RouterConfig.VRF, prefix, nodes) + if err != nil { + return fmt.Errorf("Neigh %s does not have prefix %s: %w", p.FRR.Name, prefix, err) } } return nil diff --git a/e2etests/tests/import_vrf.go b/e2etests/tests/import_vrf.go new file mode 100644 index 00000000..1acefc74 --- /dev/null +++ b/e2etests/tests/import_vrf.go @@ -0,0 +1,447 @@ +// SPDX-License-Identifier:Apache-2.0 + +package tests + +import ( + "net" + + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + frrcontainer "go.universe.tf/e2etest/pkg/frr/container" + + frrk8sv1beta1 "github.com/metallb/frr-k8s/api/v1beta1" + "github.com/metallb/frrk8stests/pkg/address" + "github.com/metallb/frrk8stests/pkg/config" + "github.com/metallb/frrk8stests/pkg/dump" + "github.com/metallb/frrk8stests/pkg/infra" + "github.com/metallb/frrk8stests/pkg/k8s" + "github.com/metallb/frrk8stests/pkg/k8sclient" + "github.com/openshift-kni/k8sreporter" + frrconfig "go.universe.tf/e2etest/pkg/frr/config" + "go.universe.tf/e2etest/pkg/ipfamily" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" +) + +var _ = ginkgo.Describe("Leaked routes with import vrfs should work", func() { + var ( + cs clientset.Interface + + updater *config.Updater + reporter *k8sreporter.KubernetesReporter + ) + + ginkgo.AfterEach(func() { + if ginkgo.CurrentSpecReport().Failed() { + testName := ginkgo.CurrentSpecReport().LeafNodeText + dump.K8sInfo(testName, reporter) + dump.BGPInfo(testName, infra.FRRContainers, cs) + } + }) + + ginkgo.BeforeEach(func() { + ginkgo.By("Clearing any previous configuration") + + for _, c := range infra.FRRContainers { + err := c.UpdateBGPConfigFile(frrconfig.Empty) + Expect(err).NotTo(HaveOccurred()) + } + reporter = dump.NewK8sReporter(k8s.FRRK8sNamespace) + var err error + updater, err = config.NewUpdater() + Expect(err).NotTo(HaveOccurred()) + err = updater.Clean() + Expect(err).NotTo(HaveOccurred()) + + cs = k8sclient.New() + }) + + initNeighbors := func(useVrf bool, ipFamily ipfamily.Family) ([]*frrcontainer.FRR, config.PeersConfig, []frrk8sv1beta1.Neighbor) { + frrs := config.ContainersForVRF(infra.FRRContainers, "") + if useVrf { + frrs = config.ContainersForVRF(infra.FRRContainers, infra.VRFName) + } + peersConfig := config.PeersForContainers(frrs, ipFamily) + neighbors := config.NeighborsFromPeers(peersConfig.PeersV4, peersConfig.PeersV6) + return frrs, peersConfig, neighbors + } + + pairWithNodes := func(frrs []*frrcontainer.FRR, family ipfamily.Family, toAdvertise []string) error { + for _, c := range frrs { + err := frrcontainer.PairWithNodes(cs, c, family, func(frr *frrcontainer.FRR) { + frr.NeighborConfig.ToAdvertiseV4 = address.FilterForFamily(toAdvertise, ipfamily.IPv4) + frr.NeighborConfig.ToAdvertiseV6 = address.FilterForFamily(toAdvertise, ipfamily.IPv6) + }) + if err != nil { + return err + } + } + return nil + } + + updateAndCheckPeered := func(config frrk8sv1beta1.FRRConfiguration, peersDefault, peersVRF config.PeersConfig, frrsDefault, frrsVRF []*frrcontainer.FRR, family ipfamily.Family) { + err := updater.Update(peersDefault.Secrets, config) + Expect(err).NotTo(HaveOccurred()) + + err = updater.Update(peersVRF.Secrets, config) + Expect(err).NotTo(HaveOccurred()) + + nodes, err := k8s.Nodes(cs) + Expect(err).NotTo(HaveOccurred()) + + for _, c := range frrsDefault { + ValidateFRRPeeredWithNodes(nodes, c, family) + } + for _, c := range frrsVRF { + ValidateFRRPeeredWithNodes(nodes, c, family) + } + } + + baseConfig := frrk8sv1beta1.FRRConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: k8s.FRRK8sNamespace, + }, + Spec: frrk8sv1beta1.FRRConfigurationSpec{ + BGP: frrk8sv1beta1.BGPConfig{ + Routers: []frrk8sv1beta1.Router{ + { + ASN: infra.FRRK8sASN, + }, + { + ASN: infra.FRRK8sASNVRF, + VRF: infra.VRFName, + }, + }, + }, + }, + } + + prefixesToSelectors := func(prefixes []string) []frrk8sv1beta1.PrefixSelector { + res := []frrk8sv1beta1.PrefixSelector{} + for _, p := range prefixes { + selector := frrk8sv1beta1.PrefixSelector{ + Prefix: p, + LE: 32, + GE: 0, + } + ip, _, err := net.ParseCIDR(p) + if err != nil { + panic(err) + } + if ip.To4() == nil { + selector.LE = 64 + } + res = append(res, selector) + } + return res + } + + ginkgo.Context("while receiving IPs", func() { + + ginkgo.DescribeTable("when the default VRF imports a vrf", func( + ipFamily ipfamily.Family, + toAdvertise, + toAdvertiseVRF []string, + allowMode frrk8sv1beta1.AllowMode) { + + frrsDefault, peersDefault, neighborsDefault := initNeighbors(false, ipFamily) + frrsVRF, peersVRF, neighborsVRF := initNeighbors(true, ipFamily) + + ginkgo.By("pairing with nodes") + err := pairWithNodes(frrsDefault, ipFamily, toAdvertise) + Expect(err).NotTo(HaveOccurred()) + err = pairWithNodes(frrsVRF, ipFamily, toAdvertiseVRF) + Expect(err).NotTo(HaveOccurred()) + + config := *baseConfig.DeepCopy() + config.Spec.BGP.Routers[0].Neighbors = neighborsDefault + config.Spec.BGP.Routers[0].Imports = []frrk8sv1beta1.Import{{VRF: infra.VRFName}} + config.Spec.BGP.Routers[1].Neighbors = neighborsVRF + + for i := range config.Spec.BGP.Routers[0].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[0].Neighbors[i].ToReceive.Allowed.Prefixes = prefixesToSelectors(append(toAdvertise, toAdvertiseVRF...)) + } + config.Spec.BGP.Routers[0].Neighbors[i].ToReceive.Allowed.Mode = allowMode + } + for i := range config.Spec.BGP.Routers[1].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[1].Neighbors[i].ToReceive.Allowed.Prefixes = prefixesToSelectors(toAdvertiseVRF) + } + config.Spec.BGP.Routers[1].Neighbors[i].ToReceive.Allowed.Mode = allowMode + + } + + updateAndCheckPeered(config, peersDefault, peersVRF, frrsDefault, frrsVRF, ipFamily) + + ginkgo.By("validating") + pods, err := k8s.FRRK8sPods(cs) + Expect(err).NotTo(HaveOccurred()) + + for _, frr := range frrsDefault { + ValidateNodesHaveRoutes(pods, *frr, toAdvertise...) + } + for _, frr := range frrsVRF { + // The routes advertised from the peers through red must appear in the default VRF too + ValidateNodesHaveRoutesVRF(pods, *frr, "", toAdvertiseVRF...) + ValidateNodesHaveRoutes(pods, *frr, toAdvertiseVRF...) + } + }, + ginkgo.Entry("with specific IPV4 imports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV4 imports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowAll, + ), + ginkgo.Entry("with specific IPV6 imports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV6 imports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowAll, + ), + ) + + ginkgo.DescribeTable("when the red VRF imports the default VRF", func( + ipFamily ipfamily.Family, + toAdvertise, + toAdvertiseVRF []string, + allowMode frrk8sv1beta1.AllowMode) { + + frrsDefault, peersDefault, neighborsDefault := initNeighbors(false, ipFamily) + frrsVRF, peersVRF, neighborsVRF := initNeighbors(true, ipFamily) + + ginkgo.By("pairing with nodes") + err := pairWithNodes(frrsDefault, ipFamily, toAdvertise) + Expect(err).NotTo(HaveOccurred()) + err = pairWithNodes(frrsVRF, ipFamily, toAdvertiseVRF) + Expect(err).NotTo(HaveOccurred()) + + config := *baseConfig.DeepCopy() + config.Spec.BGP.Routers[0].Neighbors = neighborsDefault + config.Spec.BGP.Routers[1].Neighbors = neighborsVRF + config.Spec.BGP.Routers[1].Imports = []frrk8sv1beta1.Import{{VRF: "default"}} + + for i := range config.Spec.BGP.Routers[0].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[0].Neighbors[i].ToReceive.Allowed.Prefixes = prefixesToSelectors(toAdvertise) + } + config.Spec.BGP.Routers[0].Neighbors[i].ToReceive.Allowed.Mode = allowMode + } + for i := range config.Spec.BGP.Routers[1].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[1].Neighbors[i].ToReceive.Allowed.Prefixes = prefixesToSelectors(append(toAdvertise, toAdvertiseVRF...)) + } + config.Spec.BGP.Routers[1].Neighbors[i].ToReceive.Allowed.Mode = allowMode + } + + updateAndCheckPeered(config, peersDefault, peersVRF, frrsDefault, frrsVRF, ipFamily) + + ginkgo.By("validating") + pods, err := k8s.FRRK8sPods(cs) + Expect(err).NotTo(HaveOccurred()) + + // The routes advertised from the peers through the default must appear in the red VRF too + for _, frr := range frrsDefault { + ValidateNodesHaveRoutes(pods, *frr, toAdvertise...) + ValidateNodesHaveRoutesVRF(pods, *frr, infra.VRFName, toAdvertise...) + } + for _, frr := range frrsVRF { + ValidateNodesHaveRoutes(pods, *frr, toAdvertiseVRF...) + } + }, + ginkgo.Entry("with specific IPV4 imports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV4 imports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowAll, + ), + ginkgo.Entry("with specific IPV6 imports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV6 imports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowAll, + ), + ) + + }) + + ginkgo.Context("while advertising IPs", func() { + + ginkgo.DescribeTable("when the default VRF imports a vrf", func( + ipFamily ipfamily.Family, + toAdvertise, + toAdvertiseVRF []string, + allowMode frrk8sv1beta1.AllowMode) { + + frrsDefault, peersDefault, neighborsDefault := initNeighbors(false, ipFamily) + frrsVRF, peersVRF, neighborsVRF := initNeighbors(true, ipFamily) + + ginkgo.By("pairing with nodes") + err := pairWithNodes(frrsDefault, ipFamily, []string{}) + Expect(err).NotTo(HaveOccurred()) + err = pairWithNodes(frrsVRF, ipFamily, []string{}) + Expect(err).NotTo(HaveOccurred()) + + config := *baseConfig.DeepCopy() + config.Spec.BGP.Routers[0].Neighbors = neighborsDefault + config.Spec.BGP.Routers[0].Prefixes = toAdvertise + config.Spec.BGP.Routers[0].Imports = []frrk8sv1beta1.Import{{VRF: infra.VRFName}} + config.Spec.BGP.Routers[1].Neighbors = neighborsVRF + config.Spec.BGP.Routers[1].Prefixes = toAdvertiseVRF + + for i := range config.Spec.BGP.Routers[0].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[0].Neighbors[i].ToAdvertise.Allowed.Prefixes = append(toAdvertise, toAdvertiseVRF...) + } + config.Spec.BGP.Routers[0].Neighbors[i].ToAdvertise.Allowed.Mode = allowMode + } + for i := range config.Spec.BGP.Routers[1].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[1].Neighbors[i].ToAdvertise.Allowed.Prefixes = toAdvertiseVRF + } + config.Spec.BGP.Routers[1].Neighbors[i].ToAdvertise.Allowed.Mode = allowMode + } + + updateAndCheckPeered(config, peersDefault, peersVRF, frrsDefault, frrsVRF, ipFamily) + + ginkgo.By("validating") + + nodes, err := k8s.Nodes(cs) + Expect(err).NotTo(HaveOccurred()) + + for _, frr := range frrsDefault { + ValidatePrefixesForNeighbor(*frr, nodes, toAdvertise...) + ValidatePrefixesForNeighborVRF(*frr, nodes, "", toAdvertiseVRF...) + } + for _, frr := range frrsVRF { + ValidatePrefixesForNeighbor(*frr, nodes, toAdvertiseVRF...) + } + }, + ginkgo.Entry("with specific IPV4 exports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV4 exports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowAll, + ), + ginkgo.Entry("with specific IPV6 exports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV6 exports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowAll, + ), + ) + + ginkgo.DescribeTable("when the red VRF imports from the default vrf", func( + ipFamily ipfamily.Family, + toAdvertise, + toAdvertiseVRF []string, + allowMode frrk8sv1beta1.AllowMode) { + + frrsDefault, peersDefault, neighborsDefault := initNeighbors(false, ipFamily) + frrsVRF, peersVRF, neighborsVRF := initNeighbors(true, ipFamily) + + ginkgo.By("pairing with nodes") + err := pairWithNodes(frrsDefault, ipFamily, []string{}) + Expect(err).NotTo(HaveOccurred()) + err = pairWithNodes(frrsVRF, ipFamily, []string{}) + Expect(err).NotTo(HaveOccurred()) + + config := *baseConfig.DeepCopy() + config.Spec.BGP.Routers[0].Neighbors = neighborsDefault + config.Spec.BGP.Routers[0].Prefixes = toAdvertise + config.Spec.BGP.Routers[1].Neighbors = neighborsVRF + config.Spec.BGP.Routers[1].Prefixes = toAdvertiseVRF + config.Spec.BGP.Routers[1].Imports = []frrk8sv1beta1.Import{{VRF: "default"}} + + for i := range config.Spec.BGP.Routers[0].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[0].Neighbors[i].ToAdvertise.Allowed.Prefixes = toAdvertise + } + config.Spec.BGP.Routers[0].Neighbors[i].ToAdvertise.Allowed.Mode = allowMode + } + for i := range config.Spec.BGP.Routers[1].Neighbors { + if allowMode == frrk8sv1beta1.AllowRestricted { + config.Spec.BGP.Routers[1].Neighbors[i].ToAdvertise.Allowed.Prefixes = append(toAdvertise, toAdvertiseVRF...) + } + config.Spec.BGP.Routers[1].Neighbors[i].ToAdvertise.Allowed.Mode = allowMode + } + + updateAndCheckPeered(config, peersDefault, peersVRF, frrsDefault, frrsVRF, ipFamily) + + ginkgo.By("validating") + + nodes, err := k8s.Nodes(cs) + Expect(err).NotTo(HaveOccurred()) + + for _, frr := range frrsDefault { + ValidatePrefixesForNeighbor(*frr, nodes, toAdvertise...) + } + for _, frr := range frrsVRF { + ValidatePrefixesForNeighborVRF(*frr, nodes, infra.VRFName, toAdvertiseVRF...) + ValidatePrefixesForNeighbor(*frr, nodes, toAdvertiseVRF...) + } + }, + ginkgo.Entry("with specific IPV4 exports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV4 exports", + ipfamily.IPv4, + []string{"192.168.2.0/24"}, + []string{"192.169.2.0/24"}, + frrk8sv1beta1.AllowAll, + ), + ginkgo.Entry("with specific IPV6 exports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowRestricted, + ), + ginkgo.Entry("with allow all IPV6 exports", + ipfamily.IPv6, + []string{"fc00:f853:ccd:e799::/64"}, + []string{"fc00:f853:ccd:e800::/64"}, + frrk8sv1beta1.AllowAll, + ), + ) + }) +}) diff --git a/e2etests/tests/validate.go b/e2etests/tests/validate.go index 0a058dbd..685a471f 100644 --- a/e2etests/tests/validate.go +++ b/e2etests/tests/validate.go @@ -44,13 +44,17 @@ func ValidateFRRPeeredWithNodes(nodes []corev1.Node, c *frrcontainer.FRR, ipFami } func ValidatePrefixesForNeighbor(neigh frrcontainer.FRR, nodes []v1.Node, prefixes ...string) { + ValidatePrefixesForNeighborVRF(neigh, nodes, neigh.RouterConfig.VRF, prefixes...) +} + +// Validates the given neighbor has the prefixes towards the given VRF +func ValidatePrefixesForNeighborVRF(neigh frrcontainer.FRR, nodes []v1.Node, vrfName string, prefixes ...string) { ginkgo.By(fmt.Sprintf("checking prefixes %v for %s", prefixes, neigh.Name)) Eventually(func() error { for _, prefix := range prefixes { - found, err := routes.CheckNeighborHasPrefix(neigh, prefix, nodes) - Expect(err).NotTo(HaveOccurred()) - if !found { - return fmt.Errorf("Neigh %s does not have prefix %s", neigh.Name, prefix) + err := routes.CheckNeighborHasPrefix(neigh, vrfName, prefix, nodes) + if err != nil { + return fmt.Errorf("Neigh %s does not have prefix %s: %w", neigh.Name, prefix, err) } } return nil @@ -58,17 +62,24 @@ func ValidatePrefixesForNeighbor(neigh frrcontainer.FRR, nodes []v1.Node, prefix } func ValidateNeighborNoPrefixes(neigh frrcontainer.FRR, nodes []v1.Node, prefixes ...string) { + ValidateNeighborNoPrefixesVRF(neigh, nodes, neigh.RouterConfig.VRF, prefixes...) +} + +func ValidateNeighborNoPrefixesVRF(neigh frrcontainer.FRR, nodes []v1.Node, vrfName string, prefixes ...string) { ginkgo.By(fmt.Sprintf("checking prefixes %v not announced to %s", prefixes, neigh.Name)) Eventually(func() error { for _, prefix := range prefixes { - found, err := routes.CheckNeighborHasPrefix(neigh, prefix, nodes) - Expect(err).NotTo(HaveOccurred()) - if found { - return fmt.Errorf("Neigh %s has prefix %s", neigh.Name, prefix) + err := routes.CheckNeighborHasPrefix(neigh, vrfName, prefix, nodes) + if err != nil { + return fmt.Errorf("Neigh %s does not have prefix %s: %w", neigh.Name, prefix, err) } } return nil - }, 5*time.Second, time.Second).ShouldNot(HaveOccurred()) + }, 5*time.Second, time.Second).Should( + MatchError( + Or(ContainSubstring("route not found"), + ContainSubstring("not found in nodes")))) + } func ValidateNeighborCommunityPrefixes(neigh frrcontainer.FRR, community string, prefixes []string, ipfam ipfamily.Family) { @@ -99,12 +110,12 @@ func ValidateNeighborCommunityPrefixes(neigh frrcontainer.FRR, community string, }, 5*time.Second, time.Second).ShouldNot(HaveOccurred()) } -func ValidateNodesHaveRoutes(pods []*v1.Pod, neigh frrcontainer.FRR, prefixes ...string) { +func ValidateNodesHaveRoutesVRF(pods []*v1.Pod, neigh frrcontainer.FRR, vrf string, prefixes ...string) { ginkgo.By(fmt.Sprintf("Checking routes %v from %s", prefixes, neigh.Name)) Eventually(func() error { for _, prefix := range prefixes { for _, pod := range pods { - if !routes.PodHasPrefixFromContainer(pod, neigh, prefix) { + if !routes.PodHasPrefixFromContainer(pod, neigh, vrf, prefix) { return fmt.Errorf("pod %s does not have prefix %s from %s", pod.Name, prefix, neigh.Name) } } @@ -113,12 +124,16 @@ func ValidateNodesHaveRoutes(pods []*v1.Pod, neigh frrcontainer.FRR, prefixes .. }, time.Minute, time.Second).ShouldNot(HaveOccurred()) } +func ValidateNodesHaveRoutes(pods []*v1.Pod, neigh frrcontainer.FRR, prefixes ...string) { + ValidateNodesHaveRoutesVRF(pods, neigh, neigh.RouterConfig.VRF, prefixes...) +} + func ValidateNodesDoNotHaveRoutes(pods []*v1.Pod, neigh frrcontainer.FRR, prefixes ...string) { ginkgo.By(fmt.Sprintf("Checking routes %v not injected from %s", prefixes, neigh.Name)) shouldPassConsistently(func() error { for _, prefix := range prefixes { for _, pod := range pods { - if routes.PodHasPrefixFromContainer(pod, neigh, prefix) { + if routes.PodHasPrefixFromContainer(pod, neigh, neigh.RouterConfig.VRF, prefix) { return fmt.Errorf("pod %s has prefix %s from %s", pod.Name, prefix, neigh.Name) } } diff --git a/internal/controller/api_to_config.go b/internal/controller/api_to_config.go index d6394fdc..03b45084 100644 --- a/internal/controller/api_to_config.go +++ b/internal/controller/api_to_config.go @@ -67,8 +67,31 @@ func apiToFRR(resources ClusterResources, alwaysBlock []net.IPNet) (*frr.Config, } alwaysBlockFRR := alwaysBlockToFRR(alwaysBlock) + routersPrefixes := prefixesForVRFs(cfg.Spec.BGP.Routers) + for _, r := range cfg.Spec.BGP.Routers { - routerCfg, err := routerToFRRConfig(r, alwaysBlockFRR, resources.PasswordSecrets, bfdProfiles) + if err := validatePrefixes(r.Prefixes); err != nil { + return nil, err + } + + if err := validateImportVRFs(r, routersPrefixes); err != nil { + return nil, err + } + + allPrefixes := make([]string, len(r.Prefixes)) + copy(allPrefixes, r.Prefixes) + + importedPrefixes, err := importedPrefixes(r, routersPrefixes) + if err != nil { + return nil, err + } + allPrefixes = append(allPrefixes, importedPrefixes...) + + if err := validateOutgoingPrefixes(allPrefixes, r); err != nil { + return nil, err + } + + routerCfg, err := routerToFRRConfig(r, alwaysBlockFRR, resources.PasswordSecrets, bfdProfiles, allPrefixes) if err != nil { return nil, err } @@ -95,40 +118,32 @@ func apiToFRR(resources ClusterResources, alwaysBlock []net.IPNet) (*frr.Config, return res, nil } -func routerToFRRConfig(r v1beta1.Router, alwaysBlock []frr.IncomingFilter, secrets map[string]corev1.Secret, bfdProfiles map[string]*frr.BFDProfile) (*frr.RouterConfig, error) { +func routerToFRRConfig(r v1beta1.Router, alwaysBlock []frr.IncomingFilter, secrets map[string]corev1.Secret, bfdProfiles map[string]*frr.BFDProfile, routerPrefixes []string) (*frr.RouterConfig, error) { res := &frr.RouterConfig{ MyASN: r.ASN, RouterID: r.ID, VRF: r.VRF, Neighbors: make([]*frr.NeighborConfig, 0), - IPV4Prefixes: make([]string, 0), - IPV6Prefixes: make([]string, 0), - } - - for _, p := range r.Prefixes { - family := ipfamily.ForCIDRString(p) - switch family { - case ipfamily.IPv4: - res.IPV4Prefixes = append(res.IPV4Prefixes, p) - case ipfamily.IPv6: - res.IPV6Prefixes = append(res.IPV6Prefixes, p) - case ipfamily.Unknown: - return nil, fmt.Errorf("unknown ipfamily for %s", p) - } + IPV4Prefixes: ipfamily.FilterPrefixes(r.Prefixes, ipfamily.IPv4), + IPV6Prefixes: ipfamily.FilterPrefixes(r.Prefixes, ipfamily.IPv6), + ImportVRFs: make([]string, 0), } for _, n := range r.Neighbors { - frrNeigh, err := neighborToFRR(n, res.IPV4Prefixes, res.IPV6Prefixes, alwaysBlock, r.VRF, secrets, bfdProfiles) + frrNeigh, err := neighborToFRR(n, routerPrefixes, alwaysBlock, r.VRF, secrets, bfdProfiles) if err != nil { return nil, fmt.Errorf("failed to process neighbor %s for router %d-%s: %w", neighborName(n.ASN, n.Address), r.ASN, r.VRF, err) } res.Neighbors = append(res.Neighbors, frrNeigh) } + for _, v := range r.Imports { + res.ImportVRFs = append(res.ImportVRFs, v.VRF) + } return res, nil } -func neighborToFRR(n v1beta1.Neighbor, ipv4Prefixes, ipv6Prefixes []string, alwaysBlock []frr.IncomingFilter, routerVRF string, passwordSecrets map[string]corev1.Secret, bfdProfiles map[string]*frr.BFDProfile) (*frr.NeighborConfig, error) { +func neighborToFRR(n v1beta1.Neighbor, prefixesInRouter []string, alwaysBlock []frr.IncomingFilter, routerVRF string, passwordSecrets map[string]corev1.Secret, bfdProfiles map[string]*frr.BFDProfile) (*frr.NeighborConfig, error) { neighborFamily, err := ipfamily.ForAddresses(n.Address) if err != nil { return nil, fmt.Errorf("failed to find ipfamily for %s, %w", n.Address, err) @@ -163,7 +178,7 @@ func neighborToFRR(n v1beta1.Neighbor, ipv4Prefixes, ipv6Prefixes []string, alwa if err != nil { return nil, err } - res.Outgoing, err = toAdvertiseToFRR(n.ToAdvertise, ipv4Prefixes, ipv6Prefixes) + res.Outgoing, err = toAdvertiseToFRR(n.ToAdvertise, prefixesInRouter) if err != nil { return nil, err } @@ -202,11 +217,11 @@ func passwordForNeighbor(n v1beta1.Neighbor, passwordSecrets map[string]corev1.S return string(srcPass), nil } -func toAdvertiseToFRR(toAdvertise v1beta1.Advertise, ipv4Prefixes, ipv6Prefixes []string) (frr.AllowedOut, error) { - advsV4, advsV6, err := prefixesToMap(toAdvertise, ipv4Prefixes, ipv6Prefixes) - if err != nil { - return frr.AllowedOut{}, err - } +func toAdvertiseToFRR(toAdvertise v1beta1.Advertise, prefixesInRouter []string) (frr.AllowedOut, error) { + ipv4Prefixes := ipfamily.FilterPrefixes(prefixesInRouter, ipfamily.IPv4) + ipv6Prefixes := ipfamily.FilterPrefixes(prefixesInRouter, ipfamily.IPv6) + advsV4, advsV6 := prefixesToMap(toAdvertise, ipv4Prefixes, ipv6Prefixes) + communities, err := communityPrefixesToMap(toAdvertise.PrefixesWithCommunity) if err != nil { return frr.AllowedOut{}, err @@ -240,7 +255,7 @@ func toAdvertiseToFRR(toAdvertise v1beta1.Advertise, ipv4Prefixes, ipv6Prefixes // prefixesToMap returns two maps of prefix->OutgoingFilter (ie family, advertisement, communities), one for each family. // The ipv4Prefixes and ipv6Prefixes represent the "global" allowed prefixes which are the prefixes defined on the router. -func prefixesToMap(toAdvertise v1beta1.Advertise, ipv4Prefixes, ipv6Prefixes []string) (map[string]*frr.OutgoingFilter, map[string]*frr.OutgoingFilter, error) { +func prefixesToMap(toAdvertise v1beta1.Advertise, ipv4Prefixes, ipv6Prefixes []string) (map[string]*frr.OutgoingFilter, map[string]*frr.OutgoingFilter) { resV4 := map[string]*frr.OutgoingFilter{} resV6 := map[string]*frr.OutgoingFilter{} if toAdvertise.Allowed.Mode == v1beta1.AllowAll { @@ -250,27 +265,19 @@ func prefixesToMap(toAdvertise v1beta1.Advertise, ipv4Prefixes, ipv6Prefixes []s for _, p := range ipv6Prefixes { resV6[p] = &frr.OutgoingFilter{Prefix: p, IPFamily: ipfamily.IPv6} } - return resV4, resV6, nil + return resV4, resV6 } - allowedV4 := sets.New(ipv4Prefixes...) - allowedV6 := sets.New(ipv6Prefixes...) for _, p := range toAdvertise.Allowed.Prefixes { family := ipfamily.ForCIDRString(p) switch family { case ipfamily.IPv4: - if !allowedV4.Has(p) { - return nil, nil, fmt.Errorf("prefix %s is not an allowed prefix", p) - } resV4[p] = &frr.OutgoingFilter{Prefix: p, IPFamily: family} case ipfamily.IPv6: - if !allowedV6.Has(p) { - return nil, nil, fmt.Errorf("prefix %s is not an allowed prefix", p) - } resV6[p] = &frr.OutgoingFilter{Prefix: p, IPFamily: family} } } - return resV4, resV6, nil + return resV4, resV6 } // setCommunitiesToAdvertisements takes the given communityPrefixes and fills the relevant fields to the advertisements contained in the advs map. @@ -385,6 +392,33 @@ func validateSelectorLengths(mask int, le, ge uint32) error { return nil } +func validateImportVRFs(r v1beta1.Router, allVRFs map[string][]string) error { + for _, i := range r.Imports { + if i.VRF == "default" { + continue + } + if _, ok := allVRFs[i.VRF]; !ok { + return fmt.Errorf("router %d-%s imports vrf %s which is not defined", r.ASN, r.VRF, i.VRF) + } + } + return nil +} + +func validateOutgoingPrefixes(prefixesInRouter []string, routerConfig v1beta1.Router) error { + prefixesSet := sets.New(prefixesInRouter...) + for _, n := range routerConfig.Neighbors { + if n.ToAdvertise.Allowed.Mode == v1beta1.AllowAll { + continue + } + for _, p := range n.ToAdvertise.Allowed.Prefixes { + if !prefixesSet.Has(p) { + return fmt.Errorf("trying to advertise non configured prefix %s to neighbor %s, vrf %s", p, neighborName(n.ASN, n.Address), routerConfig.VRF) + } + } + } + return nil +} + func neighborName(ASN uint32, peerAddr string) string { return fmt.Sprintf("%d@%s", ASN, peerAddr) } @@ -573,3 +607,36 @@ func parseTimers(ht, ka *v1.Duration) (*uint64, *uint64, error) { return &htSeconds, &kaSeconds, nil } + +func validatePrefixes(prefixes []string) error { + for _, p := range prefixes { + if ipfamily.ForCIDRString(p) == ipfamily.Unknown { + return fmt.Errorf("unknown ipfamily for %s", p) + } + } + return nil +} + +func prefixesForVRFs(routers []v1beta1.Router) map[string][]string { + res := map[string][]string{} + for _, r := range routers { + res[r.VRF] = r.Prefixes + } + return res +} + +func importedPrefixes(r v1beta1.Router, prefixesInRouter map[string][]string) ([]string, error) { + res := []string{} + for _, i := range r.Imports { + vrf := i.VRF + if i.VRF == "default" { // we use default when importing, but leave empty when declaring + vrf = "" + } + imported, ok := prefixesInRouter[vrf] + if !ok { + return nil, fmt.Errorf("vrf %s not found in prefixes in router", vrf) + } + res = append(res, imported...) + } + return res, nil +} diff --git a/internal/controller/api_to_config_test.go b/internal/controller/api_to_config_test.go index f063e641..1db78de1 100644 --- a/internal/controller/api_to_config_test.go +++ b/internal/controller/api_to_config_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" v1beta1 "github.com/metallb/frr-k8s/api/v1beta1" "github.com/metallb/frr-k8s/internal/frr" "github.com/metallb/frr-k8s/internal/ipfamily" @@ -87,23 +88,11 @@ func TestConversion(t *testing.T) { ConnectTime: ptr.To(uint64(2)), DisableMP: true, GracefulRestart: true, - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, - VRF: "", IPV4Prefixes: []string{"192.0.2.0/24"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -159,23 +148,11 @@ func TestConversion(t *testing.T) { HoldTime: ptr.To[uint64](40), ConnectTime: ptr.To(uint64(2)), DisableMP: true, - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, - VRF: "", IPV4Prefixes: []string{"192.0.2.0/24"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -199,7 +176,6 @@ func TestConversion(t *testing.T) { Address: "192.0.2.7", }, }, - VRF: "", Prefixes: []string{"192.0.2.0/24"}, }, { @@ -231,35 +207,15 @@ func TestConversion(t *testing.T) { Name: "65011@192.0.2.6", ASN: 65011, Addr: "192.0.2.6", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv4, Name: "65012@192.0.2.7", ASN: 65012, Addr: "192.0.2.7", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, - VRF: "", IPV4Prefixes: []string{"192.0.2.0/24"}, - IPV6Prefixes: []string{}, }, { MyASN: 65013, @@ -271,23 +227,12 @@ func TestConversion(t *testing.T) { ASN: 65014, Addr: "2001:db8::4", VRFName: "vrf2", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "vrf2", - IPV4Prefixes: []string{}, IPV6Prefixes: []string{"2001:db8::/64"}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -327,22 +272,12 @@ func TestConversion(t *testing.T) { Name: "65021@192.0.2.11", ASN: 65021, Addr: "192.0.2.11", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, IPV4Prefixes: []string{"192.0.2.0/24"}, IPV6Prefixes: []string{"2001:db8::/64"}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -351,12 +286,9 @@ func TestConversion(t *testing.T) { fromK8s: []v1beta1.FRRConfiguration{ {}, }, - secrets: map[string]v1.Secret{}, - expected: &frr.Config{ - Routers: []*frr.RouterConfig{}, - BFDProfiles: []frr.BFDProfile{}, - }, - err: nil, + secrets: map[string]v1.Secret{}, + expected: &frr.Config{}, + err: nil, }, { name: "Non default VRF", @@ -395,23 +327,12 @@ func TestConversion(t *testing.T) { ASN: 65031, Addr: "192.0.2.16", VRFName: "vrf1", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "vrf1", IPV4Prefixes: []string{"192.0.2.0/24"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -463,20 +384,12 @@ func TestConversion(t *testing.T) { Prefix: "192.0.2.0/24", }, }, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, IPV4Prefixes: []string{"192.0.2.0/24"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -541,13 +454,7 @@ func TestConversion(t *testing.T) { Prefix: "192.0.4.0/24", }, }, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv4, @@ -576,18 +483,12 @@ func TestConversion(t *testing.T) { }, }, }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, IPV4Prefixes: []string{"192.0.2.0/24", "192.0.3.0/24", "192.0.4.0/24"}, IPV6Prefixes: []string{"2001:db8::/64"}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -709,13 +610,7 @@ func TestConversion(t *testing.T) { LocalPref: 100, }, }, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv4, @@ -751,18 +646,12 @@ func TestConversion(t *testing.T) { }, }, }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, IPV4Prefixes: []string{"192.0.2.0/24", "192.0.3.0/24", "192.0.4.0/24", "192.0.6.0/24"}, IPV6Prefixes: []string{"2001:db8::/64"}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -985,31 +874,21 @@ func TestConversion(t *testing.T) { expected: &frr.Config{ Routers: []*frr.RouterConfig{ { - MyASN: 65040, - RouterID: "192.0.2.20", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + MyASN: 65040, + RouterID: "192.0.2.20", Neighbors: []*frr.NeighborConfig{ { IPFamily: ipfamily.IPv4, Name: "65041@192.0.2.21", ASN: 65041, Addr: "192.0.2.21", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, Incoming: frr.AllowedIn{ - All: true, - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, + All: true, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1059,10 +938,6 @@ func TestConversion(t *testing.T) { Name: "65041@192.0.2.21", ASN: 65041, Addr: "192.0.2.21", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, Incoming: frr.AllowedIn{ All: false, PrefixesV4: []frr.IncomingFilter{ @@ -1074,12 +949,10 @@ func TestConversion(t *testing.T) { {IPFamily: "ipv6", Prefix: "2001:db8::/64"}, }, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1129,10 +1002,6 @@ func TestConversion(t *testing.T) { Name: "65041@192.0.2.21", ASN: 65041, Addr: "192.0.2.21", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, Incoming: frr.AllowedIn{ All: false, PrefixesV4: []frr.IncomingFilter{ @@ -1144,7 +1013,6 @@ func TestConversion(t *testing.T) { {IPFamily: "ipv6", Prefix: "2001:db8::/64"}, }, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, }, @@ -1289,7 +1157,6 @@ func TestConversion(t *testing.T) { Communities: []string{"10:101"}, }, }, - PrefixesV6: []frr.OutgoingFilter{}, }, Incoming: frr.AllowedIn{ PrefixesV4: []frr.IncomingFilter{ @@ -1302,17 +1169,13 @@ func TestConversion(t *testing.T) { Prefix: "192.0.101.0/24", }, }, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "", IPV4Prefixes: []string{"192.0.2.10/32", "192.0.2.11/32"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1391,10 +1254,6 @@ func TestConversion(t *testing.T) { Name: "65012@192.0.2.7", ASN: 65012, Addr: "192.0.2.7", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, Incoming: frr.AllowedIn{ PrefixesV4: []frr.IncomingFilter{ { @@ -1425,17 +1284,13 @@ func TestConversion(t *testing.T) { GE: 26, }, }, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "", IPV4Prefixes: []string{"192.0.2.10/32", "192.0.2.11/32"}, - IPV6Prefixes: []string{}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1604,13 +1459,7 @@ func TestConversion(t *testing.T) { Prefix: "192.0.3.2/32", }, }, - PrefixesV6: []frr.OutgoingFilter{}, }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv4, @@ -1642,18 +1491,11 @@ func TestConversion(t *testing.T) { LocalPref: 200, }, }, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "", IPV4Prefixes: []string{"192.0.2.10/32", "192.0.2.11/32", "192.0.3.1/32", "192.0.3.2/32", "192.0.3.20/32", "192.0.3.21/32"}, - IPV6Prefixes: []string{}, }, { MyASN: 65013, @@ -1672,13 +1514,7 @@ func TestConversion(t *testing.T) { Prefix: "192.0.2.5/32", }, }, - PrefixesV6: []frr.OutgoingFilter{}, }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv6, @@ -1704,11 +1540,6 @@ func TestConversion(t *testing.T) { }, }, }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, VRF: "vrf2", @@ -1716,7 +1547,6 @@ func TestConversion(t *testing.T) { IPV6Prefixes: []string{"2001:db8::/64", "2001:db9::/96"}, }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1797,15 +1627,6 @@ func TestConversion(t *testing.T) { ASN: 65012, Addr: "192.0.2.7", Password: "password1", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv4, @@ -1813,20 +1634,9 @@ func TestConversion(t *testing.T) { ASN: 65012, Addr: "192.0.2.8", Password: "cleartext-password", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, - VRF: "", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + VRF: "", }, { MyASN: 65013, @@ -1838,15 +1648,6 @@ func TestConversion(t *testing.T) { ASN: 65017, Addr: "192.0.2.7", VRFName: "vrf2", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, { IPFamily: ipfamily.IPv6, @@ -1855,23 +1656,11 @@ func TestConversion(t *testing.T) { Addr: "2001:db8::4", VRFName: "vrf2", Password: "password2", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, - VRF: "vrf2", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + VRF: "vrf2", }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, @@ -1974,12 +1763,8 @@ func TestConversion(t *testing.T) { expected: &frr.Config{ Routers: []*frr.RouterConfig{ { - MyASN: 65001, - RouterID: "192.0.2.1", - Neighbors: []*frr.NeighborConfig{}, - VRF: "", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + MyASN: 65001, + RouterID: "192.0.2.1", }, }, BFDProfiles: []frr.BFDProfile{}, @@ -2031,15 +1816,10 @@ func TestConversion(t *testing.T) { expected: &frr.Config{ Routers: []*frr.RouterConfig{ { - MyASN: 65001, - RouterID: "192.0.2.1", - Neighbors: []*frr.NeighborConfig{}, - VRF: "", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + MyASN: 65001, + RouterID: "192.0.2.1", }, }, - BFDProfiles: []frr.BFDProfile{}, ExtraConfig: "bar\nfoo\nbar\nbaz\n", }, err: nil, @@ -2089,16 +1869,6 @@ func TestConversion(t *testing.T) { ASN: 65041, Addr: "192.0.2.21", BFDProfile: "bfd1", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - All: false, - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, - AlwaysBlock: []frr.IncomingFilter{}, }, }, }, @@ -2415,14 +2185,6 @@ func TestConversion(t *testing.T) { Name: "65011@192.0.2.6", ASN: 65011, Addr: "192.0.2.6", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, AlwaysBlock: []frr.IncomingFilter{ { IPFamily: ipfamily.IPv4, @@ -2436,9 +2198,6 @@ func TestConversion(t *testing.T) { }, }, }, - VRF: "", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, }, { MyASN: 65013, @@ -2450,14 +2209,6 @@ func TestConversion(t *testing.T) { ASN: 65014, Addr: "2001:db8::4", VRFName: "vrf2", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, AlwaysBlock: []frr.IncomingFilter{ { IPFamily: ipfamily.IPv4, @@ -2471,15 +2222,283 @@ func TestConversion(t *testing.T) { }, }, }, - VRF: "vrf2", - IPV4Prefixes: []string{}, - IPV6Prefixes: []string{}, + VRF: "vrf2", + }, + }, + }, + err: nil, + }, + { + name: "Multiple Routers import VRFs", + fromK8s: []v1beta1.FRRConfiguration{ + { + Spec: v1beta1.FRRConfigurationSpec{ + BGP: v1beta1.BGPConfig{ + Routers: []v1beta1.Router{ + { + ASN: 65010, + ID: "192.0.2.5", + VRF: "", + Imports: []v1beta1.Import{ + {VRF: "red"}, + }, + }, + { + ASN: 65013, + ID: "2001:db8::3", + VRF: "red", + }, + }, + }, + }, + }, + }, + secrets: map[string]v1.Secret{}, + expected: &frr.Config{ + Routers: []*frr.RouterConfig{ + { + MyASN: 65010, + RouterID: "192.0.2.5", + VRF: "", + ImportVRFs: []string{"red"}, + }, + { + MyASN: 65013, + RouterID: "2001:db8::3", + VRF: "red", }, }, - BFDProfiles: []frr.BFDProfile{}, }, err: nil, }, + { + name: "Multiple Routers import VRF, advertise ips from the imported vrf", + fromK8s: []v1beta1.FRRConfiguration{ + { + Spec: v1beta1.FRRConfigurationSpec{ + BGP: v1beta1.BGPConfig{ + Routers: []v1beta1.Router{ + { + ASN: 65040, + ID: "192.0.2.20", + VRF: "", + Imports: []v1beta1.Import{ + {VRF: "red"}, + }, + Neighbors: []v1beta1.Neighbor{ + { + ASN: 65041, + Address: "192.0.2.21", + ToAdvertise: v1beta1.Advertise{ + Allowed: v1beta1.AllowedOutPrefixes{ + Prefixes: []string{"192.0.2.0/24", "192.0.5.0/24"}, + Mode: v1beta1.AllowRestricted, + }, + }, + }, + { + ASN: 65041, + Address: "192.0.2.22", + ToAdvertise: v1beta1.Advertise{ + Allowed: v1beta1.AllowedOutPrefixes{ + Mode: v1beta1.AllowAll, + }, + }, + }, + }, + Prefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + { + ASN: 65013, + ID: "192.0.2.20", + VRF: "red", + Prefixes: []string{"192.0.5.0/24", "2001:db9::/64"}, + }, + }, + }, + }, + }, + }, + expected: &frr.Config{ + Routers: []*frr.RouterConfig{ + { + MyASN: 65040, + RouterID: "192.0.2.20", + Neighbors: []*frr.NeighborConfig{ + { + IPFamily: ipfamily.IPv4, + Name: "65041@192.0.2.21", + ASN: 65041, + Addr: "192.0.2.21", + Outgoing: frr.AllowedOut{ + PrefixesV4: []frr.OutgoingFilter{ + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.2.0/24", + }, + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.5.0/24", + }, + }, + }, + }, + { + IPFamily: ipfamily.IPv4, + Name: "65041@192.0.2.22", + ASN: 65041, + Addr: "192.0.2.22", + Outgoing: frr.AllowedOut{ + PrefixesV4: []frr.OutgoingFilter{ + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.2.0/24", + }, + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.5.0/24", + }, + }, + PrefixesV6: []frr.OutgoingFilter{ + { + IPFamily: ipfamily.IPv6, + Prefix: "2001:db8::/64", + }, { + IPFamily: ipfamily.IPv6, + Prefix: "2001:db9::/64", + }, + }, + }, + }, + }, + ImportVRFs: []string{"red"}, + IPV4Prefixes: []string{"192.0.2.0/24"}, + IPV6Prefixes: []string{"2001:db8::/64"}, + }, + { + MyASN: 65013, + RouterID: "192.0.2.20", + VRF: "red", + IPV4Prefixes: []string{"192.0.5.0/24"}, + IPV6Prefixes: []string{"2001:db9::/64"}, + }, + }, + }, + }, + { + name: "Multiple Routers import non existing VRFs", + fromK8s: []v1beta1.FRRConfiguration{ + { + Spec: v1beta1.FRRConfigurationSpec{ + BGP: v1beta1.BGPConfig{ + Routers: []v1beta1.Router{ + { + ASN: 65010, + ID: "192.0.2.5", + VRF: "", + Imports: []v1beta1.Import{ + {VRF: "blue"}, + }, + }, + { + ASN: 65013, + ID: "2001:db8::3", + VRF: "red", + }, + }, + }, + }, + }, + }, + secrets: map[string]v1.Secret{}, + err: errors.New("router 65010- imports vrf blue which is not defined"), + }, + { + name: "Multiple Routers import VRF, red imports default and advertises", + fromK8s: []v1beta1.FRRConfiguration{ + { + Spec: v1beta1.FRRConfigurationSpec{ + BGP: v1beta1.BGPConfig{ + Routers: []v1beta1.Router{ + { + ASN: 65040, + ID: "192.0.2.20", + VRF: "red", + Imports: []v1beta1.Import{ + {VRF: "default"}, + }, + Neighbors: []v1beta1.Neighbor{ + { + ASN: 65041, + Address: "192.0.2.22", + ToAdvertise: v1beta1.Advertise{ + Allowed: v1beta1.AllowedOutPrefixes{ + Mode: v1beta1.AllowAll, + }, + }, + }, + }, + Prefixes: []string{"192.0.2.0/24", "2001:db8::/64"}, + }, + { + ASN: 65013, + ID: "192.0.2.20", + Prefixes: []string{"192.0.5.0/24", "2001:db9::/64"}, + }, + }, + }, + }, + }, + }, + expected: &frr.Config{ + Routers: []*frr.RouterConfig{ + { + MyASN: 65013, + RouterID: "192.0.2.20", + IPV4Prefixes: []string{"192.0.5.0/24"}, + IPV6Prefixes: []string{"2001:db9::/64"}, + }, + { + MyASN: 65040, + VRF: "red", + RouterID: "192.0.2.20", + Neighbors: []*frr.NeighborConfig{ + { + IPFamily: ipfamily.IPv4, + Name: "65041@192.0.2.22", + ASN: 65041, + Addr: "192.0.2.22", + VRFName: "red", + Outgoing: frr.AllowedOut{ + PrefixesV4: []frr.OutgoingFilter{ + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.2.0/24", + }, + { + IPFamily: ipfamily.IPv4, + Prefix: "192.0.5.0/24", + }, + }, + PrefixesV6: []frr.OutgoingFilter{ + { + IPFamily: ipfamily.IPv6, + Prefix: "2001:db8::/64", + }, { + IPFamily: ipfamily.IPv6, + Prefix: "2001:db9::/64", + }, + }, + }, + }, + }, + ImportVRFs: []string{"default"}, + IPV4Prefixes: []string{"192.0.2.0/24"}, + IPV6Prefixes: []string{"2001:db8::/64"}, + }, + }, + }, + }, } for _, test := range tests { @@ -2495,7 +2514,7 @@ func TestConversion(t *testing.T) { if test.err == nil && err != nil { t.Fatalf("expected no error, got %v", err) } - if diff := cmp.Diff(frr, test.expected); diff != "" { + if diff := cmp.Diff(frr, test.expected, cmpopts.EquateEmpty()); diff != "" { t.Fatalf("config different from expected: %s", diff) } }) diff --git a/internal/controller/frrconfiguration_controller_test.go b/internal/controller/frrconfiguration_controller_test.go index 75896bd1..1c600ef0 100644 --- a/internal/controller/frrconfiguration_controller_test.go +++ b/internal/controller/frrconfiguration_controller_test.go @@ -99,6 +99,7 @@ var _ = Describe("Frrk8s controller", func() { }, }, } + err := k8sClient.Create(context.Background(), frrConfig) Expect(err).ToNot(HaveOccurred()) Eventually(func() *frr.Config { @@ -109,6 +110,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }}, BFDProfiles: []frr.BFDProfile{}, }, @@ -141,6 +143,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }}, BFDProfiles: []frr.BFDProfile{}, }, @@ -159,6 +162,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{"192.168.1.0/32"}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }}, BFDProfiles: []frr.BFDProfile{}, }, @@ -192,6 +196,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }}, BFDProfiles: []frr.BFDProfile{}, }, @@ -283,6 +288,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, { MyASN: uint32(52), @@ -290,6 +296,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -314,6 +321,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, { MyASN: uint32(62), @@ -321,6 +329,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, { MyASN: uint32(52), @@ -328,6 +337,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -349,6 +359,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, { MyASN: uint32(62), @@ -356,6 +367,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -415,6 +427,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -441,6 +454,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, { MyASN: uint32(52), @@ -448,6 +462,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -470,6 +485,7 @@ var _ = Describe("Frrk8s controller", func() { IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, + ImportVRFs: []string{}, }, }, BFDProfiles: []frr.BFDProfile{}, @@ -527,6 +543,7 @@ var _ = Describe("Frrk8s controller", func() { Routers: []*frr.RouterConfig{{MyASN: uint32(42), IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, + ImportVRFs: []string{}, Neighbors: []*frr.NeighborConfig{ { IPFamily: ipfamily.IPv4, @@ -563,6 +580,7 @@ var _ = Describe("Frrk8s controller", func() { Routers: []*frr.RouterConfig{{MyASN: uint32(42), IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, + ImportVRFs: []string{}, Neighbors: []*frr.NeighborConfig{ { IPFamily: ipfamily.IPv4, @@ -615,6 +633,7 @@ var _ = Describe("Frrk8s controller", func() { Routers: []*frr.RouterConfig{{MyASN: uint32(42), IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, + ImportVRFs: []string{}, Neighbors: []*frr.NeighborConfig{}, }}, BFDProfiles: []frr.BFDProfile{}, @@ -644,6 +663,7 @@ var _ = Describe("Frrk8s controller", func() { }).Should(Equal( &frr.Config{ Routers: []*frr.RouterConfig{{MyASN: uint32(42), + ImportVRFs: []string{}, IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, Neighbors: []*frr.NeighborConfig{}, @@ -704,6 +724,7 @@ var _ = Describe("Frrk8s controller", func() { Routers: []*frr.RouterConfig{{MyASN: uint32(42), IPV4Prefixes: []string{}, IPV6Prefixes: []string{}, + ImportVRFs: []string{}, Neighbors: []*frr.NeighborConfig{}, }}, BFDProfiles: []frr.BFDProfile{ diff --git a/internal/controller/merge.go b/internal/controller/merge.go index cc69e95e..4ee42610 100644 --- a/internal/controller/merge.go +++ b/internal/controller/merge.go @@ -29,6 +29,7 @@ func mergeRouterConfigs(r, toMerge *frr.RouterConfig) (*frr.RouterConfig, error) v4Prefixes := sets.New(append(r.IPV4Prefixes, toMerge.IPV4Prefixes...)...) v6Prefixes := sets.New(append(r.IPV6Prefixes, toMerge.IPV6Prefixes...)...) + importVRFs := sets.New(append(r.ImportVRFs, toMerge.ImportVRFs...)...) mergedNeighbors, err := mergeNeighbors(r.Neighbors, toMerge.Neighbors) if err != nil { @@ -37,6 +38,7 @@ func mergeRouterConfigs(r, toMerge *frr.RouterConfig) (*frr.RouterConfig, error) r.IPV4Prefixes = sets.List(v4Prefixes) r.IPV6Prefixes = sets.List(v6Prefixes) + r.ImportVRFs = sets.List(importVRFs) r.Neighbors = mergedNeighbors return r, nil diff --git a/internal/controller/merge_test.go b/internal/controller/merge_test.go index 6da66398..f26534bd 100644 --- a/internal/controller/merge_test.go +++ b/internal/controller/merge_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/metallb/frr-k8s/internal/frr" "github.com/metallb/frr-k8s/internal/ipfamily" "k8s.io/utils/ptr" @@ -538,7 +539,7 @@ func TestMergeRouters(t *testing.T) { if test.err == nil && err != nil { t.Fatalf("expected no error, got %v", err) } - if diff := cmp.Diff(merged, test.expected); diff != "" { + if diff := cmp.Diff(merged, test.expected, cmpopts.EquateEmpty()); diff != "" { t.Fatalf("config different from expected: %s", diff) } }) @@ -1359,14 +1360,6 @@ func TestMergeNeighbors(t *testing.T) { Name: "65040@192.0.1.20", ASN: 65040, Addr: "192.0.1.20", - Outgoing: frr.AllowedOut{ - PrefixesV4: []frr.OutgoingFilter{}, - PrefixesV6: []frr.OutgoingFilter{}, - }, - Incoming: frr.AllowedIn{ - PrefixesV4: []frr.IncomingFilter{}, - PrefixesV6: []frr.IncomingFilter{}, - }, }, }, err: nil, @@ -1382,7 +1375,7 @@ func TestMergeNeighbors(t *testing.T) { if test.err == nil && err != nil { t.Fatalf("expected no error, got %v", err) } - if diff := cmp.Diff(merged, test.expected); diff != "" { + if diff := cmp.Diff(merged, test.expected, cmpopts.EquateEmpty()); diff != "" { t.Fatalf("config different from expected: %s", diff) } }) diff --git a/internal/frr/config.go b/internal/frr/config.go index 6fd06fb2..cf9442e6 100644 --- a/internal/frr/config.go +++ b/internal/frr/config.go @@ -48,6 +48,7 @@ type RouterConfig struct { VRF string IPV4Prefixes []string IPV6Prefixes []string + ImportVRFs []string } type BFDProfile struct { diff --git a/internal/frr/frr_test.go b/internal/frr/frr_test.go index b19be499..e92b7980 100644 --- a/internal/frr/frr_test.go +++ b/internal/frr/frr_test.go @@ -857,3 +857,50 @@ func TestSingleSessionWithGracefulRestart(t *testing.T) { testCheckConfigFile(t) } + +func TestMultipleRoutersImportVRFs(t *testing.T) { + testSetup(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + frr := NewFRR(ctx, emptyCB, log.NewNopLogger(), logging.LevelInfo) + + config := Config{ + Routers: []*RouterConfig{ + { + MyASN: 65000, + Neighbors: []*NeighborConfig{ + { + IPFamily: ipfamily.IPv4, + ASN: 65001, + Addr: "192.168.1.2", + EBGPMultiHop: true, + }, + }, + IPV4Prefixes: []string{"192.169.1.0/24"}, + IPV6Prefixes: []string{"2001:db8:abcd::/48"}, + ImportVRFs: []string{"red"}, + }, + { + MyASN: 65000, + VRF: "red", + IPV4Prefixes: []string{"192.171.1.0/24"}, + }, + { + MyASN: 65000, + VRF: "blue", + IPV4Prefixes: []string{"192.171.1.0/24"}, + IPV6Prefixes: []string{"2001:db9:abcd::/48"}, + ImportVRFs: []string{"default"}, + }, + }, + } + + err := frr.ApplyConfig(&config) + if err != nil { + t.Fatalf("Failed to apply config: %s", err) + } + + testCheckConfigFile(t) +} diff --git a/internal/frr/templates/frr.tmpl b/internal/frr/templates/frr.tmpl index d8f67121..c64701e8 100644 --- a/internal/frr/templates/frr.tmpl +++ b/internal/frr/templates/frr.tmpl @@ -34,6 +34,18 @@ router bgp {{$r.MyASN}}{{ if $r.VRF }} vrf {{$r.VRF}}{{end}} {{ if $r.RouterID }} bgp router-id {{$r.RouterID}} {{- end }} +{{- if gt (len .ImportVRFs) 0}} + address-family ipv4 unicast +{{- range .ImportVRFs }} + import vrf {{.}} +{{- end}} + exit-address-family + address-family ipv6 unicast +{{- range .ImportVRFs }} + import vrf {{.}} +{{- end}} + exit-address-family +{{- end}} {{- range .Neighbors }} {{- template "neighborsession" dict "neighbor" . "routerASN" $r.MyASN -}} diff --git a/internal/frr/testdata/TestMultipleRoutersImportVRFs.golden b/internal/frr/testdata/TestMultipleRoutersImportVRFs.golden new file mode 100644 index 00000000..63089ad7 --- /dev/null +++ b/internal/frr/testdata/TestMultipleRoutersImportVRFs.golden @@ -0,0 +1,98 @@ +log file /etc/frr/frr.log informational +log timestamp precision 3 +hostname dummyhostname +ip nht resolve-via-default +ipv6 nht resolve-via-default + + +route-map 192.168.1.2-out permit 1 + match ip address prefix-list 192.168.1.2-pl-ipv4 +route-map 192.168.1.2-out permit 2 + match ipv6 address prefix-list 192.168.1.2-pl-ipv4 + + + +ip prefix-list 192.168.1.2-pl-ipv4 seq 1 deny any +ipv6 prefix-list 192.168.1.2-pl-ipv4 seq 2 deny any + + + + + + +ip prefix-list 192.168.1.2-inpl-ipv4 seq 1 deny any + +ipv6 prefix-list 192.168.1.2-inpl-ipv4 seq 2 deny any +route-map 192.168.1.2-in permit 3 + match ip address prefix-list 192.168.1.2-inpl-ipv4 +route-map 192.168.1.2-in permit 4 + match ipv6 address prefix-list 192.168.1.2-inpl-ipv4 + +router bgp 65000 + no bgp ebgp-requires-policy + no bgp network import-check + no bgp default ipv4-unicast + bgp graceful-restart preserve-fw-state + + address-family ipv4 unicast + import vrf red + exit-address-family + address-family ipv6 unicast + import vrf red + exit-address-family + neighbor 192.168.1.2 remote-as 65001 + neighbor 192.168.1.2 ebgp-multihop + + + + + + address-family ipv4 unicast + neighbor 192.168.1.2 activate + neighbor 192.168.1.2 route-map 192.168.1.2-in in + neighbor 192.168.1.2 route-map 192.168.1.2-out out + exit-address-family + address-family ipv6 unicast + neighbor 192.168.1.2 activate + neighbor 192.168.1.2 route-map 192.168.1.2-in in + neighbor 192.168.1.2 route-map 192.168.1.2-out out + exit-address-family + address-family ipv4 unicast + network 192.169.1.0/24 + exit-address-family + + address-family ipv6 unicast + network 2001:db8:abcd::/48 + exit-address-family + +router bgp 65000 vrf red + no bgp ebgp-requires-policy + no bgp network import-check + no bgp default ipv4-unicast + bgp graceful-restart preserve-fw-state + + address-family ipv4 unicast + network 192.171.1.0/24 + exit-address-family + +router bgp 65000 vrf blue + no bgp ebgp-requires-policy + no bgp network import-check + no bgp default ipv4-unicast + bgp graceful-restart preserve-fw-state + + address-family ipv4 unicast + import vrf default + exit-address-family + address-family ipv6 unicast + import vrf default + exit-address-family + address-family ipv4 unicast + network 192.171.1.0/24 + exit-address-family + + address-family ipv6 unicast + network 2001:db9:abcd::/48 + exit-address-family + + diff --git a/internal/ipfamily/ipfamily.go b/internal/ipfamily/ipfamily.go index 9df8de2b..83d7fd45 100644 --- a/internal/ipfamily/ipfamily.go +++ b/internal/ipfamily/ipfamily.go @@ -91,3 +91,18 @@ func ForService(svc *v1.Service) (Family, error) { addresses := []string{svc.Spec.ClusterIP} return ForAddresses(addresses...) } + +// FilterPrefixes filters the given slice of prefixes taking only those for the given family. +func FilterPrefixes(prefixes []string, familyToFilter Family) []string { + if familyToFilter == DualStack { + return prefixes + } + res := []string{} + for _, p := range prefixes { + if ForCIDRString(p) != familyToFilter { + continue + } + res = append(res, p) + } + return res +}