package store import ( "context" "database/sql" "errors" "fmt" ) // CreateDefaultRepoMaintenance inserts the default cadences for a // host. Called once at host enrolment alongside CreateHost. Safe to // re-call (uses INSERT OR IGNORE so a manual repair doesn't blow up // an already-seeded host). func (st *Store) CreateDefaultRepoMaintenance(ctx context.Context, hostID string) error { _, err := st.db.ExecContext(ctx, `INSERT OR IGNORE INTO host_repo_maintenance (host_id) VALUES (?)`, hostID) if err != nil { return fmt.Errorf("store: seed repo maintenance: %w", err) } return nil } // GetRepoMaintenance returns the cadence row for a host. Returns // ErrNotFound if absent — caller should usually treat that as // "needs CreateDefaultRepoMaintenance" rather than a hard error. func (st *Store) GetRepoMaintenance(ctx context.Context, hostID string) (*HostRepoMaintenance, error) { row := st.db.QueryRowContext(ctx, `SELECT host_id, forget_cron, forget_enabled, prune_cron, prune_enabled, check_cron, check_enabled, check_subset_pct FROM host_repo_maintenance WHERE host_id = ?`, hostID) var ( m HostRepoMaintenance forgetEnabled, pruneEnabled, checkEnabled int ) err := row.Scan(&m.HostID, &m.ForgetCron, &forgetEnabled, &m.PruneCron, &pruneEnabled, &m.CheckCron, &checkEnabled, &m.CheckSubsetPct) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("store: get repo maintenance: %w", err) } m.ForgetEnabled = forgetEnabled != 0 m.PruneEnabled = pruneEnabled != 0 m.CheckEnabled = checkEnabled != 0 return &m, nil } // ListAllMaintenance returns every host_repo_maintenance row. // Used by the server-side maintenance ticker to iterate every // host on each tick. Order is unspecified (the ticker doesn't // care). func (st *Store) ListAllMaintenance(ctx context.Context) ([]HostRepoMaintenance, error) { rows, err := st.db.QueryContext(ctx, `SELECT host_id, forget_cron, forget_enabled, prune_cron, prune_enabled, check_cron, check_enabled, check_subset_pct FROM host_repo_maintenance`) if err != nil { return nil, fmt.Errorf("store: list all maintenance: %w", err) } defer func() { _ = rows.Close() }() var out []HostRepoMaintenance for rows.Next() { var ( m HostRepoMaintenance forgetEnabled, pruneEnabled, checkEnabled int ) if err := rows.Scan(&m.HostID, &m.ForgetCron, &forgetEnabled, &m.PruneCron, &pruneEnabled, &m.CheckCron, &checkEnabled, &m.CheckSubsetPct); err != nil { return nil, fmt.Errorf("store: scan maintenance: %w", err) } m.ForgetEnabled = forgetEnabled != 0 m.PruneEnabled = pruneEnabled != 0 m.CheckEnabled = checkEnabled != 0 out = append(out, m) } return out, rows.Err() } // UpdateRepoMaintenance replaces every editable field. Doesn't bump // the schedule version — these run on the server's own ticker, not // the agent's local cron, so the agent doesn't need to know. func (st *Store) UpdateRepoMaintenance(ctx context.Context, m *HostRepoMaintenance) error { if m.HostID == "" { return errors.New("store: repo maintenance host_id required") } _, err := st.db.ExecContext(ctx, `UPDATE host_repo_maintenance SET forget_cron = ?, forget_enabled = ?, prune_cron = ?, prune_enabled = ?, check_cron = ?, check_enabled = ?, check_subset_pct = ? WHERE host_id = ?`, m.ForgetCron, boolToInt(m.ForgetEnabled), m.PruneCron, boolToInt(m.PruneEnabled), m.CheckCron, boolToInt(m.CheckEnabled), m.CheckSubsetPct, m.HostID) if err != nil { return fmt.Errorf("store: update repo maintenance: %w", err) } return nil }