Add SQLite persistence and write-through gRPC mutations

Database (internal/db) stores listeners, routes, and firewall rules with
WAL mode, foreign keys, and idempotent migrations. First run seeds from
TOML config; subsequent runs load from DB as source of truth.

gRPC admin API now writes to the database before updating in-memory state
(write-through cache pattern). Adds snapshot command for VACUUM INTO
backups. Refactors firewall.New to accept raw rule slices instead of
config struct for flexibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 03:07:30 -07:00
parent d63859c28f
commit 9cba3241e8
20 changed files with 1148 additions and 135 deletions

View File

@@ -11,6 +11,9 @@ func TestLoadValid(t *testing.T) {
path := filepath.Join(dir, "test.toml")
data := `
[database]
path = "/tmp/test.db"
[[listeners]]
addr = ":443"
@@ -49,11 +52,37 @@ level = "info"
}
}
func TestLoadNoListeners(t *testing.T) {
func TestLoadNoDatabasePath(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.toml")
data := `
[[listeners]]
addr = ":443"
[[listeners.routes]]
hostname = "example.com"
backend = "127.0.0.1:8443"
`
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
t.Fatalf("write config: %v", err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing database path")
}
}
func TestLoadNoListenersValid(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.toml")
// No listeners is valid — DB may already have them.
data := `
[database]
path = "/tmp/test.db"
[log]
level = "info"
`
@@ -62,26 +91,8 @@ level = "info"
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing listeners")
}
}
func TestLoadNoRoutes(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.toml")
data := `
[[listeners]]
addr = ":443"
`
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
t.Fatalf("write config: %v", err)
}
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing routes")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
@@ -90,6 +101,9 @@ func TestLoadDuplicateHostnames(t *testing.T) {
path := filepath.Join(dir, "test.toml")
data := `
[database]
path = "/tmp/test.db"
[[listeners]]
addr = ":443"
@@ -116,6 +130,9 @@ func TestLoadGeoIPRequiredWithCountries(t *testing.T) {
path := filepath.Join(dir, "test.toml")
data := `
[database]
path = "/tmp/test.db"
[[listeners]]
addr = ":443"
@@ -141,6 +158,9 @@ func TestLoadMultipleListeners(t *testing.T) {
path := filepath.Join(dir, "test.toml")
data := `
[database]
path = "/tmp/test.db"
[[listeners]]
addr = ":443"