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");
}
}