diff --git a/cmd/felis/breakglass.go b/cmd/felis/breakglass.go index 751e0e7..57ec012 100644 --- a/cmd/felis/breakglass.go +++ b/cmd/felis/breakglass.go @@ -158,7 +158,7 @@ func cmdBreakGlass(args []string, stdout, stderr io.Writer) int { host, _ := os.Hostname() recovery := recoveryConfig{open: hostRecoveryMailer(cfg.SMTP, platform.DefaultControlNamespace), host: host} - res, err := runBreakGlassTUI(ctx, repo, cfg.Database.URL, cfg.Server.RootDomain, cfg.Auth.AdminHostname, cfg.Auth.PanelHostname, cfg.Auth.AccessJWTAud, cfg.K8s.Namespace, accountableOSUser(), adminExists, recovery) + res, err := runBreakGlassTUI(ctx, repo, cfg.Database, cfg.Server.RootDomain, cfg.Auth.AdminHostname, cfg.Auth.PanelHostname, cfg.Auth.AccessJWTAud, cfg.K8s.Namespace, accountableOSUser(), adminExists, recovery) if err != nil { fmt.Fprintf(stderr, "felis breakGlass: %v\n", err) return 1 @@ -627,18 +627,18 @@ const ( cloudflareAPITokenDocsURL = "https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/" ) -func runBreakGlassTUI(ctx context.Context, s ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, recovery recoveryConfig) (breakGlassResult, error) { - return runConsoleTUI(ctx, s, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, consoleModeBreakGlass, recovery) +func runBreakGlassTUI(ctx context.Context, s ownerStore, db config.DatabaseConfig, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, recovery recoveryConfig) (breakGlassResult, error) { + return runConsoleTUI(ctx, s, db, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, consoleModeBreakGlass, recovery) } // runSetupTUI never reaches recovery: setup with a staff account present lands on // the status screen, so it has no relay to hand over. -func runSetupTUI(ctx context.Context, s ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool) (breakGlassResult, error) { - return runConsoleTUI(ctx, s, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, consoleModeSetup, recoveryConfig{}) +func runSetupTUI(ctx context.Context, s ownerStore, db config.DatabaseConfig, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool) (breakGlassResult, error) { + return runConsoleTUI(ctx, s, db, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, consoleModeSetup, recoveryConfig{}) } -func runConsoleTUI(ctx context.Context, s ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, mode consoleMode, recovery recoveryConfig) (breakGlassResult, error) { - rm := newRootModel(ctx, s, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, mode) +func runConsoleTUI(ctx context.Context, s ownerStore, db config.DatabaseConfig, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, mode consoleMode, recovery recoveryConfig) (breakGlassResult, error) { + rm := newRootModel(ctx, s, db, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser, adminExists, mode) rm.recovery = recovery final, err := tea.NewProgram(rm, tea.WithAltScreen()).Run() if err != nil { diff --git a/cmd/felis/db.go b/cmd/felis/db.go index 9627c61..9ba26b2 100644 --- a/cmd/felis/db.go +++ b/cmd/felis/db.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "io" + neturl "net/url" "os" "os/exec" "path/filepath" @@ -15,6 +16,7 @@ import ( "felis.lolicon.best/internal/config" "felis.lolicon.best/internal/dbbackup" + "felis.lolicon.best/internal/platform" "felis.lolicon.best/internal/retention" ) @@ -92,11 +94,43 @@ func parseWithArg(fs *flag.FlagSet, args []string) (string, bool) { } func dbDatabaseURL(path string) (string, error) { + db, err := dbDatabase(path) + return db.URL, err +} + +func dbDatabase(path string) (config.DatabaseConfig, error) { cfg, err := config.Load(path) if err != nil { - return "", err + return config.DatabaseConfig{}, err } - return cfg.Database.URL, nil + return cfg.Database, nil +} + +// dbTools places pg_dump, pg_restore and psql. The installer's database runs in +// k3s and the host has no PostgreSQL client, so when the host config names the +// [database] deployment the tools run in its postgres container over the +// container's socket, as the URL's role on the URL's database. Otherwise they +// come from PATH and connect with the URL. +func dbTools(db config.DatabaseConfig) (dbbackup.Tools, error) { + if db.Deployment == "" { + return dbbackup.Tools{}, nil + } + ns, name, _ := strings.Cut(db.Deployment, "/") + u, err := neturl.Parse(db.URL) + if err != nil || u.User == nil || u.User.Username() == "" || strings.TrimPrefix(u.Path, "/") == "" { + return dbbackup.Tools{}, errors.New("[database] url must name the role and the database to run the tools in the database's pod") + } + return dbbackup.Tools{ + Exec: []string{"k3s", "kubectl", "exec", "-i", "-n", ns, "deploy/" + name, "-c", platform.PostgresContainer, "--"}, + Conn: fmt.Sprintf("host=%s port=%d dbname=%s user=%s connect_timeout=15", + platform.PostgresSocketDir, platform.PostgresPort, + libpqQuote(strings.TrimPrefix(u.Path, "/")), libpqQuote(u.User.Username())), + }, nil +} + +// libpqQuote renders v as a single-quoted libpq connection-string value. +func libpqQuote(v string) string { + return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(v) + "'" } func dbBackup(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.Writer) int { @@ -113,7 +147,12 @@ func dbBackup(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.Wr fmt.Fprint(stderr, dbUsage) return 2 } - url, err := dbDatabaseURL(*cfgPath) + db, err := dbDatabase(*cfgPath) + if err != nil { + fmt.Fprintf(stderr, "felis db backup: %v\n", err) + return 1 + } + tools, err := dbTools(db) if err != nil { fmt.Fprintf(stderr, "felis db backup: %v\n", err) return 1 @@ -122,7 +161,7 @@ func dbBackup(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.Wr *keep = defaultKeep[*label] } o := dbbackup.BackupOptions{ - DatabaseURL: url, Dir: *dir, Label: *label, Keep: *keep, + DatabaseURL: db.URL, Tools: tools, Dir: *dir, Label: *label, Keep: *keep, StateDir: *stateDir, Version: resolvedVersion(), Log: stderr, MetricsFile: *metrics, Record: true, } @@ -177,7 +216,12 @@ func dbRestore(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.W fmt.Fprintln(stderr, "Scale felis-api and felis-operator to 0 first, then re-run with -yes.") return 2 } - url, err := dbDatabaseURL(*cfgPath) + db, err := dbDatabase(*cfgPath) + if err != nil { + fmt.Fprintf(stderr, "felis db restore: %v\n", err) + return 1 + } + tools, err := dbTools(db) if err != nil { fmt.Fprintf(stderr, "felis db restore: %v\n", err) return 1 @@ -185,7 +229,7 @@ func dbRestore(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.W ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) defer cancel() _, safety, err := dbbackup.Restore(ctx, dbbackup.RestoreOptions{ - DatabaseURL: url, Bundle: bundle, Dir: *dir, Force: *force, SkipSafetyBackup: *noSafety, + DatabaseURL: db.URL, Tools: tools, Bundle: bundle, Dir: *dir, Force: *force, SkipSafetyBackup: *noSafety, Safety: dbbackup.BackupOptions{Keep: defaultKeep[dbbackup.LabelPreRestore], StateDir: *stateDir, Version: resolvedVersion(), ExportServers: exportMinecraftServers}, Log: stderr, diff --git a/cmd/felis/db_test.go b/cmd/felis/db_test.go index 0fa40d1..ae75302 100644 --- a/cmd/felis/db_test.go +++ b/cmd/felis/db_test.go @@ -6,10 +6,14 @@ import ( "encoding/json" "flag" "io" + "os" + "path/filepath" "strings" "testing" "time" + "felis.lolicon.best/internal/config" + "felis.lolicon.best/internal/dbbackup" "felis.lolicon.best/internal/store" ) @@ -141,7 +145,7 @@ func TestPreMigrateBackupOnlyGuardsAPopulatedDatabase(t *testing.T) { {"up to date", map[int]struct{}{1: {}, 2: {}}, false}, {"pending on a populated database", map[int]struct{}{1: {}}, true}, } { - path, err := preMigrateBackup(context.Background(), appliedDriver{done: tc.done}, ms, badURL, t.TempDir(), io.Discard) + path, err := preMigrateBackup(context.Background(), appliedDriver{done: tc.done}, ms, config.DatabaseConfig{URL: badURL}, t.TempDir(), io.Discard) if attempted := err != nil; attempted != tc.attempt { t.Errorf("%s: attempted = %v (err %v), want %v", tc.name, attempted, err, tc.attempt) } @@ -183,3 +187,167 @@ func TestAuditExportBounds(t *testing.T) { } } } + +// podK3s stands in for `k3s kubectl exec ... --`: it logs its argv and runs the +// command after -- from the "container" directory, which is the only place the +// PostgreSQL tools exist, as on an installed host. +const podK3s = `#!/bin/sh +printf '%s\n' "$*" >> "$FAKE_DIR/k3s.args" +while [ $# -gt 0 ] && [ "$1" != "--" ]; do shift; done +shift +tool=$1; shift +exec /usr/bin/env -i FAKE_DIR="$FAKE_DIR" PATH=/usr/bin:/bin "$FAKE_DIR/container/$tool" "$@" +` + +var podTools = map[string]string{ + "pg_dump": `#!/bin/sh +case "$1" in --version) echo "pg_dump (PostgreSQL) 18.6"; exit 0 ;; esac +printf 'PGDMP-fake-archive' +`, + "pg_restore": `#!/bin/sh +cat > /dev/null +`, + "psql": `#!/bin/sh +for a in "$@"; do [ "$a" = "-c" ] && { echo 3; exit 0; }; done +cat > /dev/null +`, +} + +const podPassword = "pw-must-stay-on-the-host" + +// podDB is the host config's [database] on an installed host. +var podDB = config.DatabaseConfig{ + URL: "postgres://felis:" + podPassword + "@127.0.0.1:15432/felis?sslmode=disable", + Deployment: "felis/felis-postgres", +} + +const podExecPrefix = "kubectl exec -i -n felis deploy/felis-postgres -c postgres -- " + +// newPodRig puts the fake k3s on PATH, alone, and returns the directory its +// k3s.args log lands in. +func newPodRig(t *testing.T) string { + t.Helper() + dir := t.TempDir() + bin := filepath.Join(dir, "bin") + container := filepath.Join(dir, "container") + for _, d := range []string{bin, container} { + if err := os.Mkdir(d, 0o755); err != nil { + t.Fatal(err) + } + } + writeTestFile(t, filepath.Join(bin, "k3s"), podK3s, 0o755) + for name, body := range podTools { + writeTestFile(t, filepath.Join(container, name), body, 0o755) + } + t.Setenv("PATH", bin) + t.Setenv("FAKE_DIR", dir) + return dir +} + +// podRuns returns what the fake k3s ran since the last call, failing on any +// run outside the database container or with the password on its command +// line (visible to every local user in ps). +func podRuns(t *testing.T, dir string) []string { + t.Helper() + log := filepath.Join(dir, "k3s.args") + argv, err := os.ReadFile(log) + if err != nil { + t.Fatalf("nothing ran through k3s: %v", err) + } + os.Remove(log) + var runs []string + for _, line := range strings.Split(strings.TrimSpace(string(argv)), "\n") { + if !strings.HasPrefix(line, podExecPrefix) { + t.Errorf("k3s ran %q, want everything under %q", line, podExecPrefix) + } + if strings.Contains(line, podPassword) { + t.Errorf("the password crossed into the pod on a command line: %q", line) + } + runs = append(runs, strings.TrimPrefix(line, podExecPrefix)) + } + return runs +} + +func ranIn(runs []string, prefix string) bool { + for _, r := range runs { + if strings.HasPrefix(r, prefix) { + return true + } + } + return false +} + +// TestDBBackupAndRestoreRunTheToolsInTheDatabasePod: on an installed host the +// database is a k3s Deployment and no PostgreSQL client exists outside it, so +// `felis db backup` and `restore` must reach the tools through kubectl exec, +// over the pod's socket, and without putting the role's password on a command +// line. +func TestDBBackupAndRestoreRunTheToolsInTheDatabasePod(t *testing.T) { + dir := newPodRig(t) + toml := strings.Replace(installerTOML("example.com", "127.0.0.1"), + `url = "postgres://felis:pw@127.0.0.1:5432/felis?sslmode=disable"`, + `url = "`+podDB.URL+`" +deployment = "`+podDB.Deployment+`"`, 1) + cfg := filepath.Join(dir, "felis.toml") + writeTestFile(t, cfg, toml, 0o600) + + var out, errBuf bytes.Buffer + bundles := filepath.Join(dir, "bundles") + if code := run([]string{"db", "backup", "-config", cfg, "-dir", bundles, "-state-dir", "", "-no-servers"}, &out, &errBuf); code != 0 { + t.Fatalf("backup: exit %d: %s", code, errBuf.String()) + } + bundle := strings.TrimSpace(strings.TrimPrefix(out.String(), "felis db backup: wrote ")) + if _, err := dbbackupVerify(bundle); err != nil { + t.Fatalf("the bundle does not verify: %v", err) + } + runs := podRuns(t, dir) + if !ranIn(runs, "pg_dump --format=custom --no-password --dbname=host=/var/run/postgresql port=5432 dbname='felis' user='felis'") { + t.Errorf("pg_dump did not dump over the pod's socket as felis on felis: %q", runs) + } + + out.Reset() + errBuf.Reset() + if code := run([]string{"db", "restore", "-config", cfg, "-dir", bundles, "-yes", "-force", "-no-safety-backup", bundle}, &out, &errBuf); code != 0 { + t.Fatalf("restore: exit %d: %s", code, errBuf.String()) + } + runs = podRuns(t, dir) + if !ranIn(runs, "pg_restore --no-owner --no-privileges --file=-") || !ranIn(runs, "psql -X -q -w -v ON_ERROR_STOP=1 -d host=/var/run/postgresql") { + t.Errorf("the replay did not run in the pod: %q", runs) + } +} + +// TestPreMigrateBackupRunsInTheDatabasePod: the snapshot in front of an upgrade +// is the one taken most often, by bootstrap on every rerun. +func TestPreMigrateBackupRunsInTheDatabasePod(t *testing.T) { + dir := newPodRig(t) + ms := []store.Migration{{Version: 1}, {Version: 2}} + // The snapshot also bundles /etc/felis, which a test machine may lack; the + // dump runs first either way. + _, err := preMigrateBackup(context.Background(), appliedDriver{done: map[int]struct{}{1: {}}}, ms, podDB, filepath.Join(dir, "bundles"), io.Discard) + if err != nil && !strings.Contains(err.Error(), "read host state") { + t.Fatalf("snapshot: %v", err) + } + if runs := podRuns(t, dir); !ranIn(runs, "pg_dump --format=custom") { + t.Errorf("pg_dump did not run in the pod: %q", runs) + } +} + +func TestDBToolsNeedTheRoleAndDatabase(t *testing.T) { + if tools, err := dbTools(config.DatabaseConfig{URL: "postgres://felis:pw@db:5432/felis"}); err != nil || len(tools.Exec) != 0 { + t.Errorf("no deployment: tools %+v err %v, want the PATH tools", tools, err) + } + for _, u := range []string{"postgres://db:5432/felis", "postgres://felis:pw@db:5432/"} { + if _, err := dbTools(config.DatabaseConfig{URL: u, Deployment: "felis/felis-postgres"}); err == nil { + t.Errorf("%s: no error, want a refusal (the pod connection needs the role and the database)", u) + } + } + tools, err := dbTools(config.DatabaseConfig{URL: `postgres://o%27brien@db/my%20db`, Deployment: "felis/felis-postgres"}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(tools.Conn, `dbname='my db' user='o\'brien'`) { + t.Errorf("Conn = %q, want the values quoted for libpq", tools.Conn) + } +} + +var dbbackupVerify = dbbackup.Verify diff --git a/cmd/felis/migrate.go b/cmd/felis/migrate.go index b67d966..e0a7edb 100644 --- a/cmd/felis/migrate.go +++ b/cmd/felis/migrate.go @@ -58,7 +58,7 @@ func cmdMigrate(args []string, stdout, stderr io.Writer) int { } if !*noBackup { - path, err := preMigrateBackup(ctx, drv, migrations, cfg.Database.URL, *backupDir, stderr) + path, err := preMigrateBackup(ctx, drv, migrations, cfg.Database, *backupDir, stderr) if err != nil { fmt.Fprintf(stderr, "felis migrate: pre-migration backup failed, nothing applied: %v\n", err) fmt.Fprintln(stderr, " fix the backup, or re-run with -no-backup to migrate without one") @@ -85,7 +85,7 @@ func cmdMigrate(args []string, stdout, stderr io.Writer) int { // preMigrateBackup bundles the database when it already carries a schema and // some of migrations are not applied yet, and returns the bundle's path ("" when // there was nothing to protect: a fresh database, or nothing pending). -func preMigrateBackup(ctx context.Context, drv store.Driver, migrations []store.Migration, dbURL, dir string, log io.Writer) (string, error) { +func preMigrateBackup(ctx context.Context, drv store.Driver, migrations []store.Migration, db config.DatabaseConfig, dir string, log io.Writer) (string, error) { if err := drv.EnsureVersionTable(ctx); err != nil { return "", fmt.Errorf("ensure version table: %w", err) } @@ -96,8 +96,12 @@ func preMigrateBackup(ctx context.Context, drv store.Driver, migrations []store. if len(done) == 0 || !hasPending(done, migrations) { return "", nil } + tools, err := dbTools(db) + if err != nil { + return "", err + } return dbbackup.Backup(ctx, dbbackup.BackupOptions{ - DatabaseURL: dbURL, Dir: dir, Label: dbbackup.LabelPreMigrate, + DatabaseURL: db.URL, Tools: tools, Dir: dir, Label: dbbackup.LabelPreMigrate, Keep: defaultKeep[dbbackup.LabelPreMigrate], StateDir: dbbackup.DefaultStateDir, Version: resolvedVersion(), Log: log, Record: true, }) diff --git a/cmd/felis/setup.go b/cmd/felis/setup.go index ad5b25d..19c1b37 100644 --- a/cmd/felis/setup.go +++ b/cmd/felis/setup.go @@ -112,7 +112,7 @@ func cmdSetup(args []string, stdout, stderr io.Writer) int { return 1 } - res, err := runSetupTUI(ctx, setup.repo, setup.cfg.Database.URL, setup.cfg.Server.RootDomain, setup.cfg.Auth.AdminHostname, setup.cfg.Auth.PanelHostname, setup.cfg.Auth.AccessJWTAud, setup.cfg.K8s.Namespace, accountableOSUser(), setup.adminExists) + res, err := runSetupTUI(ctx, setup.repo, setup.cfg.Database, setup.cfg.Server.RootDomain, setup.cfg.Auth.AdminHostname, setup.cfg.Auth.PanelHostname, setup.cfg.Auth.AccessJWTAud, setup.cfg.K8s.Namespace, accountableOSUser(), setup.adminExists) if err != nil { fmt.Fprintf(stderr, "felis setup: %v\n", err) return 1 diff --git a/cmd/felis/tui_migration.go b/cmd/felis/tui_migration.go index f21e2a5..c8a6a0c 100644 --- a/cmd/felis/tui_migration.go +++ b/cmd/felis/tui_migration.go @@ -6,6 +6,7 @@ import ( "io" "time" + "felis.lolicon.best/internal/config" "felis.lolicon.best/internal/dbbackup" "felis.lolicon.best/internal/store" ) @@ -13,10 +14,10 @@ import ( // applyMigrations runs any pending schema migrations and returns the resulting // applied count. Used by the preflight stage to self-heal a freshly bootstrapped // (or upgraded) database. -func applyMigrations(dbURL string) (int, error) { +func applyMigrations(db config.DatabaseConfig) (int, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - drv, err := store.Open(ctx, dbURL) + drv, err := store.Open(ctx, db.URL) if err != nil { return 0, fmt.Errorf("open db: %w", err) } @@ -27,7 +28,7 @@ func applyMigrations(dbURL string) (int, error) { } // Same guard as `felis migrate up`: never roll a populated database forward // without a snapshot to roll back to. - if _, err := preMigrateBackup(ctx, drv, migrations, dbURL, dbbackup.DefaultDir, io.Discard); err != nil { + if _, err := preMigrateBackup(ctx, drv, migrations, db, dbbackup.DefaultDir, io.Discard); err != nil { return 0, fmt.Errorf("pre-migration backup: %w", err) } if _, err := store.Up(ctx, drv, migrations); err != nil { diff --git a/cmd/felis/tui_preflight.go b/cmd/felis/tui_preflight.go index e892ecd..ef3213b 100644 --- a/cmd/felis/tui_preflight.go +++ b/cmd/felis/tui_preflight.go @@ -3,6 +3,8 @@ package main import ( "fmt" + "felis.lolicon.best/internal/config" + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) @@ -14,7 +16,7 @@ import ( // of the wizard can assume a healthy backend. It is non-interactive — it runs // to completion and reports back to the root via preflightDoneMsg. type preflightModel struct { - dbURL string + db config.DatabaseConfig rootDomain string adminHost string @@ -57,11 +59,11 @@ type pfMigApplyMsg struct { type pfPanelMsg struct{ err error } -func newPreflightModel(dbURL, rootDomain, adminHostname string) *preflightModel { +func newPreflightModel(db config.DatabaseConfig, rootDomain, adminHostname string) *preflightModel { sp := spinner.New() sp.Spinner = spinner.Dot sp.Style = tuiLabel - return &preflightModel{dbURL: dbURL, rootDomain: rootDomain, adminHost: adminHostname, sp: sp, state: pfCheckDB} + return &preflightModel{db: db, rootDomain: rootDomain, adminHost: adminHostname, sp: sp, state: pfCheckDB} } func (m *preflightModel) Init() tea.Cmd { @@ -200,7 +202,7 @@ func (m *preflightModel) errText() string { } func (m *preflightModel) checkDB() tea.Cmd { - return func() tea.Msg { return pfDBMsg{err: checkPostgres(m.dbURL)} } + return func() tea.Msg { return pfDBMsg{err: checkPostgres(m.db.URL)} } } func (m *preflightModel) checkMigrations() tea.Cmd { @@ -208,7 +210,7 @@ func (m *preflightModel) checkMigrations() tea.Cmd { // The sets, not their sizes: a database a newer release migrated can hold as // many rows as this build has migrations, and must stop here rather than be // "healed" by an older binary. - s, err := readSchema(m.dbURL) + s, err := readSchema(m.db.URL) if err == nil { err = s.Newer() } @@ -218,7 +220,7 @@ func (m *preflightModel) checkMigrations() tea.Cmd { func (m *preflightModel) applyMigrations() tea.Cmd { return func() tea.Msg { - applied, err := applyMigrations(m.dbURL) + applied, err := applyMigrations(m.db) return pfMigApplyMsg{applied: applied, err: err} } } diff --git a/cmd/felis/tui_root.go b/cmd/felis/tui_root.go index 4a40a04..eda8c20 100644 --- a/cmd/felis/tui_root.go +++ b/cmd/felis/tui_root.go @@ -5,6 +5,7 @@ import ( "strings" "felis.lolicon.best/internal/cfsetup" + "felis.lolicon.best/internal/config" "felis.lolicon.best/internal/platform" tea "github.com/charmbracelet/bubbletea" @@ -133,7 +134,7 @@ type rootModel struct { err error mode consoleMode - dbURL string + db config.DatabaseConfig store ownerStore osUser string rootDomain string @@ -145,11 +146,11 @@ type rootModel struct { recovery recoveryConfig // how the account operations mail a recovery code } -func newRootModel(ctx context.Context, store ownerStore, dbURL, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, mode consoleMode) *rootModel { +func newRootModel(ctx context.Context, store ownerStore, db config.DatabaseConfig, rootDomain, adminHostname, panelHostname, accessAud, namespace, osUser string, adminExists bool, mode consoleMode) *rootModel { rm := &rootModel{ ctx: ctx, reviewing: -1, - dbURL: dbURL, + db: db, store: store, osUser: osUser, rootDomain: rootDomain, @@ -180,7 +181,7 @@ func newRootModel(ctx context.Context, store ownerStore, dbURL, rootDomain, admi } } else { rm.stage = stagePreflight - rm.screen = newPreflightModel(dbURL, rootDomain, adminHostname) + rm.screen = newPreflightModel(db, rootDomain, adminHostname) } return rm } diff --git a/cmd/felis/tui_root_test.go b/cmd/felis/tui_root_test.go index 7749ade..bfdefc5 100644 --- a/cmd/felis/tui_root_test.go +++ b/cmd/felis/tui_root_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "felis.lolicon.best/internal/config" + tea "github.com/charmbracelet/bubbletea" ) @@ -30,7 +32,7 @@ func newTestRoot(adminExists bool, mode consoleMode, accessAud string) *rootMode return newRootModel( context.Background(), &fakeOwnerStore{}, - "postgres://localhost/felis", + config.DatabaseConfig{URL: "postgres://localhost/felis"}, "felis.example.com", "admin.felis.example.com", "panel.felis.example.com",