Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/jera/src/policy.rs

//! **Job policy** — the thin classification at the heart of jera: *a job is one of
//! three kinds*, and the two that fire up an instance **delegate to draupnir**.
//!
//! ```text
//!   JobPolicy::Process   → run a built binary locally (jera's own Exe runner)
//!   JobPolicy::Vm        → draupnir KVM backend      (draupnir → tunnr)
//!   JobPolicy::Container → draupnir OCI backend      (podman/Docker REST)
//! ```
//!
//! jera owns *job* semantics — the durable ledger, the "start X as a tracked job"
//! lifecycle ([`crate::boot::BootJobs`]). It does **not** own instance-firing:
//! booting a VM or a container lives in exactly one place, [draupnir](draupnir),
//! and jera reaches it *through* the [`crate::vm`] / [`crate::container`] seams.
//! This enum is that policy made explicit: given a job kind, it says which
//! [`draupnir::Kind`] of instance (if any) it fires.
//!
//! A `Process` job is *not* a draupnir instance — spawning a built binary is a
//! plain local child process (the zero-shell charter targets container/VM control,
//! not running your own exe), so it has no draupnir backend.

use crate::jobs::kind;

/// Which of the three boot modes a target uses (fixed by the target's type).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootMode {
    /// A plain built binary — spawn it as a child process + console.
    Exe,
    /// A container image (`container::ContainerSpec`) — bring it up over the REST API.
    Container,
    /// A VM (`vm::BootSpec`) — boot it as a KVM guest.
    Vm,
}

impl BootMode {
    /// Stable label.
    pub fn as_str(&self) -> &'static str {
        match self {
            BootMode::Exe => "exe",
            BootMode::Container => "container",
            BootMode::Vm => "vm",
        }
    }

    /// The [`crate::jobs::kind`] a boot in this mode records.
    pub fn job_kind(&self) -> &'static str {
        match self {
            BootMode::Exe => kind::BOOT_EXE,
            BootMode::Container => kind::BOOT_CONTAINER,
            BootMode::Vm => kind::BOOT_VM,
        }
    }
}

/// The three kinds of job jera runs. `Vm`/`Container` delegate instance-firing to
/// draupnir; `Process` runs a built binary locally.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobPolicy {
    /// A built binary, spawned as a local child process (jera's own Exe runner).
    Process,
    /// A KVM guest, booted **through draupnir's KVM backend** (draupnir → tunnr).
    Vm,
    /// An OCI container, brought up **through draupnir's OCI backend** (podman/Docker REST).
    Container,
}

impl JobPolicy {
    /// Stable label.
    pub fn as_str(&self) -> &'static str {
        match self {
            JobPolicy::Process => "process",
            JobPolicy::Vm => "vm",
            JobPolicy::Container => "container",
        }
    }

    /// The [`BootMode`] this policy runs as (`Process` ↔ [`BootMode::Exe`]).
    pub fn boot_mode(&self) -> BootMode {
        match self {
            JobPolicy::Process => BootMode::Exe,
            JobPolicy::Vm => BootMode::Vm,
            JobPolicy::Container => BootMode::Container,
        }
    }

    /// Classify a [`BootMode`] into its policy ([`BootMode::Exe`] ↔ `Process`).
    pub fn from_boot_mode(mode: BootMode) -> Self {
        match mode {
            BootMode::Exe => JobPolicy::Process,
            BootMode::Vm => JobPolicy::Vm,
            BootMode::Container => JobPolicy::Container,
        }
    }

    /// Whether this job kind fires its instance **through draupnir** (`Vm`/`Container`),
    /// as opposed to running locally (`Process`).
    pub fn delegates_to_draupnir(&self) -> bool {
        matches!(self, JobPolicy::Vm | JobPolicy::Container)
    }

    /// The [`draupnir::Kind`] of instance this policy fires, or `None` for a
    /// local `Process` job (which is not a draupnir-provisioned instance).
    ///
    /// This is the single source of truth for "what kind of draupnir instance does
    /// this job boot" — the policy jera hands down to the boot library. Requires
    /// draupnir's default (pure-std) surface, always present.
    #[cfg(feature = "runners")]
    pub fn draupnir_kind(&self) -> Option<draupnir::Kind> {
        match self {
            JobPolicy::Process => None,
            JobPolicy::Vm => Some(draupnir::Kind::Vm),
            JobPolicy::Container => Some(draupnir::Kind::Container),
        }
    }
}

impl From<BootMode> for JobPolicy {
    fn from(mode: BootMode) -> Self {
        JobPolicy::from_boot_mode(mode)
    }
}

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

    #[test]
    fn policy_round_trips_with_boot_mode() {
        for (p, m) in [
            (JobPolicy::Process, BootMode::Exe),
            (JobPolicy::Vm, BootMode::Vm),
            (JobPolicy::Container, BootMode::Container),
        ] {
            assert_eq!(p.boot_mode(), m);
            assert_eq!(JobPolicy::from_boot_mode(m), p);
            assert_eq!(JobPolicy::from(m), p);
        }
    }

    #[test]
    fn vm_and_container_delegate_to_draupnir_process_does_not() {
        assert!(JobPolicy::Vm.delegates_to_draupnir());
        assert!(JobPolicy::Container.delegates_to_draupnir());
        assert!(!JobPolicy::Process.delegates_to_draupnir());
    }

    #[test]
    #[cfg(feature = "runners")]
    fn draupnir_kind_maps_vm_to_vm_and_container_to_container() {
        assert_eq!(JobPolicy::Vm.draupnir_kind(), Some(draupnir::Kind::Vm));
        assert_eq!(JobPolicy::Container.draupnir_kind(), Some(draupnir::Kind::Container));
        assert_eq!(JobPolicy::Process.draupnir_kind(), None);
    }

    #[test]
    fn labels_are_stable() {
        assert_eq!(JobPolicy::Process.as_str(), "process");
        assert_eq!(JobPolicy::Vm.as_str(), "vm");
        assert_eq!(JobPolicy::Container.as_str(), "container");
    }
}