From 6501b5e43128b428edec29978a5eff3cf7a84fe9 Mon Sep 17 00:00:00 2001 From: Brian Rochford Date: Fri, 20 Oct 2023 16:43:47 +0100 Subject: [PATCH 1/3] Restructure existing objects Signed-off-by: Brian Rochford --- cmd/main.go | 8 ++--- pkg/{ => objects}/bridge/bridge_test.go | 0 pkg/{ => objects}/bridge/bridge_validate.go | 0 pkg/{ => objects}/bridge/common.go | 0 pkg/{ => objects}/bridge/grpc.go | 22 ++++++------- pkg/{ => objects}/bridge/netlink.go | 21 ++++++------ pkg/objects/bridge/server.go | 34 +++++++++++++++++++ pkg/{ => objects}/bridge/server_test.go | 0 pkg/{bridge/server.go => objects/object.go} | 36 +++++++++++---------- pkg/{ => objects}/port/common.go | 0 pkg/{ => objects}/port/grpc.go | 0 pkg/{ => objects}/port/netlink.go | 0 pkg/{ => objects}/port/port_test.go | 0 pkg/{ => objects}/port/port_validate.go | 0 pkg/{ => objects}/port/server.go | 0 pkg/{ => objects}/port/server_test.go | 0 pkg/{ => objects}/svi/common.go | 3 -- pkg/{ => objects}/svi/frr.go | 0 pkg/{ => objects}/svi/grpc.go | 0 pkg/{ => objects}/svi/netlink.go | 0 pkg/{ => objects}/svi/server.go | 0 pkg/{ => objects}/svi/server_test.go | 0 pkg/{ => objects}/svi/svi_test.go | 0 pkg/{ => objects}/svi/svi_validate.go | 0 pkg/{ => objects}/vrf/common.go | 0 pkg/{ => objects}/vrf/frr.go | 0 pkg/{ => objects}/vrf/grpc.go | 0 pkg/{ => objects}/vrf/netlink.go | 0 pkg/{ => objects}/vrf/server.go | 0 pkg/{ => objects}/vrf/server_test.go | 0 pkg/{ => objects}/vrf/vrf_test.go | 0 pkg/{ => objects}/vrf/vrf_validate.go | 0 32 files changed, 79 insertions(+), 45 deletions(-) rename pkg/{ => objects}/bridge/bridge_test.go (100%) rename pkg/{ => objects}/bridge/bridge_validate.go (100%) rename pkg/{ => objects}/bridge/common.go (100%) rename pkg/{ => objects}/bridge/grpc.go (92%) rename pkg/{ => objects}/bridge/netlink.go (81%) create mode 100644 pkg/objects/bridge/server.go rename pkg/{ => objects}/bridge/server_test.go (100%) rename pkg/{bridge/server.go => objects/object.go} (68%) rename pkg/{ => objects}/port/common.go (100%) rename pkg/{ => objects}/port/grpc.go (100%) rename pkg/{ => objects}/port/netlink.go (100%) rename pkg/{ => objects}/port/port_test.go (100%) rename pkg/{ => objects}/port/port_validate.go (100%) rename pkg/{ => objects}/port/server.go (100%) rename pkg/{ => objects}/port/server_test.go (100%) rename pkg/{ => objects}/svi/common.go (98%) rename pkg/{ => objects}/svi/frr.go (100%) rename pkg/{ => objects}/svi/grpc.go (100%) rename pkg/{ => objects}/svi/netlink.go (100%) rename pkg/{ => objects}/svi/server.go (100%) rename pkg/{ => objects}/svi/server_test.go (100%) rename pkg/{ => objects}/svi/svi_test.go (100%) rename pkg/{ => objects}/svi/svi_validate.go (100%) rename pkg/{ => objects}/vrf/common.go (100%) rename pkg/{ => objects}/vrf/frr.go (100%) rename pkg/{ => objects}/vrf/grpc.go (100%) rename pkg/{ => objects}/vrf/netlink.go (100%) rename pkg/{ => objects}/vrf/server.go (100%) rename pkg/{ => objects}/vrf/server_test.go (100%) rename pkg/{ => objects}/vrf/vrf_test.go (100%) rename pkg/{ => objects}/vrf/vrf_validate.go (100%) diff --git a/cmd/main.go b/cmd/main.go index dd66cf17..c10f9a16 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,10 @@ import ( "context" "flag" "fmt" + "github.com/opiproject/opi-evpn-bridge/pkg/objects/bridge" + "github.com/opiproject/opi-evpn-bridge/pkg/objects/port" + "github.com/opiproject/opi-evpn-bridge/pkg/objects/svi" + "github.com/opiproject/opi-evpn-bridge/pkg/objects/vrf" "log" "net" "net/http" @@ -16,11 +20,7 @@ import ( pc "github.com/opiproject/opi-api/inventory/v1/gen/go" pe "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" - "github.com/opiproject/opi-evpn-bridge/pkg/bridge" - "github.com/opiproject/opi-evpn-bridge/pkg/port" - "github.com/opiproject/opi-evpn-bridge/pkg/svi" "github.com/opiproject/opi-evpn-bridge/pkg/utils" - "github.com/opiproject/opi-evpn-bridge/pkg/vrf" "github.com/opiproject/opi-smbios-bridge/pkg/inventory" "github.com/philippgille/gokv" diff --git a/pkg/bridge/bridge_test.go b/pkg/objects/bridge/bridge_test.go similarity index 100% rename from pkg/bridge/bridge_test.go rename to pkg/objects/bridge/bridge_test.go diff --git a/pkg/bridge/bridge_validate.go b/pkg/objects/bridge/bridge_validate.go similarity index 100% rename from pkg/bridge/bridge_validate.go rename to pkg/objects/bridge/bridge_validate.go diff --git a/pkg/bridge/common.go b/pkg/objects/bridge/common.go similarity index 100% rename from pkg/bridge/common.go rename to pkg/objects/bridge/common.go diff --git a/pkg/bridge/grpc.go b/pkg/objects/bridge/grpc.go similarity index 92% rename from pkg/bridge/grpc.go rename to pkg/objects/bridge/grpc.go index 45f6c564..21dcc807 100644 --- a/pkg/bridge/grpc.go +++ b/pkg/objects/bridge/grpc.go @@ -39,7 +39,7 @@ func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBr in.LogicalBridge.Name = resourceIDToFullName(resourceID) // idempotent API when called with same key, should return same object obj := new(pb.LogicalBridge) - ok, err := s.store.Get(in.LogicalBridge.Name, obj) + ok, err := s.Store.Get(in.LogicalBridge.Name, obj) if err != nil { fmt.Printf("Failed to interact with store: %v", err) return nil, err @@ -58,7 +58,7 @@ func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBr log.Printf("new object %v", models.NewBridge(response)) // save object to the database s.ListHelper[in.LogicalBridge.Name] = false - err = s.store.Set(in.LogicalBridge.Name, response) + err = s.Store.Set(in.LogicalBridge.Name, response) if err != nil { return nil, err } @@ -73,7 +73,7 @@ func (s *Server) DeleteLogicalBridge(ctx context.Context, in *pb.DeleteLogicalBr } // fetch object from the database obj := new(pb.LogicalBridge) - ok, err := s.store.Get(in.Name, obj) + ok, err := s.Store.Get(in.Name, obj) if err != nil { fmt.Printf("Failed to interact with store: %v", err) return nil, err @@ -91,7 +91,7 @@ func (s *Server) DeleteLogicalBridge(ctx context.Context, in *pb.DeleteLogicalBr } // remove from the Database delete(s.ListHelper, obj.Name) - err = s.store.Delete(obj.Name) + err = s.Store.Delete(obj.Name) if err != nil { return nil, err } @@ -106,7 +106,7 @@ func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBr } // fetch object from the database bridge := new(pb.LogicalBridge) - ok, err := s.store.Get(in.LogicalBridge.Name, bridge) + ok, err := s.Store.Get(in.LogicalBridge.Name, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) return nil, err @@ -119,21 +119,21 @@ func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBr // only if VNI is not empty if bridge.Spec.Vni != nil { vxlanName := fmt.Sprintf("vni%d", *bridge.Spec.Vni) - iface, err := s.nLink.LinkByName(ctx, vxlanName) + iface, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) return nil, err } // base := iface.Attrs() // iface.MTU = 1500 // TODO: remove this, just an example - if err := s.nLink.LinkModify(ctx, iface); err != nil { + if err := s.NLink.LinkModify(ctx, iface); err != nil { fmt.Printf("Failed to update link: %v", err) return nil, err } } response := utils.ProtoClone(in.LogicalBridge) response.Status = &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP} - err = s.store.Set(in.LogicalBridge.Name, response) + err = s.Store.Set(in.LogicalBridge.Name, response) if err != nil { return nil, err } @@ -148,7 +148,7 @@ func (s *Server) GetLogicalBridge(ctx context.Context, in *pb.GetLogicalBridgeRe } // fetch object from the database bridge := new(pb.LogicalBridge) - ok, err := s.store.Get(in.Name, bridge) + ok, err := s.Store.Get(in.Name, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) return nil, err @@ -160,7 +160,7 @@ func (s *Server) GetLogicalBridge(ctx context.Context, in *pb.GetLogicalBridgeRe // only if VNI is not empty if bridge.Spec.Vni != nil { vxlanName := fmt.Sprintf("vni%d", *bridge.Spec.Vni) - _, err := s.nLink.LinkByName(ctx, vxlanName) + _, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) return nil, err @@ -188,7 +188,7 @@ func (s *Server) ListLogicalBridges(_ context.Context, in *pb.ListLogicalBridges continue } bridge := new(pb.LogicalBridge) - ok, err := s.store.Get(key, bridge) + ok, err := s.Store.Get(key, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) return nil, err diff --git a/pkg/bridge/netlink.go b/pkg/objects/bridge/netlink.go similarity index 81% rename from pkg/bridge/netlink.go rename to pkg/objects/bridge/netlink.go index 8f4b5f97..db0a96f9 100644 --- a/pkg/bridge/netlink.go +++ b/pkg/objects/bridge/netlink.go @@ -9,6 +9,7 @@ import ( "context" "encoding/binary" "fmt" + "github.com/opiproject/opi-evpn-bridge/pkg/objects" "log" "net" @@ -24,9 +25,9 @@ func (s *Server) netlinkCreateLogicalBridge(ctx context.Context, in *pb.CreateLo // create vxlan only if VNI is not empty if in.LogicalBridge.Spec.Vni != nil { // use netlink to find br-tenant - bridge, err := s.nLink.LinkByName(ctx, tenantbridgeName) + bridge, err := s.NLink.LinkByName(ctx, objects.TenantbridgeName) if err != nil { - err := status.Errorf(codes.NotFound, "unable to find key %s", tenantbridgeName) + err := status.Errorf(codes.NotFound, "unable to find key %s", objects.TenantbridgeName) return err } // Example: ip link add vxlan- type vxlan id local dstport 4789 nolearning proxy @@ -36,22 +37,22 @@ func (s *Server) netlinkCreateLogicalBridge(ctx context.Context, in *pb.CreateLo vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*in.LogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} log.Printf("Creating Vxlan %v", vxlan) // TODO: take Port from proto instead of hard-coded - if err := s.nLink.LinkAdd(ctx, vxlan); err != nil { + if err := s.NLink.LinkAdd(ctx, vxlan); err != nil { fmt.Printf("Failed to create Vxlan link: %v", err) return err } // Example: ip link set vxlan- master br-tenant addrgenmode none - if err := s.nLink.LinkSetMaster(ctx, vxlan, bridge); err != nil { + if err := s.NLink.LinkSetMaster(ctx, vxlan, bridge); err != nil { fmt.Printf("Failed to add Vxlan to bridge: %v", err) return err } // Example: ip link set vxlan- up - if err := s.nLink.LinkSetUp(ctx, vxlan); err != nil { + if err := s.NLink.LinkSetUp(ctx, vxlan); err != nil { fmt.Printf("Failed to up Vxlan link: %v", err) return err } // Example: bridge vlan add dev vxlan- vid pvid untagged - if err := s.nLink.BridgeVlanAdd(ctx, vxlan, uint16(in.LogicalBridge.Spec.VlanId), true, true, false, false); err != nil { + if err := s.NLink.BridgeVlanAdd(ctx, vxlan, uint16(in.LogicalBridge.Spec.VlanId), true, true, false, false); err != nil { fmt.Printf("Failed to add vlan to bridge: %v", err) return err } @@ -65,24 +66,24 @@ func (s *Server) netlinkDeleteLogicalBridge(ctx context.Context, obj *pb.Logical if obj.Spec.Vni != nil { // use netlink to find vxlan device vxlanName := fmt.Sprintf("vni%d", *obj.Spec.Vni) - vxlan, err := s.nLink.LinkByName(ctx, vxlanName) + vxlan, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) return err } log.Printf("Deleting Vxlan %v", vxlan) // bring link down - if err := s.nLink.LinkSetDown(ctx, vxlan); err != nil { + if err := s.NLink.LinkSetDown(ctx, vxlan); err != nil { fmt.Printf("Failed to up link: %v", err) return err } // delete bridge vlan - if err := s.nLink.BridgeVlanDel(ctx, vxlan, uint16(obj.Spec.VlanId), true, true, false, false); err != nil { + if err := s.NLink.BridgeVlanDel(ctx, vxlan, uint16(obj.Spec.VlanId), true, true, false, false); err != nil { fmt.Printf("Failed to delete vlan to bridge: %v", err) return err } // use netlink to delete vxlan device - if err := s.nLink.LinkDel(ctx, vxlan); err != nil { + if err := s.NLink.LinkDel(ctx, vxlan); err != nil { fmt.Printf("Failed to delete link: %v", err) return err } diff --git a/pkg/objects/bridge/server.go b/pkg/objects/bridge/server.go new file mode 100644 index 00000000..0d70ca11 --- /dev/null +++ b/pkg/objects/bridge/server.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2022-2023 Dell Inc, or its subsidiaries. + +// Package bridge is the main package of the application +package bridge + +import ( + pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" + "github.com/opiproject/opi-evpn-bridge/pkg/objects" + "github.com/philippgille/gokv" + + "github.com/opiproject/opi-evpn-bridge/pkg/utils" +) + +// Server represents the Server object +type Server struct { + pb.UnimplementedLogicalBridgeServiceServer + *objects.Server +} + +// NewServer creates initialized instance of EVPN server +func NewServer(store gokv.Store) *Server { + return &Server{ + Server: objects.NewServer(store), + } +} + +// NewServerWithArgs creates initialized instance of EVPN server +// with externally created Netlink +func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Server { + return &Server{ + Server: objects.NewServerWithArgs(nLink, frr, store), + } +} diff --git a/pkg/bridge/server_test.go b/pkg/objects/bridge/server_test.go similarity index 100% rename from pkg/bridge/server_test.go rename to pkg/objects/bridge/server_test.go diff --git a/pkg/bridge/server.go b/pkg/objects/object.go similarity index 68% rename from pkg/bridge/server.go rename to pkg/objects/object.go index d089d94f..66493c10 100644 --- a/pkg/bridge/server.go +++ b/pkg/objects/object.go @@ -1,31 +1,33 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) 2022-2023 Dell Inc, or its subsidiaries. - -// Package bridge is the main package of the application -package bridge +package objects import ( - "log" - + "context" + "github.com/opiproject/opi-evpn-bridge/pkg/utils" + "github.com/philippgille/gokv" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" + "log" +) - "github.com/philippgille/gokv" - - pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" +type ObjectFactory[T any] interface { + Create(ctx context.Context, in interface{}) (T, error) + Delete(ctx context.Context, in interface{}) error + Update(ctx context.Context, in interface{}) (T, error) + Get(ctx context.Context, in interface{}) (T, error) + List(ctx context.Context, in interface{}) ([]T, error) +} - "github.com/opiproject/opi-evpn-bridge/pkg/utils" +const ( + TenantbridgeName = "br-tenant" ) -// Server represents the Server object type Server struct { - pb.UnimplementedLogicalBridgeServiceServer Pagination map[string]int ListHelper map[string]bool - nLink utils.Netlink - frr utils.Frr - tracer trace.Tracer - store gokv.Store + NLink utils.Netlink + Frr utils.Frr + Tracer trace.Tracer + Store gokv.Store } // NewServer creates initialized instance of EVPN server diff --git a/pkg/port/common.go b/pkg/objects/port/common.go similarity index 100% rename from pkg/port/common.go rename to pkg/objects/port/common.go diff --git a/pkg/port/grpc.go b/pkg/objects/port/grpc.go similarity index 100% rename from pkg/port/grpc.go rename to pkg/objects/port/grpc.go diff --git a/pkg/port/netlink.go b/pkg/objects/port/netlink.go similarity index 100% rename from pkg/port/netlink.go rename to pkg/objects/port/netlink.go diff --git a/pkg/port/port_test.go b/pkg/objects/port/port_test.go similarity index 100% rename from pkg/port/port_test.go rename to pkg/objects/port/port_test.go diff --git a/pkg/port/port_validate.go b/pkg/objects/port/port_validate.go similarity index 100% rename from pkg/port/port_validate.go rename to pkg/objects/port/port_validate.go diff --git a/pkg/port/server.go b/pkg/objects/port/server.go similarity index 100% rename from pkg/port/server.go rename to pkg/objects/port/server.go diff --git a/pkg/port/server_test.go b/pkg/objects/port/server_test.go similarity index 100% rename from pkg/port/server_test.go rename to pkg/objects/port/server_test.go diff --git a/pkg/svi/common.go b/pkg/objects/svi/common.go similarity index 98% rename from pkg/svi/common.go rename to pkg/objects/svi/common.go index ce31eef6..c3c954d5 100644 --- a/pkg/svi/common.go +++ b/pkg/objects/svi/common.go @@ -30,9 +30,6 @@ func resourceIDToFullName(resourceID string) string { } // TODO: move all of this to a common place -const ( - tenantbridgeName = "br-tenant" -) var ( testLogicalBridgeID = "opi-bridge9" diff --git a/pkg/svi/frr.go b/pkg/objects/svi/frr.go similarity index 100% rename from pkg/svi/frr.go rename to pkg/objects/svi/frr.go diff --git a/pkg/svi/grpc.go b/pkg/objects/svi/grpc.go similarity index 100% rename from pkg/svi/grpc.go rename to pkg/objects/svi/grpc.go diff --git a/pkg/svi/netlink.go b/pkg/objects/svi/netlink.go similarity index 100% rename from pkg/svi/netlink.go rename to pkg/objects/svi/netlink.go diff --git a/pkg/svi/server.go b/pkg/objects/svi/server.go similarity index 100% rename from pkg/svi/server.go rename to pkg/objects/svi/server.go diff --git a/pkg/svi/server_test.go b/pkg/objects/svi/server_test.go similarity index 100% rename from pkg/svi/server_test.go rename to pkg/objects/svi/server_test.go diff --git a/pkg/svi/svi_test.go b/pkg/objects/svi/svi_test.go similarity index 100% rename from pkg/svi/svi_test.go rename to pkg/objects/svi/svi_test.go diff --git a/pkg/svi/svi_validate.go b/pkg/objects/svi/svi_validate.go similarity index 100% rename from pkg/svi/svi_validate.go rename to pkg/objects/svi/svi_validate.go diff --git a/pkg/vrf/common.go b/pkg/objects/vrf/common.go similarity index 100% rename from pkg/vrf/common.go rename to pkg/objects/vrf/common.go diff --git a/pkg/vrf/frr.go b/pkg/objects/vrf/frr.go similarity index 100% rename from pkg/vrf/frr.go rename to pkg/objects/vrf/frr.go diff --git a/pkg/vrf/grpc.go b/pkg/objects/vrf/grpc.go similarity index 100% rename from pkg/vrf/grpc.go rename to pkg/objects/vrf/grpc.go diff --git a/pkg/vrf/netlink.go b/pkg/objects/vrf/netlink.go similarity index 100% rename from pkg/vrf/netlink.go rename to pkg/objects/vrf/netlink.go diff --git a/pkg/vrf/server.go b/pkg/objects/vrf/server.go similarity index 100% rename from pkg/vrf/server.go rename to pkg/objects/vrf/server.go diff --git a/pkg/vrf/server_test.go b/pkg/objects/vrf/server_test.go similarity index 100% rename from pkg/vrf/server_test.go rename to pkg/objects/vrf/server_test.go diff --git a/pkg/vrf/vrf_test.go b/pkg/objects/vrf/vrf_test.go similarity index 100% rename from pkg/vrf/vrf_test.go rename to pkg/objects/vrf/vrf_test.go diff --git a/pkg/vrf/vrf_validate.go b/pkg/objects/vrf/vrf_validate.go similarity index 100% rename from pkg/vrf/vrf_validate.go rename to pkg/objects/vrf/vrf_validate.go From 10cfe73493f4c641cfd2911a661b91fe698f7c32 Mon Sep 17 00:00:00 2001 From: Brian Rochford Date: Fri, 20 Oct 2023 17:30:47 +0100 Subject: [PATCH 2/3] Refactor for object factory interface Signed-off-by: Brian Rochford --- pkg/models/bridge.go | 1 + pkg/objects/bridge/bridge_validate.go | 61 ++++++++++++++----- pkg/objects/bridge/common.go | 7 +-- pkg/objects/bridge/grpc.go | 84 ++++++++++++++------------- pkg/objects/bridge/server.go | 3 +- pkg/objects/object.go | 8 +-- pkg/objects/port/common.go | 4 -- pkg/objects/svi/common.go | 1 - pkg/objects/vrf/common.go | 1 - 9 files changed, 98 insertions(+), 72 deletions(-) diff --git a/pkg/models/bridge.go b/pkg/models/bridge.go index 0e4e8ba0..d30e9e25 100644 --- a/pkg/models/bridge.go +++ b/pkg/models/bridge.go @@ -13,6 +13,7 @@ import ( // Bridge object, separate from protobuf for decoupling type Bridge struct { + Name string Vni uint32 VlanID uint32 VtepIP net.IPNet diff --git a/pkg/objects/bridge/bridge_validate.go b/pkg/objects/bridge/bridge_validate.go index 5f62bda5..b7d5a33e 100644 --- a/pkg/objects/bridge/bridge_validate.go +++ b/pkg/objects/bridge/bridge_validate.go @@ -5,6 +5,7 @@ package bridge import ( + "errors" "fmt" "go.einride.tech/aip/fieldbehavior" @@ -18,53 +19,83 @@ import ( pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" ) -func (s *Server) validateCreateLogicalBridgeRequest(in *pb.CreateLogicalBridgeRequest) error { +func (s *Server) validateCreateLogicalBridgeRequest(i any) (*pb.CreateLogicalBridgeRequest, error) { // check required fields + in, ok := i.(*pb.CreateLogicalBridgeRequest) + if ok { + return nil, errors.New("protobuf type assertion error for CreateLogicalBridgeRequest") + } + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return err + return nil, err } // check vlan id is in range if in.LogicalBridge.Spec.VlanId > 4095 { msg := fmt.Sprintf("VlanId value (%d) have to be between 1 and 4095", in.LogicalBridge.Spec.VlanId) - return status.Errorf(codes.InvalidArgument, msg) + return nil, status.Errorf(codes.InvalidArgument, msg) } // see https://google.aip.dev/133#user-specified-ids if in.LogicalBridgeId != "" { if err := resourceid.ValidateUserSettable(in.LogicalBridgeId); err != nil { - return err + return nil, err } } // TODO: check in.LogicalBridge.Spec.Vni validity - return nil + return in, nil } -func (s *Server) validateDeleteLogicalBridgeRequest(in *pb.DeleteLogicalBridgeRequest) error { +func (s *Server) validateDeleteLogicalBridgeRequest(i any) (*pb.DeleteLogicalBridgeRequest, error) { // check required fields + in, ok := i.(*pb.DeleteLogicalBridgeRequest) + if ok { + return nil, errors.New("protobuf type assertion error for DeleteLogicalBridgeRequest") + } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return err + return nil, err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return resourcename.Validate(in.Name) + return in, resourcename.Validate(in.Name) } -func (s *Server) validateUpdateLogicalBridgeRequest(in *pb.UpdateLogicalBridgeRequest) error { +func (s *Server) validateUpdateLogicalBridgeRequest(i any) (*pb.UpdateLogicalBridgeRequest, error) { // check required fields + in, ok := i.(*pb.UpdateLogicalBridgeRequest) + if ok { + return nil, errors.New("protobuf type assertion error for UpdateLogicalBridgeRequest") + } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return err + return nil, err } // update_mask = 2 if err := fieldmask.Validate(in.UpdateMask, in.LogicalBridge); err != nil { - return err + return nil, err + } + // Validate that a resource name conforms to the restrictions outlined in AIP-122. + return in, resourcename.Validate(in.LogicalBridge.Name) +} + +func (s *Server) validateGetLogicalBridgeRequest(i any) (*pb.GetLogicalBridgeRequest, error) { + // check required fields + in, ok := i.(*pb.GetLogicalBridgeRequest) + if ok { + return nil, errors.New("protobuf type assertion error for GetLogicalBridgeRequest") + } + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + return nil, err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return resourcename.Validate(in.LogicalBridge.Name) + return in, resourcename.Validate(in.Name) } -func (s *Server) validateGetLogicalBridgeRequest(in *pb.GetLogicalBridgeRequest) error { +func (s *Server) validateListLogicalBridgeRequest(i any) (*pb.ListLogicalBridgesRequest, error) { // check required fields + in, ok := i.(*pb.ListLogicalBridgesRequest) + if ok { + return nil, errors.New("protobuf type assertion error for ListLogicalBridgesRequest") + } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return err + return nil, err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return resourcename.Validate(in.Name) + return in, nil } diff --git a/pkg/objects/bridge/common.go b/pkg/objects/bridge/common.go index 45a190ee..de329ed1 100644 --- a/pkg/objects/bridge/common.go +++ b/pkg/objects/bridge/common.go @@ -7,6 +7,7 @@ package bridge import ( "context" "fmt" + "github.com/opiproject/opi-evpn-bridge/pkg/models" "log" "net" "sort" @@ -17,7 +18,7 @@ import ( pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" ) -func sortLogicalBridges(bridges []*pb.LogicalBridge) { +func sortLogicalBridges(bridges []*models.Bridge) { sort.Slice(bridges, func(i int, j int) bool { return bridges[i].Name < bridges[j].Name }) @@ -28,10 +29,6 @@ func resourceIDToFullName(resourceID string) string { } // TODO: move all of this to a common place -const ( - tenantbridgeName = "br-tenant" -) - func dialer(opi *Server) func(context.Context, string) (net.Conn, error) { listener := bufconn.Listen(1024 * 1024) server := grpc.NewServer() diff --git a/pkg/objects/bridge/grpc.go b/pkg/objects/bridge/grpc.go index 21dcc807..875e60a4 100644 --- a/pkg/objects/bridge/grpc.go +++ b/pkg/objects/bridge/grpc.go @@ -17,17 +17,16 @@ import ( pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" - "go.einride.tech/aip/fieldbehavior" "go.einride.tech/aip/resourceid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" ) -// CreateLogicalBridge executes the creation of the LogicalBridge -func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBridgeRequest) (*pb.LogicalBridge, error) { +// Create executes the creation of the LogicalBridge +func (s *Server) Create(ctx context.Context, i any) (*models.Bridge, error) { // check input correctness - if err := s.validateCreateLogicalBridgeRequest(in); err != nil { + in, err := s.validateCreateLogicalBridgeRequest(i) + if err != nil { return nil, err } // see https://google.aip.dev/133#user-specified-ids @@ -38,7 +37,7 @@ func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBr } in.LogicalBridge.Name = resourceIDToFullName(resourceID) // idempotent API when called with same key, should return same object - obj := new(pb.LogicalBridge) + obj := new(models.Bridge) ok, err := s.Store.Get(in.LogicalBridge.Name, obj) if err != nil { fmt.Printf("Failed to interact with store: %v", err) @@ -53,9 +52,8 @@ func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBr return nil, err } // translate object - response := utils.ProtoClone(in.LogicalBridge) - response.Status = &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP} - log.Printf("new object %v", models.NewBridge(response)) + response := models.NewBridge(in.LogicalBridge) + log.Printf("new object %v", response) // save object to the database s.ListHelper[in.LogicalBridge.Name] = false err = s.Store.Set(in.LogicalBridge.Name, response) @@ -65,47 +63,49 @@ func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBr return response, nil } -// DeleteLogicalBridge deletes a LogicalBridge -func (s *Server) DeleteLogicalBridge(ctx context.Context, in *pb.DeleteLogicalBridgeRequest) (*emptypb.Empty, error) { +// Delete deletes a LogicalBridge +func (s *Server) Delete(ctx context.Context, i any) error { // check input correctness - if err := s.validateDeleteLogicalBridgeRequest(in); err != nil { - return nil, err + in, err := s.validateDeleteLogicalBridgeRequest(i) + if err != nil { + return err } // fetch object from the database obj := new(pb.LogicalBridge) ok, err := s.Store.Get(in.Name, obj) if err != nil { fmt.Printf("Failed to interact with store: %v", err) - return nil, err + return err } if !ok { if in.AllowMissing { - return &emptypb.Empty{}, nil + return nil } err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) - return nil, err + return err } // configure netlink if err := s.netlinkDeleteLogicalBridge(ctx, obj); err != nil { - return nil, err + return err } // remove from the Database delete(s.ListHelper, obj.Name) err = s.Store.Delete(obj.Name) if err != nil { - return nil, err + return err } - return &emptypb.Empty{}, nil + return nil } -// UpdateLogicalBridge updates a LogicalBridge -func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBridgeRequest) (*pb.LogicalBridge, error) { +// Update updates a LogicalBridge +func (s *Server) Update(ctx context.Context, i any) (*models.Bridge, error) { // check input correctness - if err := s.validateUpdateLogicalBridgeRequest(in); err != nil { + in, err := s.validateUpdateLogicalBridgeRequest(i) + if err != nil { return nil, err } // fetch object from the database - bridge := new(pb.LogicalBridge) + bridge := new(models.Bridge) ok, err := s.Store.Get(in.LogicalBridge.Name, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) @@ -117,8 +117,8 @@ func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBr return nil, err } // only if VNI is not empty - if bridge.Spec.Vni != nil { - vxlanName := fmt.Sprintf("vni%d", *bridge.Spec.Vni) + if bridge.Vni != 0 { + vxlanName := fmt.Sprintf("vni%d", bridge.Vni) iface, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) @@ -131,8 +131,7 @@ func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBr return nil, err } } - response := utils.ProtoClone(in.LogicalBridge) - response.Status = &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP} + response := models.NewBridge(in.LogicalBridge) err = s.Store.Set(in.LogicalBridge.Name, response) if err != nil { return nil, err @@ -140,14 +139,15 @@ func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBr return response, nil } -// GetLogicalBridge gets a LogicalBridge -func (s *Server) GetLogicalBridge(ctx context.Context, in *pb.GetLogicalBridgeRequest) (*pb.LogicalBridge, error) { +// Get gets a LogicalBridge +func (s *Server) Get(ctx context.Context, i any) (*models.Bridge, error) { // check input correctness - if err := s.validateGetLogicalBridgeRequest(in); err != nil { + in, err := s.validateGetLogicalBridgeRequest(i) + if err != nil { return nil, err } // fetch object from the database - bridge := new(pb.LogicalBridge) + bridge := new(models.Bridge) ok, err := s.Store.Get(in.Name, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) @@ -158,36 +158,38 @@ func (s *Server) GetLogicalBridge(ctx context.Context, in *pb.GetLogicalBridgeRe return nil, err } // only if VNI is not empty - if bridge.Spec.Vni != nil { - vxlanName := fmt.Sprintf("vni%d", *bridge.Spec.Vni) + if bridge.Vni != 0 { + vxlanName := fmt.Sprintf("vni%d", bridge.Vni) _, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) return nil, err } } - // TODO - return &pb.LogicalBridge{Name: in.Name, Spec: &pb.LogicalBridgeSpec{Vni: bridge.Spec.Vni, VlanId: bridge.Spec.VlanId}, Status: &pb.LogicalBridgeStatus{OperStatus: pb.LBOperStatus_LB_OPER_STATUS_UP}}, nil + + return bridge, nil } -// ListLogicalBridges lists logical bridges -func (s *Server) ListLogicalBridges(_ context.Context, in *pb.ListLogicalBridgesRequest) (*pb.ListLogicalBridgesResponse, error) { +// List lists logical bridges +func (s *Server) List(_ context.Context, i any) ([]*models.Bridge, error) { // check required fields - if err := fieldbehavior.ValidateRequiredFields(in); err != nil { + in, err := s.validateListLogicalBridgeRequest(i) + if err != nil { return nil, err } + // fetch pagination from the database, calculate size and offset size, offset, perr := utils.ExtractPagination(in.PageSize, in.PageToken, s.Pagination) if perr != nil { return nil, perr } // fetch object from the database - Blobarray := []*pb.LogicalBridge{} + var Blobarray []*models.Bridge for key := range s.ListHelper { if !strings.HasPrefix(key, "//network.opiproject.org/bridges") { continue } - bridge := new(pb.LogicalBridge) + bridge := new(models.Bridge) ok, err := s.Store.Get(key, bridge) if err != nil { fmt.Printf("Failed to interact with store: %v", err) @@ -208,5 +210,5 @@ func (s *Server) ListLogicalBridges(_ context.Context, in *pb.ListLogicalBridges token = uuid.New().String() s.Pagination[token] = offset + size } - return &pb.ListLogicalBridgesResponse{LogicalBridges: Blobarray, NextPageToken: token}, nil + return Blobarray, nil } diff --git a/pkg/objects/bridge/server.go b/pkg/objects/bridge/server.go index 0d70ca11..603a4c9f 100644 --- a/pkg/objects/bridge/server.go +++ b/pkg/objects/bridge/server.go @@ -6,6 +6,7 @@ package bridge import ( pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" + "github.com/opiproject/opi-evpn-bridge/pkg/models" "github.com/opiproject/opi-evpn-bridge/pkg/objects" "github.com/philippgille/gokv" @@ -19,7 +20,7 @@ type Server struct { } // NewServer creates initialized instance of EVPN server -func NewServer(store gokv.Store) *Server { +func NewServer(store gokv.Store) objects.ObjectFactory[*models.Bridge] { return &Server{ Server: objects.NewServer(store), } diff --git a/pkg/objects/object.go b/pkg/objects/object.go index 66493c10..c4bea878 100644 --- a/pkg/objects/object.go +++ b/pkg/objects/object.go @@ -52,9 +52,9 @@ func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Se return &Server{ ListHelper: make(map[string]bool), Pagination: make(map[string]int), - nLink: nLink, - frr: frr, - tracer: otel.Tracer(""), - store: store, + NLink: nLink, + Frr: frr, + Tracer: otel.Tracer(""), + Store: store, } } diff --git a/pkg/objects/port/common.go b/pkg/objects/port/common.go index fff0961c..7dccffec 100644 --- a/pkg/objects/port/common.go +++ b/pkg/objects/port/common.go @@ -30,10 +30,6 @@ func resourceIDToFullName(resourceID string) string { } // TODO: move all of this to a common place -const ( - tenantbridgeName = "br-tenant" -) - var ( testLogicalBridgeID = "opi-bridge9" testLogicalBridgeName = resourceIDToFullName(testLogicalBridgeID) diff --git a/pkg/objects/svi/common.go b/pkg/objects/svi/common.go index c3c954d5..1adb7fd4 100644 --- a/pkg/objects/svi/common.go +++ b/pkg/objects/svi/common.go @@ -30,7 +30,6 @@ func resourceIDToFullName(resourceID string) string { } // TODO: move all of this to a common place - var ( testLogicalBridgeID = "opi-bridge9" testLogicalBridgeName = resourceIDToFullName(testLogicalBridgeID) diff --git a/pkg/objects/vrf/common.go b/pkg/objects/vrf/common.go index b4ddce9a..1e6cda41 100644 --- a/pkg/objects/vrf/common.go +++ b/pkg/objects/vrf/common.go @@ -41,7 +41,6 @@ func generateRandMAC() ([]byte, error) { } // TODO: move all of this to a common place - func dialer(opi *Server) func(context.Context, string) (net.Conn, error) { listener := bufconn.Listen(1024 * 1024) server := grpc.NewServer() From 78fccf37af7c6d25c114b35ea3b6e3b5c48afdd8 Mon Sep 17 00:00:00 2001 From: Brian Rochford Date: Mon, 23 Oct 2023 15:38:26 +0100 Subject: [PATCH 3/3] Changed abstraction for pb servers Signed-off-by: Brian Rochford --- pkg/models/bridge.go | 23 ++-- pkg/models/interface.go | 3 +- pkg/objects/bridge/bridge_test.go | 35 +++--- pkg/objects/bridge/bridge_validate.go | 61 +++------- pkg/objects/bridge/common.go | 18 +-- pkg/objects/bridge/grpc.go | 159 ++++++++++---------------- pkg/objects/bridge/server.go | 12 +- pkg/objects/object.go | 106 +++++++++++++++-- pkg/objects/utils.go | 7 ++ 9 files changed, 219 insertions(+), 205 deletions(-) create mode 100644 pkg/objects/utils.go diff --git a/pkg/models/bridge.go b/pkg/models/bridge.go index d30e9e25..8fd42571 100644 --- a/pkg/models/bridge.go +++ b/pkg/models/bridge.go @@ -22,17 +22,18 @@ type Bridge struct { // build time check that struct implements interface var _ EvpnObject[*pb.LogicalBridge] = (*Bridge)(nil) -// NewBridge creates new SVI object from protobuf message -func NewBridge(in *pb.LogicalBridge) *Bridge { - // vtepip := make(net.IP, 4) - // binary.BigEndian.PutUint32(vtepip, in.Spec.VtepIpPrefix.Addr.GetV4Addr()) - // vip := net.IPNet{IP: vtepip, Mask: net.CIDRMask(int(in.Spec.VtepIpPrefix.Len), 32)} - // TODO: Vni: *in.Spec.Vni - return &Bridge{VlanID: in.Spec.VlanId} +func NewBridge(i *pb.LogicalBridge) EvpnObject[*pb.LogicalBridge] { + return &Bridge{ + // vtepip := make(net.IP, 4) + // binary.BigEndian.PutUint32(vtepip, in.Spec.VtepIpPrefix.Addr.GetV4Addr()) + // vip := net.IPNet{IP: vtepip, Mask: net.CIDRMask(int(in.Spec.VtepIpPrefix.Len), 32)} + // TODO: Vni: *in.Spec.Vni + VlanID: i.Spec.VlanId, + } } // ToPb transforms SVI object to protobuf message -func (in *Bridge) ToPb() (*pb.LogicalBridge, error) { +func (in *Bridge) ToPb() *pb.LogicalBridge { bridge := &pb.LogicalBridge{ Spec: &pb.LogicalBridgeSpec{ Vni: &in.Vni, @@ -43,5 +44,9 @@ func (in *Bridge) ToPb() (*pb.LogicalBridge, error) { }, } // TODO: add VtepIpPrefix - return bridge, nil + return bridge +} + +func (in *Bridge) GetName() string { + return in.Name } diff --git a/pkg/models/interface.go b/pkg/models/interface.go index a57daa1e..b59cafd1 100644 --- a/pkg/models/interface.go +++ b/pkg/models/interface.go @@ -6,5 +6,6 @@ package models // EvpnObject is an interface for all domain objects in evpn-gw type EvpnObject[T any] interface { - ToPb() (T, error) + ToPb() T + GetName() string } diff --git a/pkg/objects/bridge/bridge_test.go b/pkg/objects/bridge/bridge_test.go index 63f30bba..6aa00c22 100644 --- a/pkg/objects/bridge/bridge_test.go +++ b/pkg/objects/bridge/bridge_test.go @@ -10,6 +10,7 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/opiproject/opi-evpn-bridge/pkg/objects" "log" "net" "reflect" @@ -36,7 +37,7 @@ import ( var ( testLogicalBridgeID = "opi-bridge9" - testLogicalBridgeName = resourceIDToFullName(testLogicalBridgeID) + testLogicalBridgeName = objects.ResourceIDToFullName(Prefix, testLogicalBridgeID) testLogicalBridge = pb.LogicalBridge{ Spec: &pb.LogicalBridgeSpec{ Vni: proto.Uint32(11), @@ -147,10 +148,10 @@ func Test_CreateLogicalBridge(t *testing.T) { in: &testLogicalBridge, out: nil, errCode: codes.NotFound, - errMsg: fmt.Sprintf("unable to find key %v", tenantbridgeName), + errMsg: fmt.Sprintf("unable to find key %v", objects.TenantbridgeName), exist: false, on: func(mockNetlink *mocks.Netlink, mockFrr *mocks.Frr, errMsg string) { - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(nil, errors.New(errMsg)).Once() + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(nil, errors.New(errMsg)).Once() }, }, "failed LinkAdd call": { @@ -166,8 +167,8 @@ func Test_CreateLogicalBridge(t *testing.T) { binary.BigEndian.PutUint32(myip, 167772162) vxlanName := fmt.Sprintf("vni%d", *testLogicalBridge.Spec.Vni) vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*testLogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} - bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: tenantbridgeName}} - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(bridge, nil).Once() + bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: objects.TenantbridgeName}} + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(bridge, nil).Once() mockNetlink.EXPECT().LinkAdd(mock.Anything, vxlan).Return(errors.New(errMsg)).Once() }, }, @@ -183,8 +184,8 @@ func Test_CreateLogicalBridge(t *testing.T) { binary.BigEndian.PutUint32(myip, 167772162) vxlanName := fmt.Sprintf("vni%d", *testLogicalBridge.Spec.Vni) vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*testLogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} - bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: tenantbridgeName}} - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(bridge, nil).Once() + bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: objects.TenantbridgeName}} + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(bridge, nil).Once() mockNetlink.EXPECT().LinkAdd(mock.Anything, vxlan).Return(nil).Once() mockNetlink.EXPECT().LinkSetMaster(mock.Anything, vxlan, bridge).Return(errors.New(errMsg)).Once() }, @@ -201,8 +202,8 @@ func Test_CreateLogicalBridge(t *testing.T) { binary.BigEndian.PutUint32(myip, 167772162) vxlanName := fmt.Sprintf("vni%d", *testLogicalBridge.Spec.Vni) vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*testLogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} - bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: tenantbridgeName}} - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(bridge, nil).Once() + bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: objects.TenantbridgeName}} + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(bridge, nil).Once() mockNetlink.EXPECT().LinkAdd(mock.Anything, vxlan).Return(nil).Once() mockNetlink.EXPECT().LinkSetMaster(mock.Anything, vxlan, bridge).Return(nil).Once() mockNetlink.EXPECT().LinkSetUp(mock.Anything, vxlan).Return(errors.New(errMsg)).Once() @@ -220,8 +221,8 @@ func Test_CreateLogicalBridge(t *testing.T) { binary.BigEndian.PutUint32(myip, 167772162) vxlanName := fmt.Sprintf("vni%d", *testLogicalBridge.Spec.Vni) vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*testLogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} - bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: tenantbridgeName}} - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(bridge, nil).Once() + bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: objects.TenantbridgeName}} + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(bridge, nil).Once() mockNetlink.EXPECT().LinkAdd(mock.Anything, vxlan).Return(nil).Once() mockNetlink.EXPECT().LinkSetMaster(mock.Anything, vxlan, bridge).Return(nil).Once() mockNetlink.EXPECT().LinkSetUp(mock.Anything, vxlan).Return(nil).Once() @@ -241,8 +242,8 @@ func Test_CreateLogicalBridge(t *testing.T) { binary.BigEndian.PutUint32(myip, 167772162) vxlanName := fmt.Sprintf("vni%d", *testLogicalBridge.Spec.Vni) vxlan := &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanName}, VxlanId: int(*testLogicalBridge.Spec.Vni), Port: 4789, Learning: false, SrcAddr: myip} - bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: tenantbridgeName}} - mockNetlink.EXPECT().LinkByName(mock.Anything, tenantbridgeName).Return(bridge, nil).Once() + bridge := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: objects.TenantbridgeName}} + mockNetlink.EXPECT().LinkByName(mock.Anything, objects.TenantbridgeName).Return(bridge, nil).Once() mockNetlink.EXPECT().LinkAdd(mock.Anything, vxlan).Return(nil).Once() mockNetlink.EXPECT().LinkSetMaster(mock.Anything, vxlan, bridge).Return(nil).Once() mockNetlink.EXPECT().LinkSetUp(mock.Anything, vxlan).Return(nil).Once() @@ -320,7 +321,7 @@ func Test_DeleteLogicalBridge(t *testing.T) { in: "unknown-id", out: nil, errCode: codes.NotFound, - errMsg: fmt.Sprintf("unable to find key %v", resourceIDToFullName("unknown-id")), + errMsg: fmt.Sprintf("unable to find key %v", objects.ResourceIDToFullName(Prefix, "unknown-id")), missing: false, on: nil, }, @@ -445,7 +446,7 @@ func Test_DeleteLogicalBridge(t *testing.T) { }(conn) client := pb.NewLogicalBridgeServiceClient(conn) - fname1 := resourceIDToFullName(tt.in) + fname1 := objects.ResourceIDToFullName(Prefix, tt.in) _ = opi.store.Set(testLogicalBridgeName, &testLogicalBridgeWithStatus) if tt.on != nil { @@ -502,12 +503,12 @@ func Test_UpdateLogicalBridge(t *testing.T) { "valid request with unknown key": { mask: nil, in: &pb.LogicalBridge{ - Name: resourceIDToFullName("unknown-id"), + Name: objects.ResourceIDToFullName(Prefix, "unknown-id"), Spec: spec, }, out: nil, errCode: codes.NotFound, - errMsg: fmt.Sprintf("unable to find key %v", resourceIDToFullName("unknown-id")), + errMsg: fmt.Sprintf("unable to find key %v", objects.ResourceIDToFullName(Prefix, "unknown-id")), start: false, exist: true, }, diff --git a/pkg/objects/bridge/bridge_validate.go b/pkg/objects/bridge/bridge_validate.go index b7d5a33e..5f62bda5 100644 --- a/pkg/objects/bridge/bridge_validate.go +++ b/pkg/objects/bridge/bridge_validate.go @@ -5,7 +5,6 @@ package bridge import ( - "errors" "fmt" "go.einride.tech/aip/fieldbehavior" @@ -19,83 +18,53 @@ import ( pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" ) -func (s *Server) validateCreateLogicalBridgeRequest(i any) (*pb.CreateLogicalBridgeRequest, error) { +func (s *Server) validateCreateLogicalBridgeRequest(in *pb.CreateLogicalBridgeRequest) error { // check required fields - in, ok := i.(*pb.CreateLogicalBridgeRequest) - if ok { - return nil, errors.New("protobuf type assertion error for CreateLogicalBridgeRequest") - } - if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return nil, err + return err } // check vlan id is in range if in.LogicalBridge.Spec.VlanId > 4095 { msg := fmt.Sprintf("VlanId value (%d) have to be between 1 and 4095", in.LogicalBridge.Spec.VlanId) - return nil, status.Errorf(codes.InvalidArgument, msg) + return status.Errorf(codes.InvalidArgument, msg) } // see https://google.aip.dev/133#user-specified-ids if in.LogicalBridgeId != "" { if err := resourceid.ValidateUserSettable(in.LogicalBridgeId); err != nil { - return nil, err + return err } } // TODO: check in.LogicalBridge.Spec.Vni validity - return in, nil + return nil } -func (s *Server) validateDeleteLogicalBridgeRequest(i any) (*pb.DeleteLogicalBridgeRequest, error) { +func (s *Server) validateDeleteLogicalBridgeRequest(in *pb.DeleteLogicalBridgeRequest) error { // check required fields - in, ok := i.(*pb.DeleteLogicalBridgeRequest) - if ok { - return nil, errors.New("protobuf type assertion error for DeleteLogicalBridgeRequest") - } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return nil, err + return err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return in, resourcename.Validate(in.Name) + return resourcename.Validate(in.Name) } -func (s *Server) validateUpdateLogicalBridgeRequest(i any) (*pb.UpdateLogicalBridgeRequest, error) { +func (s *Server) validateUpdateLogicalBridgeRequest(in *pb.UpdateLogicalBridgeRequest) error { // check required fields - in, ok := i.(*pb.UpdateLogicalBridgeRequest) - if ok { - return nil, errors.New("protobuf type assertion error for UpdateLogicalBridgeRequest") - } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return nil, err + return err } // update_mask = 2 if err := fieldmask.Validate(in.UpdateMask, in.LogicalBridge); err != nil { - return nil, err - } - // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return in, resourcename.Validate(in.LogicalBridge.Name) -} - -func (s *Server) validateGetLogicalBridgeRequest(i any) (*pb.GetLogicalBridgeRequest, error) { - // check required fields - in, ok := i.(*pb.GetLogicalBridgeRequest) - if ok { - return nil, errors.New("protobuf type assertion error for GetLogicalBridgeRequest") - } - if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return nil, err + return err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return in, resourcename.Validate(in.Name) + return resourcename.Validate(in.LogicalBridge.Name) } -func (s *Server) validateListLogicalBridgeRequest(i any) (*pb.ListLogicalBridgesRequest, error) { +func (s *Server) validateGetLogicalBridgeRequest(in *pb.GetLogicalBridgeRequest) error { // check required fields - in, ok := i.(*pb.ListLogicalBridgesRequest) - if ok { - return nil, errors.New("protobuf type assertion error for ListLogicalBridgesRequest") - } if err := fieldbehavior.ValidateRequiredFields(in); err != nil { - return nil, err + return err } // Validate that a resource name conforms to the restrictions outlined in AIP-122. - return in, nil + return resourcename.Validate(in.Name) } diff --git a/pkg/objects/bridge/common.go b/pkg/objects/bridge/common.go index de329ed1..33f842fa 100644 --- a/pkg/objects/bridge/common.go +++ b/pkg/objects/bridge/common.go @@ -6,28 +6,14 @@ package bridge import ( "context" - "fmt" - "github.com/opiproject/opi-evpn-bridge/pkg/models" - "log" - "net" - "sort" - "google.golang.org/grpc" "google.golang.org/grpc/test/bufconn" + "log" + "net" pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" ) -func sortLogicalBridges(bridges []*models.Bridge) { - sort.Slice(bridges, func(i int, j int) bool { - return bridges[i].Name < bridges[j].Name - }) -} - -func resourceIDToFullName(resourceID string) string { - return fmt.Sprintf("//network.opiproject.org/bridges/%s", resourceID) -} - // TODO: move all of this to a common place func dialer(opi *Server) func(context.Context, string) (net.Conn, error) { listener := bufconn.Listen(1024 * 1024) diff --git a/pkg/objects/bridge/grpc.go b/pkg/objects/bridge/grpc.go index 875e60a4..0b948787 100644 --- a/pkg/objects/bridge/grpc.go +++ b/pkg/objects/bridge/grpc.go @@ -8,25 +8,22 @@ package bridge import ( "context" "fmt" - "log" - "strings" - - "github.com/google/uuid" - "github.com/opiproject/opi-evpn-bridge/pkg/models" - "github.com/opiproject/opi-evpn-bridge/pkg/utils" - pb "github.com/opiproject/opi-api/network/evpn-gw/v1alpha1/gen/go" + "github.com/opiproject/opi-evpn-bridge/pkg/models" + "github.com/opiproject/opi-evpn-bridge/pkg/objects" + "log" + "go.einride.tech/aip/fieldbehavior" "go.einride.tech/aip/resourceid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" ) -// Create executes the creation of the LogicalBridge -func (s *Server) Create(ctx context.Context, i any) (*models.Bridge, error) { +// CreateLogicalBridge executes the creation of the LogicalBridge +func (s *Server) CreateLogicalBridge(ctx context.Context, in *pb.CreateLogicalBridgeRequest) (*pb.LogicalBridge, error) { // check input correctness - in, err := s.validateCreateLogicalBridgeRequest(i) - if err != nil { + if err := s.validateCreateLogicalBridgeRequest(in); err != nil { return nil, err } // see https://google.aip.dev/133#user-specified-ids @@ -35,80 +32,71 @@ func (s *Server) Create(ctx context.Context, i any) (*models.Bridge, error) { log.Printf("client provided the ID of a resource %v, ignoring the name field %v", in.LogicalBridgeId, in.LogicalBridge.Name) resourceID = in.LogicalBridgeId } - in.LogicalBridge.Name = resourceIDToFullName(resourceID) + in.LogicalBridge.Name = objects.ResourceIDToFullName(Prefix, resourceID) + // idempotent API when called with same key, should return same object - obj := new(models.Bridge) - ok, err := s.Store.Get(in.LogicalBridge.Name, obj) + obj, ok, err := s.Get(in.LogicalBridge.Name) if err != nil { - fmt.Printf("Failed to interact with store: %v", err) return nil, err } if ok { log.Printf("Already existing LogicalBridge with id %v", in.LogicalBridge.Name) - return obj, nil + return (*obj).ToPb(), nil } // configure netlink if err := s.netlinkCreateLogicalBridge(ctx, in); err != nil { return nil, err } // translate object - response := models.NewBridge(in.LogicalBridge) - log.Printf("new object %v", response) + val := models.NewBridge(in.LogicalBridge) // save object to the database - s.ListHelper[in.LogicalBridge.Name] = false - err = s.Store.Set(in.LogicalBridge.Name, response) + err = s.Create(&val) if err != nil { return nil, err } - return response, nil + + return val.ToPb(), nil } -// Delete deletes a LogicalBridge -func (s *Server) Delete(ctx context.Context, i any) error { +// DeleteLogicalBridge deletes a LogicalBridge +func (s *Server) DeleteLogicalBridge(ctx context.Context, in *pb.DeleteLogicalBridgeRequest) (*emptypb.Empty, error) { // check input correctness - in, err := s.validateDeleteLogicalBridgeRequest(i) - if err != nil { - return err + if err := s.validateDeleteLogicalBridgeRequest(in); err != nil { + return nil, err } // fetch object from the database - obj := new(pb.LogicalBridge) - ok, err := s.Store.Get(in.Name, obj) + obj, ok, err := s.Get(in.Name) if err != nil { - fmt.Printf("Failed to interact with store: %v", err) - return err + return nil, err } if !ok { if in.AllowMissing { - return nil + return &emptypb.Empty{}, nil } err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) - return err + return nil, err } // configure netlink - if err := s.netlinkDeleteLogicalBridge(ctx, obj); err != nil { - return err + if err := s.netlinkDeleteLogicalBridge(ctx, (*obj).ToPb()); err != nil { + return nil, err } // remove from the Database - delete(s.ListHelper, obj.Name) - err = s.Store.Delete(obj.Name) + err = s.Delete(obj) if err != nil { - return err + return nil, err } - return nil + return &emptypb.Empty{}, nil } -// Update updates a LogicalBridge -func (s *Server) Update(ctx context.Context, i any) (*models.Bridge, error) { +// UpdateLogicalBridge updates a LogicalBridge +func (s *Server) UpdateLogicalBridge(ctx context.Context, in *pb.UpdateLogicalBridgeRequest) (*pb.LogicalBridge, error) { // check input correctness - in, err := s.validateUpdateLogicalBridgeRequest(i) - if err != nil { + if err := s.validateUpdateLogicalBridgeRequest(in); err != nil { return nil, err } // fetch object from the database - bridge := new(models.Bridge) - ok, err := s.Store.Get(in.LogicalBridge.Name, bridge) + obj, ok, err := s.Get(in.LogicalBridge.Name) if err != nil { - fmt.Printf("Failed to interact with store: %v", err) return nil, err } if !ok { @@ -116,9 +104,11 @@ func (s *Server) Update(ctx context.Context, i any) (*models.Bridge, error) { err := status.Errorf(codes.NotFound, "unable to find key %s", in.LogicalBridge.Name) return nil, err } + // only if VNI is not empty - if bridge.Vni != 0 { - vxlanName := fmt.Sprintf("vni%d", bridge.Vni) + vni := (*obj).ToPb().GetSpec().Vni + if vni != nil { + vxlanName := fmt.Sprintf("vni: %d", vni) iface, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) @@ -131,35 +121,32 @@ func (s *Server) Update(ctx context.Context, i any) (*models.Bridge, error) { return nil, err } } - response := models.NewBridge(in.LogicalBridge) - err = s.Store.Set(in.LogicalBridge.Name, response) + + newBridge := models.NewBridge(in.LogicalBridge) + err = s.Set(obj, &newBridge) if err != nil { return nil, err } - return response, nil + return newBridge.ToPb(), nil } -// Get gets a LogicalBridge -func (s *Server) Get(ctx context.Context, i any) (*models.Bridge, error) { +// GetLogicalBridge gets a LogicalBridge +func (s *Server) GetLogicalBridge(ctx context.Context, in *pb.GetLogicalBridgeRequest) (*pb.LogicalBridge, error) { // check input correctness - in, err := s.validateGetLogicalBridgeRequest(i) - if err != nil { + if err := s.validateGetLogicalBridgeRequest(in); err != nil { return nil, err } // fetch object from the database - bridge := new(models.Bridge) - ok, err := s.Store.Get(in.Name, bridge) + obj, _, err := s.Get(in.Name) if err != nil { - fmt.Printf("Failed to interact with store: %v", err) - return nil, err - } - if !ok { - err := status.Errorf(codes.NotFound, "unable to find key %s", in.Name) return nil, err } + // only if VNI is not empty - if bridge.Vni != 0 { - vxlanName := fmt.Sprintf("vni%d", bridge.Vni) + toPb := (*obj).ToPb() + vni := toPb.GetSpec().Vni + if vni != nil { + vxlanName := fmt.Sprintf("vni: %d", vni) _, err := s.NLink.LinkByName(ctx, vxlanName) if err != nil { err := status.Errorf(codes.NotFound, "unable to find key %s", vxlanName) @@ -167,48 +154,20 @@ func (s *Server) Get(ctx context.Context, i any) (*models.Bridge, error) { } } - return bridge, nil + return toPb, nil } -// List lists logical bridges -func (s *Server) List(_ context.Context, i any) ([]*models.Bridge, error) { +// ListLogicalBridges lists logical bridges +func (s *Server) ListLogicalBridges(_ context.Context, in *pb.ListLogicalBridgesRequest) (*pb.ListLogicalBridgesResponse, error) { // check required fields - in, err := s.validateListLogicalBridgeRequest(i) - if err != nil { + if err := fieldbehavior.ValidateRequiredFields(in); err != nil { return nil, err } - // fetch pagination from the database, calculate size and offset - size, offset, perr := utils.ExtractPagination(in.PageSize, in.PageToken, s.Pagination) - if perr != nil { - return nil, perr + list, token, err := s.List(in.PageSize, in.PageToken, Prefix) + if err != nil { + return nil, err } - // fetch object from the database - var Blobarray []*models.Bridge - for key := range s.ListHelper { - if !strings.HasPrefix(key, "//network.opiproject.org/bridges") { - continue - } - bridge := new(models.Bridge) - ok, err := s.Store.Get(key, bridge) - if err != nil { - fmt.Printf("Failed to interact with store: %v", err) - return nil, err - } - if !ok { - err := status.Errorf(codes.NotFound, "unable to find key %s", key) - return nil, err - } - Blobarray = append(Blobarray, bridge) - } - // sort is needed, since MAP is unsorted in golang, and we might get different results - sortLogicalBridges(Blobarray) - log.Printf("Limiting result len(%d) to [%d:%d]", len(Blobarray), offset, size) - Blobarray, hasMoreElements := utils.LimitPagination(Blobarray, offset, size) - token := "" - if hasMoreElements { - token = uuid.New().String() - s.Pagination[token] = offset + size - } - return Blobarray, nil + + return &pb.ListLogicalBridgesResponse{LogicalBridges: list, NextPageToken: token}, nil } diff --git a/pkg/objects/bridge/server.go b/pkg/objects/bridge/server.go index 603a4c9f..d9d73139 100644 --- a/pkg/objects/bridge/server.go +++ b/pkg/objects/bridge/server.go @@ -13,16 +13,20 @@ import ( "github.com/opiproject/opi-evpn-bridge/pkg/utils" ) +const ( + Prefix = "//network.opiproject.org/bridges" +) + // Server represents the Server object type Server struct { pb.UnimplementedLogicalBridgeServiceServer - *objects.Server + *objects.Server[*pb.LogicalBridge] } // NewServer creates initialized instance of EVPN server -func NewServer(store gokv.Store) objects.ObjectFactory[*models.Bridge] { +func NewServer(store gokv.Store) objects.ObjectOps[*models.Bridge] { return &Server{ - Server: objects.NewServer(store), + Server: objects.NewServer[*pb.LogicalBridge](store), } } @@ -30,6 +34,6 @@ func NewServer(store gokv.Store) objects.ObjectFactory[*models.Bridge] { // with externally created Netlink func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Server { return &Server{ - Server: objects.NewServerWithArgs(nLink, frr, store), + Server: objects.NewServerWithArgs[*pb.LogicalBridge](nLink, frr, store), } } diff --git a/pkg/objects/object.go b/pkg/objects/object.go index c4bea878..710e853a 100644 --- a/pkg/objects/object.go +++ b/pkg/objects/object.go @@ -1,27 +1,33 @@ package objects import ( - "context" + "fmt" + "github.com/google/uuid" + "github.com/opiproject/opi-evpn-bridge/pkg/models" "github.com/opiproject/opi-evpn-bridge/pkg/utils" "github.com/philippgille/gokv" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "log" + "sort" + "strings" ) -type ObjectFactory[T any] interface { - Create(ctx context.Context, in interface{}) (T, error) - Delete(ctx context.Context, in interface{}) error - Update(ctx context.Context, in interface{}) (T, error) - Get(ctx context.Context, in interface{}) (T, error) - List(ctx context.Context, in interface{}) ([]T, error) +type ObjectOps[T any] interface { + Delete(o *models.EvpnObject[T]) error + Create(new *models.EvpnObject[T]) error + Set(old *models.EvpnObject[T], new *models.EvpnObject[T]) error + Get(k string) (v *models.EvpnObject[T], err error) + List(pSize int32, pToken string, prefix string) ([]T, string, error) } const ( TenantbridgeName = "br-tenant" ) -type Server struct { +type Server[T any] struct { Pagination map[string]int ListHelper map[string]bool NLink utils.Netlink @@ -31,15 +37,15 @@ type Server struct { } // NewServer creates initialized instance of EVPN server -func NewServer(store gokv.Store) *Server { +func NewServer[T any](store gokv.Store) *Server[T] { nLink := utils.NewNetlinkWrapper() frr := utils.NewFrrWrapper() - return NewServerWithArgs(nLink, frr, store) + return NewServerWithArgs[T](nLink, frr, store) } // NewServerWithArgs creates initialized instance of EVPN server // with externally created Netlink -func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Server { +func NewServerWithArgs[T any](nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Server[T] { if frr == nil { log.Panic("nil for Frr is not allowed") } @@ -49,7 +55,7 @@ func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Se if store == nil { log.Panic("nil for Store is not allowed") } - return &Server{ + return &Server[T]{ ListHelper: make(map[string]bool), Pagination: make(map[string]int), NLink: nLink, @@ -58,3 +64,79 @@ func NewServerWithArgs(nLink utils.Netlink, frr utils.Frr, store gokv.Store) *Se Store: store, } } + +func (s *Server[T]) Delete(o *models.EvpnObject[T]) error { + name := (*o).GetName() + delete(s.ListHelper, name) + return s.Store.Delete(name) +} + +func (s *Server[T]) Create(new *models.EvpnObject[T]) error { + s.ListHelper[(*new).GetName()] = false + return s.Store.Set((*new).GetName(), *new) +} + +func (s *Server[T]) Set(old *models.EvpnObject[T], new *models.EvpnObject[T]) error { + return s.Store.Set((*old).GetName(), *new) +} + +func (s *Server[T]) Get(k string) (v *models.EvpnObject[T], ok bool, err error) { + ok, err = s.Store.Get(k, v) + if err != nil { + fmt.Printf("Failed to interact with store: %v", err) + return nil, false, err + } + if !ok { + err := status.Errorf(codes.NotFound, "unable to find key %s", k) + return nil, ok, err + } + return v, ok, nil +} + +func (s *Server[T]) List(pSize int32, pToken string, prefix string) ([]T, string, error) { + // fetch pagination from the database, calculate size and offset + size, offset, perr := utils.ExtractPagination(pSize, pToken, s.Pagination) + if perr != nil { + return nil, "", perr + } + // fetch object from the database + var Blobarray []*models.EvpnObject[T] + for key := range s.ListHelper { + if !strings.HasPrefix(key, prefix) { + continue + } + o := new(models.EvpnObject[T]) + ok, err := s.Store.Get(key, o) + if err != nil { + fmt.Printf("Failed to interact with store: %v", err) + return nil, "", err + } + if !ok { + err := status.Errorf(codes.NotFound, "unable to find key %s", key) + return nil, "", err + } + Blobarray = append(Blobarray, o) + } + // sort is needed, since MAP is unsorted in golang, and we might get different results + sort.Slice(Blobarray, func(i, j int) bool { + return (*Blobarray[i]).GetName() < (*Blobarray[j]).GetName() + }) + log.Printf("Limiting result len(%d) to [%d:%d]", len(Blobarray), offset, size) + Blobarray, hasMoreElements := utils.LimitPagination(Blobarray, offset, size) + token := "" + if hasMoreElements { + token = uuid.New().String() + s.Pagination[token] = offset + size + } + + return s.convertToPbSlice(Blobarray), token, nil +} + +func (s *Server[T]) convertToPbSlice(a []*models.EvpnObject[T]) []T { + res := make([]T, 0) + for _, i := range a { + pb := (*i).ToPb() + res = append(res, pb) + } + return res +} diff --git a/pkg/objects/utils.go b/pkg/objects/utils.go new file mode 100644 index 00000000..d9c87fd7 --- /dev/null +++ b/pkg/objects/utils.go @@ -0,0 +1,7 @@ +package objects + +import "fmt" + +func ResourceIDToFullName(prefix string, resourceID string) string { + return fmt.Sprintf(prefix+"/%s", resourceID) +}