1use std::collections::{BTreeMap, HashMap};
20use std::io::{BufRead, Write};
21use std::path::{Path, PathBuf};
22use std::sync::{Arc, Mutex};
23use std::time::Duration;
24
25use serde::Deserialize;
26use serde::de::DeserializeOwned;
27use serde_json::{Value, json};
28
29use crate::compose::{self, LoadOptions};
30use crate::error::{Error, Result};
31use crate::exec::{self, ExecController, ExecEvent, ExecOptions, Stdin};
32use crate::plan::{DiffOptions, VolumeDefs};
33use crate::sandbox::{self, EnsureOptions, LabelFilter, Sandbox};
34use crate::spec::{ExecDefaults, PortSpec, ReadyCheck, SandboxSpec};
35use crate::{Client, flex};
36
37pub const PROTOCOL: u32 = 1;
39
40type Out = Arc<Mutex<Box<dyn Write + Send>>>;
41
42enum Queued {
44 Write(Vec<u8>),
45 CloseStdin,
46 Signal(i32),
47 Resize(u16, u16),
48}
49
50enum ExecSlot {
54 Pending(Vec<Queued>),
55 Running(ExecController),
56}
57
58struct Server {
59 out: Out,
60 client: Client,
61 execs: Mutex<HashMap<String, ExecSlot>>,
62}
63
64#[derive(Deserialize)]
65struct Request {
66 #[serde(default)]
67 id: Value,
68 method: String,
69 #[serde(default)]
70 params: Value,
71}
72
73pub fn serve<R: BufRead, W: Write + Send + 'static>(
75 input: R,
76 output: W,
77 client: Client,
78) -> Result<()> {
79 let server = Arc::new(Server {
80 out: Arc::new(Mutex::new(Box::new(output))),
81 client,
82 execs: Mutex::new(HashMap::new()),
83 });
84 server.send(&json!({"isb": env!("CARGO_PKG_VERSION"), "protocol": PROTOCOL}));
85 let mut workers = Vec::new();
86 for line in input.lines() {
87 let line = line?;
88 if line.trim().is_empty() {
89 continue;
90 }
91 let req: Request = match serde_json::from_str(&line) {
92 Ok(r) => r,
93 Err(e) => {
94 server.send(&json!({"id": null, "error": {"code": "bad_request", "message": format!("not a request: {e}")}}));
95 continue;
96 }
97 };
98 if req.method == "sandbox.exec" {
99 server
100 .execs
101 .lock()
102 .unwrap_or_else(|p| p.into_inner())
103 .insert(exec_key(&req.id), ExecSlot::Pending(Vec::new()));
104 }
105 if req.method.starts_with("exec.") {
107 server.clone().handle(req);
108 continue;
109 }
110 let s = server.clone();
111 workers.push(std::thread::spawn(move || s.handle(req)));
112 workers.retain(|w| !w.is_finished());
113 }
114 for w in workers {
117 let _ = w.join();
118 }
119 Ok(())
120}
121
122impl Server {
123 fn send(&self, v: &Value) {
124 let mut o = self.out.lock().unwrap_or_else(|p| p.into_inner());
125 let _ = writeln!(o, "{v}");
126 let _ = o.flush();
127 }
128
129 fn event(&self, id: &Value, event: &str, data: Value) {
130 self.send(&json!({"id": id, "event": event, "data": data}));
131 }
132
133 fn handle(self: Arc<Self>, req: Request) {
134 let id = req.id.clone();
135 let r = self.dispatch(&req);
136 match r {
137 Ok(v) => self.send(&json!({"id": id, "result": v})),
138 Err(e) => self.send(&json!({"id": id, "error": error_json(&e)})),
139 }
140 }
141
142 fn client(&self, project: &Option<String>) -> Client {
143 match project {
144 Some(p) => self.client.clone().project(p),
145 None => self.client.clone(),
146 }
147 }
148
149 #[expect(
150 clippy::too_many_lines,
151 reason = "predates the lint ratchet; split it when next changed"
152 )]
153 fn dispatch(&self, req: &Request) -> Result<Value> {
154 let id = &req.id;
155 let p = &req.params;
156 let mut progress = |line: &str| self.event(id, "progress", json!(line));
157 match req.method.as_str() {
158 "version" => Ok(json!({"isb": env!("CARGO_PKG_VERSION"), "protocol": PROTOCOL})),
159 "schema" => Ok(crate::spec::compose_schema()),
160
161 "sandbox.create" => {
162 let a: SpecParams = params(p)?;
163 let c = self.client(&a.project);
164 let opts = EnsureOptions {
165 wait_ready: a.wait_ready,
166 ..Default::default()
167 };
168 let d = resolve_spec(&c, &a)?;
169 let sb = create_desired(&c, &d, opts, &mut progress)?;
170 Ok(json!(sb.info()?))
171 }
172 "sandbox.ensure" => {
173 let a: SpecParams = params(p)?;
174 let c = self.client(&a.project);
175 let opts = EnsureOptions {
176 wait_ready: a.wait_ready,
177 diff: DiffOptions {
178 prune_devices: a.prune_devices,
179 },
180 ..Default::default()
181 };
182 let d = resolve_spec(&c, &a)?;
183 let report = sandbox::ensure(&c, &d, opts, &mut progress)?;
184 let info = Sandbox::get(&c, &d.name)?.info()?;
185 Ok(json!({"info": info, "report": report}))
186 }
187 "sandbox.plan" => {
188 let a: SpecParams = params(p)?;
189 let c = self.client(&a.project);
190 let d = resolve_spec(&c, &a)?;
191 Ok(json!(sandbox::plan_desired(
192 &c,
193 &d,
194 DiffOptions {
195 prune_devices: a.prune_devices
196 }
197 )?))
198 }
199 "sandbox.resolve" => {
200 let a: SpecParams = params(p)?;
201 let c = self.client(&a.project);
202 Ok(json!(resolve_spec(&c, &a)?))
203 }
204 "sandbox.get" => {
205 let a: NameParams = params(p)?;
206 Ok(json!(
207 Sandbox::get(&self.client(&a.project), &a.name)?.info()?
208 ))
209 }
210 "sandbox.list" => {
211 let a: ListParams = params(p)?;
212 let filters: Vec<LabelFilter> =
213 a.labels.iter().map(|l| LabelFilter::parse(l)).collect();
214 Ok(json!(Sandbox::list_with(
215 &self.client(&a.project),
216 &filters
217 )?))
218 }
219 "sandbox.remove" => {
220 let a: RemoveParams = params(p)?;
221 Sandbox::remove(&self.client(&a.project), &a.name, a.force)?;
222 Ok(Value::Null)
223 }
224 "sandbox.start" => {
225 let a: NameParams = params(p)?;
226 Sandbox::get(&self.client(&a.project), &a.name)?.start()?;
227 Ok(Value::Null)
228 }
229 "sandbox.stop" => {
230 let a: StopParams = params(p)?;
231 let t = match &a.timeout {
232 Some(s) => flex::parse_duration(s).map_err(Error::Invalid)?,
233 None => Duration::from_secs(30),
234 };
235 Sandbox::get(&self.client(&a.project), &a.name)?.stop(a.force, t)?;
236 Ok(Value::Null)
237 }
238 "sandbox.restart" => {
239 let a: NameParams = params(p)?;
240 Sandbox::get(&self.client(&a.project), &a.name)?.restart()?;
241 Ok(Value::Null)
242 }
243 "sandbox.wait_ready" => {
244 let a: ReadyParams = params(p)?;
245 let t = match &a.ready_timeout {
246 Some(s) => flex::parse_duration(s).map_err(Error::Invalid)?,
247 None => Duration::from_secs(60),
248 };
249 let checks = a.ready.unwrap_or_else(|| vec![ReadyCheck::Running]);
250 sandbox::wait_ready(&self.client(&a.project), &a.name, &checks, t, &a.exec)?;
251 Ok(Value::Null)
252 }
253 "sandbox.exec" => {
254 let r = self.exec(id, p);
255 if r.is_err() {
256 self.execs
257 .lock()
258 .unwrap_or_else(|p| p.into_inner())
259 .remove(&exec_key(id));
260 }
261 r
262 }
263 "exec.write" => {
264 let a: ExecWriteParams = params(p)?;
265 let data = b64_decode(&a.data)?;
266 self.control(&a.exec, Queued::Write(data))
267 }
268 "exec.close_stdin" => {
269 let a: ExecRef = params(p)?;
270 self.control(&a.exec, Queued::CloseStdin)
271 }
272 "exec.signal" => {
273 let a: ExecSignalParams = params(p)?;
274 self.control(&a.exec, Queued::Signal(a.signal))
275 }
276 "exec.resize" => {
277 let a: ExecResizeParams = params(p)?;
278 self.control(&a.exec, Queued::Resize(a.width, a.height))
279 }
280 "sandbox.add_port" => {
281 let a: AddPortParams = params(p)?;
282 let listen = Sandbox::get(&self.client(&a.project), &a.name)?.add_port(&a.port)?;
283 Ok(json!({"listen": listen}))
284 }
285 "sandbox.remove_device" => {
286 let a: DeviceParams = params(p)?;
287 let removed =
288 Sandbox::get(&self.client(&a.project), &a.name)?.remove_device(&a.device)?;
289 Ok(json!({"removed": removed}))
290 }
291
292 "volume.list" => {
293 let a: PoolParams = params(p)?;
294 let c = self.client(&a.project);
295 let pools = match a.pool {
296 Some(p) => vec![p],
297 None => sandbox::host_facts(&c)?.pools,
298 };
299 let mut all = Vec::new();
300 for pool in pools {
301 all.extend(crate::volume::list(&c, &pool)?);
302 }
303 Ok(json!(all))
304 }
305 "volume.get" => {
306 let a: VolumeParams = params(p)?;
307 let c = self.client(&a.project);
308 let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
309 crate::volume::get(&c, &pool, &a.name)?
310 .map(|v| json!(v))
311 .ok_or_else(|| Error::NotFound(format!("volume {} in pool {pool}", a.name)))
312 }
313 "volume.create" => {
314 let a: VolumeParams = params(p)?;
315 let c = self.client(&a.project);
316 let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
317 let created = crate::volume::ensure(&c, &pool, &a.name, &a.config)?;
318 Ok(json!({"created": created, "pool": pool}))
319 }
320 "volume.remove" => {
321 let a: VolumeParams = params(p)?;
322 let c = self.client(&a.project);
323 let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
324 crate::volume::remove(&c, &pool, &a.name)?;
325 Ok(Value::Null)
326 }
327 "prune" => {
328 let a: PruneParams = params(p)?;
329 Ok(json!(sandbox::prune_missing_path(
330 &self.client(&a.project),
331 &a.label,
332 a.dry_run,
333 &mut progress
334 )?))
335 }
336
337 "compose.load" => {
338 let a: ComposeParams = params(p)?;
339 let proj = compose::load(&a.load())?;
340 Ok(
341 json!({"name": proj.name, "base_dir": proj.base_dir, "files": proj.files, "file": proj.file}),
342 )
343 }
344 "compose.up" => {
345 let a: ComposeParams = params(p)?;
346 let proj = compose::load(&a.load())?;
347 let opts = EnsureOptions {
348 wait_ready: a.wait_ready,
349 diff: DiffOptions {
350 prune_devices: a.prune_devices,
351 },
352 ..Default::default()
353 };
354 let out = compose::up(
355 &self.client(&a.project),
356 &proj,
357 &a.services,
358 opts,
359 &mut progress,
360 )?;
361 Ok(json!(
362 out.into_iter()
363 .map(|(s, r)| json!({"service": s, "report": r}))
364 .collect::<Vec<_>>()
365 ))
366 }
367 "compose.plan" => {
368 let a: ComposeParams = params(p)?;
369 let proj = compose::load(&a.load())?;
370 Ok(json!(compose::plan(
371 &self.client(&a.project),
372 &proj,
373 &a.services,
374 DiffOptions {
375 prune_devices: a.prune_devices
376 }
377 )?))
378 }
379 "compose.down" => {
380 let a: ComposeParams = params(p)?;
381 let proj = compose::load(&a.load())?;
382 compose::down(
383 &self.client(&a.project),
384 &proj,
385 &a.services,
386 a.volumes,
387 &mut progress,
388 )?;
389 Ok(Value::Null)
390 }
391 other => Err(Error::Protocol(format!("unknown method {other:?}"))),
392 }
393 }
394
395 fn control(&self, exec: &Value, op: Queued) -> Result<Value> {
397 let key = exec_key(exec);
398 let mut execs = self.execs.lock().unwrap_or_else(|p| p.into_inner());
399 match execs.get_mut(&key) {
400 Some(ExecSlot::Pending(q)) => {
401 q.push(op);
402 Ok(Value::Null)
403 }
404 Some(ExecSlot::Running(c)) => {
405 let c = c.clone();
406 drop(execs);
407 apply_op(&c, op)?;
408 Ok(Value::Null)
409 }
410 None => Err(Error::NotFound(format!("running exec {key}"))),
411 }
412 }
413
414 fn exec(&self, id: &Value, p: &Value) -> Result<Value> {
415 let a: ExecParams = params(p)?;
416 let c = self.client(&a.project);
417 let stdin = match &a.stdin {
418 None => Stdin::Null,
419 Some(StdinParam::Mode(m)) if m == "null" => Stdin::Null,
420 Some(StdinParam::Mode(m)) if m == "piped" => Stdin::Piped,
421 Some(StdinParam::Mode(m)) => {
422 return Err(Error::invalid(format!(
423 "stdin must be \"null\", \"piped\" or {{\"data\": base64}}, got {m:?}"
424 )));
425 }
426 Some(StdinParam::Data { data }) => Stdin::Bytes(b64_decode(data)?),
427 };
428 let timeout = match &a.timeout {
429 Some(t) => Some(flex::parse_duration(t).map_err(Error::Invalid)?),
430 None => None,
431 };
432 let opts = ExecOptions {
433 cwd: a.cwd.clone(),
434 user: a.user.clone(),
435 env: a.env.clone(),
436 login: a.login,
437 tty: a.tty,
438 width: a.width,
439 height: a.height,
440 timeout,
441 stdin: stdin.clone(),
442 };
443 let key = exec_key(id);
444 let started = exec::build_request(&c, &a.name, &a.argv, &a.defaults, &opts)
445 .and_then(|req| exec::start_with_timeout(&c, &a.name, req, stdin, timeout));
446 let mut stream = match started {
447 Ok(s) => s,
448 Err(e) => {
449 self.execs
450 .lock()
451 .unwrap_or_else(|p| p.into_inner())
452 .remove(&key);
453 return Err(e);
454 }
455 };
456 {
459 let ctl = stream.controller();
460 let mut execs = self.execs.lock().unwrap_or_else(|p| p.into_inner());
461 if let Some(ExecSlot::Pending(q)) = execs.remove(&key) {
462 for op in q {
463 let _ = apply_op(&ctl, op);
464 }
465 }
466 execs.insert(key.clone(), ExecSlot::Running(ctl));
467 }
468 let mut stdout = Vec::new();
469 let mut stderr = Vec::new();
470 while let Some(ev) = stream.next_event() {
471 match (ev, a.stream) {
472 (ExecEvent::Stdout(b), true) => self.event(id, "stdout", json!(b64_encode(&b))),
473 (ExecEvent::Stderr(b), true) => self.event(id, "stderr", json!(b64_encode(&b))),
474 (ExecEvent::Stdout(b), false) => stdout.extend(b),
475 (ExecEvent::Stderr(b), false) => stderr.extend(b),
476 }
477 }
478 let code = stream.wait();
479 self.execs
480 .lock()
481 .unwrap_or_else(|p| p.into_inner())
482 .remove(&key);
483 let code = code?;
484 if a.stream {
485 Ok(json!({"exit_code": code}))
486 } else {
487 Ok(
488 json!({"exit_code": code, "stdout": b64_encode(&stdout), "stderr": b64_encode(&stderr)}),
489 )
490 }
491 }
492}
493
494fn apply_op(c: &ExecController, op: Queued) -> Result<()> {
495 match op {
496 Queued::Write(d) => c.write_stdin(&d),
497 Queued::CloseStdin => c.close_stdin(),
498 Queued::Signal(s) => c.signal(s),
499 Queued::Resize(w, h) => c.resize(w, h),
500 }
501}
502
503fn exec_key(v: &Value) -> String {
504 match v {
505 Value::String(s) => s.clone(),
506 other => other.to_string(),
507 }
508}
509
510fn params<T: DeserializeOwned>(p: &Value) -> Result<T> {
511 let v = if p.is_null() { json!({}) } else { p.clone() };
512 serde_json::from_value(v).map_err(|e| Error::Invalid(format!("params: {e}")))
513}
514
515fn resolve_spec(c: &Client, a: &SpecParams) -> Result<crate::plan::Desired> {
516 let base = match &a.base_dir {
517 Some(b) => PathBuf::from(b),
518 None => std::env::current_dir()?,
519 };
520 sandbox::resolve(c, &a.spec, &a.volumes, Path::new(&base))
521}
522
523fn create_desired(
524 c: &Client,
525 d: &crate::plan::Desired,
526 opts: EnsureOptions,
527 report: &mut dyn FnMut(&str),
528) -> Result<Sandbox> {
529 let _lock =
530 crate::lock::NameLock::acquire(c.project_name(), &d.name, opts.lock_wait, &mut |_| {})?;
531 if Sandbox::get(c, &d.name).is_ok() {
532 return Err(Error::AlreadyExists(d.name.clone()));
533 }
534 let plan = sandbox::plan_desired(c, d, opts.diff)?;
535 sandbox::apply(c, d, &plan, report)?;
536 if opts.wait_ready {
537 sandbox::wait_ready(c, &d.name, &d.ready, d.ready_timeout, &d.exec)?;
538 }
539 Sandbox::get(c, &d.name)
540}
541
542pub fn error_json(e: &Error) -> Value {
544 let (code, data) = match e {
545 Error::Connect { socket, .. } => ("connect", json!({"socket": socket})),
546 Error::RequestTimeout {
547 method,
548 path,
549 timeout,
550 } => (
551 "request_timeout",
552 json!({"method": method, "path": path, "timeout_secs": timeout.as_secs_f64()}),
553 ),
554 Error::Api {
555 status,
556 method,
557 path,
558 ..
559 } => (
560 if *status == 404 {
561 "not_found"
562 } else if e.is_conflict() {
563 "already_exists"
564 } else {
565 "api"
566 },
567 json!({"status": status, "method": method, "path": path}),
568 ),
569 Error::OperationTimeout {
570 step,
571 operation,
572 cancelled,
573 waited,
574 ..
575 } => (
576 "operation_timeout",
577 json!({"step": step, "operation": operation, "cancelled": cancelled, "waited_secs": waited.as_secs_f64()}),
578 ),
579 Error::OperationFailed { step, .. } => ("operation_failed", json!({"step": step})),
580 Error::NotReady {
581 sandbox,
582 check,
583 detail,
584 waited,
585 } => (
586 "not_ready",
587 json!({"sandbox": sandbox, "check": check, "detail": detail, "waited_secs": waited.as_secs_f64()}),
588 ),
589 Error::ExecTimeout { timeout, .. } => (
590 "exec_timeout",
591 json!({"timeout_secs": timeout.as_secs_f64()}),
592 ),
593 Error::NotFound(_) => ("not_found", Value::Null),
594 Error::AlreadyExists(_) => ("already_exists", Value::Null),
595 Error::Invalid(_) => ("invalid", Value::Null),
596 Error::Forbidden(_) => ("forbidden", Value::Null),
597 Error::Interpolation(_) => ("interpolation", Value::Null),
598 Error::Parse { path, .. } => ("parse", json!({"path": path})),
599 Error::WebSocket(_) => ("websocket", Value::Null),
600 Error::Protocol(_) => ("protocol", Value::Null),
601 Error::Io(_) => ("io", Value::Null),
602 Error::Json(_) => ("json", Value::Null),
603 Error::Remote { code, data, .. } => (code.as_str(), data.clone()),
604 };
605 let mut o = json!({"code": code, "message": e.to_string()});
606 if !data.is_null() {
607 o["data"] = data;
608 }
609 o
610}
611
612fn yes() -> bool {
613 true
614}
615
616#[derive(Deserialize)]
617#[serde(deny_unknown_fields)]
618struct SpecParams {
619 spec: SandboxSpec,
620 #[serde(default)]
621 base_dir: Option<String>,
622 #[serde(default)]
623 volumes: VolumeDefs,
624 #[serde(default = "yes")]
625 wait_ready: bool,
626 #[serde(default)]
627 prune_devices: bool,
628 #[serde(default)]
629 project: Option<String>,
630}
631
632#[derive(Deserialize)]
633#[serde(deny_unknown_fields)]
634struct NameParams {
635 name: String,
636 #[serde(default)]
637 project: Option<String>,
638}
639
640#[derive(Deserialize)]
641#[serde(deny_unknown_fields)]
642struct ListParams {
643 #[serde(default)]
644 labels: Vec<String>,
645 #[serde(default)]
646 project: Option<String>,
647}
648
649#[derive(Deserialize)]
650#[serde(deny_unknown_fields)]
651struct RemoveParams {
652 name: String,
653 #[serde(default)]
654 force: bool,
655 #[serde(default)]
656 project: Option<String>,
657}
658
659#[derive(Deserialize)]
660#[serde(deny_unknown_fields)]
661struct StopParams {
662 name: String,
663 #[serde(default)]
664 force: bool,
665 #[serde(default)]
666 timeout: Option<String>,
667 #[serde(default)]
668 project: Option<String>,
669}
670
671#[derive(Deserialize)]
672#[serde(deny_unknown_fields)]
673struct ReadyParams {
674 name: String,
675 #[serde(default)]
676 ready: Option<Vec<ReadyCheck>>,
677 #[serde(default)]
678 ready_timeout: Option<String>,
679 #[serde(default)]
680 exec: ExecDefaults,
681 #[serde(default)]
682 project: Option<String>,
683}
684
685#[derive(Deserialize)]
686#[serde(untagged)]
687enum StdinParam {
688 Mode(String),
689 Data { data: String },
690}
691
692#[derive(Deserialize)]
693#[serde(deny_unknown_fields)]
694struct ExecParams {
695 name: String,
696 argv: Vec<String>,
697 #[serde(default)]
698 defaults: ExecDefaults,
699 #[serde(default)]
700 cwd: Option<String>,
701 #[serde(default)]
702 user: Option<String>,
703 #[serde(default)]
704 env: BTreeMap<String, String>,
705 #[serde(default)]
706 login: Option<bool>,
707 #[serde(default)]
708 tty: bool,
709 #[serde(default)]
710 width: Option<u16>,
711 #[serde(default)]
712 height: Option<u16>,
713 #[serde(default)]
715 timeout: Option<String>,
716 #[serde(default)]
717 stdin: Option<StdinParam>,
718 #[serde(default)]
720 stream: bool,
721 #[serde(default)]
722 project: Option<String>,
723}
724
725#[derive(Deserialize)]
726#[serde(deny_unknown_fields)]
727struct ExecRef {
728 exec: Value,
729}
730
731#[derive(Deserialize)]
732#[serde(deny_unknown_fields)]
733struct ExecWriteParams {
734 exec: Value,
735 data: String,
736}
737
738#[derive(Deserialize)]
739#[serde(deny_unknown_fields)]
740struct ExecSignalParams {
741 exec: Value,
742 signal: i32,
743}
744
745#[derive(Deserialize)]
746#[serde(deny_unknown_fields)]
747struct ExecResizeParams {
748 exec: Value,
749 width: u16,
750 height: u16,
751}
752
753#[derive(Deserialize)]
754#[serde(deny_unknown_fields)]
755struct AddPortParams {
756 name: String,
757 port: PortSpec,
758 #[serde(default)]
759 project: Option<String>,
760}
761
762#[derive(Deserialize)]
763#[serde(deny_unknown_fields)]
764struct DeviceParams {
765 name: String,
766 device: String,
767 #[serde(default)]
768 project: Option<String>,
769}
770
771#[derive(Deserialize)]
772#[serde(deny_unknown_fields)]
773struct PoolParams {
774 #[serde(default)]
775 pool: Option<String>,
776 #[serde(default)]
777 project: Option<String>,
778}
779
780#[derive(Deserialize)]
781#[serde(deny_unknown_fields)]
782struct VolumeParams {
783 name: String,
784 #[serde(default)]
785 pool: Option<String>,
786 #[serde(default)]
787 config: BTreeMap<String, String>,
788 #[serde(default)]
789 project: Option<String>,
790}
791
792#[derive(Deserialize)]
793#[serde(deny_unknown_fields)]
794struct PruneParams {
795 label: String,
796 #[serde(default = "yes")]
797 dry_run: bool,
798 #[serde(default)]
799 project: Option<String>,
800}
801
802#[derive(Deserialize)]
803#[serde(deny_unknown_fields)]
804struct ComposeParams {
805 #[serde(default)]
806 files: Vec<PathBuf>,
807 #[serde(default)]
808 env_files: Vec<PathBuf>,
809 #[serde(default)]
810 project_name: Option<String>,
811 #[serde(default)]
812 vars: BTreeMap<String, String>,
813 #[serde(default)]
814 services: Vec<String>,
815 #[serde(default = "yes")]
816 wait_ready: bool,
817 #[serde(default)]
818 prune_devices: bool,
819 #[serde(default)]
820 volumes: bool,
821 #[serde(default)]
822 project: Option<String>,
823}
824
825impl ComposeParams {
826 fn load(&self) -> LoadOptions {
827 LoadOptions {
828 files: self.files.clone(),
829 env_files: self.env_files.clone(),
830 project_name: self.project_name.clone(),
831 vars: self.vars.clone(),
832 }
833 }
834}
835
836const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
837
838pub fn b64_encode(data: &[u8]) -> String {
840 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
841 for chunk in data.chunks(3) {
842 let b = [
843 chunk[0],
844 *chunk.get(1).unwrap_or(&0),
845 *chunk.get(2).unwrap_or(&0),
846 ];
847 let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
848 out.push(B64[(n >> 18) as usize & 63] as char);
849 out.push(B64[(n >> 12) as usize & 63] as char);
850 out.push(if chunk.len() > 1 {
851 B64[(n >> 6) as usize & 63] as char
852 } else {
853 '='
854 });
855 out.push(if chunk.len() > 2 {
856 B64[n as usize & 63] as char
857 } else {
858 '='
859 });
860 }
861 out
862}
863
864pub fn b64_decode(s: &str) -> Result<Vec<u8>> {
866 let mut out = Vec::with_capacity(s.len() / 4 * 3);
867 let (mut acc, mut bits) = (0u32, 0u32);
868 for c in s.bytes() {
869 let v = match c {
870 b'A'..=b'Z' => c - b'A',
871 b'a'..=b'z' => c - b'a' + 26,
872 b'0'..=b'9' => c - b'0' + 52,
873 b'+' => 62,
874 b'/' => 63,
875 b'=' => break,
876 b'\n' | b'\r' | b' ' => continue,
877 _ => return Err(Error::invalid("invalid base64")),
878 };
879 acc = acc << 6 | v as u32;
880 bits += 6;
881 if bits >= 8 {
882 bits -= 8;
883 out.push((acc >> bits) as u8);
884 }
885 }
886 Ok(out)
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892 use std::io::Cursor;
893
894 #[derive(Clone)]
895 struct Buf(Arc<Mutex<Vec<u8>>>);
896 impl Write for Buf {
897 fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
898 self.0.lock().unwrap().write(b)
899 }
900 fn flush(&mut self) -> std::io::Result<()> {
901 Ok(())
902 }
903 }
904
905 fn run(input: &str) -> Vec<Value> {
906 let buf = Buf(Arc::new(Mutex::new(Vec::new())));
907 let client = Client::with_socket("/nonexistent/isb-test.socket");
908 serve(Cursor::new(input.to_string()), buf.clone(), client).unwrap();
909 let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
910 out.lines()
911 .map(|l| serde_json::from_str(l).unwrap())
912 .collect()
913 }
914
915 fn by_id(msgs: &[Value], id: i64) -> Value {
916 msgs.iter()
917 .find(|m| {
918 m["id"] == json!(id) && (m.get("result").is_some() || m.get("error").is_some())
919 })
920 .cloned()
921 .unwrap_or_else(|| panic!("no reply for {id}: {msgs:?}"))
922 }
923
924 #[test]
925 fn hello_version_schema_and_errors() {
926 let msgs = run(concat!(
927 "{\"id\":1,\"method\":\"version\"}\n",
928 "{\"id\":2,\"method\":\"schema\"}\n",
929 "not json\n",
930 "{\"id\":3,\"method\":\"nope\"}\n",
931 "{\"id\":4,\"method\":\"sandbox.get\",\"params\":{\"nam\":\"x\"}}\n",
932 "{\"id\":5,\"method\":\"sandbox.get\",\"params\":{\"name\":\"x\"}}\n",
933 "{\"id\":6,\"method\":\"exec.signal\",\"params\":{\"exec\":99,\"signal\":2}}\n",
934 ));
935 assert_eq!(msgs[0]["protocol"], json!(PROTOCOL));
936 assert_eq!(by_id(&msgs, 1)["result"]["protocol"], json!(PROTOCOL));
937 assert!(by_id(&msgs, 2)["result"].to_string().contains("services"));
938 assert!(msgs.iter().any(|m| m["error"]["code"] == "bad_request"));
939 assert_eq!(by_id(&msgs, 3)["error"]["code"], "protocol");
940 assert_eq!(by_id(&msgs, 4)["error"]["code"], "invalid");
941 let e = by_id(&msgs, 5);
942 assert_eq!(e["error"]["code"], "connect");
943 assert_eq!(e["error"]["data"]["socket"], "/nonexistent/isb-test.socket");
944 assert_eq!(by_id(&msgs, 6)["error"]["code"], "not_found");
945 }
946
947 #[test]
948 fn compose_load_over_rpc() {
949 let dir = tempfile::tempdir().unwrap();
950 let f = dir.path().join("isb.yaml");
951 std::fs::write(&f, "services:\n web: {image: \"${IMG}\", cpus: 2}\n").unwrap();
952 let req = json!({"id": 1, "method": "compose.load", "params": {"files": [f], "vars": {"IMG": "dev-base"}, "project_name": "demo"}});
953 let msgs = run(&format!("{req}\n"));
954 let r = &by_id(&msgs, 1)["result"];
955 assert_eq!(r["name"], "demo");
956 assert_eq!(r["file"]["services"]["web"]["image"], "dev-base");
957 assert_eq!(r["file"]["services"]["web"]["container_name"], "demo-web");
958 }
959
960 #[test]
961 fn base64_roundtrip() {
962 for s in [
963 &b""[..],
964 b"f",
965 b"fo",
966 b"foo",
967 b"foob",
968 b"fooba",
969 b"foobar",
970 &[0, 255, 128, 7],
971 ] {
972 assert_eq!(b64_decode(&b64_encode(s)).unwrap(), s);
973 }
974 assert_eq!(b64_encode(b"foobar"), "Zm9vYmFy");
975 assert_eq!(b64_encode(b"fo"), "Zm8=");
976 assert!(b64_decode("@@").is_err());
977 }
978}