package main
import (
"context"
"errors"
"log"
"time"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/errlog"
"craftdesign.group/quarry/internal/oltp"
)
// adminsTopic is the group every fleet-wide alert is addressed to: the
// error journal's first sighting of a fault, and a route that blows its
// render budget. It is a TOPIC, not a person. "admins" as a subscriber
// id names nobody, and notify answers a trigger for an unknown
// subscriber with a success — so an alert addressed that way is
// accepted, delivered to no one, and reported as sent.
const adminsTopic = "admins"
// topicPlane is the slice of notify provisioning the reconcile uses.
type topicPlane interface {
EnsureSubscriber(ctx context.Context, account string) error
EnsureTopic(ctx context.Context, key, name string) error
AddTopicMember(ctx context.Context, key, account string) error
RemoveTopicMember(ctx context.Context, key, account string) error
}
// accountBook is what the forge knows about who exists and what they
// hold — the truth the reconcile makes notify match.
type accountBook interface {
Accounts() ([]oltp.Account, error)
GrantsFor(account string) (map[string]oltp.Role, error)
Repos() ([]oltp.Repo, error)
}
// notifySync is what a reconcile did, for the startup log line.
type notifySync struct{ Subscribers, Members int }
// reconcileNotify makes notify's subscribers and topics match the
// forge's accounts and grants.
//
// Provisioning has always happened on the WRITE: account.add creates a
// subscriber, a grant joins the repository's activity topic. Every
// account and grant older than that code is therefore unprovisioned,
// and the failure is silent in the worst way — a trigger addressed to a
// topic that was never created is refused with a 404 the caller
// discards, so the forge reports nothing while nobody is told anything.
// This is the sweep that fixes the fleet rather than the next write.
//
// Every call it makes is idempotent, so it runs on every start. A
// failure on one account is collected and the sweep goes on: one
// account notify cannot hold must not cost the other nineteen their
// alerts.
func reconcileNotify(ctx context.Context, np topicPlane, book accountBook) (notifySync, error) {
accounts, err := book.Accounts()
if err != nil {
return notifySync{}, err
}
repos, err := book.Repos()
if err != nil {
return notifySync{}, err
}
var sync notifySync
var errs []error
// Every repository gets its topic whether or not anyone is granted
// it. A repository whose only members are site admins holds no
// grant rows at all — quarry's own repository is one — and deriving
// the topics from grants left exactly those with none, so the
// forge's own activity was the one thing that could not be
// delivered.
for _, r := range repos {
if err := np.EnsureTopic(ctx, repoTopicKey(r.Name), r.Name+" activity"); err != nil {
errs = append(errs, err)
}
}
if err := np.EnsureTopic(ctx, adminsTopic, "quarry admins"); err != nil {
// Without the topic there is no point adding members to it, but
// the repository topics below are independent and still worth
// doing.
errs = append(errs, err)
}
for _, a := range accounts {
if err := np.EnsureSubscriber(ctx, a.Name); err != nil {
errs = append(errs, err)
// A subscriber that does not exist cannot join a topic;
// notify answers those with a 422 that says exactly that.
continue
}
sync.Subscribers++
// Disabled is WHEN the account was shut, empty while open. A
// shut account is not an admin for this purpose: alerts name
// production faults, and someone who can no longer sign in
// should not keep receiving them.
if a.Admin && a.Disabled == "" {
if err := np.AddTopicMember(ctx, adminsTopic, a.Name); err != nil {
errs = append(errs, err)
} else {
sync.Members++
}
} else if err := np.RemoveTopicMember(ctx, adminsTopic, a.Name); err != nil {
// Removing an absent member is already success in the
// client, so this is a real failure.
errs = append(errs, err)
}
if a.Disabled != "" {
continue
}
// A site admin holds every repository by the capability model,
// which is why they have no grant rows to read: their
// membership has to come from the repository list instead.
// Mirrors are in it and cost nothing — activity is what people
// write, and nobody writes to a mirror.
member := make([]string, 0, len(repos))
if a.Admin {
for _, r := range repos {
member = append(member, r.Name)
}
} else {
grants, err := book.GrantsFor(a.Name)
if err != nil {
errs = append(errs, err)
continue
}
for repo := range grants {
member = append(member, repo)
}
}
for _, repo := range member {
key := repoTopicKey(repo)
if err := np.EnsureTopic(ctx, key, repo+" activity"); err != nil {
errs = append(errs, err)
continue
}
if err := np.AddTopicMember(ctx, key, a.Name); err != nil {
errs = append(errs, err)
continue
}
sync.Members++
}
}
return sync, errors.Join(errs...)
}
// alertAdmins puts a fault in every site admin's forge inbox.
//
// The attention stream is quarry's own half of RFC 0011 §5 — no notify,
// no external service, nothing to provision — and it is the half the
// inbox page renders when notify has nothing to say. A fleet alert is
// deliberately not "attention" in the person-addressed sense (an error
// is nobody's mention), so it is written here rather than derived from
// the notify event.
//
// Best-effort and silent about its own failures beyond the journal: it
// runs inside the error journal's first-sighting hook, and reporting a
// failure here would be reporting into the thing that is reporting.
func alertAdmins(book accountBook, att api.AttentionPlane, e errlog.Entry) {
if att == nil || book == nil {
return
}
accounts, err := book.Accounts()
if err != nil {
log.Printf("notify: admin alert: %v", err)
return
}
ev := api.AttentionEvent{
Kind: "error", Repo: e.Repo, From: "quarry",
Summary: e.Surface + ": " + e.Message,
Link: "/fleet/exceptions",
At: e.First.UTC().Format(time.RFC3339),
// The same key notify was handed, so the inbox pairs this row
// with its notify twin instead of showing the fault twice.
TxnID: "err:" + e.First.Format(time.RFC3339Nano),
}
for _, a := range accounts {
if !a.Admin || a.Disabled != "" {
continue
}
if _, err := att.Attend(a.Name, ev); err != nil {
log.Printf("notify: admin alert for %s: %v", a.Name, err)
}
}
}