Merge remote-tracking branch 'origin/main' into fix_debug_upload_url_from_mgmt

# Conflicts:
#	client/proto/daemon.pb.go
#	management/internals/controllers/network_map/controller/controller.go
#	management/internals/controllers/network_map/nmaptest/runner.go
#	management/internals/shared/grpc/server.go
#	management/server/account.go
#	shared/management/proto/management.pb.go
#	upload-server/server/server.go
This commit is contained in:
riccardom
2026-10-09 11:13:57 +02:00
518 changed files with 30626 additions and 12765 deletions
+76 -321
View File
@@ -2,25 +2,13 @@ package store
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
log "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
nbdns "github.com/netbirdio/netbird/dns"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
@@ -30,6 +18,7 @@ import (
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
"github.com/netbirdio/netbird/management/internals/shared/db"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
@@ -42,7 +31,6 @@ import (
)
const (
storeSqliteFileName = "store.db"
idQueryCondition = "id = ?"
keyQueryCondition = "key = ?"
mysqlKeyQueryCondition = "`key` = ?"
@@ -52,71 +40,44 @@ const (
accountAndIDsQueryCondition = "account_id = ? AND id IN ?"
accountIDCondition = "account_id = ?"
peerNotFoundFMT = "peer %s not found"
pgMaxConnections = 30
pgMinConnections = 1
pgMaxConnLifetime = 60 * time.Minute
pgHealthCheckPeriod = 1 * time.Minute
)
var testPoolConfig = db.PoolConfig{
MaxConns: 5,
MinConns: 1,
MaxConnLifetime: 30 * time.Second,
HealthCheckPeriod: 10 * time.Second,
}
// SqlStore represents an account storage backed by a Sql DB persisted to disk
type SqlStore struct {
db *gorm.DB
globalAccountLock sync.Mutex
metrics telemetry.AppMetrics
installationPK int
storeEngine types.Engine
pool *pgxpool.Pool
fieldEncrypt *crypt.FieldEncrypt
transactionTimeout time.Duration
conn *db.Conn
db *gorm.DB
tx *db.Tx
globalAccountLock sync.Mutex
metrics telemetry.AppMetrics
installationPK int
fieldEncrypt *crypt.FieldEncrypt
}
type migrationFunc func(*gorm.DB) error
// NewSqlStore creates a new SqlStore instance.
func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
sql, err := db.DB()
if err != nil {
return nil, err
// NewSqlStore creates a new SqlStore instance on top of an open connection.
func NewSqlStore(ctx context.Context, conn *db.Conn, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
if metrics != nil {
conn.SetTxMetrics(metrics.StoreMetrics())
}
conns, err := strconv.Atoi(os.Getenv("NB_SQL_MAX_OPEN_CONNS"))
if err != nil {
conns = runtime.NumCPU()
}
transactionTimeout := 5 * time.Minute
if v := os.Getenv("NB_STORE_TRANSACTION_TIMEOUT"); v != "" {
if parsed, err := time.ParseDuration(v); err == nil {
transactionTimeout = parsed
}
}
log.WithContext(ctx).Infof("Setting transaction timeout to %v", transactionTimeout)
if storeEngine == types.SqliteStoreEngine {
if err == nil {
log.WithContext(ctx).Warnf("setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1")
}
conns = 1
}
sql.SetMaxOpenConns(conns)
sql.SetMaxIdleConns(conns)
sql.SetConnMaxLifetime(time.Hour)
sql.SetConnMaxIdleTime(3 * time.Minute)
log.WithContext(ctx).Infof("Set max open db connections to %d, max idle to %d, max lifetime to %v, max idle time to %v",
conns, conns, time.Hour, 3*time.Minute)
store := &SqlStore{conn: conn, db: conn.DB(nil), metrics: metrics, installationPK: 1}
if skipMigration {
log.WithContext(ctx).Infof("skipping migration")
return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil
return store, nil
}
if err := migratePreAuto(ctx, db); err != nil {
if err := migratePreAuto(ctx, store.db); err != nil {
return nil, fmt.Errorf("migratePreAuto: %w", err)
}
err = db.AutoMigrate(
err := conn.AutoMigrate(
&types.SetupKey{}, &nbpeer.Peer{}, &types.User{}, &types.PersonalAccessToken{}, &types.ProxyAccessToken{},
&types.Group{}, &types.GroupPeer{},
&types.Account{}, &types.Policy{}, &types.PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{},
@@ -132,15 +93,34 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met
if err != nil {
return nil, fmt.Errorf("auto migratePreAuto: %w", err)
}
if err := migratePostAuto(ctx, db); err != nil {
if err := migratePostAuto(ctx, store.db); err != nil {
return nil, fmt.Errorf("migratePostAuto: %w", err)
}
return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1, transactionTimeout: transactionTimeout}, nil
return store, nil
}
// newStore runs the migrations on conn and releases it when they fail.
func newStore(ctx context.Context, conn *db.Conn, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
store, err := NewSqlStore(ctx, conn, metrics, skipMigration)
if err != nil {
_ = conn.Close()
return nil, err
}
return store, nil
}
// Conn returns the shared connection so domain repositories can run alongside this store.
func (s *SqlStore) Conn() *db.Conn {
return s.conn
}
func (s *SqlStore) pgxPool() *pgxpool.Pool {
return s.conn.Pool(s.tx)
}
func GetKeyQueryCondition(s *SqlStore) string {
if s.storeEngine == types.MysqlStoreEngine {
if s.conn.Engine() == db.MysqlStoreEngine {
return mysqlKeyQueryCondition
}
return keyQueryCondition
@@ -168,145 +148,39 @@ func (s *SqlStore) AcquireGlobalLock(ctx context.Context) (unlock func()) {
// Close closes the underlying DB connection
func (s *SqlStore) Close(_ context.Context) error {
sql, err := s.db.DB()
if err != nil {
return fmt.Errorf("get db: %w", err)
}
return sql.Close()
return s.conn.Close()
}
// GetStoreEngine returns underlying store engine
func (s *SqlStore) GetStoreEngine() types.Engine {
return s.storeEngine
return s.conn.Engine()
}
// NewSqliteStore creates a new SQLite store.
func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
storeFile := storeSqliteFileName
if envFile, ok := os.LookupEnv("NB_STORE_ENGINE_SQLITE_FILE"); ok && envFile != "" {
storeFile = envFile
}
// Separate file path from any SQLite URI query parameters (e.g., "store.db?mode=rwc")
filePath, query, hasQuery := strings.Cut(storeFile, "?")
connStr := filePath
if !filepath.IsAbs(filePath) {
connStr = filepath.Join(dataDir, filePath)
}
// Compose query parameters. User-provided ?_busy_timeout (or its mattn alias
// ?_timeout) overrides our default; otherwise inject 30s so SQLite waits at
// most that long on a lock instead of blocking the only Go-side connection.
// mattn/go-sqlite3 applies PRAGMA from the DSN on every fresh connection, so
// the value survives ConnMaxIdleTime/ConnMaxLifetime recycling. cache=shared
// stays the default on non-Windows for the same reason as before.
parsed, _ := url.ParseQuery(query)
var defaults []string
if parsed.Get("_busy_timeout") == "" && parsed.Get("_timeout") == "" {
defaults = append(defaults, "_busy_timeout=30000")
}
if !hasQuery && runtime.GOOS != "windows" {
// To avoid `The process cannot access the file because it is being used by another process` on Windows
defaults = append(defaults, "cache=shared")
}
parts := defaults
if hasQuery {
parts = append(parts, query)
}
if len(parts) > 0 {
connStr += "?" + strings.Join(parts, "&")
}
db, err := gorm.Open(sqlite.Open(connStr), getGormConfig())
conn, err := db.OpenSqlite(ctx, dataDir)
if err != nil {
return nil, err
}
return NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)
return newStore(ctx, conn, metrics, skipMigration)
}
// NewPostgresqlStore creates a new Postgres store.
func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
db, err := gorm.Open(postgres.Open(dsn), getGormConfig())
conn, err := db.OpenPostgres(ctx, dsn, db.DefaultPoolConfig)
if err != nil {
return nil, err
}
pool, err := connectToPgDb(context.Background(), dsn)
if err != nil {
return nil, err
}
store, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)
if err != nil {
pool.Close()
return nil, err
}
store.pool = pool
return store, nil
}
func connectToPgDb(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("unable to parse database config: %w", err)
}
config.MaxConns = pgMaxConnections
config.MinConns = pgMinConnections
config.MaxConnLifetime = pgMaxConnLifetime
config.HealthCheckPeriod = pgHealthCheckPeriod
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("unable to ping database: %w", err)
}
return pool, nil
return newStore(ctx, conn, metrics, skipMigration)
}
// NewMysqlStore creates a new MySQL store.
func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig())
conn, err := db.OpenMysql(ctx, dsn)
if err != nil {
return nil, err
}
store, err := NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration)
if err != nil {
closeGormDB(db)
return nil, err
}
return store, nil
}
func getGormConfig() *gorm.Config {
return &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
CreateBatchSize: 400,
}
}
// newPostgresStore initializes a new Postgres store.
func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
dsn, ok := lookupDSNEnv(PostgresDsnEnv, PostgresDsnEnvLegacy)
if !ok {
return nil, fmt.Errorf("%s is not set", PostgresDsnEnv)
}
return NewPostgresqlStore(ctx, dsn, metrics, skipMigration)
}
// newMysqlStore initializes a new MySQL store.
func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
dsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)
if !ok {
return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
}
return NewMysqlStore(ctx, dsn, metrics, skipMigration)
return newStore(ctx, conn, metrics, skipMigration)
}
// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.
@@ -350,7 +224,7 @@ func newPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore,
}
if err := seedFromSqliteStore(ctx, store, sqliteStore); err != nil {
closeStore(ctx, store)
_ = store.Close(ctx)
return nil, err
}
@@ -359,49 +233,11 @@ func newPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore,
// used for tests only
func NewPostgresqlStoreForTests(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
db, err := gorm.Open(postgres.Open(dsn), getGormConfig())
conn, err := db.OpenPostgres(ctx, dsn, testPoolConfig)
if err != nil {
return nil, err
}
pool, err := connectToPgDbForTests(context.Background(), dsn)
if err != nil {
closeGormDB(db)
return nil, err
}
store, err := NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)
if err != nil {
// Release the sessions, or the caller cannot drop the database.
pool.Close()
closeGormDB(db)
return nil, err
}
store.pool = pool
return store, nil
}
// used for tests only
func connectToPgDbForTests(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("unable to parse database config: %w", err)
}
config.MaxConns = 5
config.MinConns = 1
config.MaxConnLifetime = 30 * time.Second
config.HealthCheckPeriod = 10 * time.Second
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("unable to ping database: %w", err)
}
return pool, nil
return newStore(ctx, conn, metrics, skipMigration)
}
// NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.
@@ -423,15 +259,6 @@ func seedFromSqliteStore(ctx context.Context, store, sqliteStore *SqlStore) erro
return nil
}
// closeStore releases a store that is not handed to the caller, so a failed
// seed does not leak its connection and pool.
func closeStore(ctx context.Context, store *SqlStore) {
store.Close(ctx)
if store.pool != nil {
store.pool.Close()
}
}
func newMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
store, err := NewMysqlStore(ctx, dsn, metrics, skipMigration)
if err != nil {
@@ -439,113 +266,41 @@ func newMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn s
}
if err := seedFromSqliteStore(ctx, store, sqliteStore); err != nil {
closeStore(ctx, store)
_ = store.Close(ctx)
return nil, err
}
return store, nil
}
// ExecuteInTransaction runs operation in a transaction. A store that is already
// bound to one joins it instead of opening a second, independent transaction.
func (s *SqlStore) ExecuteInTransaction(ctx context.Context, operation func(store Store) error) error {
timeoutCtx, cancel := context.WithTimeout(ctx, s.transactionTimeout)
defer cancel()
startTime := time.Now()
tx := s.db.WithContext(timeoutCtx).Begin()
if tx.Error != nil {
return tx.Error
if s.tx != nil {
return operation(s)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if s.storeEngine == types.PostgresStoreEngine {
if err := tx.Exec("SET LOCAL statement_timeout = '1min'").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to set statement timeout: %w", err)
}
if err := tx.Exec("SET LOCAL lock_timeout = '1min'").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to set lock timeout: %w", err)
}
}
// For MySQL, disable FK checks within this transaction to avoid deadlocks
// This is session-scoped and doesn't require SUPER privileges
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to disable FK checks: %w", err)
}
}
repo := s.withTx(tx)
err := operation(repo)
if err != nil {
tx.Rollback()
if errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
log.WithContext(ctx).Warnf("transaction exceeded %s timeout after %v, stack: %s", s.transactionTimeout, time.Since(startTime), debug.Stack())
}
return err
}
// Re-enable FK checks before commit (optional, as transaction end resets it)
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 1").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to re-enable FK checks: %w", err)
}
}
err = tx.Commit().Error
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
log.WithContext(ctx).Warnf("transaction commit exceeded %s timeout after %v, stack: %s", s.transactionTimeout, time.Since(startTime), debug.Stack())
}
return err
}
log.WithContext(ctx).Tracef("transaction took %v", time.Since(startTime))
if s.metrics != nil {
s.metrics.StoreMetrics().CountTransactionDuration(time.Since(startTime))
}
return nil
return s.conn.RunInTx(ctx, func(tx *db.Tx) error {
return operation(s.withTx(tx))
})
}
func (s *SqlStore) withTx(tx *gorm.DB) Store {
func (s *SqlStore) withTx(tx *db.Tx) Store {
return &SqlStore{
db: tx,
storeEngine: s.storeEngine,
conn: s.conn,
db: s.conn.DB(tx),
tx: tx,
fieldEncrypt: s.fieldEncrypt,
}
}
// transaction wraps a GORM transaction with MySQL-specific FK checks handling
// Use this instead of db.Transaction() directly to avoid deadlocks on MySQL/Aurora
func (s *SqlStore) transaction(fn func(*gorm.DB) error) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// For MySQL, disable FK checks within this transaction to avoid deadlocks
// This is session-scoped and doesn't require SUPER privileges
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
return fmt.Errorf("failed to disable FK checks: %w", err)
}
}
err := fn(tx)
// Re-enable FK checks before commit (optional, as transaction end resets it)
if s.storeEngine == types.MysqlStoreEngine && err == nil {
if fkErr := tx.Exec("SET FOREIGN_KEY_CHECKS = 1").Error; fkErr != nil {
return fmt.Errorf("failed to re-enable FK checks: %w", fkErr)
}
}
return err
// transaction runs fn as a savepoint of the bound transaction, or in a new
// transaction when the store is not bound to one.
func (s *SqlStore) transaction(ctx context.Context, fn func(tx *gorm.DB) error) error {
if s.tx != nil {
return s.db.Transaction(fn)
}
return s.conn.RunInTx(ctx, func(tx *db.Tx) error {
return fn(s.conn.DB(tx))
})
}
@@ -1,149 +0,0 @@
package store
import (
"context"
"strings"
"time"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
"github.com/netbirdio/netbird/shared/management/status"
)
// CreateAccessLog creates a new access log entry in the database
func (s *SqlStore) CreateAccessLog(ctx context.Context, logEntry *accesslogs.AccessLogEntry) error {
result := s.db.Create(logEntry)
if result.Error != nil {
log.WithContext(ctx).WithFields(log.Fields{
"service_id": logEntry.ServiceID,
"method": logEntry.Method,
"host": logEntry.Host,
"path": logEntry.Path,
}).Errorf("failed to create access log entry in store: %v", result.Error)
return status.Errorf(status.Internal, "failed to create access log entry in store")
}
return nil
}
// GetAccountAccessLogs retrieves access logs for a given account with pagination and filtering
func (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {
var logs []*accesslogs.AccessLogEntry
var totalCount int64
baseQuery := s.db.
Model(&accesslogs.AccessLogEntry{}).
Where(accountIDCondition, accountID)
baseQuery = s.applyAccessLogFilters(baseQuery, filter)
if err := baseQuery.Count(&totalCount).Error; err != nil {
log.WithContext(ctx).Errorf("failed to count access logs: %v", err)
return nil, 0, status.Errorf(status.Internal, "failed to count access logs")
}
query := s.db.
Where(accountIDCondition, accountID)
query = s.applyAccessLogFilters(query, filter)
sortColumns := filter.GetSortColumn()
sortOrder := strings.ToUpper(filter.GetSortOrder())
var orderClauses []string
for _, col := range strings.Split(sortColumns, ",") {
col = strings.TrimSpace(col)
if col != "" {
orderClauses = append(orderClauses, col+" "+sortOrder)
}
}
orderClause := strings.Join(orderClauses, ", ")
query = query.
Order(orderClause).
Limit(filter.GetLimit()).
Offset(filter.GetOffset())
if lockStrength != LockingStrengthNone {
query = query.Clauses(clause.Locking{Strength: string(lockStrength)})
}
result := query.Find(&logs)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to get access logs from store: %v", result.Error)
return nil, 0, status.Errorf(status.Internal, "failed to get access logs from store")
}
return logs, totalCount, nil
}
// DeleteOldAccessLogs deletes all access logs older than the specified time
func (s *SqlStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error) {
result := s.db.
Where("timestamp < ?", olderThan).
Delete(&accesslogs.AccessLogEntry{})
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to delete old access logs: %v", result.Error)
return 0, status.Errorf(status.Internal, "failed to delete old access logs")
}
return result.RowsAffected, nil
}
// applyAccessLogFilters applies filter conditions to the query
func (s *SqlStore) applyAccessLogFilters(query *gorm.DB, filter accesslogs.AccessLogFilter) *gorm.DB {
if filter.Search != nil {
searchPattern := "%" + *filter.Search + "%"
query = query.Where(
"id LIKE ? OR location_connection_ip LIKE ? OR host LIKE ? OR path LIKE ? OR CONCAT(host, path) LIKE ? OR user_id IN (SELECT id FROM users WHERE email LIKE ? OR name LIKE ?)",
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern,
)
}
if filter.SourceIP != nil {
query = query.Where("location_connection_ip = ?", *filter.SourceIP)
}
if filter.Host != nil {
query = query.Where("host = ?", *filter.Host)
}
if filter.Path != nil {
// Support LIKE pattern for path filtering
query = query.Where("path LIKE ?", "%"+*filter.Path+"%")
}
if filter.UserID != nil {
query = query.Where("user_id = ?", *filter.UserID)
}
if filter.Method != nil {
query = query.Where("method = ?", *filter.Method)
}
if filter.Status != nil {
switch *filter.Status {
case "success":
query = query.Where("status_code >= ? AND status_code < ?", 200, 400)
case "failed":
query = query.Where("status_code < ? OR status_code >= ?", 200, 400)
}
}
if filter.StatusCode != nil {
query = query.Where("status_code = ?", *filter.StatusCode)
}
if filter.StartDate != nil {
query = query.Where("timestamp >= ?", *filter.StartDate)
}
if filter.EndDate != nil {
query = query.Where("timestamp <= ?", *filter.EndDate)
}
return query
}
+32 -4
View File
@@ -18,6 +18,7 @@ import (
"gorm.io/gorm/clause"
nbdns "github.com/netbirdio/netbird/dns"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/route"
@@ -51,7 +52,7 @@ func (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) erro
group.StoreGroupPeers()
}
err := s.transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id)
if result.Error != nil {
return result.Error
@@ -146,7 +147,7 @@ func (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID,
func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) error {
start := time.Now()
err := s.transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id)
if result.Error != nil {
return result.Error
@@ -162,6 +163,10 @@ func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) er
return result.Error
}
if err := deleteAgentNetworkAccountConfig(tx, account.Id); err != nil {
return err
}
result = tx.Select(clause.Associations).Delete(account)
if result.Error != nil {
return result.Error
@@ -179,6 +184,29 @@ func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) er
return err
}
// deleteAgentNetworkAccountConfig removes the account's agent network configuration. These
// tables are not account associations, so deleting the account does not reach them. The
// settings row holds the account's globally unique gateway domain and the provider rows
// hold its upstream API keys. Tables that grow with traffic are left out: consumption
// counters and access logs are swept in the background, and usage records are kept.
func deleteAgentNetworkAccountConfig(tx *gorm.DB, accountID string) error {
// Dependents first: policies point at providers and guardrails, and settings
// go last, as DeleteSettings refuses while providers exist.
models := []any{
&agentNetworkTypes.Policy{},
&agentNetworkTypes.Provider{},
&agentNetworkTypes.Guardrail{},
&agentNetworkTypes.AccountBudgetRule{},
&agentNetworkTypes.Settings{},
}
for _, model := range models {
if err := tx.Delete(model, "account_id = ?", accountID).Error; err != nil {
return fmt.Errorf("delete %T rows: %w", model, err)
}
}
return nil
}
func (s *SqlStore) UpdateAccountDomainAttributes(ctx context.Context, accountID string, domain string, category string, isPrimaryDomain bool) error {
accountCopy := types.Account{
Domain: domain,
@@ -281,7 +309,7 @@ func (s *SqlStore) GetAccountMeta(ctx context.Context, lockStrength LockingStren
}
func (s *SqlStore) GetAccount(ctx context.Context, accountID string) (*types.Account, error) {
if s.pool != nil {
if s.pgxPool() != nil {
return s.getAccountPgx(ctx, accountID)
}
return s.getAccountGorm(ctx, accountID)
@@ -763,7 +791,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
networkSerial sql.NullInt64
createdAt sql.NullTime
)
err := s.pool.QueryRow(ctx, accountQuery, accountID).Scan(
err := s.pgxPool().QueryRow(ctx, accountQuery, accountID).Scan(
&account.Id, &account.CreatedBy, &createdAt, &account.Domain, &account.DomainCategory, &account.IsDomainPrimaryAccount,
&networkIdentifier, &networkNet, &networkNetV6, &networkDns, &networkSerial,
&dnsSettingsDisabledGroups,
@@ -44,7 +44,7 @@ func (s *SqlStore) getAccountOnboarding(ctx context.Context, accountID string, a
const query = `SELECT account_id, onboarding_flow_pending, signup_form_pending, created_at, updated_at FROM account_onboardings WHERE account_id = $1`
var onboardingFlowPending, signupFormPending sql.NullBool
var createdAt, updatedAt sql.NullTime
err := s.pool.QueryRow(ctx, query, accountID).Scan(
err := s.pgxPool().QueryRow(ctx, query, accountID).Scan(
&account.Onboarding.AccountID,
&onboardingFlowPending,
&signupFormPending,
@@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/require"
nbdns "github.com/netbirdio/netbird/dns"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
@@ -401,6 +402,25 @@ func TestSqlite_DeleteAccount(t *testing.T) {
require.NoError(t, err)
require.Equal(t, o.AccountID, account.Id)
err = store.CreateAgentNetworkSettings(context.Background(), &agentNetworkTypes.Settings{
AccountID: account.Id,
Domain: "gw.example.com",
ProxyAddress: "gw.example.com",
})
require.NoError(t, err)
agentNetworkConfig := []any{
&agentNetworkTypes.Provider{ID: "an_provider", AccountID: account.Id, APIKey: "sk-test"},
&agentNetworkTypes.Policy{ID: "an_policy", AccountID: account.Id},
&agentNetworkTypes.Guardrail{ID: "an_guardrail", AccountID: account.Id},
&agentNetworkTypes.AccountBudgetRule{ID: "an_budget_rule", AccountID: account.Id},
}
for _, row := range agentNetworkConfig {
require.NoError(t, store.(*SqlStore).db.Create(row).Error, "creating %T", row)
}
otherProvider := &agentNetworkTypes.Provider{ID: "other_provider", AccountID: "other_account"}
require.NoError(t, store.(*SqlStore).db.Create(otherProvider).Error)
err = store.DeleteAccount(context.Background(), account)
require.NoError(t, err)
@@ -465,6 +485,32 @@ func TestSqlite_DeleteAccount(t *testing.T) {
err = store.(*SqlStore).db.Model(&rpservice.Target{}).Find(&targets, "account_id = ?", account.Id).Error
require.NoError(t, err, "expecting no error after DeleteAccount when searching for service targets")
require.Len(t, targets, 0, "expecting no service targets to be found after DeleteAccount")
_, err = store.GetAgentNetworkSettings(context.Background(), LockingStrengthNone, account.Id)
require.Error(t, err, "expecting agent network settings to be deleted with the account")
sErr, ok := status.FromError(err)
require.True(t, ok, "expecting a status error when getting agent network settings, got %v", err)
require.Equal(t, status.NotFound, sErr.Type(), "expecting agent network settings to be deleted with the account")
// The domain is globally unique, so a leftover row would keep it from another account.
err = store.CreateAgentNetworkSettings(context.Background(), &agentNetworkTypes.Settings{
AccountID: "other_account",
Domain: "gw.example.com",
ProxyAddress: "gw.example.com",
})
require.NoError(t, err, "expecting the deleted account's gateway domain to be free for another account")
for _, row := range agentNetworkConfig {
var count int64
err = store.(*SqlStore).db.Model(row).Where("account_id = ?", account.Id).Count(&count).Error
require.NoError(t, err, "counting %T rows after DeleteAccount", row)
assert.Zero(t, count, "expecting no %T rows to be found after DeleteAccount", row)
}
var otherProviders int64
err = store.(*SqlStore).db.Model(&agentNetworkTypes.Provider{}).Where("account_id = ?", "other_account").Count(&otherProviders).Error
require.NoError(t, err)
assert.Equal(t, int64(1), otherProviders, "expecting another account's agent network provider to survive DeleteAccount")
}
func Test_GetAccount(t *testing.T) {
@@ -16,7 +16,7 @@ import (
// entry together with its authorising-group child rows in a single
// transaction.
func (s *SqlStore) CreateAgentNetworkAccessLog(ctx context.Context, entry *agentNetworkTypes.AgentNetworkAccessLog, groups []agentNetworkTypes.AgentNetworkAccessLogGroup) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
// Idempotent on the log id / (log_id, group_id) so a proxy resend of the
// same entry can't fail the request.
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(entry).Error; err != nil {
@@ -46,7 +46,7 @@ func (s *SqlStore) CreateAgentNetworkAccessLog(ctx context.Context, entry *agent
// deleted.
func (s *SqlStore) DeleteOldAgentNetworkAccessLogs(ctx context.Context, accountID string, olderThan time.Time) (int64, error) {
var deleted int64
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
// Remove group child rows for the soon-to-be-deleted logs first.
if err := tx.Exec(
"DELETE FROM agent_network_access_log_group WHERE account_id = ? AND log_id IN (SELECT id FROM agent_network_access_log WHERE account_id = ? AND timestamp < ?)",
@@ -69,6 +69,23 @@ func (s *SqlStore) DeleteOldAgentNetworkAccessLogs(ctx context.Context, accountI
return deleted, nil
}
// GetDeletedAccountIDsWithAgentNetworkAccessLogs returns the IDs of accounts that no
// longer exist but still have access-log rows. The retention sweep is driven by settings
// rows, which are deleted with the account, so it uses this to find logs it would
// otherwise never expire.
func (s *SqlStore) GetDeletedAccountIDsWithAgentNetworkAccessLogs(ctx context.Context) ([]string, error) {
var accountIDs []string
err := s.db.Model(&agentNetworkTypes.AgentNetworkAccessLog{}).
Distinct("account_id").
Where("NOT EXISTS (SELECT 1 FROM accounts WHERE accounts.id = agent_network_access_log.account_id)").
Pluck("account_id", &accountIDs).Error
if err != nil {
log.WithContext(ctx).Errorf("failed to get deleted accounts with agent-network access logs: %v", err)
return nil, status.Errorf(status.Internal, "failed to get deleted accounts with agent-network access logs")
}
return accountIDs, nil
}
// GetAgentNetworkAccessLogs retrieves flattened agent-network access logs for
// an account with server-side pagination, filtering and sorting. Authorising
// group ids are hydrated from the group child table for the returned page.
@@ -14,7 +14,7 @@ import (
// CreateAgentNetworkUsage persists a stripped agent-network usage record
// together with its authorising-group child rows in a single transaction.
func (s *SqlStore) CreateAgentNetworkUsage(ctx context.Context, usage *agentNetworkTypes.AgentNetworkUsage, groups []agentNetworkTypes.AgentNetworkUsageGroup) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
// Idempotent on the usage id / (usage_id, group_id) so a proxy resend of
// the same entry can't fail the request.
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(usage).Error; err != nil {
@@ -619,7 +619,7 @@ func (s *SqlStore) IncrementAgentNetworkConsumptionBatch(
}
const tbl = "agent_network_consumption"
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
for _, k := range keys {
if k.DimID == "" || k.WindowSeconds <= 0 {
return status.Errorf(status.InvalidArgument, "dim_id and window_seconds must be set")
@@ -663,6 +663,21 @@ func (s *SqlStore) IncrementAgentNetworkConsumptionBatch(
return nil
}
// DeleteAgentNetworkConsumptionOfDeletedAccounts deletes every consumption counter whose
// account no longer exists and returns the number of rows deleted. Counters grow with
// traffic, so they are swept in the background instead of in the account-deletion
// transaction, and the sweep also catches counters a proxy writes after the deletion.
func (s *SqlStore) DeleteAgentNetworkConsumptionOfDeletedAccounts(ctx context.Context) (int64, error) {
res := s.db.
Where("NOT EXISTS (SELECT 1 FROM accounts WHERE accounts.id = agent_network_consumption.account_id)").
Delete(&agentNetworkTypes.Consumption{})
if res.Error != nil {
log.WithContext(ctx).Errorf("failed to delete agent-network consumption of deleted accounts: %v", res.Error)
return 0, status.Errorf(status.Internal, "failed to delete agent-network consumption of deleted accounts")
}
return res.RowsAffected, nil
}
// ListAgentNetworkConsumption returns every consumption row recorded
// for the account, ordered by window_start descending. Backs the
// dashboard's basic counter view.
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
"github.com/netbirdio/netbird/management/server/types"
)
// TestAgentNetworkUsage_RealStore_RoundTrip drives CreateAgentNetworkUsage and
@@ -300,3 +301,37 @@ func TestDeleteOldAgentNetworkAccessLogs(t *testing.T) {
require.NoError(t, err)
require.Len(t, usage, 1, "usage record for the deleted log must survive")
}
// TestDeleteAgentNetworkConsumptionOfDeletedAccounts verifies that the sweep removes the
// consumption counters of accounts that no longer exist and leaves live accounts' counters,
// including those of a live account without a settings row.
func TestDeleteAgentNetworkConsumptionOfDeletedAccounts(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, s Store) {
ctx := context.Background()
const (
liveAccountID = "acc-anet-consumption-live"
deletedAccountID = "acc-anet-consumption-deleted"
)
require.NoError(t, s.SaveAccount(ctx, &types.Account{Id: liveAccountID}))
windowStart := time.Now().UTC().Truncate(time.Hour)
for _, accountID := range []string{liveAccountID, deletedAccountID} {
for _, dimID := range []string{"user-1", "user-2"} {
require.NoError(t, s.IncrementAgentNetworkConsumption(ctx, accountID,
agentNetworkTypes.DimensionUser, dimID, 3600, windowStart, 10, 5, 0.01))
}
}
deleted, err := s.DeleteAgentNetworkConsumptionOfDeletedAccounts(ctx)
require.NoError(t, err)
assert.Equal(t, int64(2), deleted, "both of the deleted account's counters should be removed")
rows, err := s.ListAgentNetworkConsumption(ctx, LockingStrengthNone, deletedAccountID)
require.NoError(t, err)
assert.Empty(t, rows, "the deleted account should have no consumption counters left")
rows, err = s.ListAgentNetworkConsumption(ctx, LockingStrengthNone, liveAccountID)
require.NoError(t, err)
assert.Len(t, rows, 2, "the live account's consumption counters should survive")
})
}
@@ -4,13 +4,16 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/status"
)
@@ -131,16 +134,45 @@ func (s *SqlStore) UpdateCustomDomain(ctx context.Context, accountID string, d *
return d, nil
}
func (s *SqlStore) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error {
result := s.db.Delete(domain.Domain{}, accountAndIDQueryCondition, accountID, domainID)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to delete reverse proxy custom domain from store: %v", result.Error)
return status.Errorf(status.Internal, "failed to delete reverse proxy custom domain from store")
// LockCustomDomains holds shared locks on registrations covering a service until commit.
func (s *SqlStore) LockCustomDomains(ctx context.Context, accountID string, serviceDomain nbdomain.Domain) ([]*domain.Domain, error) {
var names []string
for name := serviceDomain.PunycodeString(); name != ""; {
names = append(names, name)
_, name, _ = strings.Cut(name, ".")
}
if result.RowsAffected == 0 {
return status.Errorf(status.NotFound, "reverse proxy custom domain %s not found", domainID)
var domains []*domain.Domain
if err := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: string(LockingStrengthShare)}).
Where(accountIDCondition, accountID).Where("domain IN ?", names).
Order("id").Find(&domains).Error; err != nil {
return nil, fmt.Errorf("lock custom domains: %w", err)
}
return domains, nil
}
return nil
// DeleteCustomDomain removes a registration only when no service uses its namespace.
func (s *SqlStore) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var d domain.Domain
// Service writes hold a shared lock on this row through commit, so neither
// operation can proceed against the other's outdated view of the domain.
if err := tx.Clauses(clause.Locking{Strength: string(LockingStrengthUpdate)}).
Take(&d, accountAndIDQueryCondition, accountID, domainID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return status.Errorf(status.NotFound, "custom domain not found")
}
return fmt.Errorf("lock custom domain for deletion: %w", err)
}
result := tx.Where(accountAndIDQueryCondition, accountID, domainID).
Where("NOT EXISTS (?)", customDomainServices(tx, &d).Select("1")).Delete(&domain.Domain{})
if result.Error != nil {
return fmt.Errorf("delete custom domain: %w", result.Error)
}
if result.RowsAffected == 0 {
return status.Errorf(status.PreconditionFailed, "custom domain has dependent services; delete or move them before deleting the domain")
}
return nil
})
}
@@ -0,0 +1,182 @@
package store
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/server/types"
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/status"
)
func TestLockCustomDomains_ConcurrentServices(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
if store.GetStoreEngine() == types.SqliteStoreEngine {
t.Skip("SQLite serializes transactions on one connection")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
require.NoError(t, store.SaveAccount(ctx, newAccountWithId(ctx, "owner", "admin", "")))
_, err := store.CreateCustomDomain(ctx, "owner", "one.example.com", "cluster", true)
require.NoError(t, err)
_, err = store.CreateCustomDomain(ctx, "owner", "two.example.com", "cluster", true)
require.NoError(t, err)
locked := make(chan error, 1)
release := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- store.ExecuteInTransaction(ctx, func(tx Store) error {
_, err := tx.LockCustomDomains(ctx, "owner", "app.one.example.com")
locked <- err
if err != nil {
return err
}
select {
case <-release:
return nil
case <-ctx.Done():
return ctx.Err()
}
})
}()
var lockErr error
select {
case lockErr = <-locked:
case err := <-done:
t.Fatalf("transaction ended before locking: %v", err)
}
writeCtx, writeCancel := context.WithTimeout(ctx, 3*time.Second)
defer writeCancel()
var writeErr error
for _, name := range []nbdomain.Domain{"app.one.example.com", "app.two.example.com"} {
writeErr = store.ExecuteInTransaction(writeCtx, func(tx Store) error {
if _, err := tx.LockCustomDomains(writeCtx, "owner", name); err != nil {
return err
}
return tx.CreateService(writeCtx, &rpservice.Service{
ID: name.PunycodeString(), AccountID: "owner", Domain: name.PunycodeString(),
})
})
if writeErr != nil {
break
}
}
close(release)
require.NoError(t, <-done)
require.NoError(t, lockErr)
require.NoError(t, writeErr, "domain authorization locks must allow concurrent service writes")
services, err := store.GetAccountServices(ctx, LockingStrengthNone, "owner")
require.NoError(t, err)
assert.Len(t, services, 2, "both services must commit while the first domain is locked")
})
}
func TestDeleteCustomDomain_ServiceDependencies(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
ctx := context.Background()
require.NoError(t, store.SaveAccount(ctx, newAccountWithId(ctx, "owner", "admin", "")))
d, err := store.CreateCustomDomain(ctx, "owner", "example.com", "cluster", true)
require.NoError(t, err)
svc := &rpservice.Service{ID: "service", AccountID: "owner", Domain: "APP.EXAMPLE.COM."}
require.NoError(t, store.CreateService(ctx, svc))
err = store.DeleteCustomDomain(ctx, "other", d.ID)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok, "cross-account deletion must return a typed error")
assert.Equal(t, status.NotFound, sErr.Type(), "cross-account deletion must not reveal dependencies")
err = store.DeleteCustomDomain(ctx, "owner", d.ID)
require.Error(t, err)
sErr, ok = status.FromError(err)
require.True(t, ok, "dependent services must return a typed error")
assert.Equal(t, status.PreconditionFailed, sErr.Type(), "deletion must fail until services are removed")
stored, err := store.GetCustomDomain(ctx, "owner", d.ID)
require.NoError(t, err)
assert.True(t, stored.Validated, "rejected deletion must preserve validation")
require.NoError(t, store.DeleteService(ctx, "owner", svc.ID))
require.NoError(t, store.DeleteCustomDomain(ctx, "owner", d.ID))
_, err = store.GetCustomDomain(ctx, "owner", d.ID)
require.Error(t, err, "the registration must be gone after successful deletion")
})
}
func TestDeleteCustomDomain_OtherAccountSubdomainIsNotADependency(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
ctx := context.Background()
require.NoError(t, store.SaveAccount(ctx, newAccountWithId(ctx, "owner", "admin", "")))
require.NoError(t, store.SaveAccount(ctx, newAccountWithId(ctx, "other", "admin", "")))
// Registrations are unique by name, so a second account can hold a
// subdomain of the first account's registration and serve from it.
parent, err := store.CreateCustomDomain(ctx, "owner", "example.com", "cluster", true)
require.NoError(t, err)
_, err = store.CreateCustomDomain(ctx, "other", "team.example.com", "cluster", true)
require.NoError(t, err)
require.NoError(t, store.CreateService(ctx, &rpservice.Service{
ID: "service", AccountID: "other", Domain: "app.team.example.com",
}))
require.NoError(t, store.DeleteCustomDomain(ctx, "owner", parent.ID),
"another account's service must not hold the registration open")
})
}
func TestDeleteCustomDomain_ConcurrentServiceCreation(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
require.NoError(t, store.SaveAccount(ctx, newAccountWithId(ctx, "owner", "admin", "")))
for i := range 10 {
d, err := store.CreateCustomDomain(ctx, "owner", fmt.Sprintf("app%d.example.com", i), "cluster", true)
require.NoError(t, err)
svc := &rpservice.Service{ID: fmt.Sprintf("service-%d", i), AccountID: "owner", Domain: "nested." + d.Domain}
start := make(chan struct{})
created := make(chan error, 1)
deleted := make(chan error, 1)
go func() {
<-start
created <- store.ExecuteInTransaction(ctx, func(tx Store) error {
domains, err := tx.LockCustomDomains(ctx, "owner", nbdomain.Domain(svc.Domain))
if err != nil {
return err
}
for _, candidate := range domains {
if candidate.ID == d.ID && candidate.Validated {
return tx.CreateService(ctx, svc)
}
}
return status.Errorf(status.PreconditionFailed, "registration was deleted")
})
}()
go func() {
<-start
deleted <- store.DeleteCustomDomain(ctx, "owner", d.ID)
}()
close(start)
createErr, deleteErr := <-created, <-deleted
require.True(t, createErr == nil || deleteErr == nil, "one operation must succeed: create=%v, delete=%v", createErr, deleteErr)
if createErr == nil {
require.Error(t, deleteErr, "a committed service must block deletion")
stored, err := store.GetCustomDomain(ctx, "owner", d.ID)
require.NoError(t, err)
assert.True(t, stored.Validated, "the service must retain its authorization")
require.NoError(t, store.DeleteService(ctx, "owner", svc.ID))
require.NoError(t, store.DeleteCustomDomain(ctx, "owner", d.ID))
continue
}
require.NoError(t, deleteErr)
services, err := store.GetAccountServices(ctx, LockingStrengthNone, "owner")
require.NoError(t, err)
assert.Empty(t, services, "a deleted registration must not leave a new service")
}
})
}
@@ -53,7 +53,10 @@ func customDomainServices(db *gorm.DB, d *domain.Domain) *gorm.DB {
// Shared domain validation permits underscores, and older rows may contain
// other LIKE metacharacters.
escaped := strings.NewReplacer("!", "!!", "%", "!%", "_", "!_").Replace(name)
return db.Model(&rpservice.Service{}).Where(
// Registrations are unique by name, so another account can hold a subdomain
// of this one and serve from it. Its services derive their cluster from that
// account's own registration and are not dependents of this one.
return db.Model(&rpservice.Service{}).Where(accountIDCondition, d.AccountID).Where(
"LOWER(domain) IN ? OR LOWER(domain) LIKE ? ESCAPE '!' OR LOWER(domain) LIKE ? ESCAPE '!'",
[]string{name, name + "."}, "%."+escaped, "%."+escaped+".",
)
+3 -3
View File
@@ -37,7 +37,7 @@ func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []
return nil
}
return s.db.Transaction(func(tx *gorm.DB) error {
return s.transaction(ctx, func(tx *gorm.DB) error {
result := tx.
Clauses(
clause.OnConflict{
@@ -63,7 +63,7 @@ func (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []
return nil
}
return s.db.Transaction(func(tx *gorm.DB) error {
return s.transaction(ctx, func(tx *gorm.DB) error {
result := tx.
Clauses(
clause.OnConflict{
@@ -137,7 +137,7 @@ func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingSt
func (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {
const query = `SELECT id, account_id, public_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -19,7 +19,7 @@ func (s *SqlStore) getGroupPeers(ctx context.Context, groupIDs []string) ([]type
return nil, nil
}
const query = `SELECT account_id, group_id, peer_id FROM group_peers WHERE group_id = ANY($1)`
rows, err := s.pool.Query(ctx, query, groupIDs)
rows, err := s.pgxPool().Query(ctx, query, groupIDs)
if err != nil {
return nil, err
}
@@ -57,11 +57,11 @@ func (s *SqlStore) ListUsers(ctx context.Context) ([]*types.User, error) {
// txDeferFKConstraints defers foreign key constraint checks for the duration of the transaction.
// MySQL is already handled by s.transaction (SET FOREIGN_KEY_CHECKS = 0).
func (s *SqlStore) txDeferFKConstraints(tx *gorm.DB) error {
if s.storeEngine == types.SqliteStoreEngine {
if s.conn.Engine() == types.SqliteStoreEngine {
return tx.Exec("PRAGMA defer_foreign_keys = ON").Error
}
if s.storeEngine != types.PostgresStoreEngine {
if s.conn.Engine() != types.PostgresStoreEngine {
return nil
}
@@ -86,7 +86,7 @@ func (s *SqlStore) txDeferFKConstraints(tx *gorm.DB) error {
// txRestoreFKConstraints reverts FK constraints back to NOT DEFERRABLE after the
// deferred updates are done but before the transaction commits.
func (s *SqlStore) txRestoreFKConstraints(tx *gorm.DB) error {
if s.storeEngine != types.PostgresStoreEngine {
if s.conn.Engine() != types.PostgresStoreEngine {
return nil
}
@@ -138,7 +138,7 @@ func (s *SqlStore) UpdateUserID(ctx context.Context, accountID, oldUserID, newUs
}
log.Info("Updating user ID in the store")
err := s.transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
if err := s.txDeferFKConstraints(tx); err != nil {
return err
}
@@ -161,7 +161,7 @@ func (s *SqlStore) UpdateUserID(ctx context.Context, accountID, oldUserID, newUs
}
log.Info("Restoring FK constraints")
err = s.transaction(func(tx *gorm.DB) error {
err = s.transaction(ctx, func(tx *gorm.DB) error {
if err := s.txRestoreFKConstraints(tx); err != nil {
return fmt.Errorf("restore FK constraints: %w", err)
}
@@ -17,7 +17,7 @@ import (
func (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {
const query = `SELECT id, account_id, public_id, name, description, name_servers, groups, "primary", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
func (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {
const query = `SELECT id, account_id, public_id, name, description FROM networks WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -17,7 +17,7 @@ import (
func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {
const query = `SELECT id, network_id, account_id, public_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -19,7 +19,7 @@ import (
func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {
const query = `SELECT id, network_id, account_id, public_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
+8 -3
View File
@@ -25,7 +25,7 @@ func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.
peerCopy := peer.Copy()
peerCopy.AccountID = accountID
err := s.transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
// check if peer exists before saving
var peerID string
result := tx.Model(&nbpeer.Peer{}).Select("id").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)
@@ -190,7 +190,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
peer_status_connected, peer_status_login_expired, peer_status_requires_approval, location_connection_ip,
location_country_code, location_city_name, location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster, ipv6, meta_sync_message_version
FROM peers WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -492,7 +492,7 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking
}
// GetAccountPeers retrieves peers for an account.
func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error) {
var peers []*nbpeer.Peer
tx := s.db
if lockStrength != LockingStrengthNone {
@@ -506,6 +506,11 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
if ipFilter != "" {
query = query.Where("ip LIKE ? OR ipv6 LIKE ?", "%"+ipFilter+"%", "%"+ipFilter+"%")
}
// MAC addresses live in the JSON-serialized meta_network_addresses column,
// so we match the raw JSON text rather than a dedicated column.
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)
+44 -2
View File
@@ -512,7 +512,7 @@ func TestSqlStore_GetAccountPeers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
peers, err := store.GetAccountPeers(context.Background(), LockingStrengthNone, tt.accountID, tt.nameFilter, tt.ipFilter)
peers, err := store.GetAccountPeers(context.Background(), LockingStrengthNone, tt.accountID, tt.nameFilter, tt.ipFilter, "")
require.NoError(t, err)
require.Len(t, peers, tt.expectedCount)
})
@@ -520,6 +520,48 @@ func TestSqlStore_GetAccountPeers(t *testing.T) {
}
func TestSqlStore_GetAccountPeers_FilterByMac(t *testing.T) {
ctx := context.Background()
store, cleanup, err := NewTestStoreFromSQL(ctx, "", t.TempDir())
require.NoError(t, err)
t.Cleanup(cleanup)
accountID := "test-account-mac"
userID := "test-user-mac"
account := newAccountWithId(ctx, accountID, userID, "example.com")
account.Peers["peer-mac-1"] = &nbpeer.Peer{
ID: "peer-mac-1",
AccountID: accountID,
Key: "peer-mac-key-1",
Name: "macpeer",
IP: netip.MustParseAddr("100.64.0.10"),
Meta: nbpeer.PeerSystemMeta{
NetworkAddresses: []nbpeer.NetworkAddress{
{NetIP: netip.MustParsePrefix("192.168.0.11/24"), Mac: "00:93:37:bd:83:0f"},
},
},
}
require.NoError(t, store.SaveAccount(ctx, account))
tests := []struct {
name string
macFilter string
expectedCount int
}{
{name: "full mac matches", macFilter: "00:93:37:bd:83:0f", expectedCount: 1},
{name: "mac prefix matches", macFilter: "00:93:37", expectedCount: 1},
{name: "unknown mac does not match", macFilter: "11:22:33:44:55:66", expectedCount: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
peers, err := store.GetAccountPeers(ctx, LockingStrengthNone, accountID, "", "", tt.macFilter)
require.NoError(t, err)
require.Len(t, peers, tt.expectedCount)
})
}
}
func TestSqlStore_GetAccountPeersWithExpiration(t *testing.T) {
store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store_with_expired_peers.sql", t.TempDir())
t.Cleanup(cleanup)
@@ -878,7 +920,7 @@ func TestSqlStore_ApproveAccountPeers(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 2, count)
allPeers, err := store.GetAccountPeers(ctx, LockingStrengthNone, accountID, "", "")
allPeers, err := store.GetAccountPeers(ctx, LockingStrengthNone, accountID, "", "", "")
require.NoError(t, err)
for _, peer := range allPeers {
@@ -45,7 +45,7 @@ func (s *SqlStore) getPersonalAccessTokens(ctx context.Context, userIDs []string
return nil, nil
}
const query = `SELECT id, user_id, name, hashed_token, expiration_date, created_by, created_at, last_used FROM personal_access_tokens WHERE user_id = ANY($1)`
rows, err := s.pool.Query(ctx, query, userIDs)
rows, err := s.pgxPool().Query(ctx, query, userIDs)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -18,7 +18,7 @@ import (
func (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {
const query = `SELECT id, account_id, public_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -128,7 +128,7 @@ func (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {
}
func (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error {
return s.transaction(func(tx *gorm.DB) error {
return s.transaction(ctx, func(tx *gorm.DB) error {
if err := tx.Where("policy_id = ?", policyID).Delete(&types.PolicyRule{}).Error; err != nil {
return fmt.Errorf("delete policy rules: %w", err)
}
@@ -18,7 +18,7 @@ func (s *SqlStore) getPolicyRules(ctx context.Context, policyIDs []string) ([]*t
return nil, nil
}
const query = `SELECT id, policy_id, name, description, enabled, action, destinations, destination_resource, sources, source_resource, bidirectional, protocol, ports, port_ranges, authorized_groups, authorized_user FROM policy_rules WHERE policy_id = ANY($1)`
rows, err := s.pool.Query(ctx, query, policyIDs)
rows, err := s.pgxPool().Query(ctx, query, policyIDs)
if err != nil {
return nil, err
}
@@ -16,7 +16,7 @@ import (
func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {
const query = `SELECT id, account_id, public_id, name, description, checks FROM posture_checks WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -358,6 +358,14 @@ func (s *SqlStore) GetClusterSupportsPrivate(ctx context.Context, clusterAddr st
return s.getClusterCapability(ctx, clusterAddr, "private")
}
// GetClusterAllProxiesPrivate reports whether every active proxy in the cluster
// has the private capability. Returns nil when no proxy reported the capability.
// Use it where any proxy in the cluster may serve the result, since a single
// non-private proxy would serve it without the private guarantees.
func (s *SqlStore) GetClusterAllProxiesPrivate(ctx context.Context, clusterAddr string) *bool {
return s.getClusterUnanimousCapability(ctx, clusterAddr, "private")
}
// GetClusterSupportsCrowdSec returns whether all active proxies in the cluster
// have CrowdSec configured. Returns nil when no proxy reported the capability.
// Unlike other capabilities that use ANY-true (for rolling upgrades), CrowdSec
@@ -367,6 +375,21 @@ func (s *SqlStore) GetClusterSupportsCrowdSec(ctx context.Context, clusterAddr s
return s.getClusterUnanimousCapability(ctx, clusterAddr, "supports_crowdsec")
}
// GetActiveProxyVersions returns every active proxy version in a cluster.
func (s *SqlStore) GetActiveProxyVersions(ctx context.Context, clusterAddr string) ([]string, error) {
var versions []string
err := s.db.WithContext(ctx).
Model(&proxy.Proxy{}).
Where("LOWER(cluster_address) = LOWER(?) AND status = ? AND last_seen > ?",
clusterAddr, proxy.StatusConnected, time.Now().Add(-proxyActiveThreshold)).
Pluck("version", &versions).Error
if err != nil {
log.WithContext(ctx).Errorf("failed to get active proxy versions for %s: %v", clusterAddr, err)
return nil, status.Errorf(status.Internal, "get active proxy versions")
}
return versions, nil
}
// getClusterUnanimousCapability returns an aggregated boolean capability
// requiring all active proxies in the cluster to report true.
func (s *SqlStore) getClusterUnanimousCapability(ctx context.Context, clusterAddr, column string) *bool {
+1 -1
View File
@@ -17,7 +17,7 @@ import (
func (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {
const query = `SELECT id, account_id, public_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -30,7 +30,7 @@ const serviceSelectColumns = `id, account_id, name, domain, enabled, auth, restr
func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpservice.Service, error) {
const serviceQuery = `SELECT ` + serviceSelectColumns + ` FROM services WHERE account_id = $1`
serviceRows, err := s.pool.Query(ctx, serviceQuery, accountID)
serviceRows, err := s.pgxPool().Query(ctx, serviceQuery, accountID)
if err != nil {
return nil, err
}
@@ -205,7 +205,7 @@ func (s *SqlStore) UpdateService(ctx context.Context, service *rpservice.Service
targetType := &rpservice.Target{}
// Use a transaction to ensure atomic updates of the service and its targets
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
// Delete existing targets
if err := tx.Where("service_id = ?", serviceCopy.ID).Delete(targetType).Error; err != nil {
return err
@@ -26,7 +26,7 @@ const targetSelectColumns = `id, account_id, service_id, path, host, port, proto
func (s *SqlStore) getServiceTargets(ctx context.Context, serviceIDs []string) ([]*rpservice.Target, error) {
const targetsQuery = `SELECT ` + targetSelectColumns + ` FROM targets WHERE service_id = ANY($1)`
rows, err := s.pool.Query(ctx, targetsQuery, serviceIDs)
rows, err := s.pgxPool().Query(ctx, targetsQuery, serviceIDs)
if err != nil {
return nil, err
}
@@ -37,7 +37,7 @@ func (s *SqlStore) GetAccountBySetupKey(ctx context.Context, setupKey string) (*
func (s *SqlStore) getSetupKeys(ctx context.Context, accountID string) ([]types.SetupKey, error) {
const query = `SELECT id, account_id, key, key_secret, name, type, created_at, expires_at, updated_at,
revoked, used_times, last_used, auto_groups, usage_limit, ephemeral, allow_extra_dns_labels FROM setup_keys WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -127,7 +127,7 @@ func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength Locking
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.PreconditionFailed, "setup key not found")
return nil, status.Errorf(status.PermissionDenied, "couldn't add peer: setup key is invalid")
}
log.WithContext(ctx).Errorf("failed to get setup key by secret from store: %v", result.Error)
return nil, status.Errorf(status.Internal, "failed to get setup key by secret from store")
+87 -1
View File
@@ -15,6 +15,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/clause"
nbdns "github.com/netbirdio/netbird/dns"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
@@ -346,7 +348,6 @@ func TestSqlStore_ExecuteInTransaction_Timeout(t *testing.T) {
sqlStore, ok := store.(*SqlStore)
require.True(t, ok)
assert.Equal(t, 1*time.Second, sqlStore.transactionTimeout)
ctx := context.Background()
err = sqlStore.ExecuteInTransaction(ctx, func(transaction Store) error {
@@ -411,3 +412,88 @@ func TestNewSqliteStore_BusyTimeoutRespectsUserOverride(t *testing.T) {
})
}
}
func TestSqlStore_ExecuteInTransaction_RestoresForeignKeyChecksOnMysql(t *testing.T) {
runTestForAllEngines(t, "", func(t *testing.T, store Store) {
sqlStore := store.(*SqlStore)
if sqlStore.conn.Engine() != types.MysqlStoreEngine {
t.Skip("FOREIGN_KEY_CHECKS is MySQL specific")
}
sqlDB, err := sqlStore.GetDB().DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
ctx := context.Background()
foreignKeyChecks := func() int {
var enabled int
require.NoError(t, sqlStore.GetDB().Raw("SELECT @@SESSION.foreign_key_checks").Scan(&enabled).Error)
return enabled
}
err = store.ExecuteInTransaction(ctx, func(Store) error { return assert.AnError })
require.ErrorIs(t, err, assert.AnError)
assert.Equal(t, 1, foreignKeyChecks())
require.Panics(t, func() {
_ = store.ExecuteInTransaction(ctx, func(Store) error { panic("boom") })
})
assert.Equal(t, 1, foreignKeyChecks())
err = sqlStore.transaction(ctx, func(*gorm.DB) error { return assert.AnError })
require.ErrorIs(t, err, assert.AnError)
assert.Equal(t, 1, foreignKeyChecks())
err = store.ExecuteInTransaction(ctx, func(transaction Store) error {
bound := transaction.(*SqlStore)
require.NoError(t, bound.transaction(ctx, func(*gorm.DB) error { return nil }))
var enabled int
require.NoError(t, bound.GetDB().Raw("SELECT @@SESSION.foreign_key_checks").Scan(&enabled).Error)
assert.Equal(t, 0, enabled, "a savepoint must not re-enable FK checks for the rest of the transaction")
return nil
})
require.NoError(t, err)
assert.Equal(t, 1, foreignKeyChecks())
})
}
func TestSqlStore_Transaction_RollsBackOnError(t *testing.T) {
runTestForAllEngines(t, "../testdata/extended-store.sql", func(t *testing.T, store Store) {
ctx := context.Background()
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
group := &types.Group{ID: "rolled-back-group", AccountID: accountID, Name: "rolled back", Issued: "api"}
err := store.(*SqlStore).transaction(ctx, func(tx *gorm.DB) error {
require.NoError(t, tx.Omit(clause.Associations).Create(group).Error)
return assert.AnError
})
require.ErrorIs(t, err, assert.AnError)
_, err = store.GetGroupByID(ctx, LockingStrengthNone, accountID, group.ID)
require.Error(t, err)
})
}
func TestSqlStore_Transaction_NestedIsSavepoint(t *testing.T) {
runTestForAllEngines(t, "../testdata/extended-store.sql", func(t *testing.T, store Store) {
ctx := context.Background()
accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
outer := &types.Group{ID: "outer-group", AccountID: accountID, Name: "outer", Issued: "api"}
inner := &types.Group{ID: "inner-group", AccountID: accountID, Name: "inner", Issued: "api"}
err := store.ExecuteInTransaction(ctx, func(transaction Store) error {
require.NoError(t, transaction.CreateGroup(ctx, outer))
err := transaction.(*SqlStore).transaction(ctx, func(tx *gorm.DB) error {
require.NoError(t, tx.Omit(clause.Associations).Create(inner).Error)
return assert.AnError
})
require.ErrorIs(t, err, assert.AnError)
return nil
})
require.NoError(t, err)
_, err = store.GetGroupByID(ctx, LockingStrengthNone, accountID, outer.ID)
require.NoError(t, err)
_, err = store.GetGroupByID(ctx, LockingStrengthNone, accountID, inner.ID)
require.Error(t, err)
})
}
+2 -2
View File
@@ -108,7 +108,7 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre
}
func (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error {
err := s.transaction(func(tx *gorm.DB) error {
err := s.transaction(ctx, func(tx *gorm.DB) error {
result := tx.Delete(&types.PersonalAccessToken{}, "user_id = ?", userID)
if result.Error != nil {
return result.Error
@@ -173,7 +173,7 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre
func (s *SqlStore) getUsers(ctx context.Context, accountID string) ([]types.User, error) {
const query = `SELECT id, account_id, role, is_service_user, non_deletable, service_user_name, auto_groups, blocked, pending_approval, last_login, created_at, issued, integration_ref_id, integration_ref_integration_type, email, name FROM users WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
rows, err := s.pgxPool().Query(ctx, query, accountID)
if err != nil {
return nil, err
}
@@ -22,6 +22,7 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
nbdb "github.com/netbirdio/netbird/management/internals/shared/db"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
@@ -281,10 +282,11 @@ func setupBenchmarkDB(b testing.TB) (*SqlStore, func(), string) {
b.Fatalf("failed to migrate database: %v", err)
}
store := &SqlStore{
db: db,
pool: pool,
conn, err := nbdb.NewConn(context.Background(), db, nbdb.PostgresStoreEngine, pool)
if err != nil {
b.Fatalf("failed to create connection: %v", err)
}
store := &SqlStore{conn: conn, db: conn.DB(nil)}
const (
accountID = "benchmark-account-id"
@@ -527,7 +529,7 @@ func BenchmarkGetAccount(b *testing.B) {
}
}
})
store.pool.Close()
_ = store.Close(ctx)
}
func TestAccountEquivalence(t *testing.T) {
+53 -44
View File
@@ -13,7 +13,6 @@ import (
"path"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"sync"
@@ -23,19 +22,19 @@ import (
log "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
"github.com/netbirdio/netbird/management/internals/shared/db"
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/testutil"
"github.com/netbirdio/netbird/management/server/types"
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/util/crypt"
@@ -49,14 +48,14 @@ import (
"github.com/netbirdio/netbird/route"
)
type LockingStrength string
type LockingStrength = db.LockingStrength
const (
LockingStrengthUpdate LockingStrength = "UPDATE" // Strongest lock, preventing any changes by other transactions until your transaction completes.
LockingStrengthShare LockingStrength = "SHARE" // Allows reading but prevents changes by other transactions.
LockingStrengthNoKeyUpdate LockingStrength = "NO KEY UPDATE" // Similar to UPDATE but allows changes to related rows.
LockingStrengthKeyShare LockingStrength = "KEY SHARE" // Protects against changes to primary/unique keys but allows other updates.
LockingStrengthNone LockingStrength = "NONE" // No locking, allowing all transactions to proceed without restrictions.
LockingStrengthUpdate = db.LockingStrengthUpdate
LockingStrengthShare = db.LockingStrengthShare
LockingStrengthNoKeyUpdate = db.LockingStrengthNoKeyUpdate
LockingStrengthKeyShare = db.LockingStrengthKeyShare
LockingStrengthNone = db.LockingStrengthNone
)
type Store interface {
@@ -161,7 +160,7 @@ type Store interface {
RemoveResourceFromGroup(ctx context.Context, accountId string, groupID string, resourceID string) error
AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error
GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error)
GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error)
GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*nbpeer.Peer, error)
GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error)
GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) (*nbpeer.Peer, error)
GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error)
@@ -307,6 +306,7 @@ type Store interface {
GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error)
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error)
LockCustomDomains(ctx context.Context, accountID string, serviceDomain nbdomain.Domain) ([]*domain.Domain, error)
GetCustomDomainByName(ctx context.Context, domainName string) (*domain.Domain, error)
CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error)
UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error)
@@ -314,15 +314,13 @@ type Store interface {
DeleteExpiredCustomDomain(ctx context.Context, d *domain.Domain, now time.Time) (bool, error)
DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error
CreateAccessLog(ctx context.Context, log *accesslogs.AccessLogEntry) error
GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error)
DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error)
CreateAgentNetworkAccessLog(ctx context.Context, entry *agentNetworkTypes.AgentNetworkAccessLog, groups []agentNetworkTypes.AgentNetworkAccessLogGroup) error
CreateAgentNetworkUsage(ctx context.Context, usage *agentNetworkTypes.AgentNetworkUsage, groups []agentNetworkTypes.AgentNetworkUsageGroup) error
GetAgentNetworkAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkAccessLog, int64, error)
GetAgentNetworkAccessLogSessions(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkAccessLogSession, int64, error)
GetAgentNetworkUsageRows(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkUsage, error)
DeleteOldAgentNetworkAccessLogs(ctx context.Context, accountID string, olderThan time.Time) (int64, error)
GetDeletedAccountIDsWithAgentNetworkAccessLogs(ctx context.Context) ([]string, error)
GetServiceTargetByTargetID(ctx context.Context, lockStrength LockingStrength, accountID string, targetID string) (*rpservice.Target, error)
GetTargetsByServiceID(ctx context.Context, lockStrength LockingStrength, accountID string, serviceID string) ([]*rpservice.Target, error)
DeleteTarget(ctx context.Context, accountID string, serviceID string, targetID uint) error
@@ -338,6 +336,8 @@ type Store interface {
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
GetClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
GetClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
GetClusterAllProxiesPrivate(ctx context.Context, clusterAddr string) *bool
GetActiveProxyVersions(ctx context.Context, clusterAddr string) ([]string, error)
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
GetAllProxies(ctx context.Context) ([]*proxy.Proxy, error)
DisconnectAllProxies(ctx context.Context) (int64, error)
@@ -390,6 +390,7 @@ type Store interface {
GetAgentNetworkConsumption(ctx context.Context, lockStrength LockingStrength, accountID string, kind agentNetworkTypes.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time) (*agentNetworkTypes.Consumption, error)
GetAgentNetworkConsumptionBatch(ctx context.Context, lockStrength LockingStrength, accountID string, keys []agentNetworkTypes.ConsumptionKey) (map[agentNetworkTypes.ConsumptionKey]*agentNetworkTypes.Consumption, error)
ListAgentNetworkConsumption(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Consumption, error)
DeleteAgentNetworkConsumptionOfDeletedAccounts(ctx context.Context) (int64, error)
GetAccountAgentNetworkBudgetRules(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.AccountBudgetRule, error)
GetAgentNetworkBudgetRuleByID(ctx context.Context, lockStrength LockingStrength, accountID, ruleID string) (*agentNetworkTypes.AccountBudgetRule, error)
SaveAgentNetworkBudgetRule(ctx context.Context, rule *agentNetworkTypes.AccountBudgetRule) error
@@ -490,7 +491,7 @@ func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) type
// Migrate if it is the first run with a JSON file existing and no SQLite file present
jsonStoreFile := filepath.Join(dataDir, storeFileName)
sqliteStoreFile := filepath.Join(dataDir, storeSqliteFileName)
sqliteStoreFile := filepath.Join(dataDir, db.SqliteFileName)
if util.FileExists(jsonStoreFile) && !util.FileExists(sqliteStoreFile) {
log.WithContext(ctx).Warnf("unsupported store engine specified, but found %s. Automatically migrating to SQLite.", jsonStoreFile)
@@ -509,6 +510,16 @@ func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) type
// NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics
func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
conn, err := OpenConn(ctx, kind, dataDir)
if err != nil {
return nil, err
}
return newStore(ctx, conn, metrics, skipMigration)
}
// OpenConn resolves the configured engine and opens the connection that the
// store and the domain repositories share.
func OpenConn(ctx context.Context, kind types.Engine, dataDir string) (*db.Conn, error) {
kind = getStoreEngine(ctx, dataDir, kind)
if err := checkFileStoreEngine(kind, dataDir); err != nil {
@@ -518,13 +529,21 @@ func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics te
switch kind {
case types.SqliteStoreEngine:
log.WithContext(ctx).Info("using SQLite store engine")
return NewSqliteStore(ctx, dataDir, metrics, skipMigration)
return db.OpenSqlite(ctx, dataDir)
case types.PostgresStoreEngine:
log.WithContext(ctx).Info("using Postgres store engine")
return newPostgresStore(ctx, metrics, skipMigration)
dsn, ok := lookupDSNEnv(PostgresDsnEnv, PostgresDsnEnvLegacy)
if !ok {
return nil, fmt.Errorf("%s is not set", PostgresDsnEnv)
}
return db.OpenPostgres(ctx, dsn, db.DefaultPoolConfig)
case types.MysqlStoreEngine:
log.WithContext(ctx).Info("using MySQL store engine")
return newMysqlStore(ctx, metrics, skipMigration)
dsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)
if !ok {
return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
}
return db.OpenMysql(ctx, dsn)
default:
return nil, fmt.Errorf("unsupported kind of store: %s", kind)
}
@@ -698,32 +717,28 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
kind = types.SqliteStoreEngine
}
storeStr := fmt.Sprintf("%s?cache=shared", storeSqliteFileName)
if runtime.GOOS == "windows" {
// Vo avoid `The process cannot access the file because it is being used by another process` on Windows
storeStr = storeSqliteFileName
}
file := filepath.Join(dataDir, storeStr)
db, err := gorm.Open(sqlite.Open(file), getGormConfig())
conn, err := db.OpenSqliteFile(ctx, dataDir, db.SqliteFileName)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("failed to create test store: %v", err)
}
if filename != "" {
err = LoadSQL(db, filename)
err = LoadSQL(conn.DB(nil), filename)
if err != nil {
_ = conn.Close()
return nil, nil, fmt.Errorf("failed to load SQL file: %v", err)
}
}
store, err := NewSqlStore(ctx, db, types.SqliteStoreEngine, nil, false)
store, err := NewSqlStore(ctx, conn, nil, false)
if err != nil {
_ = conn.Close()
return nil, nil, fmt.Errorf("failed to create test store: %v", err)
}
err = addAllGroupToAccount(ctx, store)
if err != nil {
_ = store.Close(ctx)
return nil, nil, fmt.Errorf("failed to add all group to account: %v", err)
}
@@ -788,9 +803,6 @@ func getSqlStoreEngine(ctx context.Context, sqliteStore *SqlStore, kind types.En
closeConnection := func() {
cleanup()
store.Close(ctx)
if store.pool != nil {
store.pool.Close()
}
if store != sqliteStore {
// The sqlite store only seeded the engine under test; without this
// every test leaks its connection and the opener goroutines.
@@ -947,9 +959,6 @@ func postgresSchemaTemplate(ctx context.Context, baseDSN string, admin *gorm.DB)
// TEMPLATE refuses a source that still has sessions, so release both handles
// before the first clone.
tplStore.Close(ctx)
if tplStore.pool != nil {
tplStore.pool.Close()
}
schemaTemplates[key] = &schemaTemplate{dbName: name}
return name, nil
@@ -1034,11 +1043,11 @@ func mysqlTableNames(ctx context.Context, sqlDB *sql.DB, dbName string) ([]strin
// cloneMysqlSchema replays the template's CREATE TABLE statements into the
// database the DSN points at.
func cloneMysqlSchema(ctx context.Context, dsn string, tableDDL []string) error {
db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig())
gormDB, err := gorm.Open(mysql.Open(db.MysqlDSN(dsn)), db.GormConfig())
if err != nil {
return fmt.Errorf("connect to test database: %w", err)
}
sqlDB, err := db.DB()
sqlDB, err := gormDB.DB()
if err != nil {
return err
}
@@ -1078,19 +1087,19 @@ func closeGormDB(db *gorm.DB) {
}
func openDBWithRetry(dsn string, engine types.Engine, maxRetries int) (*gorm.DB, error) {
var db *gorm.DB
var gormDB *gorm.DB
var err error
for i := range maxRetries {
switch engine {
case types.PostgresStoreEngine:
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
gormDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
case types.MysqlStoreEngine:
db, err = gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{})
gormDB, err = gorm.Open(mysql.Open(db.MysqlDSN(dsn)), &gorm.Config{})
}
if err == nil {
return db, nil
return gormDB, nil
}
if i < maxRetries-1 {
@@ -1104,14 +1113,14 @@ func openDBWithRetry(dsn string, engine types.Engine, maxRetries int) (*gorm.DB,
// createRandomDB creates a uniquely named database for one test. On postgres a
// non-empty template is copied server-side with CREATE DATABASE ... TEMPLATE.
func createRandomDB(dsn string, db *gorm.DB, engine types.Engine, template string) (string, func(), error) {
func createRandomDB(dsn string, admin *gorm.DB, engine types.Engine, template string) (string, func(), error) {
dbName := newTestDBName("test_db")
createStmt := fmt.Sprintf("CREATE DATABASE %s", dbName)
if template != "" && engine == types.PostgresStoreEngine {
createStmt = fmt.Sprintf("CREATE DATABASE %s TEMPLATE %s", dbName, template)
}
if err := execWithTemplateRetry(db, createStmt); err != nil {
if err := execWithTemplateRetry(admin, createStmt); err != nil {
return "", nil, fmt.Errorf("failed to create database: %v", err)
}
@@ -1146,7 +1155,7 @@ func createRandomDB(dsn string, db *gorm.DB, engine types.Engine, template strin
err = dropDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s WITH (FORCE)", dbName)).Error
case types.MysqlStoreEngine:
dropDB, err = gorm.Open(mysql.Open(originalDSN+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{
dropDB, err = gorm.Open(mysql.Open(db.MysqlDSN(originalDSN)), &gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: false,
})
@@ -1224,7 +1233,7 @@ func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error {
return fmt.Errorf("%s doesn't exist, couldn't continue the operation", fileStorePath)
}
sqlStorePath := path.Join(dataDir, storeSqliteFileName)
sqlStorePath := path.Join(dataDir, db.SqliteFileName)
if _, err := os.Stat(sqlStorePath); err == nil {
return fmt.Errorf("%s already exists, couldn't continue the operation", sqlStorePath)
}
+82 -53
View File
@@ -18,7 +18,6 @@ import (
dns "github.com/netbirdio/netbird/dns"
types "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
accesslogs "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
domain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
proxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
service "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
@@ -31,6 +30,7 @@ import (
posture "github.com/netbirdio/netbird/management/server/posture"
types3 "github.com/netbirdio/netbird/management/server/types"
route "github.com/netbirdio/netbird/route"
domain0 "github.com/netbirdio/netbird/shared/management/domain"
crypt "github.com/netbirdio/netbird/util/crypt"
gomock "go.uber.org/mock/gomock"
)
@@ -246,20 +246,6 @@ func (mr *MockStoreMockRecorder) CountProxiesByAccountID(ctx, accountID any) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountProxiesByAccountID", reflect.TypeOf((*MockStore)(nil).CountProxiesByAccountID), ctx, accountID)
}
// CreateAccessLog mocks base method.
func (m *MockStore) CreateAccessLog(ctx context.Context, log *accesslogs.AccessLogEntry) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAccessLog", ctx, log)
ret0, _ := ret[0].(error)
return ret0
}
// CreateAccessLog indicates an expected call of CreateAccessLog.
func (mr *MockStoreMockRecorder) CreateAccessLog(ctx, log any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessLog", reflect.TypeOf((*MockStore)(nil).CreateAccessLog), ctx, log)
}
// CreateAgentNetworkAccessLog mocks base method.
func (m *MockStore) CreateAgentNetworkAccessLog(ctx context.Context, entry *types.AgentNetworkAccessLog, groups []types.AgentNetworkAccessLogGroup) error {
m.ctrl.T.Helper()
@@ -471,6 +457,21 @@ func (mr *MockStoreMockRecorder) DeleteAgentNetworkBudgetRule(ctx, accountID, ru
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkBudgetRule", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkBudgetRule), ctx, accountID, ruleID)
}
// DeleteAgentNetworkConsumptionOfDeletedAccounts mocks base method.
func (m *MockStore) DeleteAgentNetworkConsumptionOfDeletedAccounts(ctx context.Context) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAgentNetworkConsumptionOfDeletedAccounts", ctx)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteAgentNetworkConsumptionOfDeletedAccounts indicates an expected call of DeleteAgentNetworkConsumptionOfDeletedAccounts.
func (mr *MockStoreMockRecorder) DeleteAgentNetworkConsumptionOfDeletedAccounts(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkConsumptionOfDeletedAccounts", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkConsumptionOfDeletedAccounts), ctx)
}
// DeleteAgentNetworkGuardrail mocks base method.
func (m *MockStore) DeleteAgentNetworkGuardrail(ctx context.Context, accountID, guardrailID string) error {
m.ctrl.T.Helper()
@@ -668,21 +669,6 @@ func (mr *MockStoreMockRecorder) DeleteNetworkRouter(ctx, accountID, routerID an
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetworkRouter", reflect.TypeOf((*MockStore)(nil).DeleteNetworkRouter), ctx, accountID, routerID)
}
// DeleteOldAccessLogs mocks base method.
func (m *MockStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOldAccessLogs", ctx, olderThan)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOldAccessLogs indicates an expected call of DeleteOldAccessLogs.
func (mr *MockStoreMockRecorder) DeleteOldAccessLogs(ctx, olderThan any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldAccessLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldAccessLogs), ctx, olderThan)
}
// DeleteOldAgentNetworkAccessLogs mocks base method.
func (m *MockStore) DeleteOldAgentNetworkAccessLogs(ctx context.Context, accountID string, olderThan time.Time) (int64, error) {
m.ctrl.T.Helper()
@@ -967,22 +953,6 @@ func (mr *MockStoreMockRecorder) GetAccount(ctx, accountID any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), ctx, accountID)
}
// GetAccountAccessLogs mocks base method.
func (m *MockStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccountAccessLogs", ctx, lockStrength, accountID, filter)
ret0, _ := ret[0].([]*accesslogs.AccessLogEntry)
ret1, _ := ret[1].(int64)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetAccountAccessLogs indicates an expected call of GetAccountAccessLogs.
func (mr *MockStoreMockRecorder) GetAccountAccessLogs(ctx, lockStrength, accountID, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAccessLogs", reflect.TypeOf((*MockStore)(nil).GetAccountAccessLogs), ctx, lockStrength, accountID, filter)
}
// GetAccountAgentNetworkBudgetRules mocks base method.
func (m *MockStore) GetAccountAgentNetworkBudgetRules(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.AccountBudgetRule, error) {
m.ctrl.T.Helper()
@@ -1360,18 +1330,18 @@ func (mr *MockStoreMockRecorder) GetAccountOwner(ctx, lockStrength, accountID an
}
// GetAccountPeers mocks base method.
func (m *MockStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*peer.Peer, error) {
func (m *MockStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter, macFilter string) ([]*peer.Peer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccountPeers", ctx, lockStrength, accountID, nameFilter, ipFilter)
ret := m.ctrl.Call(m, "GetAccountPeers", ctx, lockStrength, accountID, nameFilter, ipFilter, macFilter)
ret0, _ := ret[0].([]*peer.Peer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccountPeers indicates an expected call of GetAccountPeers.
func (mr *MockStoreMockRecorder) GetAccountPeers(ctx, lockStrength, accountID, nameFilter, ipFilter any) *gomock.Call {
func (mr *MockStoreMockRecorder) GetAccountPeers(ctx, lockStrength, accountID, nameFilter, ipFilter, macFilter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeers", reflect.TypeOf((*MockStore)(nil).GetAccountPeers), ctx, lockStrength, accountID, nameFilter, ipFilter)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeers", reflect.TypeOf((*MockStore)(nil).GetAccountPeers), ctx, lockStrength, accountID, nameFilter, ipFilter, macFilter)
}
// GetAccountPeersWithExpiration mocks base method.
@@ -1584,6 +1554,21 @@ func (mr *MockStoreMockRecorder) GetActiveProxyClusterAddressesForAccount(ctx, a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyClusterAddressesForAccount", reflect.TypeOf((*MockStore)(nil).GetActiveProxyClusterAddressesForAccount), ctx, accountID)
}
// GetActiveProxyVersions mocks base method.
func (m *MockStore) GetActiveProxyVersions(ctx context.Context, clusterAddr string) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetActiveProxyVersions", ctx, clusterAddr)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetActiveProxyVersions indicates an expected call of GetActiveProxyVersions.
func (mr *MockStoreMockRecorder) GetActiveProxyVersions(ctx, clusterAddr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyVersions", reflect.TypeOf((*MockStore)(nil).GetActiveProxyVersions), ctx, clusterAddr)
}
// GetAgentNetworkAccessLogSessions mocks base method.
func (m *MockStore) GetAgentNetworkAccessLogSessions(ctx context.Context, lockStrength LockingStrength, accountID string, filter types.AgentNetworkAccessLogFilter) ([]*types.AgentNetworkAccessLogSession, int64, error) {
m.ctrl.T.Helper()
@@ -1885,6 +1870,20 @@ func (mr *MockStoreMockRecorder) GetAnyAccountID(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnyAccountID", reflect.TypeOf((*MockStore)(nil).GetAnyAccountID), ctx)
}
// GetClusterAllProxiesPrivate mocks base method.
func (m *MockStore) GetClusterAllProxiesPrivate(ctx context.Context, clusterAddr string) *bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClusterAllProxiesPrivate", ctx, clusterAddr)
ret0, _ := ret[0].(*bool)
return ret0
}
// GetClusterAllProxiesPrivate indicates an expected call of GetClusterAllProxiesPrivate.
func (mr *MockStoreMockRecorder) GetClusterAllProxiesPrivate(ctx, clusterAddr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterAllProxiesPrivate", reflect.TypeOf((*MockStore)(nil).GetClusterAllProxiesPrivate), ctx, clusterAddr)
}
// GetClusterRequireSubdomain mocks base method.
func (m *MockStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
m.ctrl.T.Helper()
@@ -2002,6 +2001,21 @@ func (mr *MockStoreMockRecorder) GetDNSRecordByID(ctx, lockStrength, accountID,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSRecordByID", reflect.TypeOf((*MockStore)(nil).GetDNSRecordByID), ctx, lockStrength, accountID, zoneID, recordID)
}
// GetDeletedAccountIDsWithAgentNetworkAccessLogs mocks base method.
func (m *MockStore) GetDeletedAccountIDsWithAgentNetworkAccessLogs(ctx context.Context) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeletedAccountIDsWithAgentNetworkAccessLogs", ctx)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDeletedAccountIDsWithAgentNetworkAccessLogs indicates an expected call of GetDeletedAccountIDsWithAgentNetworkAccessLogs.
func (mr *MockStoreMockRecorder) GetDeletedAccountIDsWithAgentNetworkAccessLogs(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeletedAccountIDsWithAgentNetworkAccessLogs", reflect.TypeOf((*MockStore)(nil).GetDeletedAccountIDsWithAgentNetworkAccessLogs), ctx)
}
// GetEmbeddedProxyPeerIDsByCluster mocks base method.
func (m *MockStore) GetEmbeddedProxyPeerIDsByCluster(ctx context.Context, accountID string) (map[string][]string, error) {
m.ctrl.T.Helper()
@@ -2176,7 +2190,7 @@ func (m *MockStore) GetNetworkResourceByIDOrPublicID(ctx context.Context, lockSt
}
// GetNetworkResourceByIDOrPublicID indicates an expected call of GetNetworkResourceByIDOrPublicID.
func (mr *MockStoreMockRecorder) GetNetworkResourceByIDOrPublicID(ctx, lockStrength, accountID, resourceID interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetNetworkResourceByIDOrPublicID(ctx, lockStrength, accountID, resourceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourceByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourceByIDOrPublicID), ctx, lockStrength, accountID, resourceID)
}
@@ -2521,7 +2535,7 @@ func (m *MockStore) GetPolicyByIDOrPublicID(ctx context.Context, lockStrength Lo
}
// GetPolicyByIDOrPublicID indicates an expected call of GetPolicyByIDOrPublicID.
func (mr *MockStoreMockRecorder) GetPolicyByIDOrPublicID(ctx, lockStrength, accountID, policyID interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetPolicyByIDOrPublicID(ctx, lockStrength, accountID, policyID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetPolicyByIDOrPublicID), ctx, lockStrength, accountID, policyID)
}
@@ -2716,7 +2730,7 @@ func (m *MockStore) GetRouteByIDOrPublicID(ctx context.Context, lockStrength Loc
}
// GetRouteByIDOrPublicID indicates an expected call of GetRouteByIDOrPublicID.
func (mr *MockStoreMockRecorder) GetRouteByIDOrPublicID(ctx, lockStrength, accountID, routeID interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetRouteByIDOrPublicID(ctx, lockStrength, accountID, routeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouteByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetRouteByIDOrPublicID), ctx, lockStrength, accountID, routeID)
}
@@ -3302,6 +3316,21 @@ func (mr *MockStoreMockRecorder) ListFreeDomains(ctx, accountID any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFreeDomains", reflect.TypeOf((*MockStore)(nil).ListFreeDomains), ctx, accountID)
}
// LockCustomDomains mocks base method.
func (m *MockStore) LockCustomDomains(ctx context.Context, accountID string, serviceDomain domain0.Domain) ([]*domain.Domain, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LockCustomDomains", ctx, accountID, serviceDomain)
ret0, _ := ret[0].([]*domain.Domain)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LockCustomDomains indicates an expected call of LockCustomDomains.
func (mr *MockStoreMockRecorder) LockCustomDomains(ctx, accountID, serviceDomain any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockCustomDomains", reflect.TypeOf((*MockStore)(nil).LockCustomDomains), ctx, accountID, serviceDomain)
}
// MarkAccountPrimary mocks base method.
func (m *MockStore) MarkAccountPrimary(ctx context.Context, accountID string) error {
m.ctrl.T.Helper()