Files
deploy_server/store.go
T
2026-04-28 22:12:20 +02:00

209 lines
6.6 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
// DeployTarget represents a deploy configuration stored in operations.db.
type DeployTarget struct {
App string `json:"app"`
Host string `json:"host"` // SSH alias from ~/.ssh/config
RemoteDir string `json:"remote_dir"`
BinaryName string `json:"binary_name"`
BuildCmd string `json:"build_cmd"`
ServiceUser string `json:"service_user"`
Port int `json:"port"`
HealthPath string `json:"health_path"`
Env map[string]string `json:"env"`
Strategy string `json:"strategy"` // "systemd" (default), "systemd-remote", "docker-compose"
SourceDir string `json:"source_dir"` // override for appDir(); empty = use default apps/<app>
Branch string `json:"branch"` // git branch for remote deploys; default "main"
ComposeFiles string `json:"compose_files"` // comma-separated extra compose files
CreatedAt time.Time `json:"created_at"`
}
// DeployLog records a deploy execution.
type DeployLog struct {
ID int64 `json:"id"`
App string `json:"app"`
Host string `json:"host"`
Status string `json:"status"` // "success", "failure"
Trigger string `json:"trigger"` // "manual", "webhook"
Error string `json:"error"`
Duration int64 `json:"duration_ms"`
StartedAt time.Time `json:"started_at"`
}
// Store manages the deploy_server's SQLite database.
type Store struct {
db *sql.DB
}
func OpenStore(path string) (*Store, error) {
db, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, err
}
s := &Store{db: db}
if err := s.migrate(); err != nil {
db.Close()
return nil, err
}
return s, nil
}
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) migrate() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS deploy_targets (
app TEXT NOT NULL,
host TEXT NOT NULL,
remote_dir TEXT NOT NULL DEFAULT '',
binary_name TEXT NOT NULL DEFAULT '',
build_cmd TEXT NOT NULL DEFAULT '',
service_user TEXT NOT NULL DEFAULT '',
port INTEGER NOT NULL DEFAULT 0,
health_path TEXT NOT NULL DEFAULT '',
env TEXT NOT NULL DEFAULT '{}',
strategy TEXT NOT NULL DEFAULT 'systemd',
source_dir TEXT NOT NULL DEFAULT '',
branch TEXT NOT NULL DEFAULT 'main',
compose_files TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
PRIMARY KEY (app, host)
);
CREATE TABLE IF NOT EXISTS deploy_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app TEXT NOT NULL,
host TEXT NOT NULL,
status TEXT NOT NULL,
trigger TEXT NOT NULL DEFAULT 'manual',
error TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
started_at TEXT NOT NULL
);
`)
if err != nil {
return err
}
// Idempotent column additions for existing databases
for _, col := range []string{
"ALTER TABLE deploy_targets ADD COLUMN strategy TEXT NOT NULL DEFAULT 'systemd'",
"ALTER TABLE deploy_targets ADD COLUMN source_dir TEXT NOT NULL DEFAULT ''",
"ALTER TABLE deploy_targets ADD COLUMN branch TEXT NOT NULL DEFAULT 'main'",
"ALTER TABLE deploy_targets ADD COLUMN compose_files TEXT NOT NULL DEFAULT ''",
} {
s.db.Exec(col) // ignore "duplicate column" errors
}
return nil
}
func (s *Store) AddTarget(t DeployTarget) error {
envJSON, _ := json.Marshal(t.Env)
if t.Strategy == "" {
t.Strategy = "systemd"
}
if t.Branch == "" {
t.Branch = "main"
}
_, err := s.db.Exec(`
INSERT OR REPLACE INTO deploy_targets (app, host, remote_dir, binary_name, build_cmd, service_user, port, health_path, env, strategy, source_dir, branch, compose_files, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
t.App, t.Host, t.RemoteDir, t.BinaryName, t.BuildCmd, t.ServiceUser, t.Port, t.HealthPath,
string(envJSON), t.Strategy, t.SourceDir, t.Branch, t.ComposeFiles,
time.Now().UTC().Format(time.RFC3339))
return err
}
func (s *Store) RemoveTarget(app string) error {
res, err := s.db.Exec("DELETE FROM deploy_targets WHERE app = ?", app)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("target %q not found", app)
}
return nil
}
const selectTargetCols = "app, host, remote_dir, binary_name, build_cmd, service_user, port, health_path, env, strategy, source_dir, branch, compose_files, created_at"
func (s *Store) GetTargets(app string) ([]DeployTarget, error) {
rows, err := s.db.Query("SELECT "+selectTargetCols+" FROM deploy_targets WHERE app = ?", app)
if err != nil {
return nil, err
}
defer rows.Close()
return scanTargets(rows)
}
func (s *Store) ListTargets() ([]DeployTarget, error) {
rows, err := s.db.Query("SELECT "+selectTargetCols+" FROM deploy_targets ORDER BY app, host")
if err != nil {
return nil, err
}
defer rows.Close()
return scanTargets(rows)
}
func scanTargets(rows *sql.Rows) ([]DeployTarget, error) {
var targets []DeployTarget
for rows.Next() {
var t DeployTarget
var envStr, createdStr string
if err := rows.Scan(&t.App, &t.Host, &t.RemoteDir, &t.BinaryName, &t.BuildCmd, &t.ServiceUser, &t.Port, &t.HealthPath, &envStr, &t.Strategy, &t.SourceDir, &t.Branch, &t.ComposeFiles, &createdStr); err != nil {
return nil, err
}
json.Unmarshal([]byte(envStr), &t.Env)
t.CreatedAt, _ = time.Parse(time.RFC3339, createdStr)
targets = append(targets, t)
}
return targets, rows.Err()
}
func (s *Store) LogDeploy(l DeployLog) error {
_, err := s.db.Exec(`
INSERT INTO deploy_logs (app, host, status, trigger, error, duration_ms, started_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
l.App, l.Host, l.Status, l.Trigger, l.Error, l.Duration, l.StartedAt.UTC().Format(time.RFC3339))
return err
}
func (s *Store) RecentLogs(app string, limit int) ([]DeployLog, error) {
query := "SELECT id, app, host, status, trigger, error, duration_ms, started_at FROM deploy_logs"
var args []any
if app != "" {
query += " WHERE app = ?"
args = append(args, app)
}
query += " ORDER BY started_at DESC LIMIT ?"
args = append(args, limit)
rows, err := s.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []DeployLog
for rows.Next() {
var l DeployLog
var startedStr string
if err := rows.Scan(&l.ID, &l.App, &l.Host, &l.Status, &l.Trigger, &l.Error, &l.Duration, &startedStr); err != nil {
return nil, err
}
l.StartedAt, _ = time.Parse(time.RFC3339, startedStr)
logs = append(logs, l)
}
return logs, rows.Err()
}