Skip to content
quarry Artifact [750c11ab4e]
Login

Artifact [750c11ab4e]

Artifact 750c11ab4ed312b9eb92e2ac9c01775984c9dc0d5ccb8ede808a9df44c644e68:


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