Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015a8P6WMRPeqdpd5VCh6jAy
193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Run statuses.
|
|
const (
|
|
RunRunning = "running"
|
|
RunOK = "ok" // applied and verified, or dry-run without blocking error
|
|
RunNoop = "noop" // nothing to do
|
|
RunError = "error" // at least one client failed
|
|
RunBlocked = "blocked" // dry-run with blocking errors
|
|
)
|
|
|
|
// Run is one entry of the execution journal: a dry-run or a synchronization.
|
|
type Run struct {
|
|
ID int64 `json:"id"`
|
|
StartedAt time.Time `json:"startedAt"`
|
|
FinishedAt *time.Time `json:"finishedAt,omitempty"`
|
|
User string `json:"user"`
|
|
Mode string `json:"mode"` // dry-run or apply
|
|
Scope string `json:"scope"` // global, client or object
|
|
Clients []string `json:"clients"`
|
|
Objects []string `json:"objects,omitempty"`
|
|
Status string `json:"status"`
|
|
Summary string `json:"summary"`
|
|
Detail json.RawMessage `json:"detail,omitempty"`
|
|
}
|
|
|
|
// StartRun records a run and returns its id.
|
|
func (s *Store) StartRun(ctx context.Context, r Run) (int64, error) {
|
|
if r.StartedAt.IsZero() {
|
|
r.StartedAt = time.Now()
|
|
}
|
|
res, err := s.db.ExecContext(ctx, `INSERT INTO runs (started_at, user, mode, scope, clients, objects, status, summary, detail)
|
|
VALUES (?,?,?,?,?,?,?,?,?)`, r.StartedAt.UnixMilli(), r.User, r.Mode, r.Scope,
|
|
strings.Join(r.Clients, ","), strings.Join(r.Objects, ","), r.Status, r.Summary, detail(r.Detail))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
// UpdateRun stores the progress (detail) of a run; a final status sets finished_at.
|
|
func (s *Store) UpdateRun(ctx context.Context, id int64, status, summary string, d json.RawMessage) error {
|
|
fin := int64(0)
|
|
if status != RunRunning {
|
|
fin = time.Now().UnixMilli()
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `UPDATE runs SET status = ?, summary = ?, detail = ?, finished_at = ? WHERE id = ?`,
|
|
status, summary, detail(d), fin, id)
|
|
return err
|
|
}
|
|
|
|
func detail(d json.RawMessage) string {
|
|
if len(d) == 0 {
|
|
return "{}"
|
|
}
|
|
return string(d)
|
|
}
|
|
|
|
// Runs lists the journal, newest first; before (an id) pages backwards.
|
|
func (s *Store) Runs(ctx context.Context, limit int, before int64) ([]Run, error) {
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 100
|
|
}
|
|
if before <= 0 {
|
|
before = 1 << 62
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, started_at, finished_at, user, mode, scope, clients, objects, status, summary
|
|
FROM runs WHERE id < ? ORDER BY id DESC LIMIT ?`, before, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Run
|
|
for rows.Next() {
|
|
r, err := scanRun(rows.Scan, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Run returns one run with its detail.
|
|
func (s *Store) Run(ctx context.Context, id int64) (Run, error) {
|
|
row := s.db.QueryRowContext(ctx, `SELECT id, started_at, finished_at, user, mode, scope, clients, objects, status, summary, detail
|
|
FROM runs WHERE id = ?`, id)
|
|
r, err := scanRun(row.Scan, true)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return r, ErrNotFound
|
|
}
|
|
return r, err
|
|
}
|
|
|
|
// FailRunning closes the runs left "running" by a stop of the server.
|
|
func (s *Store) FailRunning(ctx context.Context) error {
|
|
_, err := s.db.ExecContext(ctx, `UPDATE runs SET status = 'error', summary = 'interrompu (arrêt du serveur)', finished_at = ?
|
|
WHERE status = 'running'`, time.Now().UnixMilli())
|
|
return err
|
|
}
|
|
|
|
func scanRun(scan func(...any) error, withDetail bool) (Run, error) {
|
|
var r Run
|
|
var start, fin int64
|
|
var clients, objects, d string
|
|
dest := []any{&r.ID, &start, &fin, &r.User, &r.Mode, &r.Scope, &clients, &objects, &r.Status, &r.Summary}
|
|
if withDetail {
|
|
dest = append(dest, &d)
|
|
}
|
|
if err := scan(dest...); err != nil {
|
|
return r, err
|
|
}
|
|
r.StartedAt = time.UnixMilli(start).UTC()
|
|
if fin > 0 {
|
|
t := time.UnixMilli(fin).UTC()
|
|
r.FinishedAt = &t
|
|
}
|
|
r.Clients = split(clients)
|
|
r.Objects = split(objects)
|
|
if withDetail {
|
|
r.Detail = json.RawMessage(d)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func split(s string) []string {
|
|
if s == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(s, ",")
|
|
}
|
|
|
|
// ---- audit ----
|
|
|
|
// Event is one entry of the audit trail: who did what, from where, with which result.
|
|
type Event struct {
|
|
ID int64 `json:"id"`
|
|
At time.Time `json:"at"`
|
|
User string `json:"user"`
|
|
Role string `json:"role"`
|
|
IP string `json:"ip"`
|
|
Action string `json:"action"`
|
|
Target string `json:"target"`
|
|
OK bool `json:"ok"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
// Audit records an event. Callers never put secrets in Detail.
|
|
func (s *Store) Audit(ctx context.Context, e Event) error {
|
|
if e.At.IsZero() {
|
|
e.At = time.Now()
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO audit (at, user, role, ip, action, target, ok, detail) VALUES (?,?,?,?,?,?,?,?)`,
|
|
e.At.UnixMilli(), e.User, e.Role, e.IP, e.Action, e.Target, e.OK, e.Detail)
|
|
return err
|
|
}
|
|
|
|
// Events lists the audit trail, newest first.
|
|
func (s *Store) Events(ctx context.Context, limit int, before int64) ([]Event, error) {
|
|
if limit <= 0 || limit > 1000 {
|
|
limit = 200
|
|
}
|
|
if before <= 0 {
|
|
before = 1 << 62
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, at, user, role, ip, action, target, ok, detail FROM audit
|
|
WHERE id < ? ORDER BY id DESC LIMIT ?`, before, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Event
|
|
for rows.Next() {
|
|
var e Event
|
|
var at int64
|
|
if err := rows.Scan(&e.ID, &at, &e.User, &e.Role, &e.IP, &e.Action, &e.Target, &e.OK, &e.Detail); err != nil {
|
|
return nil, err
|
|
}
|
|
e.At = time.UnixMilli(at).UTC()
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|