gunnar-core 1.5.0

gunnar's repository model, identity table, ACL and path sandbox. gunnar-ssh, gunnar-grpc and gunnar-cli all sit on it.
Documentation
//! **Where a repository's pull-mirror registration lives** (`refs/gunnar/mirrors`).
//!
//! The fourth document in the control store, and the one that closes
//! `Mirror.Add` / `Mirror.Status` / `Mirror.Remove`. Those answered
//! `Unimplemented` with one reason:
//!
//! > `Mirror.*`: mirroring runs over gunnar-client (nornir-git ls-remote +
//! > fetch), which an S crate must never link (the dependency budget). Use `gunnar mirror` on a
//! > normal-OS host instead.
//!
//! That reason is exactly right about **`Sync`** and wrong about the other
//! three. Adding, listing and removing a mirror registration is bookkeeping:
//! a URL, a flag and a few counters. It needs no fetch, no `ls-remote` and no
//! client code — only somewhere to keep the record, which is what this is.
//! `Sync` still needs the client half and is still refused where that half is
//! absent, which is now a much smaller and much more honest gap.
//!
//! # It is deliberately not `mirrors.json`
//!
//! `gunnar-client`'s `MirrorRegistry` keeps a `mirrors.json` beside the data
//! root, and that file stays: it is the **pull-mirror client's** own state, on
//! a host running `gunnar mirror`. This is the *server's* registry, reachable
//! over the control plane, versioned with the catalog and replicated with it.
//! Two files would be two answers to "what does this server mirror"; two
//! *systems* with one file each is one answer per system.

use std::sync::Arc;

use crate::control::ControlStore;
use crate::doc::{impl_doc, DocRef, ShardedMap};
use crate::error::Result;
use crate::sandbox::RepoRef;

/// **The name the mirror document is stored under** in the control store.
pub const MIRROR_REF: &str = "refs/gunnar/mirrors";

/// **Which kind of credential a mirror dials its upstream with.**
///
/// The kind lives in the mirror *document*; the VALUE lives in the sealed
/// [`SecretStore`](gunnar_store::SecretStore), under
/// [`credential_secret_key`]. That split is the whole design and it is the
/// webhook secret's, one step wider:
///
/// * a **listing** must be able to say "this mirror has a key" without being a
///   way to read the key, so the fact and the value are in different stores and
///   only one of them is reachable from a `Mirror.Status`;
/// * the *kind* is not a secret and is load-bearing — an SSH key against an
///   `https://` remote is a mirror that fails on authentication for ever, and
///   an operator has to be able to see that before it does;
/// * the value is sealed under the KEK, exactly as the SSH host key is, so a
///   copy of `control.redb` is not a copy of a customer's forge token.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MirrorCredentialKind {
    /// Nothing stored. A public upstream needs nothing, and mirrors fine.
    #[default]
    None,
    /// An OpenSSH private key, for an `ssh://` upstream.
    SshKey,
    /// A token, for an `https://` upstream.
    Token,
}

impl MirrorCredentialKind {
    /// Whether a credential is stored at all — the `has_secret` question, kept
    /// as a method so no caller has to spell the `None` comparison itself.
    pub fn is_set(self) -> bool {
        !matches!(self, MirrorCredentialKind::None)
    }

    /// The word an operator sees in a refusal. Not `Debug`: these strings are
    /// in errors an operator reads and acts on, and `Debug` is free to change.
    pub fn name(self) -> &'static str {
        match self {
            MirrorCredentialKind::None => "none",
            MirrorCredentialKind::SshKey => "an ssh private key",
            MirrorCredentialKind::Token => "a token",
        }
    }
}

/// **Where one mirror's credential is sealed**, in the server's secret store.
///
/// Namespaced under `mirror/` so it cannot collide with the host key, the
/// issuer key or the transparency key, all of which live in the same table
/// under bare names. The repository key (`<namespace>/<name>`) is the rest of
/// it, so the credential is removed by the same identity the registration is.
///
/// One function rather than a `format!` at each call site: a `put` and a `get`
/// that spelled the key differently would be a credential that is stored and
/// never found, which reads exactly like a credential that was never set.
pub fn credential_secret_key(repo: &RepoRef) -> String {
    format!("mirror/{}", repo.key())
}

/// What this server knows about one repository's upstream.
///
/// Every field is a **fact that was observed**, not a derivation: a mirror that
/// has never synced carries `last_sync_at_unix_ms: None` rather than a zero
/// that reads like 1970, and `last_error` empty rather than a cheerful "".
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MirrorRecord {
    /// `<namespace>/<repo>` — the key this record is filed under, carried
    /// inside it as well.
    ///
    /// The catalog's `Repo` does the same with its namespace and name, for the
    /// same reason: a listing reads values and would otherwise have no way to
    /// say which repository each one belongs to.
    pub repo: String,
    /// Where the upstream is.
    pub remote_url: String,
    /// Delete local references the upstream no longer has.
    pub prune: bool,
    /// When the registration was made.
    pub added_at_unix_ms: i64,
    /// When a sync last **completed**, or `None` if none ever has.
    ///
    /// `Option`, not `0`: "never synced" and "synced at the epoch" are
    /// different facts and a UI renders them differently. A zero here would be
    /// the same absence-as-a-value defect the push attestation had.
    pub last_sync_at_unix_ms: Option<i64>,
    /// Why the last sync failed, empty if the last one succeeded or none has
    /// run. Kept beside the timestamp so a mirror that failed *after*
    /// succeeding still shows both facts.
    pub last_error: String,
    /// How many syncs have completed.
    pub sync_count: u64,
    /// **Which kind of credential is stored for this upstream — never the
    /// credential.** See [`MirrorCredentialKind`].
    ///
    /// `#[serde(default)]` and not a new document version: a registration
    /// written by a server that predates credentials is a registration with
    /// none, which is exactly what [`MirrorCredentialKind::None`] means.
    #[serde(default)]
    pub credential: MirrorCredentialKind,
    /// The Basic-auth username a [`MirrorCredentialKind::Token`] is paired
    /// with; empty means `Bearer`.
    ///
    /// **Not a secret.** A forge token's username is `x-access-token`,
    /// `oauth2` or the account name, and every forge documents it in public. It
    /// is in the document rather than beside the sealed value so a listing can
    /// show the operator which half of the pair they configured.
    #[serde(default)]
    pub credential_username: String,
    /// **The repository is created by the first sync that reaches the
    /// upstream** (2026-10-18), at the upstream's object format and default
    /// branch. Set by `Mirror.Add { create_repo }` for a repository that does
    /// not exist; cleared by the first sync that lands. While it is set there
    /// is a registration and no repository, so a failed first import leaves no
    /// empty repository behind.
    ///
    /// Not written while false, so a registration that never had it reads the
    /// same to a server that predates it.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub creates_repo: bool,
    /// The last successful sync found Git LFS in use (`filter=lfs` in the
    /// default branch's `.gitattributes`): this mirror holds the pointer files
    /// and not the LFS content, which is not copied.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub lfs_detected: bool,
}

impl MirrorRecord {
    /// A fresh registration: never synced, no error.
    pub fn new(
        repo: &RepoRef,
        remote_url: impl Into<String>,
        prune: bool,
        now_unix_ms: i64,
    ) -> Self {
        MirrorRecord {
            repo: repo.key(),
            remote_url: remote_url.into(),
            prune,
            added_at_unix_ms: now_unix_ms,
            last_sync_at_unix_ms: None,
            last_error: String::new(),
            sync_count: 0,
            credential: MirrorCredentialKind::None,
            credential_username: String::new(),
            creates_repo: false,
            lfs_detected: false,
        }
    }
}

/// The mirror registrations as they are stored.
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct MirrorDoc {
    /// Keyed by `<namespace>/<repo>`, the same key the catalog uses.
    #[serde(default)]
    repos: ShardedMap<MirrorRecord>,
}

impl_doc!(MirrorDoc { repos });

/// Register, list and remove pull-mirror upstreams.
///
/// Cheap to clone; every clone reads and writes the same document.
#[derive(Clone, Debug)]
pub struct MirrorStore {
    doc: DocRef<MirrorDoc>,
}

impl MirrorStore {
    /// Open the mirror document held in `store` (the server's control store).
    pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
        Ok(MirrorStore {
            doc: DocRef::new(store, MIRROR_REF)?,
        })
    }

    /// The name the document is stored under.
    pub fn ref_name(&self) -> &str {
        self.doc.ref_name()
    }

    /// One repository's registration, if it has one.
    pub fn get(&self, repo: &RepoRef) -> Result<Option<MirrorRecord>> {
        self.doc.load()?.repos.get(&repo.key())
    }

    /// Every registration. Each one names its own repository.
    ///
    /// **O(mirrors on the server)**, the listing path, exactly like
    /// [`crate::RepoCatalog::namespaces`]. Mirrors are not indexed by namespace
    /// because nothing looks one up that way: the push path never asks, and the
    /// only caller is an operator listing them.
    pub fn all(&self) -> Result<Vec<MirrorRecord>> {
        self.doc.load()?.repos.values()
    }

    /// Register `repo`'s upstream, replacing any earlier registration.
    pub fn set(&self, repo: &RepoRef, record: MirrorRecord) -> Result<MirrorRecord> {
        let key = repo.key();
        self.doc.update(|doc| {
            doc.repos.insert(key.clone(), record.clone())?;
            Ok(record.clone())
        })
    }

    /// Record the outcome of a sync against an existing registration.
    ///
    /// Returns `None` when there is no registration to record against — which
    /// is a different answer from "recorded a failure", and the caller must not
    /// collapse them: a sync of an unregistered repository is a bug in the
    /// caller, not a mirror that failed.
    pub fn record_sync(
        &self,
        repo: &RepoRef,
        at_unix_ms: i64,
        error: Option<&str>,
    ) -> Result<Option<MirrorRecord>> {
        let key = repo.key();
        let error = error.map(str::to_owned);
        self.doc.update(|doc| {
            let Some(mut record) = doc.repos.get(&key)? else {
                return Ok(None);
            };
            match &error {
                None => {
                    record.last_sync_at_unix_ms = Some(at_unix_ms);
                    record.last_error.clear();
                    record.sync_count += 1;
                    // A sync that landed made the repository if it was to come.
                    record.creates_repo = false;
                }
                // A failure does **not** move `last_sync_at_unix_ms`: that
                // field means "when this mirror was last correct", and moving
                // it on a failure would make a mirror that has been broken for
                // a week look fresh.
                Some(why) => record.last_error = why.clone(),
            }
            doc.repos.insert(key.clone(), record.clone())?;
            Ok(Some(record))
        })
    }

    /// **Say which kind of credential this mirror now has**, leaving every
    /// other field alone.
    ///
    /// Separate from [`MirrorStore::set`] for the reason `Mirror.SetCredential`
    /// is separate from `Mirror.Add`: `set` writes a whole record, so routing
    /// the credential through it would make every later re-registration —
    /// somebody flipping `prune` — silently forget the key. This edits the two
    /// fields and nothing else.
    ///
    /// **It does not touch the value.** The sealed secret is the caller's to
    /// write and to delete, and it is written FIRST: a document that claims a
    /// credential the store does not hold is a mirror that fails at dial time
    /// with "no credential stored", which is a confusing lie, whereas a sealed
    /// value with no document pointing at it is unreachable and harmless.
    ///
    /// Returns `None` when there is no registration to record against — a
    /// different answer from "recorded", and the caller must not collapse them.
    pub fn set_credential(
        &self,
        repo: &RepoRef,
        kind: MirrorCredentialKind,
        username: &str,
    ) -> Result<Option<MirrorRecord>> {
        let key = repo.key();
        let username = username.to_owned();
        self.doc.update(|doc| {
            let Some(mut record) = doc.repos.get(&key)? else {
                return Ok(None);
            };
            record.credential = kind;
            record.credential_username = if kind == MirrorCredentialKind::Token {
                username.clone()
            } else {
                // A username on an SSH key would be a field that is read by
                // nothing and shown to an operator as if it meant something.
                String::new()
            };
            doc.repos.insert(key.clone(), record.clone())?;
            Ok(Some(record))
        })
    }

    /// Say whether the last successful sync found Git LFS in use, leaving
    /// every other field alone. `None` when there is no registration.
    pub fn set_lfs_detected(&self, repo: &RepoRef, detected: bool) -> Result<Option<MirrorRecord>> {
        let key = repo.key();
        self.doc.update(|doc| {
            let Some(mut record) = doc.repos.get(&key)? else {
                return Ok(None);
            };
            if record.lfs_detected != detected {
                record.lfs_detected = detected;
                doc.repos.insert(key.clone(), record.clone())?;
            }
            Ok(Some(record))
        })
    }

    /// Remove `repo`'s registration, reporting whether there was one.
    pub fn remove(&self, repo: &RepoRef) -> Result<bool> {
        let key = repo.key();
        self.doc.update(|doc| Ok(doc.repos.remove(&key)?.is_some()))
    }
}