5.1 KiB · text History d69022d
//! iroh-docs key conventions.
//!
//! All entries for a host live under the prefix `host/<host_id>/`:
//!
//! - `host/<id>/manifest`        — controller-written [`crate::Manifest`] (JSON)
//! - `host/<id>/status`          — agent-written [`crate::Status`] (JSON)
//! - `host/<id>/files/<path>`    — controller-written raw file body, one entry
//!   per [`crate::FileResource`], so bodies replicate alongside the manifest.
//!
//! Keys are opaque byte strings to iroh-docs; we use UTF-8. The agent never
//! parses file keys back into paths — it derives each key from the manifest's
//! file list — so embedding the destination path verbatim (slashes and all) is
//! fine and keeps entries human-inspectable.

use crate::HostId;

/// Top-level prefix under which every host's entries live.
pub const HOST_PREFIX: &str = "host/";

/// Key for a host's manifest entry.
pub fn manifest(host: &HostId) -> String {
    format!("host/{host}/manifest")
}

/// Key for a host's status entry.
pub fn status(host: &HostId) -> String {
    format!("host/{host}/status")
}

/// Key for a host's live service-state report ([`crate::ServiceReport`]),
/// written by the agent whenever a monitored service's runtime state changes.
pub fn service_report(host: &HostId) -> String {
    format!("host/{host}/service-states")
}

/// Key for a host's periodic system metrics ([`crate::HostMetrics`]) —
/// uptime and load average.
pub fn metrics(host: &HostId) -> String {
    format!("host/{host}/metrics")
}

/// Prefix matching every entry belonging to a host (manifest, status, files).
pub fn host_prefix(host: &HostId) -> String {
    format!("host/{host}/")
}

/// Prefix matching every file body entry for a host.
pub fn files_prefix(host: &HostId) -> String {
    format!("host/{host}/files/")
}

/// Key for a single file body entry, given its absolute destination path.
pub fn file(host: &HostId, dest_path: &str) -> String {
    // Destination paths are absolute (leading `/`); strip it so the key reads
    // `host/<id>/files/etc/...` rather than `host/<id>/files//etc/...`.
    let rel = dest_path.strip_prefix('/').unwrap_or(dest_path);
    format!("host/{host}/files/{rel}")
}

// ---- adoption keyspace -----------------------------------------------------
//
// Adoption is an *inbound proposal* from an agent: it never touches the
// controller-owned `manifest`/`files` entries. Instead the agent writes its
// discovered files under `host/<id>/adopt/`, which the operator reviews and
// accepts (turning them into normal managed config). Keeping it in a separate
// prefix means a proposal can never be mistaken for desired state.

/// Suffix identifying an adoption report entry; see [`host_from_adopt_key`].
pub const ADOPT_MANIFEST_SUFFIX: &str = "/adopt/manifest";

/// Key for a host's adoption report ([`crate::AdoptionReport`]).
pub fn adopt_manifest(host: &HostId) -> String {
    format!("host/{host}/adopt/manifest")
}

/// Prefix matching every adoption entry (report + proposed file bodies) for a
/// host — used to clear a proposal once accepted or dismissed.
pub fn adopt_prefix(host: &HostId) -> String {
    format!("host/{host}/adopt/")
}

/// Prefix matching a host's proposed file bodies.
pub fn adopt_files_prefix(host: &HostId) -> String {
    format!("host/{host}/adopt/files/")
}

/// Key for a single proposed file body, given its absolute source path on the
/// agent's machine.
pub fn adopt_file(host: &HostId, src_path: &str) -> String {
    let rel = src_path.strip_prefix('/').unwrap_or(src_path);
    format!("host/{host}/adopt/files/{rel}")
}

/// If `key` is an adoption-report key (`host/<id>/adopt/manifest`), return the
/// host id. Used to enumerate hosts with pending proposals.
pub fn host_from_adopt_key(key: &str) -> Option<&str> {
    key.strip_prefix("host/")?
        .strip_suffix(ADOPT_MANIFEST_SUFFIX)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn file_key_is_under_files_prefix() {
        let host = "web-01".to_string();
        let k = file(&host, "/etc/containers/systemd/web.container");
        assert!(k.starts_with(&files_prefix(&host)));
        assert!(k.starts_with(&host_prefix(&host)));
        assert_eq!(k, "host/web-01/files/etc/containers/systemd/web.container");
    }

    #[test]
    fn manifest_and_status_share_host_prefix() {
        let host = "db".to_string();
        assert!(manifest(&host).starts_with(&host_prefix(&host)));
        assert!(status(&host).starts_with(&host_prefix(&host)));
    }

    #[test]
    fn adopt_keys_are_isolated_from_managed_state() {
        let host = "web-01".to_string();
        // A proposed file must never collide with the managed files prefix.
        assert!(!adopt_file(&host, "/etc/x").starts_with(&files_prefix(&host)));
        assert!(adopt_file(&host, "/etc/x").starts_with(&adopt_files_prefix(&host)));
        assert!(adopt_manifest(&host).starts_with(&adopt_prefix(&host)));
    }

    #[test]
    fn host_from_adopt_key_roundtrips() {
        let host = "web-01".to_string();
        assert_eq!(host_from_adopt_key(&adopt_manifest(&host)), Some("web-01"));
        assert_eq!(host_from_adopt_key(&manifest(&host)), None);
        assert_eq!(host_from_adopt_key("garbage"), None);
    }
}