2021-07-30 17:46:38 +02:00
|
|
|
package server
|
|
|
|
|
2023-10-12 15:42:36 +02:00
|
|
|
import (
|
2024-05-16 18:28:37 +02:00
|
|
|
"context"
|
2023-10-12 15:42:36 +02:00
|
|
|
"fmt"
|
2024-05-16 18:28:37 +02:00
|
|
|
"net"
|
|
|
|
"net/netip"
|
2023-10-12 15:42:36 +02:00
|
|
|
"os"
|
2024-02-20 15:06:32 +01:00
|
|
|
"path/filepath"
|
2023-10-16 11:19:39 +02:00
|
|
|
"strings"
|
2023-10-12 15:42:36 +02:00
|
|
|
"time"
|
|
|
|
|
2023-10-19 21:14:05 +02:00
|
|
|
log "github.com/sirupsen/logrus"
|
2024-05-16 18:28:37 +02:00
|
|
|
"github.com/testcontainers/testcontainers-go"
|
|
|
|
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
|
|
"gorm.io/gorm"
|
2023-10-19 21:14:05 +02:00
|
|
|
|
2024-05-16 18:28:37 +02:00
|
|
|
"github.com/netbirdio/netbird/management/server/migration"
|
2023-11-28 13:45:26 +01:00
|
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
2023-10-12 15:42:36 +02:00
|
|
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
2024-05-16 18:28:37 +02:00
|
|
|
"github.com/netbirdio/netbird/route"
|
2023-10-12 15:42:36 +02:00
|
|
|
)
|
2023-08-18 19:23:11 +02:00
|
|
|
|
2021-07-30 17:46:38 +02:00
|
|
|
type Store interface {
|
2022-05-21 15:21:39 +02:00
|
|
|
GetAllAccounts() []*Account
|
2022-11-07 12:10:56 +01:00
|
|
|
GetAccount(accountID string) (*Account, error)
|
2023-11-28 14:23:38 +01:00
|
|
|
DeleteAccount(account *Account) error
|
2022-11-07 12:10:56 +01:00
|
|
|
GetAccountByUser(userID string) (*Account, error)
|
|
|
|
GetAccountByPeerPubKey(peerKey string) (*Account, error)
|
2024-05-07 14:30:03 +02:00
|
|
|
GetAccountIDByPeerPubKey(peerKey string) (string, error)
|
2023-02-03 10:33:28 +01:00
|
|
|
GetAccountByPeerID(peerID string) (*Account, error)
|
2023-03-16 15:57:44 +01:00
|
|
|
GetAccountBySetupKey(setupKey string) (*Account, error) // todo use key hash later
|
2022-03-01 15:22:18 +01:00
|
|
|
GetAccountByPrivateDomain(domain string) (*Account, error)
|
2023-03-16 15:57:44 +01:00
|
|
|
GetTokenIDByHashedToken(secret string) (string, error)
|
|
|
|
GetUserByTokenID(tokenID string) (*User, error)
|
2021-07-30 17:46:38 +02:00
|
|
|
SaveAccount(account *Account) error
|
2023-03-20 16:14:55 +01:00
|
|
|
DeleteHashedPAT2TokenIDIndex(hashedToken string) error
|
|
|
|
DeleteTokenID2UserIDIndex(tokenID string) error
|
2022-10-16 13:33:46 +02:00
|
|
|
GetInstallationID() string
|
2022-11-08 10:46:12 +01:00
|
|
|
SaveInstallationID(ID string) error
|
2024-05-07 14:30:03 +02:00
|
|
|
// AcquireAccountWriteLock should attempt to acquire account lock for write purposes and return a function that releases the lock
|
|
|
|
AcquireAccountWriteLock(accountID string) func()
|
|
|
|
// AcquireAccountReadLock should attempt to acquire account lock for read purposes and return a function that releases the lock
|
|
|
|
AcquireAccountReadLock(accountID string) func()
|
2022-11-07 17:52:23 +01:00
|
|
|
// AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock
|
|
|
|
AcquireGlobalLock() func()
|
2023-11-28 13:45:26 +01:00
|
|
|
SavePeerStatus(accountID, peerID string, status nbpeer.PeerStatus) error
|
2024-02-20 09:59:56 +01:00
|
|
|
SavePeerLocation(accountID string, peer *nbpeer.Peer) error
|
2023-08-18 19:23:11 +02:00
|
|
|
SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error
|
2022-11-08 10:46:12 +01:00
|
|
|
// Close should close the store persisting all unsaved data.
|
|
|
|
Close() error
|
2023-10-16 11:19:39 +02:00
|
|
|
// GetStoreEngine should return StoreEngine of the current store implementation.
|
2023-10-12 15:42:36 +02:00
|
|
|
// This is also a method of metrics.DataSource interface.
|
2023-10-16 11:19:39 +02:00
|
|
|
GetStoreEngine() StoreEngine
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
|
|
|
|
2023-10-16 11:19:39 +02:00
|
|
|
type StoreEngine string
|
2023-10-12 15:42:36 +02:00
|
|
|
|
|
|
|
const (
|
2024-05-16 18:28:37 +02:00
|
|
|
FileStoreEngine StoreEngine = "jsonfile"
|
|
|
|
SqliteStoreEngine StoreEngine = "sqlite"
|
|
|
|
PostgresStoreEngine StoreEngine = "postgres"
|
|
|
|
|
|
|
|
postgresDsnEnv = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
2023-10-12 15:42:36 +02:00
|
|
|
)
|
|
|
|
|
2023-10-16 11:19:39 +02:00
|
|
|
func getStoreEngineFromEnv() StoreEngine {
|
2024-02-20 15:06:32 +01:00
|
|
|
// NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file.
|
2023-10-16 11:19:39 +02:00
|
|
|
kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE")
|
2023-10-12 15:42:36 +02:00
|
|
|
if !ok {
|
2024-02-20 15:06:32 +01:00
|
|
|
return ""
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
|
|
|
|
2023-10-16 11:19:39 +02:00
|
|
|
value := StoreEngine(strings.ToLower(kind))
|
2024-05-16 18:28:37 +02:00
|
|
|
if value == FileStoreEngine || value == SqliteStoreEngine || value == PostgresStoreEngine {
|
2023-10-12 15:42:36 +02:00
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
2024-02-20 15:06:32 +01:00
|
|
|
return SqliteStoreEngine
|
|
|
|
}
|
|
|
|
|
|
|
|
func getStoreEngineFromDatadir(dataDir string) StoreEngine {
|
|
|
|
storeFile := filepath.Join(dataDir, storeFileName)
|
|
|
|
if _, err := os.Stat(storeFile); err != nil {
|
|
|
|
// json file not found then use sqlite as default
|
|
|
|
return SqliteStoreEngine
|
|
|
|
}
|
2023-10-16 11:19:39 +02:00
|
|
|
return FileStoreEngine
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
|
|
|
|
2023-10-16 11:19:39 +02:00
|
|
|
func NewStore(kind StoreEngine, dataDir string, metrics telemetry.AppMetrics) (Store, error) {
|
2023-10-12 15:42:36 +02:00
|
|
|
if kind == "" {
|
2024-02-20 15:06:32 +01:00
|
|
|
// if store engine is not set in the config we first try to evaluate NETBIRD_STORE_ENGINE
|
2023-10-16 11:19:39 +02:00
|
|
|
kind = getStoreEngineFromEnv()
|
2024-02-20 15:06:32 +01:00
|
|
|
if kind == "" {
|
|
|
|
// NETBIRD_STORE_ENGINE is not set we evaluate default based on dataDir
|
|
|
|
kind = getStoreEngineFromDatadir(dataDir)
|
|
|
|
}
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
|
|
|
switch kind {
|
2023-10-16 11:19:39 +02:00
|
|
|
case FileStoreEngine:
|
2023-10-19 21:14:05 +02:00
|
|
|
log.Info("using JSON file store engine")
|
2023-10-12 15:42:36 +02:00
|
|
|
return NewFileStore(dataDir, metrics)
|
2023-10-16 11:19:39 +02:00
|
|
|
case SqliteStoreEngine:
|
2023-10-19 21:14:05 +02:00
|
|
|
log.Info("using SQLite store engine")
|
2023-10-12 15:42:36 +02:00
|
|
|
return NewSqliteStore(dataDir, metrics)
|
2024-05-16 18:28:37 +02:00
|
|
|
case PostgresStoreEngine:
|
|
|
|
log.Info("using Postgres store engine")
|
|
|
|
dsn, ok := os.LookupEnv(postgresDsnEnv)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
|
|
|
}
|
|
|
|
return NewPostgresqlStore(dsn, metrics)
|
2023-10-12 15:42:36 +02:00
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported kind of store %s", kind)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-16 18:28:37 +02:00
|
|
|
// migrate migrates the SQLite database to the latest schema
|
|
|
|
func migrate(db *gorm.DB) error {
|
|
|
|
migrations := getMigrations()
|
|
|
|
|
|
|
|
for _, m := range migrations {
|
|
|
|
if err := m(db); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getMigrations() []migrationFunc {
|
|
|
|
return []migrationFunc{
|
|
|
|
func(db *gorm.DB) error {
|
|
|
|
return migration.MigrateFieldFromGobToJSON[Account, net.IPNet](db, "network_net")
|
|
|
|
},
|
|
|
|
func(db *gorm.DB) error {
|
|
|
|
return migration.MigrateFieldFromGobToJSON[route.Route, netip.Prefix](db, "network")
|
|
|
|
},
|
|
|
|
func(db *gorm.DB) error {
|
|
|
|
return migration.MigrateFieldFromGobToJSON[route.Route, []string](db, "peer_groups")
|
|
|
|
},
|
|
|
|
func(db *gorm.DB) error {
|
|
|
|
return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "location_connection_ip", "")
|
|
|
|
},
|
|
|
|
func(db *gorm.DB) error {
|
|
|
|
return migration.MigrateNetIPFieldFromBlobToJSON[nbpeer.Peer](db, "ip", "idx_peers_account_id_ip")
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTestStoreFromJson is only used in tests
|
|
|
|
func NewTestStoreFromJson(dataDir string) (Store, func(), error) {
|
2023-10-12 15:42:36 +02:00
|
|
|
fstore, err := NewFileStore(dataDir, nil)
|
|
|
|
if err != nil {
|
2024-05-16 18:28:37 +02:00
|
|
|
return nil, nil, err
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
|
|
|
|
2024-05-16 18:28:37 +02:00
|
|
|
cleanUp := func() {}
|
|
|
|
|
2024-03-15 10:50:02 +01:00
|
|
|
// if store engine is not set in the config we first try to evaluate NETBIRD_STORE_ENGINE
|
|
|
|
kind := getStoreEngineFromEnv()
|
|
|
|
if kind == "" {
|
|
|
|
// NETBIRD_STORE_ENGINE is not set we evaluate default based on dataDir
|
|
|
|
kind = getStoreEngineFromDatadir(dataDir)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch kind {
|
2023-10-16 11:19:39 +02:00
|
|
|
case FileStoreEngine:
|
2024-05-16 18:28:37 +02:00
|
|
|
return fstore, cleanUp, nil
|
2023-10-16 11:19:39 +02:00
|
|
|
case SqliteStoreEngine:
|
2024-05-16 18:28:37 +02:00
|
|
|
store, err := NewSqliteStoreFromFileStore(fstore, dataDir, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return store, cleanUp, nil
|
|
|
|
case PostgresStoreEngine:
|
|
|
|
cleanUp, err = createPGDB()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dsn, ok := os.LookupEnv(postgresDsnEnv)
|
|
|
|
if !ok {
|
|
|
|
return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
|
|
|
}
|
|
|
|
|
|
|
|
store, err := NewPostgresqlStoreFromFileStore(fstore, dsn, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return store, cleanUp, nil
|
2023-10-12 15:42:36 +02:00
|
|
|
default:
|
2024-05-16 18:28:37 +02:00
|
|
|
store, err := NewSqliteStoreFromFileStore(fstore, dataDir, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return store, cleanUp, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createPGDB() (func(), error) {
|
|
|
|
ctx := context.Background()
|
|
|
|
c, err := postgres.RunContainer(ctx,
|
|
|
|
testcontainers.WithImage("postgres:alpine"),
|
|
|
|
postgres.WithDatabase("test"),
|
|
|
|
postgres.WithUsername("postgres"),
|
|
|
|
postgres.WithPassword("postgres"),
|
|
|
|
testcontainers.WithWaitStrategy(
|
|
|
|
wait.ForLog("database system is ready to accept connections").
|
|
|
|
WithOccurrence(2).WithStartupTimeout(15*time.Second)),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup := func() {
|
|
|
|
timeout := 10 * time.Second
|
|
|
|
err = c.Stop(ctx, &timeout)
|
|
|
|
if err != nil {
|
|
|
|
log.Warnf("failed to stop container: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
talksConn, err := c.ConnectionString(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return cleanup, err
|
2023-10-12 15:42:36 +02:00
|
|
|
}
|
2024-05-16 18:28:37 +02:00
|
|
|
return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn)
|
2021-07-30 17:46:38 +02:00
|
|
|
}
|