use serde::{Deserialize, Serialize};
use tatara_build_remote::{BuildRef, BuildTransportChain};
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
use tatara_wasm::{WasiPreview, WasmFeatures, WasmRuntime};
use crate::config::{NetworkSpec, ShareSpec, VmSpec};
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defguest")]
pub struct GuestSpec {
pub name: String,
pub kind: GuestKind,
#[serde(default)]
pub cmdline: Vec<String>,
#[serde(default)]
pub network: NetworkSpec,
#[serde(default)]
pub shares: Vec<ShareSpec>,
#[serde(default)]
pub resources: ResourceLimits,
#[serde(default = "default_build_on")]
pub build_on: BuildTransportChain,
}
fn default_build_on() -> BuildTransportChain {
BuildTransportChain::quero_lol()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum GuestKind {
Vm(VmSpec),
Wasm(WasmSpec),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WasmSpec {
#[serde(default)]
pub runtime: WasmRuntime,
#[serde(default)]
pub wasi_preview: WasiPreview,
pub component: BuildRef,
#[serde(default)]
pub features: WasmFeatures,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ResourceLimits {
#[serde(default)]
pub memory_mib: Option<u32>,
#[serde(default)]
pub cpu_ms_budget: Option<u32>,
#[serde(default)]
pub fd_limit: Option<u32>,
}
impl GuestSpec {
#[must_use]
pub fn from_vm(spec: VmSpec) -> Self {
Self {
name: spec.name.clone(),
cmdline: spec.cmdline.clone(),
network: spec.network.clone(),
shares: spec.shares.clone(),
kind: GuestKind::Vm(spec),
resources: ResourceLimits::default(),
build_on: default_build_on(),
}
}
#[must_use]
pub const fn is_vm(&self) -> bool {
matches!(self.kind, GuestKind::Vm(_))
}
#[must_use]
pub const fn is_wasm(&self) -> bool {
matches!(self.kind, GuestKind::Wasm(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Hypervisor;
#[test]
fn from_vm_wraps_cleanly() {
let vm = VmSpec::plex_default("plex");
let g = GuestSpec::from_vm(vm.clone());
assert_eq!(g.name, "plex");
assert!(g.is_vm());
assert!(!g.is_wasm());
match g.kind {
GuestKind::Vm(inner) => assert_eq!(inner.hypervisor, Hypervisor::Vfkit),
_ => panic!(),
}
}
#[test]
fn wasm_guest_serializes_with_runtime_variant() {
let g = GuestSpec {
name: "fast-fn".into(),
kind: GuestKind::Wasm(WasmSpec {
runtime: WasmRuntime::Wasmtime,
wasi_preview: WasiPreview::P2,
component: BuildRef::Flake {
url: "github:pleme-io/cors-proxy".into(),
attr: "wasi".into(),
},
features: WasmFeatures {
simd: true,
wasi_http: true,
..Default::default()
},
}),
cmdline: vec![],
network: NetworkSpec::default(),
shares: vec![],
resources: ResourceLimits {
memory_mib: Some(64),
cpu_ms_budget: Some(100),
fd_limit: None,
},
build_on: BuildTransportChain::quero_lol(),
};
let j = serde_json::to_string(&g).unwrap();
assert!(j.contains("\"kind\":\"wasm\""));
assert!(j.contains("\"wasmtime\""));
assert!(j.contains("\"simd\":true"));
let back: GuestSpec = serde_json::from_str(&j).unwrap();
assert_eq!(back, g);
}
#[test]
fn default_build_on_targets_quero_lol() {
let d = default_build_on();
assert_eq!(d.attic.as_deref(), Some("quero.lol"));
assert_eq!(d.remote.as_deref(), Some("ssh://builder.quero.lol"));
assert!(d.local);
}
#[test]
fn defguest_vm_compiles_from_lisp() {
use tatara_lisp::domain::TataraDomain;
use tatara_lisp::reader;
let src = r#"(defguest :name "sample"
:kind (:kind "vm"
:name "sample"
:cpus 4
:memoryMib 4096
:hypervisor (:kind "Vfkit")
:kernel (:kind "Bridge" :attr_path "linuxPackages.kernel")
:rootfs (:kind "Bridge" :attr_path "minimal")
:network (:kind "Nat")
:cmdline ("console=hvc0" "init=/bin/tatara-init"))
:cmdline ())"#;
let forms = reader::read(src).expect("read");
let args = &forms[0].as_list().unwrap()[1..];
let guest = GuestSpec::compile_from_args(args).expect("compile defguest");
assert_eq!(guest.name, "sample");
assert!(guest.is_vm());
}
#[test]
fn defguest_wasm_compiles_from_lisp() {
use tatara_lisp::domain::TataraDomain;
use tatara_lisp::reader;
let src = r#"(defguest :name "fast-fn"
:kind (:kind "wasm"
:runtime "wasmtime"
:wasiPreview "p2"
:component (:kind "flake"
:value (:url "github:pleme-io/cors-proxy"
:attr "wasi"))
:features (:simd #t :wasiHttp #t))
:cmdline ())"#;
let forms = reader::read(src).expect("read");
let args = &forms[0].as_list().unwrap()[1..];
let guest = GuestSpec::compile_from_args(args).expect("compile defguest wasm");
assert_eq!(guest.name, "fast-fn");
assert!(guest.is_wasm());
match &guest.kind {
GuestKind::Wasm(w) => {
assert_eq!(w.runtime, WasmRuntime::Wasmtime);
assert_eq!(w.wasi_preview, WasiPreview::P2);
assert!(w.features.simd);
assert!(w.features.wasi_http);
}
_ => panic!(),
}
}
}