Files
Provisioning/internal/store/operations.go
T
josh 1317ff6369
build-and-push / test (push) Successful in 34s
build-and-push / build-and-push (push) Successful in 1m8s
Add job detail page with activity log and cancel support
Operations are now clickable from the host detail page, linking to
/ops/{id} which shows the operation info, host link, duration, and
activity log filtered to that operation. Active operations can be
cancelled, which transitions the host to failed and releases the lock.
SSE activity events now include operation_id for real-time filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 10:37:18 -04:00

110 lines
3.5 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"time"
"provisioning/internal/model"
)
type Operations struct {
DB *sql.DB
}
func (s *Operations) Create(ctx context.Context, op model.Operation) (int64, error) {
res, err := s.DB.ExecContext(ctx, `
INSERT INTO operations(host_id, kind, state, image_id)
VALUES(?,?,?,?)
`, op.HostID, op.Kind, model.OpActive, nullInt64(op.ImageID))
if err != nil {
return 0, fmt.Errorf("insert operation: %w", err)
}
return res.LastInsertId()
}
func (s *Operations) Complete(ctx context.Context, id int64) error {
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, model.OpCompleted, id)
return err
}
func (s *Operations) Fail(ctx context.Context, id int64, errMsg string) error {
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now'), error_message = ? WHERE id = ?`, model.OpFailed, errMsg, id)
return err
}
func (s *Operations) ListByHost(ctx context.Context, hostID int64) ([]model.Operation, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
FROM operations WHERE host_id = ? ORDER BY started_at DESC
`, hostID)
if err != nil {
return nil, fmt.Errorf("list operations: %w", err)
}
defer rows.Close()
var out []model.Operation
for rows.Next() {
var op model.Operation
var startedAt string
var completedAt sql.NullString
if err := rows.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
return nil, fmt.Errorf("scan operation: %w", err)
}
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
if completedAt.Valid {
t, _ := time.Parse(time.RFC3339, completedAt.String)
op.CompletedAt = &t
}
out = append(out, op)
}
return out, rows.Err()
}
func (s *Operations) Get(ctx context.Context, id int64) (*model.Operation, error) {
row := s.DB.QueryRowContext(ctx, `
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
FROM operations WHERE id = ?
`, id)
var op model.Operation
var startedAt string
var completedAt sql.NullString
if err := row.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
if err == sql.ErrNoRows {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get operation: %w", err)
}
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
if completedAt.Valid {
t, _ := time.Parse(time.RFC3339, completedAt.String)
op.CompletedAt = &t
}
return &op, nil
}
func (s *Operations) GetActive(ctx context.Context, hostID int64) (*model.Operation, error) {
row := s.DB.QueryRowContext(ctx, `
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
FROM operations WHERE host_id = ? AND state = ? ORDER BY started_at DESC LIMIT 1
`, hostID, model.OpActive)
var op model.Operation
var startedAt string
var completedAt sql.NullString
if err := row.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
if err == sql.ErrNoRows {
return nil, ErrNotFound
}
return nil, fmt.Errorf("get active operation: %w", err)
}
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
return &op, nil
}
func nullInt64(v int64) any {
if v == 0 {
return nil
}
return v
}