2026-02-23 11:15:39 +00:00
|
|
|
use super::{json_pretty, EXIT_SUCCESS};
|
|
|
|
|
use dialoguer::{Confirm, Input, Select};
|
|
|
|
|
use karapace_schema::manifest::{
|
2026-02-23 11:42:00 +00:00
|
|
|
parse_manifest_str, BaseSection, GuiSection, HardwareSection, ManifestV1, MountsSection,
|
|
|
|
|
RuntimeSection, SystemSection,
|
2026-02-23 11:15:39 +00:00
|
|
|
};
|
2026-02-25 10:48:58 +00:00
|
|
|
use std::io::{stderr, stdin, IsTerminal};
|
2026-02-23 11:15:39 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use tempfile::NamedTempFile;
|
|
|
|
|
|
|
|
|
|
const DEST_MANIFEST: &str = "karapace.toml";
|
|
|
|
|
|
|
|
|
|
fn template_source(name: &str) -> Option<&'static str> {
|
|
|
|
|
match name {
|
|
|
|
|
"minimal" => Some(include_str!("../../../../examples/minimal.toml")),
|
|
|
|
|
"dev" => Some(include_str!("../../../../examples/dev.toml")),
|
|
|
|
|
"gui-dev" => Some(include_str!("../../../../examples/gui-dev.toml")),
|
|
|
|
|
"rust-dev" => Some(include_str!("../../../../examples/rust-dev.toml")),
|
|
|
|
|
"ubuntu-dev" => Some(include_str!("../../../../examples/ubuntu-dev.toml")),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_template(name: &str) -> Result<ManifestV1, String> {
|
|
|
|
|
let src = template_source(name).ok_or_else(|| {
|
|
|
|
|
format!("unknown template '{name}' (expected: minimal, dev, gui-dev, rust-dev, ubuntu-dev)")
|
|
|
|
|
})?;
|
|
|
|
|
parse_manifest_str(src).map_err(|e| format!("template parse error: {e}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write_atomic(dest: &Path, content: &str) -> Result<(), String> {
|
|
|
|
|
let dir = dest
|
|
|
|
|
.parent()
|
2026-02-23 11:42:00 +00:00
|
|
|
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
|
2026-02-23 11:15:39 +00:00
|
|
|
let mut tmp = NamedTempFile::new_in(&dir).map_err(|e| format!("write temp file: {e}"))?;
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
tmp.write_all(content.as_bytes())
|
|
|
|
|
.map_err(|e| format!("write temp file: {e}"))?;
|
|
|
|
|
tmp.as_file()
|
|
|
|
|
.sync_all()
|
|
|
|
|
.map_err(|e| format!("fsync temp file: {e}"))?;
|
|
|
|
|
tmp.persist(dest)
|
|
|
|
|
.map_err(|e| format!("persist manifest: {}", e.error))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 11:42:00 +00:00
|
|
|
fn ensure_can_write(dest: &Path, force: bool, is_tty: bool) -> Result<(), String> {
|
|
|
|
|
if !dest.exists() || force {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
if !is_tty {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"refusing to overwrite existing ./{DEST_MANIFEST} (pass --force)"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let overwrite = Confirm::new()
|
|
|
|
|
.with_prompt(format!("overwrite ./{DEST_MANIFEST}?"))
|
|
|
|
|
.default(false)
|
|
|
|
|
.interact()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
|
|
|
|
if overwrite {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!(
|
|
|
|
|
"refusing to overwrite existing ./{DEST_MANIFEST} (pass --force)"
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 11:15:39 +00:00
|
|
|
|
2026-02-23 11:42:00 +00:00
|
|
|
fn print_result(name: &str, template: Option<&str>, json: bool) -> Result<(), String> {
|
|
|
|
|
if json {
|
|
|
|
|
let payload = serde_json::json!({
|
|
|
|
|
"status": "written",
|
|
|
|
|
"path": format!("./{DEST_MANIFEST}"),
|
|
|
|
|
"name": name,
|
|
|
|
|
"template": template,
|
|
|
|
|
});
|
|
|
|
|
println!("{}", json_pretty(&payload)?);
|
|
|
|
|
} else {
|
|
|
|
|
println!("wrote ./{DEST_MANIFEST} for '{name}'");
|
|
|
|
|
if let Some(tpl) = template {
|
|
|
|
|
println!("template: {tpl}");
|
2026-02-23 11:15:39 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 11:42:00 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-02-23 11:15:39 +00:00
|
|
|
|
2026-02-23 11:42:00 +00:00
|
|
|
pub fn run(name: &str, template: Option<&str>, force: bool, json: bool) -> Result<u8, String> {
|
|
|
|
|
let dest = Path::new(DEST_MANIFEST);
|
2026-02-25 10:48:58 +00:00
|
|
|
let is_tty = stdin().is_terminal() && stderr().is_terminal();
|
2026-02-23 11:15:39 +00:00
|
|
|
|
2026-02-23 11:42:00 +00:00
|
|
|
let mut manifest = if let Some(tpl) = template {
|
2026-02-25 10:48:58 +00:00
|
|
|
let m = load_template(tpl)?;
|
|
|
|
|
ensure_can_write(dest, force, is_tty)?;
|
|
|
|
|
m
|
2026-02-23 11:42:00 +00:00
|
|
|
} else {
|
2026-02-25 10:48:58 +00:00
|
|
|
ensure_can_write(dest, force, is_tty)?;
|
2026-02-23 11:42:00 +00:00
|
|
|
if !is_tty {
|
|
|
|
|
return Err("no --template provided and stdin is not a TTY".to_owned());
|
|
|
|
|
}
|
|
|
|
|
let image: String = Input::new()
|
|
|
|
|
.with_prompt("base image")
|
|
|
|
|
.default("rolling".to_owned())
|
|
|
|
|
.interact_text()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
|
|
|
|
ManifestV1 {
|
|
|
|
|
manifest_version: 1,
|
|
|
|
|
base: BaseSection { image },
|
|
|
|
|
system: SystemSection::default(),
|
|
|
|
|
gui: GuiSection::default(),
|
|
|
|
|
hardware: HardwareSection::default(),
|
|
|
|
|
mounts: MountsSection::default(),
|
|
|
|
|
runtime: RuntimeSection::default(),
|
2026-02-23 11:15:39 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if is_tty {
|
|
|
|
|
let packages: String = Input::new()
|
|
|
|
|
.with_prompt("packages (space-separated, empty to skip)")
|
|
|
|
|
.allow_empty(true)
|
|
|
|
|
.interact_text()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
|
|
|
|
if !packages.trim().is_empty() {
|
|
|
|
|
manifest
|
|
|
|
|
.system
|
|
|
|
|
.packages
|
|
|
|
|
.extend(packages.split_whitespace().map(str::to_owned));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mount: String = Input::new()
|
|
|
|
|
.with_prompt("mount (format '<host>:<container>', empty to skip)")
|
|
|
|
|
.allow_empty(true)
|
|
|
|
|
.interact_text()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
|
|
|
|
if !mount.trim().is_empty() {
|
|
|
|
|
manifest
|
|
|
|
|
.mounts
|
|
|
|
|
.entries
|
|
|
|
|
.insert("workspace".to_owned(), mount);
|
|
|
|
|
}
|
|
|
|
|
let backends = ["namespace", "oci", "mock"];
|
|
|
|
|
let default_idx = backends
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|b| *b == manifest.runtime.backend.as_str())
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
let idx = Select::new()
|
|
|
|
|
.with_prompt("runtime backend")
|
|
|
|
|
.items(&backends)
|
|
|
|
|
.default(default_idx)
|
|
|
|
|
.interact()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
2026-02-23 11:42:00 +00:00
|
|
|
manifest.runtime.backend.clear();
|
|
|
|
|
manifest.runtime.backend.push_str(backends[idx]);
|
2026-02-23 11:15:39 +00:00
|
|
|
let isolated = Confirm::new()
|
|
|
|
|
.with_prompt("enable network isolation?")
|
|
|
|
|
.default(manifest.runtime.network_isolation)
|
|
|
|
|
.interact()
|
|
|
|
|
.map_err(|e| format!("prompt failed: {e}"))?;
|
|
|
|
|
manifest.runtime.network_isolation = isolated;
|
|
|
|
|
} else if template.is_none() {
|
|
|
|
|
return Err("interactive prompts require a TTY".to_owned());
|
|
|
|
|
}
|
|
|
|
|
let toml =
|
|
|
|
|
toml::to_string_pretty(&manifest).map_err(|e| format!("TOML serialization failed: {e}"))?;
|
|
|
|
|
write_atomic(dest, &toml)?;
|
2026-02-23 11:42:00 +00:00
|
|
|
print_result(name, template, json)?;
|
2026-02-23 11:15:39 +00:00
|
|
|
Ok(EXIT_SUCCESS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn templates_parse() {
|
|
|
|
|
for tpl in ["minimal", "dev", "gui-dev", "rust-dev", "ubuntu-dev"] {
|
|
|
|
|
let m = load_template(tpl).unwrap();
|
|
|
|
|
assert_eq!(m.manifest_version, 1);
|
|
|
|
|
assert!(!m.base.image.is_empty());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|