1use serde::{Deserialize, Serialize};
16use tatara_build_remote::{BuildRef, BuildTransportChain};
17use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
18use tatara_wasm::{WasiPreview, WasmCapabilities, WasmFeatures, WasmRuntime};
19
20use crate::config::{NetworkSpec, ShareSpec, VmSpec};
21
22#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25#[tatara(keyword = "defguest")]
26pub struct GuestSpec {
27 pub name: String,
28
29 pub kind: GuestKind,
31
32 #[serde(default)]
34 pub cmdline: Vec<String>,
35
36 #[serde(default)]
38 pub network: NetworkSpec,
39
40 #[serde(default)]
41 pub shares: Vec<ShareSpec>,
42
43 #[serde(default)]
45 pub resources: ResourceLimits,
46
47 #[serde(default = "default_build_on")]
50 pub build_on: BuildTransportChain,
51}
52
53fn default_build_on() -> BuildTransportChain {
54 BuildTransportChain::quero_lol()
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(tag = "kind", rename_all = "kebab-case")]
60pub enum GuestKind {
61 Vm(VmSpec),
63
64 Wasm(WasmSpec),
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct WasmSpec {
72 #[serde(default)]
74 pub runtime: WasmRuntime,
75
76 #[serde(default)]
78 pub wasi_preview: WasiPreview,
79
80 pub component: BuildRef,
83
84 #[serde(default)]
86 pub features: WasmFeatures,
87
88 #[serde(default)]
97 pub capabilities: WasmCapabilities,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
102#[serde(rename_all = "camelCase")]
103pub struct ResourceLimits {
104 #[serde(default)]
106 pub memory_mib: Option<u32>,
107
108 #[serde(default)]
110 pub cpu_ms_budget: Option<u32>,
111
112 #[serde(default)]
114 pub fd_limit: Option<u32>,
115}
116
117impl GuestSpec {
118 #[must_use]
120 pub fn from_vm(spec: VmSpec) -> Self {
121 Self {
122 name: spec.name.clone(),
123 cmdline: spec.cmdline.clone(),
124 network: spec.network.clone(),
125 shares: spec.shares.clone(),
126 kind: GuestKind::Vm(spec),
127 resources: ResourceLimits::default(),
128 build_on: default_build_on(),
129 }
130 }
131
132 #[must_use]
134 pub const fn is_vm(&self) -> bool {
135 matches!(self.kind, GuestKind::Vm(_))
136 }
137
138 #[must_use]
140 pub const fn is_wasm(&self) -> bool {
141 matches!(self.kind, GuestKind::Wasm(_))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::config::Hypervisor;
149
150 #[test]
151 fn from_vm_wraps_cleanly() {
152 let vm = VmSpec::plex_default("plex");
153 let g = GuestSpec::from_vm(vm.clone());
154 assert_eq!(g.name, "plex");
155 assert!(g.is_vm());
156 assert!(!g.is_wasm());
157 match g.kind {
158 GuestKind::Vm(inner) => assert_eq!(inner.hypervisor, Hypervisor::Vfkit),
159 _ => panic!(),
160 }
161 }
162
163 #[test]
164 fn wasm_guest_serializes_with_runtime_variant() {
165 let g = GuestSpec {
166 name: "fast-fn".into(),
167 kind: GuestKind::Wasm(WasmSpec {
168 runtime: WasmRuntime::Wasmtime,
169 wasi_preview: WasiPreview::P2,
170 component: BuildRef::Flake {
171 url: "github:pleme-io/cors-proxy".into(),
172 attr: "wasi".into(),
173 },
174 capabilities: WasmCapabilities::default(),
175 features: WasmFeatures {
176 simd: true,
177 wasi_http: true,
178 ..Default::default()
179 },
180 }),
181 cmdline: vec![],
182 network: NetworkSpec::default(),
183 shares: vec![],
184 resources: ResourceLimits {
185 memory_mib: Some(64),
186 cpu_ms_budget: Some(100),
187 fd_limit: None,
188 },
189 build_on: BuildTransportChain::quero_lol(),
190 };
191 let j = serde_json::to_string(&g).unwrap();
192 assert!(j.contains("\"kind\":\"wasm\""));
193 assert!(j.contains("\"wasmtime\""));
194 assert!(j.contains("\"simd\":true"));
195
196 let back: GuestSpec = serde_json::from_str(&j).unwrap();
197 assert_eq!(back, g);
198 }
199
200 #[test]
201 fn default_build_on_targets_quero_lol() {
202 let d = default_build_on();
203 assert_eq!(d.attic.as_deref(), Some("quero.lol"));
204 assert_eq!(d.remote.as_deref(), Some("ssh://builder.quero.lol"));
205 assert!(d.local);
206 }
207
208 #[test]
209 fn defguest_vm_compiles_from_lisp() {
210 use tatara_lisp::domain::TataraDomain;
211 use tatara_lisp::reader;
212
213 let src = r#"(defguest :name "sample"
217 :kind (:kind "vm"
218 :name "sample"
219 :cpus 4
220 :memoryMib 4096
221 :hypervisor (:kind "Vfkit")
222 :kernel (:kind "Bridge" :attr_path "linuxPackages.kernel")
223 :rootfs (:kind "Bridge" :attr_path "minimal")
224 :network (:kind "Nat")
225 :cmdline ("console=hvc0" "init=/bin/tatara-init"))
226 :cmdline ())"#;
227 let forms = reader::read(src).expect("read");
228 let args = &forms[0].as_list().unwrap()[1..];
229 let guest = GuestSpec::compile_from_args(args).expect("compile defguest");
230 assert_eq!(guest.name, "sample");
231 assert!(guest.is_vm());
232 }
233
234 #[test]
235 fn defguest_wasm_compiles_from_lisp() {
236 use tatara_lisp::domain::TataraDomain;
237 use tatara_lisp::reader;
238
239 let src = r#"(defguest :name "fast-fn"
240 :kind (:kind "wasm"
241 :runtime "wasmtime"
242 :wasiPreview "p2"
243 :component (:kind "flake"
244 :value (:url "github:pleme-io/cors-proxy"
245 :attr "wasi"))
246 :features (:simd #t :wasiHttp #t))
247 :cmdline ())"#;
248 let forms = reader::read(src).expect("read");
249 let args = &forms[0].as_list().unwrap()[1..];
250 let guest = GuestSpec::compile_from_args(args).expect("compile defguest wasm");
251 assert_eq!(guest.name, "fast-fn");
252 assert!(guest.is_wasm());
253 match &guest.kind {
254 GuestKind::Wasm(w) => {
255 assert_eq!(w.runtime, WasmRuntime::Wasmtime);
256 assert_eq!(w.wasi_preview, WasiPreview::P2);
257 assert!(w.features.simd);
258 assert!(w.features.wasi_http);
259 }
260 _ => panic!(),
261 }
262 }
263}