mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
config: block startup on mixed SQL backends
Reject `litd` startup when another SQL database exists than the selected SQL `databasebackend` choice. When starting with the `databasebackend` config flag set to `postgres`, we check whether an `sqlite` database file exists at the default path and fail startup if it does. When starting with `sqlite`, we instead check whether `postgres` database configuration has been provided and, if so, whether a database exists at the configured `postgres` connection parameters. This prevents accidental switches between SQL backends. This is especially important because allowing such a switch would retrigger the KVDB-to-SQL migration for the newly configured SQL backend. In that scenario, the data would already have been migrated previously, meaning the migration source would be stale and could result in outdated data being imported. Additionally, it would create two divergent copies of the data across separate SQL backends. Since we do not support migrations between SQL backends, recovering from such a situation would not be possible.
This commit is contained in:
parent
1903010be5
commit
6d2bf233e8
2 changed files with 270 additions and 0 deletions
127
config.go
127
config.go
|
|
@ -3,6 +3,7 @@ package terminal
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/lib/pq"
|
||||
"github.com/lightninglabs/faraday"
|
||||
"github.com/lightninglabs/faraday/chain"
|
||||
"github.com/lightninglabs/faraday/frdrpcserver"
|
||||
|
|
@ -727,6 +729,11 @@ func loadAndValidateConfig(interceptor signal.Interceptor) (*Config, error) {
|
|||
)
|
||||
}
|
||||
|
||||
err = validateExclusiveSQLBackends(cfg, litDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cfg.DevConfig.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -1149,6 +1156,126 @@ func readAutoMigrateKVDB(config *Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// validateExclusiveSQLBackends errors and thereby rejects startup when the
|
||||
// inactive SQL backend still has data at its default location. This prevents
|
||||
// silently switching to a different SQL store and starting against an empty
|
||||
// database.
|
||||
func validateExclusiveSQLBackends(cfg *Config, litDir string) error {
|
||||
switch cfg.DatabaseBackend {
|
||||
case DatabaseBackendPostgres:
|
||||
sqlitePath := filepath.Join(
|
||||
litDir, cfg.Network, defaultSqliteDatabaseFileName,
|
||||
)
|
||||
|
||||
exists, err := sqliteDatabaseExists(sqlitePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to check for existing "+
|
||||
"sqlite database file at %s: %w", sqlitePath,
|
||||
err)
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("cannot start litd with postgres "+
|
||||
"backend: sqlite database file already exists "+
|
||||
"at %s. If you really intend to switch to "+
|
||||
"postgres you must delete the sqlite "+
|
||||
"database (effectively deleting all stored "+
|
||||
"data) before restarting with postgres again",
|
||||
sqlitePath)
|
||||
}
|
||||
|
||||
case DatabaseBackendSqlite:
|
||||
exists, err := postgresDatabaseExists(cfg.Postgres)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to check for existing "+
|
||||
"postgres database %q at %s:%d for user %q. "+
|
||||
"Note that a postgres configuration is set "+
|
||||
"despite sqlite being set as the "+
|
||||
"databasebackend: %w", cfg.Postgres.DBName,
|
||||
cfg.Postgres.Host, cfg.Postgres.Port,
|
||||
cfg.Postgres.User, err)
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("cannot start litd with sqlite "+
|
||||
"backend: postgres database %q already exists "+
|
||||
"at %s:%d for user %q; If you really intend "+
|
||||
"to switch to sqlite you must delete the "+
|
||||
"postgres database (effectively deleting all "+
|
||||
"stored data) before restarting with sqlite "+
|
||||
"again", cfg.Postgres.DBName, cfg.Postgres.Host,
|
||||
cfg.Postgres.Port, cfg.Postgres.User)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sqliteDatabaseExists reports whether a SQLite database file exists at the
|
||||
// given path.
|
||||
func sqliteDatabaseExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
|
||||
case os.IsNotExist(err):
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// postgresDatabaseExists reports whether a Postgres database can be reached
|
||||
// with the configured connection info. If the configuration does not identify
|
||||
// a concrete database, the check is skipped.
|
||||
func postgresDatabaseExists(cfg *db.PostgresConfig) (bool, error) {
|
||||
if !hasPostgresConnectionInfo(cfg) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
dbConn, err := sql.Open("postgres", cfg.DSN(false))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to check for existing "+
|
||||
"postgres database %q: %w", cfg.DBName, err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = dbConn.PingContext(ctx)
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
|
||||
case isMissingPostgresDatabase(err):
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unable to check for existing "+
|
||||
"postgres database %q: %w", cfg.DBName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// hasPostgresConnectionInfo reports whether the config identifies a concrete
|
||||
// Postgres database to probe.
|
||||
func hasPostgresConnectionInfo(cfg *db.PostgresConfig) bool {
|
||||
return cfg != nil && cfg.Host != "" && cfg.Port != 0 &&
|
||||
cfg.User != "" && cfg.DBName != ""
|
||||
}
|
||||
|
||||
// isMissingPostgresDatabase reports whether the probe failed because the
|
||||
// target database does not exist. The lib/pq driver surfaces that as SQLSTATE
|
||||
// 3D000 (invalid_catalog_name).
|
||||
func isMissingPostgresDatabase(err error) bool {
|
||||
var pqErr *pq.Error
|
||||
if errors.As(err, &pqErr) {
|
||||
return string(pqErr.Code) == "3D000"
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildTLSConfigForHttp2(config *Config) (*tls.Config, error) {
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
|
|
|
|||
143
config_test.go
Normal file
143
config_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package terminal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/lightninglabs/lightning-terminal/db"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBlockStartupForPostgresIfSqliteDBExists verifies that a Postgres startup
|
||||
// is rejected when the default SQLite database file still exists for the
|
||||
// selected network.
|
||||
func TestBlockStartupForPostgresIfSqliteDBExists(t *testing.T) {
|
||||
litDir := t.TempDir()
|
||||
sqlitePath := filepath.Join(
|
||||
litDir, "regtest", defaultSqliteDatabaseFileName,
|
||||
)
|
||||
|
||||
fixture := db.NewTestPgFixture(
|
||||
t, db.DefaultPostgresFixtureLifetime, true,
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
fixture.TearDown(t)
|
||||
})
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(sqlitePath), 0700))
|
||||
require.NoError(t, os.WriteFile(sqlitePath, []byte("sqlite"), 0600))
|
||||
|
||||
cfg := &Config{
|
||||
DatabaseBackend: DatabaseBackendPostgres,
|
||||
LitDir: litDir,
|
||||
Network: "regtest",
|
||||
Postgres: fixture.GetConfig(),
|
||||
}
|
||||
|
||||
err := validateExclusiveSQLBackends(cfg, litDir)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "sqlite database file already exists")
|
||||
require.Contains(t, err.Error(), sqlitePath)
|
||||
}
|
||||
|
||||
// TestBlockStartupForSqliteIfPostgresDBExists verifies that a SQLite startup is
|
||||
// rejected when the configured Postgres database already exists.
|
||||
func TestBlockStartupForSqliteIfPostgresDBExists(t *testing.T) {
|
||||
fixture := db.NewTestPgFixture(
|
||||
t, db.DefaultPostgresFixtureLifetime, true,
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
fixture.TearDown(t)
|
||||
})
|
||||
|
||||
cfg := &Config{
|
||||
DatabaseBackend: DatabaseBackendSqlite,
|
||||
Postgres: fixture.GetConfig(),
|
||||
}
|
||||
|
||||
err := validateExclusiveSQLBackends(cfg, "")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "postgres database")
|
||||
require.Contains(t, err.Error(), cfg.Postgres.DBName)
|
||||
}
|
||||
|
||||
// TestDontBlockSqliteOnlyStartup verifies that SQLite startup is allowed when
|
||||
// no concrete Postgres database is configured.
|
||||
func TestDontBlockSqliteOnlyStartup(t *testing.T) {
|
||||
cfg := &Config{
|
||||
DatabaseBackend: DatabaseBackendSqlite,
|
||||
}
|
||||
|
||||
require.NoError(t, validateExclusiveSQLBackends(cfg, ""))
|
||||
}
|
||||
|
||||
// TestDontBlockSqliteStartupIfConfiguredPostgresDoesntExist verifies that a
|
||||
// SQLite startup is allowed when the configured Postgres database does not
|
||||
// exist.
|
||||
func TestDontBlockSqliteStartupIfConfiguredPostgresDoesntExist(t *testing.T) {
|
||||
fixture := db.NewTestPgFixture(
|
||||
t, db.DefaultPostgresFixtureLifetime, true,
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
fixture.TearDown(t)
|
||||
})
|
||||
|
||||
pgCfg := fixture.GetConfig()
|
||||
pgCfg.DBName = "does_not_exist_for_config_validation"
|
||||
|
||||
cfg := &Config{
|
||||
DatabaseBackend: DatabaseBackendSqlite,
|
||||
Postgres: pgCfg,
|
||||
}
|
||||
|
||||
require.NoError(t, validateExclusiveSQLBackends(cfg, ""))
|
||||
|
||||
// We also validate that the validateExclusiveSQLBackends passed because
|
||||
// the db with the configured DBName doesn't exist and not because the
|
||||
// connection parameters are wrong. This proves that we're ok with a
|
||||
// postgres setup existing, as long as the specific database doesn't
|
||||
// exist.
|
||||
dbConn, err := sql.Open("postgres", pgCfg.DSN(false))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, dbConn.Close())
|
||||
})
|
||||
|
||||
err = dbConn.Ping()
|
||||
require.Error(t, err)
|
||||
require.True(t, isMissingPostgresDatabase(err))
|
||||
}
|
||||
|
||||
// TestDontBlockPostgresOnlyStartup verifies that a Postgres is allowed when
|
||||
// no sqlite database file exists for the selected network, despite the actual
|
||||
// folder where the file would be placed exists.
|
||||
func TestDontBlockPostgresOnlyStartup(t *testing.T) {
|
||||
litDir := t.TempDir()
|
||||
sqlitePath := filepath.Join(
|
||||
litDir, "regtest", defaultSqliteDatabaseFileName,
|
||||
)
|
||||
|
||||
fixture := db.NewTestPgFixture(
|
||||
t, db.DefaultPostgresFixtureLifetime, true,
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
fixture.TearDown(t)
|
||||
})
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(sqlitePath), 0700))
|
||||
|
||||
// NOTE: we don't write any file to the sqlite path here, so the sqlite
|
||||
// database file never exists, only the default directory where it would
|
||||
// be located.
|
||||
|
||||
cfg := &Config{
|
||||
DatabaseBackend: DatabaseBackendPostgres,
|
||||
LitDir: litDir,
|
||||
Network: "regtest",
|
||||
Postgres: fixture.GetConfig(),
|
||||
}
|
||||
|
||||
require.NoError(t, validateExclusiveSQLBackends(cfg, litDir))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue