banger hasn't shipped a public release — every "legacy", "pre-opt-in",
"previously", "migration note", "no longer" reference in the tree is
pinning against a state no real user's install has ever been in.
That scaffolding has weight: it's a coordinate system future readers
have to decode, and it keeps dead code alive.
Removed (code):
- internal/daemon/ssh_client_config.go
- vmSSHConfigIncludeBegin / vmSSHConfigIncludeEnd constants and
every `removeManagedBlock(existing, vm...)` call they enabled
(legacy inline `Host *.vm` block scrub)
- cleanupLegacySSHConfigDir (+ its caller in syncVMSSHClientConfig)
— wiped a pre-opt-in sibling file under $ConfigDir/ssh
- sameDirOrParent + resolvePathForComparison — only ever used
by cleanupLegacySSHConfigDir
- the "also check legacy marker" fallback in
UserSSHIncludeInstalled / UninstallUserSSHInclude
- internal/store/migrations.go
- migrateDropDeadImageColumns (migration 2) + its slice entry
- dropColumnIfExists (orphaned after the above)
- addColumnIfMissing + the whole "columns added across the pre-
versioning lifetime" block at the end of migrateBaseline —
subsumed into the baseline CREATE TABLE
- `packages_path TEXT` column on the images table (the
throwaway migration 2 dropped it, but there was never any
reader)
- internal/daemon/vm.go
- vmDNSRecordName local wrapper — was justified as "avoid
pulling vmdns into every file"; three of four callers already
imported vmdns directly, so inline the one stray call
- internal/cli/cli_test.go
- TestLegacyRemovedCommandIsRejected (`tui` subcommand never
shipped)
Removed / simplified (tests):
- ssh_client_config_test.go: dropped TestSameDirOrParentHandlesSymlinks,
TestSyncVMSSHClientConfigPreservesUserKeyInLegacyDir,
TestSyncVMSSHClientConfigNarrowsCleanupToLegacyFile,
TestSyncVMSSHClientConfigLeavesUnexpectedLegacyContents,
TestInstallUserSSHIncludeMigratesLegacyInlineBlock, plus the
"legacy posture" regression strings in the remaining happy-path
test; TestUninstallUserSSHIncludeRemovesBothMarkerBlocks collapsed
to a single-block test
- migrations_test.go: dropped TestMigrateDropDeadImageColumns_AcrossInstallPaths,
TestDropColumnIfExistsIsIdempotent; TestOpenReadOnlyDoesNotRunMigrations
simplified to test against the baseline marker
Removed (docs):
- README.md "**Migration note.**" blockquote about the SSH-key path move
- docs/advanced.md parenthetical "(the old behaviour)"
Reworded (comments):
- Dropped "Previously this file also contained LogLevel DEBUG3..."
history from vm_disk.go's sshdGuestConfig doc
- Dropped "Call sites that previously read vm.Runtime.{PID,...}"
from vm_handles.go; now documents the current contract
- Dropped "Pre-v0.1 the defaults are" scaffolding in doctor_test.go
- Dropped "no longer does its own git inspection" phrasing in vm_run.go
- Dropped the "(also cleans up legacy inline block from pre-opt-in
builds)" aside on the `ssh-config` CLI docstring
- Renamed test var `legacyKey` → `existingKey` in vm_test.go; its
purpose was "pre-existing authorized_keys line," not banger-legacy
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// migration is one ordered, atomic schema step. id must be unique and
|
|
// strictly increasing across the slice. name is a human-readable label
|
|
// stored alongside the id for debugging, and up receives a *sql.Tx so
|
|
// DDL + data backfills land atomically — either the migration fully
|
|
// applies and a schema_migrations row is written, or the whole thing
|
|
// rolls back and gets retried on next Open().
|
|
type migration struct {
|
|
id int
|
|
name string
|
|
up func(*sql.Tx) error
|
|
}
|
|
|
|
// migrations is the canonical ordered history. Append new migrations
|
|
// at the bottom with the next id. Never edit or reorder existing
|
|
// entries — installed DBs key off the id column.
|
|
var migrations = []migration{
|
|
{id: 1, name: "baseline", up: migrateBaseline},
|
|
}
|
|
|
|
// runMigrations ensures schema_migrations exists, then applies every
|
|
// migration whose id hasn't been recorded yet, in id order.
|
|
func runMigrations(db *sql.DB) error {
|
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL
|
|
)`); err != nil {
|
|
return fmt.Errorf("create schema_migrations: %w", err)
|
|
}
|
|
|
|
applied, err := loadAppliedMigrations(db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sorted := make([]migration, len(migrations))
|
|
copy(sorted, migrations)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].id < sorted[j].id })
|
|
seen := map[int]bool{}
|
|
for _, m := range sorted {
|
|
if seen[m.id] {
|
|
return fmt.Errorf("duplicate migration id %d (%q)", m.id, m.name)
|
|
}
|
|
seen[m.id] = true
|
|
}
|
|
|
|
for _, m := range sorted {
|
|
if _, ok := applied[m.id]; ok {
|
|
continue
|
|
}
|
|
if err := applyMigration(db, m); err != nil {
|
|
return fmt.Errorf("migration %d (%s): %w", m.id, m.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadAppliedMigrations(db *sql.DB) (map[int]struct{}, error) {
|
|
rows, err := db.Query("SELECT id FROM schema_migrations")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load schema_migrations: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
applied := map[int]struct{}{}
|
|
for rows.Next() {
|
|
var id int
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
applied[id] = struct{}{}
|
|
}
|
|
return applied, rows.Err()
|
|
}
|
|
|
|
func applyMigration(db *sql.DB, m migration) error {
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := m.up(tx); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(
|
|
"INSERT INTO schema_migrations (id, name, applied_at) VALUES (?, ?, ?)",
|
|
m.id, m.name, time.Now().UTC().Format(time.RFC3339),
|
|
); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("record migration: %w", err)
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// migrateBaseline creates the full current schema.
|
|
func migrateBaseline(tx *sql.Tx) error {
|
|
stmts := []string{
|
|
`CREATE TABLE IF NOT EXISTS images (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
managed INTEGER NOT NULL DEFAULT 0,
|
|
artifact_dir TEXT,
|
|
rootfs_path TEXT NOT NULL,
|
|
work_seed_path TEXT,
|
|
kernel_path TEXT NOT NULL,
|
|
initrd_path TEXT,
|
|
modules_dir TEXT,
|
|
build_size TEXT,
|
|
seeded_ssh_public_key_fingerprint TEXT,
|
|
docker INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);`,
|
|
`CREATE TABLE IF NOT EXISTS vms (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
image_id TEXT NOT NULL,
|
|
guest_ip TEXT NOT NULL UNIQUE,
|
|
state TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
last_touched_at TEXT NOT NULL,
|
|
spec_json TEXT NOT NULL,
|
|
runtime_json TEXT NOT NULL,
|
|
stats_json TEXT NOT NULL DEFAULT '{}',
|
|
FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE RESTRICT
|
|
);`,
|
|
}
|
|
for _, stmt := range stmts {
|
|
if _, err := tx.Exec(stmt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|