Skip to content
quarry Artifact [624b7748c4]
Login

Artifact [624b7748c4]

Artifact 624b7748c451df7600d9e0e8f0a70784b22d3602209fc44ccb0204f4508d8935:


package main

import (
	"context"
	"errors"
	"log"
	"time"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/errlog"
	"craftdesign.group/quarry/internal/notify"
	"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, email 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
	Preferences(ctx context.Context, account string) (notify.SubscriberPreferences, error)
	SetPreferences(ctx context.Context, account string, workflows map[string]map[string]bool,
		digests map[string]*notify.DigestWindow) 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, Quieted int }

// emailOffByDefault writes the explicit "no email" a subscriber has not
// yet answered, for every family that carries an email step.
//
// notify reads an absent preference as ENABLED. The day the family
// workflows gained an email step, that default would have mailed every
// account on the forge about every mention, reply and landing without
// anybody asking — the one thing a notification system must not do.
// So the forge answers for them, once, and only where they have not
// answered themselves: a channel somebody switched on (or off) is a
// key that is present, and is left exactly as they set it.
//
// It returns how many families were quieted, which is zero on every
// start after the first.
func emailOffByDefault(ctx context.Context, np topicPlane, account string) (int, error) {
	prefs, err := np.Preferences(ctx, account)
	if err != nil {
		return 0, err
	}
	want := map[string]map[string]bool{}
	for _, wf := range notify.EmailFamilies() {
		if _, chosen := prefs.Workflows[wf]["email"]; chosen {
			continue
		}
		want[wf] = map[string]bool{"email": false}
	}
	if len(want) == 0 {
		return 0, nil
	}
	// notify merges per workflow, so this touches the email cell alone
	// and no digest window: what somebody set elsewhere stays.
	if err := np.SetPreferences(ctx, account, want, nil); err != nil {
		return 0, err
	}
	return len(want), nil
}

// 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 {
		// The address travels with the account: notify's upsert replaces
		// the row, so a sweep that omitted it would blank every address
		// it had.
		if err := np.EnsureSubscriber(ctx, a.Name, a.Email); 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++
		if n, err := emailOffByDefault(ctx, np, a.Name); err != nil {
			errs = append(errs, err)
		} else {
			sync.Quieted += n
		}
		// 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)
		}
	}
}