Files
pve-confsync/internal/store/journal.go
T

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()
}