32de5b26a8
Blue-green zero-downtime deploys, promote flow validation. Dual auth: local (bcrypt + JWT) and OAuth2/OIDC (any provider). Auth middleware, login page, auth settings UI. Structured logging (slog JSON), config export to YAML. Graceful shutdown with deploy draining. Multi-stage Dockerfile and production docker-compose.yml. Swap phase order: Volumes & Env before UI Polish.
189 lines
5.8 KiB
Go
189 lines
5.8 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// ErrNotFound is returned when a requested entity does not exist.
|
|
var ErrNotFound = errors.New("not found")
|
|
|
|
// Store wraps the SQLite database connection and provides access to all query methods.
|
|
type Store struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// New opens a SQLite database at the given path and runs auto-migration.
|
|
func New(dbPath string) (*Store, error) {
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
|
|
// Enable WAL mode and foreign keys for better concurrency and referential integrity.
|
|
pragmas := []string{
|
|
"PRAGMA journal_mode=WAL",
|
|
"PRAGMA foreign_keys=ON",
|
|
"PRAGMA busy_timeout=5000",
|
|
}
|
|
for _, p := range pragmas {
|
|
if _, err := db.Exec(p); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("exec pragma %q: %w", p, err)
|
|
}
|
|
}
|
|
|
|
s := &Store{db: db}
|
|
if err := s.migrate(); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("migrate: %w", err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Close closes the underlying database connection.
|
|
func (s *Store) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
// DB returns the underlying *sql.DB for advanced operations like transactions.
|
|
func (s *Store) DB() *sql.DB {
|
|
return s.db
|
|
}
|
|
|
|
// migrate creates all tables if they do not already exist.
|
|
func (s *Store) migrate() error {
|
|
_, err := s.db.Exec(schema)
|
|
return err
|
|
}
|
|
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
registry TEXT NOT NULL DEFAULT '',
|
|
image TEXT NOT NULL,
|
|
port INTEGER NOT NULL DEFAULT 0,
|
|
healthcheck TEXT NOT NULL DEFAULT '',
|
|
env TEXT NOT NULL DEFAULT '{}',
|
|
volumes TEXT NOT NULL DEFAULT '{}',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stages (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
tag_pattern TEXT NOT NULL DEFAULT '*',
|
|
auto_deploy INTEGER NOT NULL DEFAULT 0,
|
|
max_instances INTEGER NOT NULL DEFAULT 1,
|
|
confirm INTEGER NOT NULL DEFAULT 0,
|
|
promote_from TEXT NOT NULL DEFAULT '',
|
|
subdomain TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(project_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS registries (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
url TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'generic',
|
|
token TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
domain TEXT NOT NULL DEFAULT '',
|
|
server_ip TEXT NOT NULL DEFAULT '',
|
|
network TEXT NOT NULL DEFAULT '',
|
|
subdomain_pattern TEXT NOT NULL DEFAULT 'stage-{stage}-{project}',
|
|
notification_url TEXT NOT NULL DEFAULT '',
|
|
npm_url TEXT NOT NULL DEFAULT '',
|
|
npm_email TEXT NOT NULL DEFAULT '',
|
|
npm_password TEXT NOT NULL DEFAULT '',
|
|
webhook_secret TEXT NOT NULL DEFAULT '',
|
|
polling_interval TEXT NOT NULL DEFAULT '5m',
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS instances (
|
|
id TEXT PRIMARY KEY,
|
|
stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
container_id TEXT NOT NULL DEFAULT '',
|
|
image_tag TEXT NOT NULL,
|
|
subdomain TEXT NOT NULL DEFAULT '',
|
|
npm_proxy_id INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'stopped',
|
|
port INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS deploys (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
|
instance_id TEXT NOT NULL DEFAULT '',
|
|
image_tag TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
finished_at TEXT NOT NULL DEFAULT '',
|
|
error TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS deploy_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
deploy_id TEXT NOT NULL REFERENCES deploys(id) ON DELETE CASCADE,
|
|
message TEXT NOT NULL,
|
|
level TEXT NOT NULL DEFAULT 'info',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS poll_states (
|
|
stage_id TEXT PRIMARY KEY REFERENCES stages(id) ON DELETE CASCADE,
|
|
last_tag TEXT NOT NULL DEFAULT '',
|
|
last_polled TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL DEFAULT '',
|
|
email TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT 'viewer',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_settings (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
auth_mode TEXT NOT NULL DEFAULT 'local',
|
|
oidc_client_id TEXT NOT NULL DEFAULT '',
|
|
oidc_client_secret TEXT NOT NULL DEFAULT '',
|
|
oidc_issuer_url TEXT NOT NULL DEFAULT '',
|
|
oidc_redirect_url TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
-- Seed the settings row if it does not exist.
|
|
INSERT OR IGNORE INTO settings (id) VALUES (1);
|
|
|
|
-- Seed the auth_settings row if it does not exist.
|
|
INSERT OR IGNORE INTO auth_settings (id) VALUES (1);
|
|
`
|
|
|
|
// now returns the current time formatted for SQLite storage.
|
|
func now() string {
|
|
return time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
}
|