cellos-supervisor 0.5.1

CellOS execution-cell runner — boots cells in Firecracker microVMs or gVisor, enforces narrow typed authority, emits signed CloudEvents.
Documentation
//! Helpers that translate `spec.run.limits` into cgroup v2 controller-file payloads.
//!
//! **Doctrine — D1 (no ambient defaults):**
//! - `spec.run.limits.cpuMax` is the **primary** source for the cgroup `cpu.max`
//!   controller value applied to the host-subprocess backend.
//! - `CELLOS_CGROUP_CPU_MAX` is an **explicit** operator override. It only
//!   applies when the spec did **not** set `cpuMax`. Setting the env var alone
//!   is *not* a default — operators opt in by exporting it.
//!
//! These helpers are pure (no I/O, no `std::env::var` reads at the call sites
//! that exercise them in unit tests) so the resolution + normalization rules
//! can be tested directly without a real cgroup v2 hierarchy. The actual file
//! write is performed by `supervisor::linux_cgroup_write_optional_controller_files`.
//!
//! These helpers are platform-agnostic at the type level — they operate on
//! strings — but only the Linux subprocess path consumes their output.

use cellos_core::types::RunLimits;

/// Default cgroup v2 CPU period when the spec omits `periodMicros`. Matches the
/// schema description (`Defaults to 100000 when omitted by the runtime.`) and
/// the kernel default for `cpu.max`.
pub const DEFAULT_CPU_PERIOD_MICROS: u64 = 100_000;

/// Resolve the `cpu.max` payload for the host-subprocess cgroup leaf.
///
/// Resolution order (highest priority first):
/// 1. `spec.run.limits.cpuMax` — typed source of truth from the contract.
/// 2. `env_cpu_max` — operator-supplied `CELLOS_CGROUP_CPU_MAX` override; used
///    only when the spec is silent. The caller passes the env value verbatim
///    (or `None` when unset / empty); validation/normalization happens here.
///
/// Returns `Some(payload)` when a value should be written to the leaf's
/// `cpu.max` file (without a trailing newline; the writer adds one). Returns
/// `None` when neither source produced an applicable value, or when the env
/// override was syntactically invalid (caller should warn — see
/// `cpu_max_env_validation_error` for the diagnostic string).
///
/// Accepted env-override syntaxes (per cgroup v2 `cpu.max`):
/// - `max`                              → `"max 100000"`
/// - `max <period>`                     → `"max <period>"`
/// - `<quota>`                          → `"<quota> 100000"`
/// - `<quota> <period>`                 → `"<quota> <period>"`
///
/// Whitespace is collapsed; `quota` and `period` must parse as `u64` and be
/// `>= 1` (cgroup v2 rejects 0). Anything else returns `None` here and the
/// caller is expected to log a warning.
pub fn cpu_max_to_write(
    spec_limits: Option<&RunLimits>,
    env_cpu_max: Option<&str>,
) -> Option<String> {
    // Spec wins.
    if let Some(cpu) = spec_limits.and_then(|l| l.cpu_max.as_ref()) {
        if cpu.quota_micros == 0 {
            // Spec validation should have rejected this; defend in depth.
            return None;
        }
        let period = cpu
            .period_micros
            .unwrap_or(DEFAULT_CPU_PERIOD_MICROS)
            .max(1);
        let quota = cpu.quota_micros;
        return Some(format!("{quota} {period}"));
    }
    // Fall back to explicit env override.
    let raw = env_cpu_max?.trim();
    if raw.is_empty() {
        return None;
    }
    normalize_env_cpu_max(raw)
}

/// Validate + normalize `CELLOS_CGROUP_CPU_MAX` into the cgroup v2 wire format.
///
/// Returns `None` for syntactically invalid input — the caller should warn.
fn normalize_env_cpu_max(raw: &str) -> Option<String> {
    let mut parts = raw.split_ascii_whitespace();
    let first = parts.next()?;
    let second = parts.next();
    if parts.next().is_some() {
        return None; // too many tokens
    }
    let quota = match first {
        "max" => "max".to_string(),
        n => {
            let v: u64 = n.parse().ok()?;
            if v == 0 {
                return None;
            }
            v.to_string()
        }
    };
    let period: u64 = match second {
        Some(p) => {
            let v: u64 = p.parse().ok()?;
            if v == 0 {
                return None;
            }
            v
        }
        None => DEFAULT_CPU_PERIOD_MICROS,
    };
    Some(format!("{quota} {period}"))
}

/// Whether the env override produced an unusable value. Useful for the
/// supervisor's warn log when the spec is silent and the env value is
/// malformed.
pub fn cpu_max_env_validation_error(env_cpu_max: Option<&str>) -> Option<&'static str> {
    let raw = env_cpu_max?.trim();
    if raw.is_empty() {
        return None;
    }
    if normalize_env_cpu_max(raw).is_none() {
        Some("expected `max`, `<quota_micros>`, `max <period_micros>`, or `<quota_micros> <period_micros>` (each integer >= 1)")
    } else {
        None
    }
}

/// Outcome of [`apply_cpu_max_to_leaf`] — useful for tests and structured logs.
#[derive(Debug, PartialEq, Eq)]
pub enum CpuMaxApplyOutcome {
    /// `cpu.max` was written. Carries the source label and the exact payload
    /// (without trailing newline) that hit the file.
    Wrote {
        /// Which input produced the payload (spec vs. env override).
        source: CpuMaxSource,
        /// The exact `cpu.max` payload written (no trailing newline).
        payload: String,
    },
    /// No applicable input — spec silent and env unset/empty.
    Skipped,
    /// Spec silent and env override was malformed; caller should warn.
    InvalidEnvOverride,
    /// Write failed (permissions, controller not delegated, etc.). Carries a
    /// human-readable diagnostic.
    WriteError(String),
}

/// Which source produced the `cpu.max` payload.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum CpuMaxSource {
    /// Value came from `spec.run.limits.cpuMax`.
    Spec,
    /// Value came from `CELLOS_CGROUP_CPU_MAX`.
    EnvOverride,
}

/// Apply the resolved `cpu.max` payload to `<leaf>/cpu.max`, if any.
///
/// This is the side-effecting wrapper used by the host-subprocess backend; it
/// is intentionally separated from [`cpu_max_to_write`] so the resolution
/// rules stay testable on every platform while the file write is exercised by
/// Linux-only integration tests.
///
/// The function never panics. Callers translate the returned outcome into
/// `tracing::warn!` events with their own structured fields.
pub fn apply_cpu_max_to_leaf(
    leaf: &std::path::Path,
    spec_limits: Option<&RunLimits>,
    env_cpu_max: Option<&str>,
) -> CpuMaxApplyOutcome {
    if let Some(payload) = cpu_max_to_write(spec_limits, env_cpu_max) {
        let source = if spec_limits.and_then(|l| l.cpu_max.as_ref()).is_some() {
            CpuMaxSource::Spec
        } else {
            CpuMaxSource::EnvOverride
        };
        let target = leaf.join("cpu.max");
        match std::fs::write(&target, format!("{payload}\n")) {
            Ok(()) => CpuMaxApplyOutcome::Wrote { source, payload },
            Err(e) => CpuMaxApplyOutcome::WriteError(format!("{}: {e}", target.display())),
        }
    } else if spec_limits.and_then(|l| l.cpu_max.as_ref()).is_none()
        && cpu_max_env_validation_error(env_cpu_max).is_some()
    {
        CpuMaxApplyOutcome::InvalidEnvOverride
    } else {
        CpuMaxApplyOutcome::Skipped
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cellos_core::types::{RunCpuMax, RunLimits};

    fn limits_with_cpu(quota: u64, period: Option<u64>) -> RunLimits {
        RunLimits {
            memory_max_bytes: None,
            cpu_max: Some(RunCpuMax {
                quota_micros: quota,
                period_micros: period,
            }),
            graceful_shutdown_seconds: None,
        }
    }

    #[test]
    fn spec_with_quota_and_period_wins() {
        let l = limits_with_cpu(50_000, Some(100_000));
        assert_eq!(
            cpu_max_to_write(Some(&l), Some("max 200000")),
            Some("50000 100000".to_string())
        );
    }

    #[test]
    fn spec_quota_only_uses_default_period() {
        let l = limits_with_cpu(75_000, None);
        assert_eq!(
            cpu_max_to_write(Some(&l), None),
            Some("75000 100000".to_string())
        );
    }

    #[test]
    fn spec_quota_zero_returns_none_defense_in_depth() {
        let l = limits_with_cpu(0, None);
        assert_eq!(cpu_max_to_write(Some(&l), Some("50000 100000")), None);
    }

    #[test]
    fn env_override_used_when_spec_absent() {
        assert_eq!(
            cpu_max_to_write(None, Some("50000 100000")),
            Some("50000 100000".to_string())
        );
    }

    #[test]
    fn env_max_alone_normalizes_with_default_period() {
        assert_eq!(
            cpu_max_to_write(None, Some("max")),
            Some("max 100000".to_string())
        );
    }

    #[test]
    fn env_max_with_period() {
        assert_eq!(
            cpu_max_to_write(None, Some("max 200000")),
            Some("max 200000".to_string())
        );
    }

    #[test]
    fn env_quota_alone_uses_default_period() {
        assert_eq!(
            cpu_max_to_write(None, Some("25000")),
            Some("25000 100000".to_string())
        );
    }

    #[test]
    fn env_whitespace_collapsed() {
        assert_eq!(
            cpu_max_to_write(None, Some("  50000\t  100000  ")),
            Some("50000 100000".to_string())
        );
    }

    #[test]
    fn env_invalid_returns_none() {
        assert_eq!(cpu_max_to_write(None, Some("not-a-number")), None);
        assert_eq!(cpu_max_to_write(None, Some("0 100000")), None);
        assert_eq!(cpu_max_to_write(None, Some("50000 0")), None);
        assert_eq!(cpu_max_to_write(None, Some("50000 100000 200000")), None);
        assert_eq!(cpu_max_to_write(None, Some("-50000")), None);
    }

    #[test]
    fn env_empty_treated_as_unset() {
        assert_eq!(cpu_max_to_write(None, Some("")), None);
        assert_eq!(cpu_max_to_write(None, Some("   ")), None);
        assert_eq!(cpu_max_to_write(None, None), None);
    }

    #[test]
    fn env_validation_error_diagnostic() {
        assert!(cpu_max_env_validation_error(Some("not-a-number")).is_some());
        assert!(cpu_max_env_validation_error(Some("0")).is_some());
        assert!(cpu_max_env_validation_error(Some("50000 100000")).is_none());
        assert!(cpu_max_env_validation_error(Some("max")).is_none());
        assert!(cpu_max_env_validation_error(None).is_none());
        assert!(cpu_max_env_validation_error(Some("")).is_none());
    }

    /// Doctrine D1 — env var alone cannot create a default; spec wins when both
    /// are present.
    #[test]
    fn spec_overrides_env_when_both_present() {
        let l = limits_with_cpu(10_000, Some(50_000));
        assert_eq!(
            cpu_max_to_write(Some(&l), Some("max 200000")),
            Some("10000 50000".to_string())
        );
    }
}