feat(docker-watcher): phase 1 - project scaffold & SQLite store
Initialize Go module, directory structure, and full SQLite store layer: - 7-table schema (projects, stages, registries, settings, instances, deploys, deploy_logs) with auto-migration - CRUD operations for all entities with proper error handling - ErrNotFound sentinel for distinguishing 404 from 500 in handlers - WAL mode, foreign keys, busy timeout pragmas
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CreateDeploy inserts a new deploy record.
|
||||
func (s *Store) CreateDeploy(d Deploy) (Deploy, error) {
|
||||
d.ID = uuid.New().String()
|
||||
d.StartedAt = now()
|
||||
if d.Status == "" {
|
||||
d.Status = "pending"
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO deploys (id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
d.ID, d.ProjectID, d.StageID, d.InstanceID, d.ImageTag, d.Status, d.StartedAt, d.FinishedAt, d.Error,
|
||||
)
|
||||
if err != nil {
|
||||
return Deploy{}, fmt.Errorf("insert deploy: %w", err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetDeployByID returns a single deploy by its ID.
|
||||
func (s *Store) GetDeployByID(id string) (Deploy, error) {
|
||||
var d Deploy
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
||||
FROM deploys WHERE id = ?`, id,
|
||||
).Scan(&d.ID, &d.ProjectID, &d.StageID, &d.InstanceID, &d.ImageTag, &d.Status, &d.StartedAt, &d.FinishedAt, &d.Error)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Deploy{}, fmt.Errorf("deploy %s: %w", id, ErrNotFound)
|
||||
}
|
||||
if err != nil {
|
||||
return Deploy{}, fmt.Errorf("query deploy: %w", err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetDeploysByProjectID returns all deploys for a project, newest first.
|
||||
func (s *Store) GetDeploysByProjectID(projectID string) ([]Deploy, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
||||
FROM deploys WHERE project_id = ? ORDER BY started_at DESC`, projectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query deploys: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanDeploys(rows)
|
||||
}
|
||||
|
||||
// GetRecentDeploys returns the most recent deploys across all projects.
|
||||
func (s *Store) GetRecentDeploys(limit int) ([]Deploy, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, project_id, stage_id, instance_id, image_tag, status, started_at, finished_at, error
|
||||
FROM deploys ORDER BY started_at DESC LIMIT ?`, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query recent deploys: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanDeploys(rows)
|
||||
}
|
||||
|
||||
// UpdateDeployStatus sets the status (and optionally error and finished_at) on a deploy.
|
||||
func (s *Store) UpdateDeployStatus(id string, status string, deployErr string) error {
|
||||
ts := now()
|
||||
var finishedAt string
|
||||
if isTerminalDeployStatus(status) {
|
||||
finishedAt = ts
|
||||
}
|
||||
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE deploys SET status=?, error=?, finished_at=? WHERE id=?`,
|
||||
status, deployErr, finishedAt, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update deploy status: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("deploy %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeployInstanceID links a deploy to the instance it created.
|
||||
func (s *Store) SetDeployInstanceID(deployID string, instanceID string) error {
|
||||
result, err := s.db.Exec(`UPDATE deploys SET instance_id=? WHERE id=?`, instanceID, deployID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set deploy instance: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("deploy %s: %w", deployID, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendDeployLog adds a log entry for a deploy.
|
||||
func (s *Store) AppendDeployLog(deployID string, message string, level string) error {
|
||||
if level == "" {
|
||||
level = "info"
|
||||
}
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO deploy_logs (deploy_id, message, level, created_at) VALUES (?, ?, ?, ?)`,
|
||||
deployID, message, level, now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("append deploy log: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDeployLogs returns all log entries for a deploy, ordered chronologically.
|
||||
func (s *Store) GetDeployLogs(deployID string) ([]DeployLog, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, deploy_id, message, level, created_at
|
||||
FROM deploy_logs WHERE deploy_id = ? ORDER BY id`, deployID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query deploy logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []DeployLog
|
||||
for rows.Next() {
|
||||
var l DeployLog
|
||||
if err := rows.Scan(&l.ID, &l.DeployID, &l.Message, &l.Level, &l.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan deploy log: %w", err)
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, rows.Err()
|
||||
}
|
||||
|
||||
// scanDeploys is a helper that scans deploy rows from a cursor.
|
||||
func scanDeploys(rows *sql.Rows) ([]Deploy, error) {
|
||||
var deploys []Deploy
|
||||
for rows.Next() {
|
||||
var d Deploy
|
||||
if err := rows.Scan(&d.ID, &d.ProjectID, &d.StageID, &d.InstanceID, &d.ImageTag, &d.Status, &d.StartedAt, &d.FinishedAt, &d.Error); err != nil {
|
||||
return nil, fmt.Errorf("scan deploy: %w", err)
|
||||
}
|
||||
deploys = append(deploys, d)
|
||||
}
|
||||
return deploys, rows.Err()
|
||||
}
|
||||
|
||||
// isTerminalDeployStatus returns true if the status indicates the deploy is finished.
|
||||
func isTerminalDeployStatus(status string) bool {
|
||||
switch status {
|
||||
case "success", "failed", "rolled_back":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user