outrig_cli/image_setup/
build.rs1use std::collections::BTreeMap;
14use std::path::Path;
15use std::time::Duration;
16
17use outrig::McpClient;
18use outrig::container::embedded::{
19 StandaloneImageToml, parse_standalone_image_toml, read_standalone_image_mcp,
20 standalone_config_to_labels,
21};
22use outrig::container::{Container, ContainerLaunchSpec};
23use outrig::image::{self, ImageTag};
24
25use crate::cli::session_setup::plural;
26use crate::error::{OutrigError, Result};
27
28const STOP_GRACE: Duration = Duration::from_secs(2);
29
30pub async fn run(
33 cwd: &Path,
34 dir: Option<&Path>,
35 tag_override: Option<&str>,
36 no_test: bool,
37 no_cache: bool,
38) -> Result<()> {
39 let project_dir = dir.map_or_else(|| cwd.to_path_buf(), |d| cwd.join(d));
40 let parsed = load_project_image_toml(&project_dir)?;
41 let labels = standalone_config_to_labels(&parsed)?;
42 let tag = ImageTag(
43 tag_override
44 .map(str::to_string)
45 .unwrap_or_else(|| parsed.image.image_ref.clone()),
46 );
47
48 eprintln!("[outrig] building image {tag}");
49 eprintln!(
50 "[outrig] dockerfile: {}",
51 parsed.build.dockerfile.display()
52 );
53 eprintln!("[outrig] context: {}", parsed.build.context.display());
54 image::build_standalone(
55 &project_dir,
56 &parsed.build.dockerfile,
57 &parsed.build.context,
58 &tag,
59 no_cache,
60 &labels,
61 )
62 .await?;
63 eprintln!("[outrig] image ready");
64
65 let scratch = tempfile::tempdir()?;
70 let host_ws = scratch.path().join("workspace");
71 std::fs::create_dir_all(&host_ws)?;
72 let log_dir = scratch.path().join("logs");
73
74 let mut container = Container::start(
75 &tag,
76 ContainerLaunchSpec::workspace(&host_ws, Path::new("/workspace")),
77 )
78 .await?;
79
80 let outcome = validate_and_test(&mut container, &log_dir, no_test).await;
84 let stop = container.stop(STOP_GRACE).await;
85 outcome?;
86 stop?;
87
88 eprintln!("[outrig] image ok");
89 Ok(())
90}
91
92fn load_project_image_toml(project_dir: &Path) -> Result<StandaloneImageToml> {
97 let path = project_dir.join("image.toml");
98 let text = std::fs::read_to_string(&path).map_err(|source| {
99 OutrigError::Configuration(format!("could not read {}: {source}", path.display()))
100 })?;
101 let parsed = parse_standalone_image_toml(&text)
102 .map_err(|source| OutrigError::Configuration(format!("{}: {source}", path.display())))?;
103 Ok(parsed)
104}
105
106async fn validate_and_test(container: &mut Container, log_dir: &Path, no_test: bool) -> Result<()> {
109 let mcp = read_standalone_image_mcp(container).await?;
110 let count = mcp.len();
111 eprintln!(
112 "[outrig] image config validated ({count} mcp {})",
113 plural(count, "server", "servers")
114 );
115
116 if no_test {
117 eprintln!("[outrig] skipping live mcp test (--no-test)");
118 return Ok(());
119 }
120
121 container.bootstrap_user().await?;
122 for (name, spec) in &mcp {
123 let client =
124 McpClient::connect_via_podman_exec(container, spec, name, log_dir, &BTreeMap::new())
125 .await?;
126 let tools = client.list_tools().await?.len();
127 eprintln!(
128 "[outrig] mcp {name}: initialized ({tools} {})",
129 plural(tools, "tool", "tools")
130 );
131 client.shutdown().await?;
132 }
133 Ok(())
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn load_errors_when_image_toml_missing() {
142 let tmp = tempfile::tempdir().unwrap();
143 let err = load_project_image_toml(tmp.path()).unwrap_err();
144 assert!(err.to_string().contains("could not read"), "{err}");
145 assert!(err.to_string().contains("image.toml"), "{err}");
146 }
147
148 #[test]
149 fn load_errors_on_malformed_toml() {
150 let tmp = tempfile::tempdir().unwrap();
151 std::fs::write(tmp.path().join("image.toml"), "[image\nref =").unwrap();
152 let err = load_project_image_toml(tmp.path()).unwrap_err();
153 assert!(err.to_string().contains("image.toml"), "{err}");
154 }
155
156 #[test]
157 fn load_errors_on_missing_image_ref() {
158 let tmp = tempfile::tempdir().unwrap();
159 std::fs::write(tmp.path().join("image.toml"), "[mcp]\nfs = [\"x\"]\n").unwrap();
160 let err = load_project_image_toml(tmp.path()).unwrap_err();
161 assert!(err.to_string().contains("image.ref is required"), "{err}");
162 }
163}