gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! Push attestation, rendered as one JSON line.
//!
//! At receive-pack time gunnar already knows who pushed, with which key, into
//! which repo, which refs moved and which objects are new. That is a
//! provenance record for free, and gunnar does exactly two things with it:
//!
//! 1. emits a `PushAttestation` on the event stream ([`crate::bus`]), and
//! 2. appends **one JSON line** (this function) to a log in its own store.
//!
//! It does **not** link the Build Thing, nornir, skade, arrow or parquet.
//! Anything that wants a warehouse row subscribes to the event and writes the
//! row itself. The coupling is a proto message, not a dependency edge. That is
//! the only shape that survives the appliance case, where none of those crates
//! could be linked at all.

use gunnar_proto::PushAttestation;

/// Render an attestation as a single JSONL record: no trailing newline, and no
/// embedded one either, so a caller can append it with exactly one `\n` and a
/// reader can split on `\n` without a parser.
pub fn attestation_json_line(attestation: &PushAttestation) -> Result<String, serde_json::Error> {
    serde_json::to_string(attestation)
}

#[cfg(test)]
mod tests {
    use super::*;
    use gunnar_proto::{HashKind, RefUpdate, RepoPath};

    fn sample() -> PushAttestation {
        PushAttestation {
            attestation_id: "att-1".into(),
            repo: Some(RepoPath::new("nordisk", "gunnar")),
            principal_id: "principal-00000001".into(),
            principal_name: "alice".into(),
            key_fingerprint: "SHA256:abc".into(),
            credential: "key:SHA256:abc".into(),
            hash_kind: HashKind::Sha256 as i32,
            ref_updates: vec![RefUpdate {
                ref_name: "refs/heads/main".into(),
                old_oid: "a".repeat(64),
                new_oid: "b".repeat(64),
            }],
            new_object_oids: vec!["c".repeat(64)],
            new_object_count: 1,
            // Empty: this sample's object set is known.
            new_object_oids_unknown: String::new(),
            new_object_bytes: 4096,
            pack_hash: "d".repeat(64),
            received_at_unix_ms: 1_700_000_000_000,
            server_agent: gunnar_proto::AGENT.into(),
            transport: "ssh".into(),
        }
    }

    #[test]
    fn renders_exactly_one_line() {
        let line = attestation_json_line(&sample()).expect("serialises");
        assert!(
            !line.contains('\n'),
            "a JSONL record must not contain a newline: {line}"
        );
        assert_eq!(line.lines().count(), 1);
    }

    #[test]
    fn carries_the_facts_receive_pack_already_had() {
        let line = attestation_json_line(&sample()).expect("serialises");
        // Assert the APPLIED output (the actual bytes a subscriber would
        // read), not that serialisation returned Ok.
        for expected in [
            "\"namespace\":\"nordisk\"",
            "\"repo\":\"gunnar\"",
            "\"principal_name\":\"alice\"",
            "\"key_fingerprint\":\"SHA256:abc\"",
            "\"ref_name\":\"refs/heads/main\"",
            "\"new_object_count\":1",
            "\"transport\":\"ssh\"",
        ] {
            assert!(line.contains(expected), "missing {expected} in {line}");
        }
    }

    /// **An empty object list carries the reason it is empty, on the durable
    /// line too.**
    ///
    /// The JSONL record is the copy that survives, and it is read by things
    /// that have no gunnar to ask. A record whose `new_object_oids` is `[]`
    /// with nothing beside it says "this push introduced no objects", which is
    /// a different fact from "nobody worked out which objects these were" —
    /// and on the engine the appliance ships the second is the ordinary case.
    #[test]
    fn an_unenumerated_record_says_so_on_the_line_rather_than_showing_an_empty_list() {
        let mut att = sample();
        att.new_object_oids.clear();
        att.new_object_oids_unknown =
            "the policy has no gate over objects, so the object set was not enumerated".to_owned();
        att.new_object_count = 3;
        let line = attestation_json_line(&att).expect("serialises");
        assert!(
            line.contains("\"new_object_oids\":[]")
                && line.contains("\"new_object_count\":3")
                && line.contains("was not enumerated"),
            "the line must carry the empty list, the count that is still true and the reason \
             the two disagree: {line}"
        );
        // The other direction, or the assertion above would pass on a writer
        // that always emitted the sentence.
        let enumerated = attestation_json_line(&sample()).expect("serialises");
        assert!(
            enumerated.contains("\"new_object_oids_unknown\":\"\""),
            "an enumerated set must leave the reason EMPTY, or every reader is told the list \
             is untrustworthy: {enumerated}"
        );
    }
}