[management] Add support for pagination in GetAllPeers call (#8190)

* use libopenapi OpenAPI generator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added a handler for v1alpha1/peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up request validator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wired up spec-based validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use sync validation; set base url to v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moved stuff around, extracted runtime libopenapu deps into runtime_tooling

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* testing /peers path params

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added user schemas and paths

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use api validation in tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* post-merge fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* deleted tmp command used to test validator integration

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* require request body in post/put requests in order to force validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* making linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* switch to api/v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* go mod tidy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* keep the original order of middleware: metrics, cors, then auth

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* return 422 on validation errors

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* v1alpha1 spec cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* no need for a double-pointer

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more linter fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* removed more double pointers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use di to inject api_v0 and api_v1 http routers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* do not re-add middleware on repeated call to ApiHandler

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for pagination and filtering in /peers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* calling ApiRouter() now also calls ApiV1Router()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* adding peer filters

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* introduced paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up pagination

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* convert pagination state to query scope

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* peer filter wip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* implemented peer filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* test for filtering by os

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for sorting of peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for ordering + tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up total number of records in paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleaned up GetAllPeers response

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups and fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moar cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* remove duplicated test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* make linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 2

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added peer manager tests for GetPeersPaginated

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* order peers in GetAccountPeersPaginated by their ids by default

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* always order by peer_id, filter out proxies

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for filtering of peers by peer_id in GetAllPeers()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* filtering by peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use lowercase in 'like' queries, drop ordering by ip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop sort_by ip key

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop forced_approve filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* replace mac with darwin in peer os filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fix peers query parameter: use peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken peer_test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* responded to feedback

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
dmitri-netbird
2026-10-19 22:19:39 +02:00
committed by GitHub
parent 36fe467aba
commit 8fef487e42
24 changed files with 1571 additions and 230 deletions
+157 -2
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"net"
"net/netip"
"strings"
"time"
"github.com/jackc/pgx/v5"
@@ -494,7 +495,6 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking
return &peer, nil
}
// GetAccountPeers retrieves peers for an account.
func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
var peers []*nbpeer.Peer
tx := s.db
@@ -514,7 +514,6 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
if macFilter != "" {
query = query.Where("meta_network_addresses LIKE ?", "%"+macFilter+"%")
}
if err := query.Find(&peers).Error; err != nil {
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
return nil, status.Errorf(status.Internal, "failed to get peers from store")
@@ -523,6 +522,162 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
return peers, nil
}
// GetAccountPeers retrieves peers for an account.
func (s *SqlStore) GetAccountPeersPaginated(ctx context.Context, lockStrength LockingStrength, accountID string, pagination PaginationState, filters PeerFilters, sorting PeerSorting) ([]*nbpeer.Peer, int, error) {
// main query
var peers []*nbpeer.Peer
{
tx := s.db
if lockStrength != LockingStrengthNone {
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
}
query := tx.Where("peers.account_id=?", accountID)
query = query.Where("(peers.proxy_meta_embedded=false or peers.proxy_meta_embedded is null)")
query = filters.AsConditions(query)
query = pagination.AsScope(query)
query = sorting.AsOrderBy(ctx, query)
if err := query.Distinct().Find(&peers).Error; err != nil {
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
return nil, 0, status.Errorf(status.Internal, "failed to get peers from store")
}
}
// total number of records
var peerCount int64
{
tx := s.db
if lockStrength != LockingStrengthNone {
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
}
query := tx.Where("peers.account_id=?", accountID)
query = query.Where("(peers.proxy_meta_embedded=false or peers.proxy_meta_embedded is null)")
query = filters.AsConditions(query)
if err := query.Table("peers").Select("count(distinct(peers.id))").Count(&peerCount).Error; err != nil {
log.WithContext(ctx).Errorf("failed to get total number of peers from the store: %s", err)
return nil, 0, status.Errorf(status.Internal, "failed to get total number of peers from store")
}
}
return peers, int(peerCount), nil
}
type PaginationState struct {
Page int
PageSize int
}
func (p *PaginationState) AsScope(db *gorm.DB) *gorm.DB {
// empty pagination state
if p.PageSize == 0 {
return db
}
offset := max(p.Page-1, 0) * p.PageSize
return db.Offset(offset).Limit(p.PageSize)
}
type PeerFilters struct {
UserId string
GroupIds []string
Connected *bool
ApprovalRequired *bool
Os []string
IP string
MAC string
Hostname string
Name string
IsServer *bool
PeersIds []string
}
func (f *PeerFilters) AsConditions(db *gorm.DB) *gorm.DB {
if len(f.GroupIds) > 0 {
db = db.Joins("join group_peers on peers.id=group_peers.peer_id and peers.account_id=group_peers.account_id").Where("group_peers.group_id in (?)", f.GroupIds)
}
if f.UserId != "" {
db = db.Where("user_id=?", f.UserId)
}
if f.Connected != nil {
db = db.Where("peer_status_connected=?", *f.Connected)
}
if f.ApprovalRequired != nil {
db = db.Where("peer_status_requires_approval=?", *f.ApprovalRequired)
}
if len(f.Os) > 0 {
db = db.Where("meta_go_os in (?)", f.Os)
}
if f.IP != "" {
db = db.Where("ip like ? or lower(ipv6) like ?", fmt.Sprintf("%%%s%%", f.IP), fmt.Sprintf("%%%s%%", strings.ToLower(f.IP)))
}
if f.MAC != "" {
db = db.Where("lower(meta_network_addresses) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.MAC)))
}
if f.Hostname != "" {
db = db.Where("lower(meta_hostname) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Hostname)))
}
if f.Name != "" {
db = db.Where("lower(name) like ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Name)))
}
if f.IsServer != nil {
if *f.IsServer {
db = db.Where("(user_id='' or user_id is null)")
} else {
db = db.Where("user_id<>'' and user_id is not null")
}
}
if len(f.PeersIds) > 0 {
db = db.Where("id in (?)", f.PeersIds)
}
return db
}
type PeerSortByField int
const (
PeerSortByName PeerSortByField = iota
PeerSortByLastSeen
PeerSortByVersion
)
type OrderBy int
const (
AscOrderBy OrderBy = iota
DescOrderBy
)
type PeerSortingKey struct {
Field PeerSortByField
Order OrderBy
}
type PeerSorting struct {
Fields []PeerSortingKey
}
func (s *PeerSorting) AsOrderBy(ctx context.Context, db *gorm.DB) *gorm.DB {
order := make([]clause.OrderByColumn, 0)
for _, key := range s.Fields {
switch key.Field {
case PeerSortByName:
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.name"}, Desc: key.Order == DescOrderBy})
case PeerSortByLastSeen:
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.peer_status_last_seen"}, Desc: key.Order == DescOrderBy})
case PeerSortByVersion:
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "meta_wt_version"}, Desc: key.Order == DescOrderBy})
default:
log.WithContext(ctx).Warnf("invalid order by column '%v', skipping", key.Field)
}
}
// always order/break ties by id asc order
order = append(order, clause.OrderByColumn{Column: clause.Column{Name: "peers.id"}, Desc: false})
return db.Order(clause.OrderBy{Columns: order})
}
// GetUserPeers retrieves peers for a user.
func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {
tx := s.db