From 8b6afd62f3b8d45038c919abd6b737f62040e2d5 Mon Sep 17 00:00:00 2001 From: Emanuele Di Pascale Date: Thu, 7 Aug 2025 15:26:23 +0200 Subject: [PATCH 1/6] feat(agent): externals as VPCs add l3vni to external VRFs, advertise routes we learn from the external (filtered by BGP community) over EVPN. This way the gateway will see them just as another VPC and we will be able to do external peering via GW with no special code. add also the IRB VLAN, else routes from the external are not going to be advertised over BGP EVPN. for the external outbound route-map, pre-emptively allow any prefix belonging to the external's ipv4namespace, as opposed to selectively adding VPC prefixes to a prefix-list depending on the external peering. this way, routes learned via the gateway will be advertised to the external, which allows traffic to be routed back to the VPC. place externals VNIs and IRB VLANs before the VPCs, as they share the same space and are less likely to change dynamically. This way we will not see a different external VNI/VLAN every time we create or delete a VPC. make the catalog errors warnings for now, to allow agent upgrade. these should be made into full blown errors eventually. Signed-off-by: Emanuele Di Pascale --- api/agent/v1beta1/catalog_types.go | 2 + api/agent/v1beta1/zz_generated.deepcopy.go | 7 ++ .../bases/agent.githedgehog.com_agents.yaml | 7 ++ .../bases/agent.githedgehog.com_catalogs.yaml | 7 ++ pkg/agent/dozer/bcm/plan.go | 54 +++++++------- pkg/ctrl/agent_ctrl.go | 14 ++-- pkg/ctrl/vpc_ctrl.go | 7 +- pkg/manager/librarian/librarian.go | 70 +++++++++++++++---- 8 files changed, 123 insertions(+), 45 deletions(-) diff --git a/api/agent/v1beta1/catalog_types.go b/api/agent/v1beta1/catalog_types.go index 9ca9ec89..c21c05d7 100644 --- a/api/agent/v1beta1/catalog_types.go +++ b/api/agent/v1beta1/catalog_types.go @@ -30,6 +30,8 @@ type CatalogSpec struct { VPCVNIs map[string]uint32 `json:"vpcVNIs,omitempty"` // VPCSubnetVNIs stores VPC name -> subnet name -> VPC Subnet VNI, globally unique for the fabric VPCSubnetVNIs map[string]map[string]uint32 `json:"vpcSubnetVNIs,omitempty"` + // ExternalVNIs stores external name -> external VNI, globally unique for the fabric + ExternalVNIs map[string]uint32 `json:"externalVNIs,omitempty"` // Per redundancy group (or switch if no redundancy group) diff --git a/api/agent/v1beta1/zz_generated.deepcopy.go b/api/agent/v1beta1/zz_generated.deepcopy.go index 7a5d1cd9..b144c100 100644 --- a/api/agent/v1beta1/zz_generated.deepcopy.go +++ b/api/agent/v1beta1/zz_generated.deepcopy.go @@ -433,6 +433,13 @@ func (in *CatalogSpec) DeepCopyInto(out *CatalogSpec) { (*out)[key] = outVal } } + if in.ExternalVNIs != nil { + in, out := &in.ExternalVNIs, &out.ExternalVNIs + *out = make(map[string]uint32, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.IRBVLANs != nil { in, out := &in.IRBVLANs, &out.IRBVLANs *out = make(map[string]uint16, len(*in)) diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index 9f9428ae..0186f2e9 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -203,6 +203,13 @@ spec: description: ExternalIDs stores external name -> ID, unique per switch type: object + externalVNIs: + additionalProperties: + format: int32 + type: integer + description: ExternalVNIs stores external name -> external VNI, + globally unique for the fabric + type: object irbVLANs: additionalProperties: type: integer diff --git a/config/crd/bases/agent.githedgehog.com_catalogs.yaml b/config/crd/bases/agent.githedgehog.com_catalogs.yaml index f6e70c63..feb30c93 100644 --- a/config/crd/bases/agent.githedgehog.com_catalogs.yaml +++ b/config/crd/bases/agent.githedgehog.com_catalogs.yaml @@ -53,6 +53,13 @@ spec: type: integer description: ExternalIDs stores external name -> ID, unique per switch type: object + externalVNIs: + additionalProperties: + format: int32 + type: integer + description: ExternalVNIs stores external name -> external VNI, globally + unique for the fabric + type: object irbVLANs: additionalProperties: type: integer diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index 418d52d6..3e2f4263 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -983,7 +983,9 @@ func planExternals(agent *agentapi.Agent, spec *dozer.Spec) error { ImportVRFs: map[string]*dozer.SpecVRFBGPImportVRF{}, }, L2VPNEVPN: dozer.SpecVRFBGPL2VPNEVPN{ - Enabled: agent.IsSpineLeaf(), + Enabled: agent.IsSpineLeaf(), + AdvertiseIPv4Unicast: pointer.To(true), + AdvertiseIPv4UnicastRouteMaps: []string{extInboundRouteMapName(externalName)}, }, Neighbors: map[string]*dozer.SpecVRFBGPNeighbor{}, }, @@ -1015,16 +1017,11 @@ func planExternals(agent *agentapi.Agent, spec *dozer.Spec) error { }, } - prefList := extOutboundPrefixList(externalName) - spec.PrefixLists[prefList] = &dozer.SpecPrefixList{ - Prefixes: map[uint32]*dozer.SpecPrefixListEntry{}, - } - spec.RouteMaps[extOutboundRouteMapName(externalName)] = &dozer.SpecRouteMap{ Statements: map[string]*dozer.SpecRouteMapStatement{ "10": { Conditions: dozer.SpecRouteMapConditions{ - MatchPrefixList: pointer.To(prefList), + MatchPrefixList: pointer.To(ipnsSubnetsPrefixListName(external.IPv4Namespace)), }, SetCommunities: []string{external.OutboundCommunity}, Result: dozer.SpecRouteMapResultAccept, @@ -1034,6 +1031,31 @@ func planExternals(agent *agentapi.Agent, spec *dozer.Spec) error { }, }, } + + irbVLAN := agent.Spec.Catalog.IRBVLANs[externalName] + extVNI := agent.Spec.Catalog.ExternalVNIs[externalName] + if irbVLAN == 0 { //nolint:gocritic + // TODO: make this an error eventually, but not now to allow agent updates + slog.Warn("IRB VLAN for external not found in catalog, not configuring it", "external", externalName) + } else if extVNI == 0 { + // TODO: make this an error eventually, but not now to allow agent updates + slog.Warn("VNI for external not found in catalog, not configuring it", "external", externalName) + } else { + irbIface := vlanName(irbVLAN) + spec.Interfaces[irbIface] = &dozer.SpecInterface{ + Enabled: pointer.To(true), + Description: pointer.To(fmt.Sprintf("External %s IRB", externalName)), + } + spec.VRFs[extVrfName].Interfaces[irbIface] = &dozer.SpecVRFInterface{} + spec.VRFVNIMap[extVrfName] = &dozer.SpecVRFVNIEntry{ + VNI: pointer.To(extVNI), + } + spec.VXLANTunnelMap[fmt.Sprintf("map_%d_%s", extVNI, irbIface)] = &dozer.SpecVXLANTunnelMap{ + VTEP: pointer.To(VTEPFabric), + VNI: pointer.To(extVNI), + VLAN: pointer.To(irbVLAN), + } + } } for name, attach := range agent.Spec.ExternalAttachments { @@ -2813,22 +2835,10 @@ func planExternalPeerings(agent *agentapi.Agent, spec *dozer.Spec) error { } for _, subnetName := range peering.Permit.VPC.Subnets { - subnet, exists := vpc.Subnets[subnetName] + _, exists := vpc.Subnets[subnetName] if !exists { return errors.Errorf("VPC %s subnet %s not found for external peering %s", vpcName, subnetName, name) } - - vni, exists := agent.Spec.Catalog.GetVPCSubnetVNI(vpcName, subnetName) - if vni == 0 || !exists { - return errors.Errorf("VNI for VPC %s subnet %s not found for external peering %s", vpcName, subnetName, name) - } - - spec.PrefixLists[extOutboundPrefixList(externalName)].Prefixes[vni] = &dozer.SpecPrefixListEntry{ - Prefix: dozer.SpecPrefixListPrefix{ - Prefix: subnet.Subnet, - }, - Action: dozer.SpecPrefixListActionPermit, - } } extPrefixesName := vpcExtPrefixesPrefixListName(vpcName) @@ -3115,10 +3125,6 @@ func extInboundRouteMapName(external string) string { return fmt.Sprintf("ext-inbound--%s", external) } -func extOutboundPrefixList(external string) string { - return fmt.Sprintf("ext-outbound--%s", external) -} - func extOutboundRouteMapName(external string) string { return fmt.Sprintf("ext-outbound--%s", external) } diff --git a/pkg/ctrl/agent_ctrl.go b/pkg/ctrl/agent_ctrl.go index 6e48053b..2c13f9e4 100644 --- a/pkg/ctrl/agent_ctrl.go +++ b/pkg/ctrl/agent_ctrl.go @@ -485,6 +485,7 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kct externals := map[string]vpcapi.ExternalSpec{} externalsToConfig := map[string]vpcapi.ExternalSpec{} externalList := &vpcapi.ExternalList{} + externalsReq := map[string]bool{} err = r.List(ctx, externalList, kclient.InNamespace(sw.Namespace)) if err != nil { return kctrl.Result{}, errors.Wrapf(err, "error listing externals") @@ -493,6 +494,7 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kct externals[ext.Name] = ext.Spec if attachedExternals[ext.Name] { externalsToConfig[ext.Name] = ext.Spec + externalsReq[ext.Name] = true } } @@ -603,9 +605,14 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kct idConns[name] = true } + err = r.libr.UpdateVNIs(ctx, r.Client) + if err != nil { + return kctrl.Result{}, errors.Wrapf(err, "error updating VNIs catalog") + } + cat := &agentapi.CatalogSpec{} - err = r.libr.CatalogForRedundancyGroup(ctx, r.Client, cat, sw.Name, sw.Spec.Redundancy, usedVPCs, portChanConns, idConns) + err = r.libr.CatalogForRedundancyGroup(ctx, r.Client, cat, sw.Name, sw.Spec.Redundancy, usedVPCs, portChanConns, idConns, externalsReq) if err != nil { return kctrl.Result{}, errors.Wrapf(err, "error getting redundancy group catalog") } @@ -656,11 +663,6 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kct } } - externalsReq := map[string]bool{} - for name := range externalsToConfig { - externalsReq[name] = true - } - subnetsReq := map[string]bool{} for _, vpc := range vpcs { for _, subnet := range vpc.Subnets { diff --git a/pkg/ctrl/vpc_ctrl.go b/pkg/ctrl/vpc_ctrl.go index 1f5c523b..7d69e69d 100644 --- a/pkg/ctrl/vpc_ctrl.go +++ b/pkg/ctrl/vpc_ctrl.go @@ -102,12 +102,13 @@ func (r *VPCReconciler) enqueueOneVPC(ctx context.Context, _ kclient.Object) []r func (r *VPCReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kctrl.Result, error) { l := kctrllog.FromContext(ctx) - if err := r.libr.UpdateVPCs(ctx, r.Client); err != nil { - return kctrl.Result{}, errors.Wrapf(err, "error updating vpcs catalog") + err := r.libr.UpdateVNIs(ctx, r.Client) + if err != nil { + return kctrl.Result{}, errors.Wrapf(err, "error updating VNIs catalog") } vpc := &vpcapi.VPC{} - err := r.Get(ctx, req.NamespacedName, vpc) + err = r.Get(ctx, req.NamespacedName, vpc) if err != nil { if kapierrors.IsNotFound(err) { l.Info("vpc deleted, cleaning up dhcp subnets") diff --git a/pkg/manager/librarian/librarian.go b/pkg/manager/librarian/librarian.go index b33d598b..bf99e577 100644 --- a/pkg/manager/librarian/librarian.go +++ b/pkg/manager/librarian/librarian.go @@ -16,6 +16,7 @@ package librarian import ( "context" + "maps" "math" "sync" @@ -33,7 +34,7 @@ import ( const ( Namespace = kmetav1.NamespaceDefault CatConns = "connections" - CatVPCs = "vpcs" + CatVNIs = "vpcs" // contains both VPC and External VNIs CatSwitchPrefix = "switch." CatRedGroupPrefix = "redundancy." VPCVNIOffset = 100 @@ -74,6 +75,9 @@ func (m *Manager) getCatalog(ctx context.Context, kube kclient.Client, key strin if cat.Spec.VPCSubnetVNIs == nil { cat.Spec.VPCSubnetVNIs = map[string]map[string]uint32{} } + if cat.Spec.ExternalVNIs == nil { + cat.Spec.ExternalVNIs = map[string]uint32{} + } if cat.Spec.IRBVLANs == nil { cat.Spec.IRBVLANs = map[string]uint16{} } @@ -130,11 +134,11 @@ func (m *Manager) UpdateConnections(ctx context.Context, kube kclient.Client) er return m.saveCatalog(ctx, kube, CatConns, cat) } -func (m *Manager) UpdateVPCs(ctx context.Context, kube kclient.Client) error { +func (m *Manager) UpdateVNIs(ctx context.Context, kube kclient.Client) error { m.mutex.Lock() defer m.mutex.Unlock() - cat, err := m.getCatalog(ctx, kube, CatVPCs) + cat, err := m.getCatalog(ctx, kube, CatVNIs) if err != nil { return err } @@ -144,14 +148,30 @@ func (m *Manager) UpdateVPCs(ctx context.Context, kube kclient.Client) error { return errors.Wrapf(err, "error listing VPCs") } + externalList := &vpcapi.ExternalList{} + if err := kube.List(ctx, externalList); err != nil { + return errors.Wrapf(err, "error listing externals") + } + vpcs := map[string]bool{} for _, vpc := range vpcList.Items { vpcs[vpc.Name] = true } + exts := map[string]bool{} + for _, ext := range externalList.Items { + exts[ext.Name] = true + } + a := &Allocator[uint32]{ Values: NewNextFreeValueFromRanges([][2]uint32{{VPCVNIOffset, VPCVNIMax}}, VPCVNIOffset), } + + cat.Spec.ExternalVNIs, err = a.Allocate(cat.Spec.ExternalVNIs, exts) + if err != nil { + return errors.Wrapf(err, "failed to allocate external VNIs") + } + cat.Spec.VPCVNIs, err = a.Allocate(cat.Spec.VPCVNIs, vpcs) if err != nil { return errors.Wrapf(err, "failed to allocate VPC VNIs") @@ -173,7 +193,7 @@ func (m *Manager) UpdateVPCs(ctx context.Context, kube kclient.Client) error { } } - return m.saveCatalog(ctx, kube, CatVPCs, cat) + return m.saveCatalog(ctx, kube, CatVNIs, cat) } func (m *Manager) getRedundancyGroupKey(swName string, redundancy wiringapi.SwitchRedundancy) string { @@ -188,7 +208,7 @@ func (m *Manager) getSwitchKey(swName string) string { return CatSwitchPrefix + swName } -func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Client, ret *agentapi.CatalogSpec, swName string, redundancy wiringapi.SwitchRedundancy, vpcs, portChanConns, idConns map[string]bool) error { +func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Client, ret *agentapi.CatalogSpec, swName string, redundancy wiringapi.SwitchRedundancy, vpcs, portChanConns, idConns map[string]bool, externals map[string]bool) error { m.mutex.Lock() defer m.mutex.Unlock() @@ -203,10 +223,16 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl a := &Allocator[uint16]{ Values: NewNextFreeValueFromVLANRanges(m.cfg.VPCIRBVLANRanges), } + extVlans, err := a.Allocate(cat.Spec.IRBVLANs, externals) + if err != nil { + return errors.Wrapf(err, "failed to allocate IRB VLANs for externals %s", key) + } + cat.Spec.IRBVLANs, err = a.Allocate(cat.Spec.IRBVLANs, vpcs) if err != nil { return errors.Wrapf(err, "failed to allocate IRB VLANs for %s", key) } + maps.Copy(cat.Spec.IRBVLANs, extVlans) } { @@ -228,9 +254,9 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl return errors.Errorf("failed to get connections catalog %s", CatConns) } - vpcsCat, err := m.getCatalog(ctx, kube, CatVPCs) + vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) if err != nil { - return errors.Errorf("failed to get VPCs catalog %s", CatVPCs) + return errors.Errorf("failed to get VNIs catalog %s", CatVNIs) } ret.ConnectionIDs = map[string]uint32{} @@ -245,9 +271,9 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl ret.VPCVNIs = map[string]uint32{} ret.VPCSubnetVNIs = map[string]map[string]uint32{} for name := range vpcs { - if vni, exists := vpcsCat.Spec.VPCVNIs[name]; exists { + if vni, exists := vnisCat.Spec.VPCVNIs[name]; exists { ret.VPCVNIs[name] = vni - ret.VPCSubnetVNIs[name] = vpcsCat.Spec.VPCSubnetVNIs[name] // TODO pass configured subnets and check if they exist or even pass only configured ones + ret.VPCSubnetVNIs[name] = vnisCat.Spec.VPCSubnetVNIs[name] // TODO pass configured subnets and check if they exist or even pass only configured ones } else { return errors.Errorf("failed to find VPC VNI for vpc %s", name) } @@ -261,6 +287,13 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl return errors.Errorf("failed to find IRB VLAN for vpc %s", name) } } + for name := range externals { + if vlan, exists := cat.Spec.IRBVLANs[name]; exists { + ret.IRBVLANs[name] = vlan + } else { + return errors.Errorf("failed to find IRB VLAN for external %s", name) + } + } ret.PortChannelIDs = map[string]uint16{} for name := range portChanConns { @@ -325,6 +358,19 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube kclient.Client, ret } } + vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) + if err != nil { + return errors.Errorf("failed to get VNIs catalog %s", CatVNIs) + } + ret.ExternalVNIs = map[string]uint32{} + for ext := range externals { + if vni, exists := vnisCat.Spec.ExternalVNIs[ext]; exists { + ret.ExternalVNIs[ext] = vni + } else { + return errors.Errorf("failed to find external VNI for %s", ext) + } + } + if err := m.saveCatalog(ctx, kube, key, cat); err != nil { return errors.Errorf("failed to save switch catalog %s", key) } @@ -363,12 +409,12 @@ func LoWReqForExt(extPeeringName string) string { } func (m *Manager) GetVPCVNI(ctx context.Context, kube kclient.Client, vpc string) (uint32, error) { - vpcsCat, err := m.getCatalog(ctx, kube, CatVPCs) + vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) if err != nil { - return 0, errors.Errorf("failed to get VPCs catalog %s", CatVPCs) + return 0, errors.Errorf("failed to get VNIs catalog %s", CatVNIs) } - if vni, exists := vpcsCat.Spec.VPCVNIs[vpc]; exists { + if vni, exists := vnisCat.Spec.VPCVNIs[vpc]; exists { return vni, nil } From b27d55f97bca37cba7e4698b0eb588f1e18a55e7 Mon Sep 17 00:00:00 2001 From: Emanuele Di Pascale Date: Mon, 15 Sep 2025 16:02:08 +0200 Subject: [PATCH 2/6] feat: reconciler for external VpcInfo Signed-off-by: Emanuele Di Pascale --- cmd/main.go | 3 + config/rbac/role.yaml | 13 ++-- pkg/ctrl/gw_vpc_sync.go | 106 +++++++++++++++++++++++++++++ pkg/manager/librarian/librarian.go | 13 ++++ 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index bb1e16ee..2f320875 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -195,6 +195,9 @@ func run() error { if err := ctrl.SetupGwVPCSyncReconcilerWith(mgr, cfg, libMngr); err != nil { return fmt.Errorf("setting up gateway vpc sync controller: %w", err) } + if err := ctrl.SetupGwExternalSyncReconcilerWith(mgr, cfg, libMngr); err != nil { + return fmt.Errorf("setting up gateway external sync controller: %w", err) + } } if err = connectionwh.SetupWithManager(mgr, cfg); err != nil { diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 6ebac278..88edb472 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -130,6 +130,13 @@ rules: - get - patch - update +- apiGroups: + - vpc.githedgehog.com + resources: + - externals/finalizers + - vpcs/finalizers + verbs: + - update - apiGroups: - vpc.githedgehog.com resources: @@ -142,12 +149,6 @@ rules: - patch - update - watch -- apiGroups: - - vpc.githedgehog.com - resources: - - vpcs/finalizers - verbs: - - update - apiGroups: - wiring.githedgehog.com resources: diff --git a/pkg/ctrl/gw_vpc_sync.go b/pkg/ctrl/gw_vpc_sync.go index 30cfa833..60e990a4 100644 --- a/pkg/ctrl/gw_vpc_sync.go +++ b/pkg/ctrl/gw_vpc_sync.go @@ -126,3 +126,109 @@ func (r *GwVPCSync) Reconcile(ctx context.Context, req kctrl.Request) (kctrl.Res return kctrl.Result{}, nil } + +// External equivalent of the above code + +type GwExternalSync struct { + kclient.Client + cfg *meta.FabricConfig + libr *librarian.Manager +} + +func SetupGwExternalSyncReconcilerWith(mgr kctrl.Manager, cfg *meta.FabricConfig, libMngr *librarian.Manager) error { + if cfg == nil { + return fmt.Errorf("fabric config is nil") //nolint:goerr113 + } + if libMngr == nil { + return fmt.Errorf("librarian manager is nil") //nolint:goerr113 + } + + r := &GwExternalSync{ + Client: mgr.GetClient(), + cfg: cfg, + libr: libMngr, + } + + if err := kctrl.NewControllerManagedBy(mgr). + Named("GwExternalSync"). + For(&vpcapi.External{}). + // TODO consider relying on the owner reference + Watches(&gwapi.VPCInfo{}, handler.EnqueueRequestsFromMapFunc(r.enqueueForVPCInfo)). + Complete(r); err != nil { + return fmt.Errorf("failed to setup controller: %w", err) + } + + return nil +} + +func (r *GwExternalSync) enqueueForVPCInfo(ctx context.Context, obj kclient.Object) []reconcile.Request { + vpcInfo, ok := obj.(*gwapi.VPCInfo) + if !ok { + kctrllog.FromContext(ctx).Info("Enqueue: object is not a VPCInfo", "obj", obj) + + return nil + } + + return []reconcile.Request{ + {NamespacedName: ktypes.NamespacedName{ + Namespace: vpcInfo.Namespace, + Name: vpcInfo.Name, + }}, + } +} + +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externals,verbs=get;list;watch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externals/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externals/finalizers,verbs=update + +//+kubebuilder:rbac:groups=gateway.githedgehog.com,resources=vpcinfos,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gateway.githedgehog.com,resources=vpcinfos/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=gateway.githedgehog.com,resources=vpcinfos/finalizers,verbs=update + +func (r *GwExternalSync) Reconcile(ctx context.Context, req kctrl.Request) (kctrl.Result, error) { + l := kctrllog.FromContext(ctx) + + external := &vpcapi.External{} + if err := r.Get(ctx, req.NamespacedName, external); err != nil { + if kapierrors.IsNotFound(err) { + return kctrl.Result{}, nil + } + + return kctrl.Result{}, fmt.Errorf("getting External %s: %w", req.NamespacedName, err) + } + + vni, err := r.libr.GetExternalVNI(ctx, r.Client, external.Name) + if err != nil { + return kctrl.Result{}, fmt.Errorf("getting External %s VNI: %w", external.Name, err) + } + + subnets := map[string]*gwapi.VPCInfoSubnet{} + // FIXME: the external spec does not have the prefixes we are importing, they are part of the externalPeering + subnets["internet"] = &gwapi.VPCInfoSubnet{ + CIDR: "0.0.0.0/0", + } + + vpcInfo := &gwapi.VPCInfo{ObjectMeta: kmetav1.ObjectMeta{ + Name: external.Name, + Namespace: external.Namespace, + }} + if op, err := ctrlutil.CreateOrUpdate(ctx, r.Client, vpcInfo, func() error { + if err := ctrlutil.SetControllerReference(external, vpcInfo, r.Scheme(), + ctrlutil.WithBlockOwnerDeletion(false)); err != nil { + return fmt.Errorf("setting controller reference: %w", err) + } + + vpcInfo.Spec = gwapi.VPCInfoSpec{ + VNI: vni, + Subnets: subnets, + } + + return nil + }); err != nil { + return kctrl.Result{}, fmt.Errorf("creating/updating VPCInfo %s: %w", req.NamespacedName, err) + } else if op == ctrlutil.OperationResultCreated || op == ctrlutil.OperationResultUpdated { + l.Info("Gateway VPCInfo synced", "op", op) + } + + return kctrl.Result{}, nil +} diff --git a/pkg/manager/librarian/librarian.go b/pkg/manager/librarian/librarian.go index bf99e577..27fda55d 100644 --- a/pkg/manager/librarian/librarian.go +++ b/pkg/manager/librarian/librarian.go @@ -420,3 +420,16 @@ func (m *Manager) GetVPCVNI(ctx context.Context, kube kclient.Client, vpc string return 0, errors.Errorf("failed to find VPC VNI for vpc %s", vpc) } + +func (m *Manager) GetExternalVNI(ctx context.Context, kube kclient.Client, external string) (uint32, error) { + vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) + if err != nil { + return 0, errors.Errorf("failed to get VNIs catalog %s", CatVNIs) + } + + if vni, exists := vnisCat.Spec.ExternalVNIs[external]; exists { + return vni, nil + } + + return 0, errors.Errorf("failed to find VNI for external %s", external) +} From e20ab139927d0e1a419152011a9245ea8bf95104 Mon Sep 17 00:00:00 2001 From: Emanuele Di Pascale Date: Wed, 29 Oct 2025 13:00:43 +0100 Subject: [PATCH 3/6] fix(librarian): joint allocation of VNIs Signed-off-by: Emanuele Di Pascale --- pkg/hhfctl/inspect/conn.go | 8 ++-- pkg/manager/librarian/librarian.go | 72 +++++++++++++++++++----------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/pkg/hhfctl/inspect/conn.go b/pkg/hhfctl/inspect/conn.go index e0ee5258..8b868d02 100644 --- a/pkg/hhfctl/inspect/conn.go +++ b/pkg/hhfctl/inspect/conn.go @@ -318,8 +318,8 @@ func loopbackWorkaroundInfo(ctx context.Context, kube kclient.Reader, agent *age out[link] = loWo } - if strings.HasPrefix(workaround, librarian.LoWorkaroundReqPrefixVPC) { //nolint:gocritic - vpcPeeringName := strings.TrimPrefix(workaround, librarian.LoWorkaroundReqPrefixVPC) + if strings.HasPrefix(workaround, librarian.ReqPrefixVPC) { //nolint:gocritic + vpcPeeringName := strings.TrimPrefix(workaround, librarian.ReqPrefixVPC) vpcPeering := &vpcapi.VPCPeering{} if err := kube.Get(ctx, kclient.ObjectKey{Name: vpcPeeringName, Namespace: kmetav1.NamespaceDefault}, vpcPeering); err != nil { @@ -327,8 +327,8 @@ func loopbackWorkaroundInfo(ctx context.Context, kube kclient.Reader, agent *age } loWo.VPCPeerings[vpcPeeringName] = &vpcPeering.Spec - } else if strings.HasPrefix(workaround, librarian.LoWorkaroundReqPrefixExt) { - extPeeringName := strings.TrimPrefix(workaround, librarian.LoWorkaroundReqPrefixExt) + } else if strings.HasPrefix(workaround, librarian.ReqPrefixExt) { + extPeeringName := strings.TrimPrefix(workaround, librarian.ReqPrefixExt) extPeering := &vpcapi.ExternalPeering{} if err := kube.Get(ctx, kclient.ObjectKey{Name: extPeeringName, Namespace: kmetav1.NamespaceDefault}, extPeering); err != nil { diff --git a/pkg/manager/librarian/librarian.go b/pkg/manager/librarian/librarian.go index 27fda55d..832e546c 100644 --- a/pkg/manager/librarian/librarian.go +++ b/pkg/manager/librarian/librarian.go @@ -18,6 +18,7 @@ import ( "context" "maps" "math" + "strings" "sync" "github.com/pkg/errors" @@ -32,17 +33,17 @@ import ( ) const ( - Namespace = kmetav1.NamespaceDefault - CatConns = "connections" - CatVNIs = "vpcs" // contains both VPC and External VNIs - CatSwitchPrefix = "switch." - CatRedGroupPrefix = "redundancy." - VPCVNIOffset = 100 - VPCVNIMax = (16_777_215 - VPCVNIOffset) / VPCVNIOffset * VPCVNIOffset - PortChannelMin = 1 - PortChannelMax = 249 - LoWorkaroundReqPrefixVPC = "vpc@" - LoWorkaroundReqPrefixExt = "ext@" + Namespace = kmetav1.NamespaceDefault + CatConns = "connections" + CatVNIs = "vpcs" // contains both VPC and External VNIs + CatSwitchPrefix = "switch." + CatRedGroupPrefix = "redundancy." + VPCVNIOffset = 100 + VPCVNIMax = (16_777_215 - VPCVNIOffset) / VPCVNIOffset * VPCVNIOffset + PortChannelMin = 1 + PortChannelMax = 249 + ReqPrefixVPC = "vpc@" + ReqPrefixExt = "ext@" ) type Manager struct { @@ -153,28 +154,41 @@ func (m *Manager) UpdateVNIs(ctx context.Context, kube kclient.Client) error { return errors.Wrapf(err, "error listing externals") } - vpcs := map[string]bool{} + vniReqs := map[string]bool{} + vniKnown := map[string]uint32{} for _, vpc := range vpcList.Items { - vpcs[vpc.Name] = true + if vni, exists := cat.Spec.VPCVNIs[vpc.Name]; exists { + vniKnown[VNIReqForVPC(vpc.Name)] = vni + } + vniReqs[VNIReqForVPC(vpc.Name)] = true } - - exts := map[string]bool{} for _, ext := range externalList.Items { - exts[ext.Name] = true + if vni, exists := cat.Spec.ExternalVNIs[ext.Name]; exists { + vniKnown[VNIReqForExt(ext.Name)] = vni + } + vniReqs[VNIReqForExt(ext.Name)] = true } a := &Allocator[uint32]{ Values: NewNextFreeValueFromRanges([][2]uint32{{VPCVNIOffset, VPCVNIMax}}, VPCVNIOffset), } - cat.Spec.ExternalVNIs, err = a.Allocate(cat.Spec.ExternalVNIs, exts) - if err != nil { - return errors.Wrapf(err, "failed to allocate external VNIs") - } - - cat.Spec.VPCVNIs, err = a.Allocate(cat.Spec.VPCVNIs, vpcs) + newVnis, err := a.Allocate(vniKnown, vniReqs) if err != nil { - return errors.Wrapf(err, "failed to allocate VPC VNIs") + return errors.Wrapf(err, "failed to allocate VNIs") + } + cat.Spec.VPCVNIs = map[string]uint32{} + cat.Spec.ExternalVNIs = map[string]uint32{} + + for req, vni := range newVnis { + switch { + case strings.HasPrefix(req, ReqPrefixVPC): + vpcName := strings.TrimPrefix(req, ReqPrefixVPC) + cat.Spec.VPCVNIs[vpcName] = vni + case strings.HasPrefix(req, ReqPrefixExt): + extName := strings.TrimPrefix(req, ReqPrefixExt) + cat.Spec.ExternalVNIs[extName] = vni + } } for _, vpc := range vpcList.Items { @@ -401,11 +415,19 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube kclient.Client, ret } func LoWReqForVPC(vpcPeeringName string) string { - return LoWorkaroundReqPrefixVPC + vpcPeeringName + return ReqPrefixVPC + vpcPeeringName } func LoWReqForExt(extPeeringName string) string { - return LoWorkaroundReqPrefixExt + extPeeringName + return ReqPrefixExt + extPeeringName +} + +func VNIReqForVPC(vpcName string) string { + return ReqPrefixVPC + vpcName +} + +func VNIReqForExt(extName string) string { + return ReqPrefixExt + extName } func (m *Manager) GetVPCVNI(ctx context.Context, kube kclient.Client, vpc string) (uint32, error) { From f45f2fcce135fad15fbb7a76b24e27f51a7b572e Mon Sep 17 00:00:00 2001 From: Sergei Lukianov Date: Sun, 2 Nov 2025 23:23:40 -0800 Subject: [PATCH 4/6] fix(catalog): allocation of vnis/irbvlans for externals Signed-off-by: Sergei Lukianov --- api/agent/v1beta1/catalog_types.go | 6 +- api/agent/v1beta1/zz_generated.deepcopy.go | 7 -- .../bases/agent.githedgehog.com_agents.yaml | 12 +-- .../bases/agent.githedgehog.com_catalogs.yaml | 11 +-- pkg/agent/dozer/bcm/plan.go | 6 +- pkg/ctrl/agent_ctrl.go | 2 +- pkg/manager/librarian/librarian.go | 81 +++++-------------- 7 files changed, 32 insertions(+), 93 deletions(-) diff --git a/api/agent/v1beta1/catalog_types.go b/api/agent/v1beta1/catalog_types.go index c21c05d7..b28322fc 100644 --- a/api/agent/v1beta1/catalog_types.go +++ b/api/agent/v1beta1/catalog_types.go @@ -26,16 +26,14 @@ type CatalogSpec struct { // ConnectionSystemIDs stores connection name -> ID, globally unique for the fabric ConnectionIDs map[string]uint32 `json:"connectionIDs,omitempty"` - // VPCVNIs stores VPC name -> VPC VNI, globally unique for the fabric + // VPCVNIs stores VPC name -> VPC VNI, globally unique for the fabric, it includes Externals too with ext@ prefix VPCVNIs map[string]uint32 `json:"vpcVNIs,omitempty"` // VPCSubnetVNIs stores VPC name -> subnet name -> VPC Subnet VNI, globally unique for the fabric VPCSubnetVNIs map[string]map[string]uint32 `json:"vpcSubnetVNIs,omitempty"` - // ExternalVNIs stores external name -> external VNI, globally unique for the fabric - ExternalVNIs map[string]uint32 `json:"externalVNIs,omitempty"` // Per redundancy group (or switch if no redundancy group) - // IRBVLANs stores VPC name -> IRB VLAN ID, unique per redundancy group (or switch) + // IRBVLANs stores VPC name -> IRB VLAN ID, unique per redundancy group (or switch), it includes Externals too with ext@ prefix IRBVLANs map[string]uint16 `json:"irbVLANs,omitempty"` // PortChannelIDs stores Connection name -> PortChannel ID, unique per redundancy group (or switch) PortChannelIDs map[string]uint16 `json:"portChannelIDs,omitempty"` diff --git a/api/agent/v1beta1/zz_generated.deepcopy.go b/api/agent/v1beta1/zz_generated.deepcopy.go index b144c100..7a5d1cd9 100644 --- a/api/agent/v1beta1/zz_generated.deepcopy.go +++ b/api/agent/v1beta1/zz_generated.deepcopy.go @@ -433,13 +433,6 @@ func (in *CatalogSpec) DeepCopyInto(out *CatalogSpec) { (*out)[key] = outVal } } - if in.ExternalVNIs != nil { - in, out := &in.ExternalVNIs, &out.ExternalVNIs - *out = make(map[string]uint32, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } if in.IRBVLANs != nil { in, out := &in.IRBVLANs, &out.IRBVLANs *out = make(map[string]uint16, len(*in)) diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index 0186f2e9..46c48271 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -203,18 +203,12 @@ spec: description: ExternalIDs stores external name -> ID, unique per switch type: object - externalVNIs: - additionalProperties: - format: int32 - type: integer - description: ExternalVNIs stores external name -> external VNI, - globally unique for the fabric - type: object irbVLANs: additionalProperties: type: integer description: IRBVLANs stores VPC name -> IRB VLAN ID, unique per - redundancy group (or switch) + redundancy group (or switch), it includes Externals too with + ext@ prefix type: object loopbackWorkaroundLinks: additionalProperties: @@ -255,7 +249,7 @@ spec: format: int32 type: integer description: VPCVNIs stores VPC name -> VPC VNI, globally unique - for the fabric + for the fabric, it includes Externals too with ext@ prefix type: object type: object config: diff --git a/config/crd/bases/agent.githedgehog.com_catalogs.yaml b/config/crd/bases/agent.githedgehog.com_catalogs.yaml index feb30c93..530c4741 100644 --- a/config/crd/bases/agent.githedgehog.com_catalogs.yaml +++ b/config/crd/bases/agent.githedgehog.com_catalogs.yaml @@ -53,18 +53,11 @@ spec: type: integer description: ExternalIDs stores external name -> ID, unique per switch type: object - externalVNIs: - additionalProperties: - format: int32 - type: integer - description: ExternalVNIs stores external name -> external VNI, globally - unique for the fabric - type: object irbVLANs: additionalProperties: type: integer description: IRBVLANs stores VPC name -> IRB VLAN ID, unique per redundancy - group (or switch) + group (or switch), it includes Externals too with ext@ prefix type: object loopbackWorkaroundLinks: additionalProperties: @@ -105,7 +98,7 @@ spec: format: int32 type: integer description: VPCVNIs stores VPC name -> VPC VNI, globally unique for - the fabric + the fabric, it includes Externals too with ext@ prefix type: object type: object status: diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index 3e2f4263..e529f1b2 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -1032,8 +1032,8 @@ func planExternals(agent *agentapi.Agent, spec *dozer.Spec) error { }, } - irbVLAN := agent.Spec.Catalog.IRBVLANs[externalName] - extVNI := agent.Spec.Catalog.ExternalVNIs[externalName] + irbVLAN := agent.Spec.Catalog.IRBVLANs[librarian.ReqForExt(externalName)] + extVNI := agent.Spec.Catalog.VPCVNIs[librarian.ReqForExt(externalName)] if irbVLAN == 0 { //nolint:gocritic // TODO: make this an error eventually, but not now to allow agent updates slog.Warn("IRB VLAN for external not found in catalog, not configuring it", "external", externalName) @@ -2925,7 +2925,7 @@ func planExternalPeerings(agent *agentapi.Agent, spec *dozer.Spec) error { spec.VRFs[extVrf].BGP.IPv4Unicast.ImportVRFs[vpcVrf] = &dozer.SpecVRFBGPImportVRF{} spec.VRFs[vpcVrf].BGP.IPv4Unicast.ImportVRFs[extVrf] = &dozer.SpecVRFBGPImportVRF{} } else { - sub1, sub2, ip1, ip2, err := planLoopbackWorkaround(agent, spec, librarian.LoWReqForExt(name)) + sub1, sub2, ip1, ip2, err := planLoopbackWorkaround(agent, spec, librarian.ReqForExt(name)) if err != nil { return errors.Wrapf(err, "failed to plan loopback workaround for external peering %s", name) } diff --git a/pkg/ctrl/agent_ctrl.go b/pkg/ctrl/agent_ctrl.go index 2c13f9e4..bc5d6836 100644 --- a/pkg/ctrl/agent_ctrl.go +++ b/pkg/ctrl/agent_ctrl.go @@ -659,7 +659,7 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req kctrl.Request) (kct continue } - loWorkaroundReqs[librarian.LoWReqForExt(name)] = true + loWorkaroundReqs[librarian.ReqForExt(name)] = true } } diff --git a/pkg/manager/librarian/librarian.go b/pkg/manager/librarian/librarian.go index 832e546c..915b1c61 100644 --- a/pkg/manager/librarian/librarian.go +++ b/pkg/manager/librarian/librarian.go @@ -18,7 +18,6 @@ import ( "context" "maps" "math" - "strings" "sync" "github.com/pkg/errors" @@ -76,9 +75,6 @@ func (m *Manager) getCatalog(ctx context.Context, kube kclient.Client, key strin if cat.Spec.VPCSubnetVNIs == nil { cat.Spec.VPCSubnetVNIs = map[string]map[string]uint32{} } - if cat.Spec.ExternalVNIs == nil { - cat.Spec.ExternalVNIs = map[string]uint32{} - } if cat.Spec.IRBVLANs == nil { cat.Spec.IRBVLANs = map[string]uint16{} } @@ -154,41 +150,21 @@ func (m *Manager) UpdateVNIs(ctx context.Context, kube kclient.Client) error { return errors.Wrapf(err, "error listing externals") } - vniReqs := map[string]bool{} - vniKnown := map[string]uint32{} + reqs := map[string]bool{} for _, vpc := range vpcList.Items { - if vni, exists := cat.Spec.VPCVNIs[vpc.Name]; exists { - vniKnown[VNIReqForVPC(vpc.Name)] = vni - } - vniReqs[VNIReqForVPC(vpc.Name)] = true + reqs[vpc.Name] = true } for _, ext := range externalList.Items { - if vni, exists := cat.Spec.ExternalVNIs[ext.Name]; exists { - vniKnown[VNIReqForExt(ext.Name)] = vni - } - vniReqs[VNIReqForExt(ext.Name)] = true + reqs[ReqForExt(ext.Name)] = true } a := &Allocator[uint32]{ Values: NewNextFreeValueFromRanges([][2]uint32{{VPCVNIOffset, VPCVNIMax}}, VPCVNIOffset), } - newVnis, err := a.Allocate(vniKnown, vniReqs) + cat.Spec.VPCVNIs, err = a.Allocate(cat.Spec.VPCVNIs, reqs) if err != nil { - return errors.Wrapf(err, "failed to allocate VNIs") - } - cat.Spec.VPCVNIs = map[string]uint32{} - cat.Spec.ExternalVNIs = map[string]uint32{} - - for req, vni := range newVnis { - switch { - case strings.HasPrefix(req, ReqPrefixVPC): - vpcName := strings.TrimPrefix(req, ReqPrefixVPC) - cat.Spec.VPCVNIs[vpcName] = vni - case strings.HasPrefix(req, ReqPrefixExt): - extName := strings.TrimPrefix(req, ReqPrefixExt) - cat.Spec.ExternalVNIs[extName] = vni - } + return errors.Wrapf(err, "failed to allocate VPC/External VNIs") } for _, vpc := range vpcList.Items { @@ -237,16 +213,14 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl a := &Allocator[uint16]{ Values: NewNextFreeValueFromVLANRanges(m.cfg.VPCIRBVLANRanges), } - extVlans, err := a.Allocate(cat.Spec.IRBVLANs, externals) - if err != nil { - return errors.Wrapf(err, "failed to allocate IRB VLANs for externals %s", key) + irbVLANReqs := maps.Clone(vpcs) + for ext := range externals { + irbVLANReqs[ReqPrefixExt+ext] = true } - - cat.Spec.IRBVLANs, err = a.Allocate(cat.Spec.IRBVLANs, vpcs) + cat.Spec.IRBVLANs, err = a.Allocate(cat.Spec.IRBVLANs, irbVLANReqs) if err != nil { return errors.Wrapf(err, "failed to allocate IRB VLANs for %s", key) } - maps.Copy(cat.Spec.IRBVLANs, extVlans) } { @@ -292,6 +266,13 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl return errors.Errorf("failed to find VPC VNI for vpc %s", name) } } + for name := range externals { + if vni, exists := vnisCat.Spec.VPCVNIs[ReqForExt(name)]; exists { + ret.VPCVNIs[ReqForExt(name)] = vni + } else { + return errors.Errorf("failed to find external VNI for external %s", name) + } + } ret.IRBVLANs = map[string]uint16{} for name := range vpcs { @@ -302,8 +283,8 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube kclient.Cl } } for name := range externals { - if vlan, exists := cat.Spec.IRBVLANs[name]; exists { - ret.IRBVLANs[name] = vlan + if vlan, exists := cat.Spec.IRBVLANs[ReqPrefixExt+name]; exists { + ret.IRBVLANs[ReqPrefixExt+name] = vlan } else { return errors.Errorf("failed to find IRB VLAN for external %s", name) } @@ -372,19 +353,6 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube kclient.Client, ret } } - vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) - if err != nil { - return errors.Errorf("failed to get VNIs catalog %s", CatVNIs) - } - ret.ExternalVNIs = map[string]uint32{} - for ext := range externals { - if vni, exists := vnisCat.Spec.ExternalVNIs[ext]; exists { - ret.ExternalVNIs[ext] = vni - } else { - return errors.Errorf("failed to find external VNI for %s", ext) - } - } - if err := m.saveCatalog(ctx, kube, key, cat); err != nil { return errors.Errorf("failed to save switch catalog %s", key) } @@ -414,22 +382,15 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube kclient.Client, ret return nil } +// TODO drop with loopback workaround cleanup, only use vpc@ prefix for loopback workarounds func LoWReqForVPC(vpcPeeringName string) string { return ReqPrefixVPC + vpcPeeringName } -func LoWReqForExt(extPeeringName string) string { +func ReqForExt(extPeeringName string) string { return ReqPrefixExt + extPeeringName } -func VNIReqForVPC(vpcName string) string { - return ReqPrefixVPC + vpcName -} - -func VNIReqForExt(extName string) string { - return ReqPrefixExt + extName -} - func (m *Manager) GetVPCVNI(ctx context.Context, kube kclient.Client, vpc string) (uint32, error) { vnisCat, err := m.getCatalog(ctx, kube, CatVNIs) if err != nil { @@ -449,7 +410,7 @@ func (m *Manager) GetExternalVNI(ctx context.Context, kube kclient.Client, exter return 0, errors.Errorf("failed to get VNIs catalog %s", CatVNIs) } - if vni, exists := vnisCat.Spec.ExternalVNIs[external]; exists { + if vni, exists := vnisCat.Spec.VPCVNIs[ReqForExt(external)]; exists { return vni, nil } From d69beaef522993bed54836951cd330012131c5aa Mon Sep 17 00:00:00 2001 From: Sergei Lukianov Date: Sun, 2 Nov 2025 23:38:25 -0800 Subject: [PATCH 5/6] fix(gwsync): sync externals with prefix to avoid conflicts Signed-off-by: Sergei Lukianov --- api/vpc/v1beta1/external_types.go | 4 ++++ api/vpc/v1beta1/vpc_types.go | 4 ++++ pkg/ctrl/{gw_vpc_sync.go => gw_sync.go} | 11 ++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) rename pkg/ctrl/{gw_vpc_sync.go => gw_sync.go} (96%) diff --git a/api/vpc/v1beta1/external_types.go b/api/vpc/v1beta1/external_types.go index 7ddd4426..cb832455 100644 --- a/api/vpc/v1beta1/external_types.go +++ b/api/vpc/v1beta1/external_types.go @@ -28,6 +28,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) +const ( + VPCInfoExtPrefix = "ext." +) + var communityCheck = regexp.MustCompile("^(6553[0-5]|655[0-2][0-9]|654[0-9]{2}|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9]):(6553[0-5]|655[0-2][0-9]|654[0-9]{2}|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$") // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. diff --git a/api/vpc/v1beta1/vpc_types.go b/api/vpc/v1beta1/vpc_types.go index 5476d6eb..ce52f6d8 100644 --- a/api/vpc/v1beta1/vpc_types.go +++ b/api/vpc/v1beta1/vpc_types.go @@ -18,6 +18,7 @@ import ( "context" "net" "slices" + "strings" "github.com/pkg/errors" "go.githedgehog.com/fabric/api/meta" @@ -336,6 +337,9 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me if len(vpc.Name) > 11 { return nil, errors.Errorf("name %s is too long, must be <= 11 characters", vpc.Name) } + if strings.HasPrefix(vpc.Name, VPCInfoExtPrefix) { + return nil, errors.Errorf("vpc name cannot start with '%s': %s", VPCInfoExtPrefix, vpc.Name) + } if vpc.Spec.IPv4Namespace == "" { return nil, errors.Errorf("ipv4Namespace is required") } diff --git a/pkg/ctrl/gw_vpc_sync.go b/pkg/ctrl/gw_sync.go similarity index 96% rename from pkg/ctrl/gw_vpc_sync.go rename to pkg/ctrl/gw_sync.go index 60e990a4..278ad26a 100644 --- a/pkg/ctrl/gw_vpc_sync.go +++ b/pkg/ctrl/gw_sync.go @@ -6,6 +6,7 @@ package ctrl import ( "context" "fmt" + "strings" "go.githedgehog.com/fabric/api/meta" vpcapi "go.githedgehog.com/fabric/api/vpc/v1beta1" @@ -169,10 +170,14 @@ func (r *GwExternalSync) enqueueForVPCInfo(ctx context.Context, obj kclient.Obje return nil } + if !strings.HasPrefix(vpcInfo.Name, vpcapi.VPCInfoExtPrefix) { + return nil + } + return []reconcile.Request{ {NamespacedName: ktypes.NamespacedName{ Namespace: vpcInfo.Namespace, - Name: vpcInfo.Name, + Name: strings.TrimPrefix(vpcInfo.Name, vpcapi.VPCInfoExtPrefix), }}, } } @@ -204,12 +209,12 @@ func (r *GwExternalSync) Reconcile(ctx context.Context, req kctrl.Request) (kctr subnets := map[string]*gwapi.VPCInfoSubnet{} // FIXME: the external spec does not have the prefixes we are importing, they are part of the externalPeering - subnets["internet"] = &gwapi.VPCInfoSubnet{ + subnets["external"] = &gwapi.VPCInfoSubnet{ CIDR: "0.0.0.0/0", } vpcInfo := &gwapi.VPCInfo{ObjectMeta: kmetav1.ObjectMeta{ - Name: external.Name, + Name: vpcapi.VPCInfoExtPrefix + external.Name, Namespace: external.Namespace, }} if op, err := ctrlutil.CreateOrUpdate(ctx, r.Client, vpcInfo, func() error { From 567c14e977cb42407f42ad8361a3d2b31624344d Mon Sep 17 00:00:00 2001 From: Emanuele Di Pascale Date: Mon, 3 Nov 2025 10:12:54 +0100 Subject: [PATCH 6/6] fix(agent): error on missing ext VNI/IRB VLAN Signed-off-by: Emanuele Di Pascale --- pkg/agent/dozer/bcm/plan.go | 40 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index e529f1b2..3ffd1671 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -1034,27 +1034,25 @@ func planExternals(agent *agentapi.Agent, spec *dozer.Spec) error { irbVLAN := agent.Spec.Catalog.IRBVLANs[librarian.ReqForExt(externalName)] extVNI := agent.Spec.Catalog.VPCVNIs[librarian.ReqForExt(externalName)] - if irbVLAN == 0 { //nolint:gocritic - // TODO: make this an error eventually, but not now to allow agent updates - slog.Warn("IRB VLAN for external not found in catalog, not configuring it", "external", externalName) - } else if extVNI == 0 { - // TODO: make this an error eventually, but not now to allow agent updates - slog.Warn("VNI for external not found in catalog, not configuring it", "external", externalName) - } else { - irbIface := vlanName(irbVLAN) - spec.Interfaces[irbIface] = &dozer.SpecInterface{ - Enabled: pointer.To(true), - Description: pointer.To(fmt.Sprintf("External %s IRB", externalName)), - } - spec.VRFs[extVrfName].Interfaces[irbIface] = &dozer.SpecVRFInterface{} - spec.VRFVNIMap[extVrfName] = &dozer.SpecVRFVNIEntry{ - VNI: pointer.To(extVNI), - } - spec.VXLANTunnelMap[fmt.Sprintf("map_%d_%s", extVNI, irbIface)] = &dozer.SpecVXLANTunnelMap{ - VTEP: pointer.To(VTEPFabric), - VNI: pointer.To(extVNI), - VLAN: pointer.To(irbVLAN), - } + if irbVLAN == 0 { + return fmt.Errorf("IRB VLAN for external %s not found in catalog", externalName) //nolint:goerr113 + } + if extVNI == 0 { + return fmt.Errorf("VNI for external %s not found in catalog", externalName) //nolint:goerr113 + } + irbIface := vlanName(irbVLAN) + spec.Interfaces[irbIface] = &dozer.SpecInterface{ + Enabled: pointer.To(true), + Description: pointer.To(fmt.Sprintf("External %s IRB", externalName)), + } + spec.VRFs[extVrfName].Interfaces[irbIface] = &dozer.SpecVRFInterface{} + spec.VRFVNIMap[extVrfName] = &dozer.SpecVRFVNIEntry{ + VNI: pointer.To(extVNI), + } + spec.VXLANTunnelMap[fmt.Sprintf("map_%d_%s", extVNI, irbIface)] = &dozer.SpecVXLANTunnelMap{ + VTEP: pointer.To(VTEPFabric), + VNI: pointer.To(extVNI), + VLAN: pointer.To(irbVLAN), } }