outrig_cli/image_setup/
inspect.rs1use std::collections::BTreeMap;
4
5use outrig::container::embedded::{StandaloneImageLabels, parse_standalone_image_labels};
6use outrig::image::{self, ImageTag};
7
8use crate::error::{OutrigError, Result};
9
10pub async fn run(image_ref: &str, remote: bool) -> Result<()> {
15 let labels = if remote {
16 image::read_remote_image_labels(image_ref).await?
17 } else {
18 read_local_image_labels(image_ref).await?
19 };
20 let parsed = parse_standalone_image_labels(image_ref, &labels)?;
21 print!("{}", render_inspect(image_ref, &parsed));
22 Ok(())
23}
24
25async fn read_local_image_labels(image_ref: &str) -> Result<BTreeMap<String, String>> {
26 let tag = ImageTag(image_ref.to_string());
27 if !image::probe_pulled(&tag).await? {
28 return Err(OutrigError::Configuration(format!(
29 "local image {image_ref:?} not found; `outrig image inspect` is local-only and does not pull"
30 ))
31 .into());
32 }
33 Ok(image::read_image_labels(&tag, None).await?)
34}
35
36pub fn render_inspect(image_ref: &str, labels: &StandaloneImageLabels) -> String {
37 let mut out = String::new();
38 out.push_str("image: ");
39 out.push_str(image_ref);
40 out.push('\n');
41
42 if let Some(description) = &labels.description {
43 out.push_str("description: ");
44 out.push_str(description);
45 out.push('\n');
46 }
47 if let Some(version) = &labels.version {
48 out.push_str("version: ");
49 out.push_str(version);
50 out.push('\n');
51 }
52 if !labels.tags.is_empty() {
53 out.push_str("tags: ");
54 out.push_str(&json(&labels.tags));
55 out.push('\n');
56 }
57
58 if let Some(mcp) = &labels.mcp {
59 out.push_str("mcp:\n");
60 for (server, spec) in mcp {
61 let (command, env) = spec.normalize();
62 out.push_str(" ");
63 out.push_str(server);
64 out.push_str(":\n");
65 out.push_str(" command: ");
66 out.push_str(&json(&command));
67 out.push('\n');
68
69 if !env.is_empty() {
70 out.push_str(" env:\n");
71 for (key, value) in env {
72 out.push_str(" ");
73 out.push_str(&key);
74 out.push_str(": ");
75 out.push_str(&json(&value));
76 out.push('\n');
77 }
78 }
79 }
80 }
81
82 out
83}
84
85fn json<T: serde::Serialize>(value: &T) -> String {
86 serde_json::to_string(value).expect("inspect values serialize to JSON")
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 use std::collections::BTreeMap;
94
95 use outrig::config::{EnvValue, McpServerSpec};
96
97 #[test]
98 fn render_inspect_prints_metadata_commands_and_env() {
99 let mut env = BTreeMap::new();
100 env.insert(
101 "CARGO_HOME".to_string(),
102 EnvValue::Literal("/cache".to_string()),
103 );
104 env.insert(
105 "TOKEN".to_string(),
106 EnvValue::EnvRef("DB_TOKEN".to_string()),
107 );
108
109 let mut mcp = BTreeMap::new();
110 mcp.insert(
111 "build".to_string(),
112 McpServerSpec::Full {
113 command: vec!["cargo-mcp".to_string(), "--stdio".to_string()],
114 env,
115 },
116 );
117 mcp.insert(
118 "fs".to_string(),
119 McpServerSpec::Short(vec![
120 "mcp-server-filesystem".to_string(),
121 "/workspace".to_string(),
122 ]),
123 );
124
125 let labels = StandaloneImageLabels {
126 description: Some("Rust tooling".to_string()),
127 version: Some("0.1.0".to_string()),
128 tags: vec!["rust".to_string(), "build".to_string()],
129 mcp: Some(mcp),
130 };
131
132 assert_eq!(
133 render_inspect("rust-dev", &labels),
134 concat!(
135 "image: rust-dev\n",
136 "description: Rust tooling\n",
137 "version: 0.1.0\n",
138 "tags: [\"rust\",\"build\"]\n",
139 "mcp:\n",
140 " build:\n",
141 " command: [\"cargo-mcp\",\"--stdio\"]\n",
142 " env:\n",
143 " CARGO_HOME: \"/cache\"\n",
144 " TOKEN: \"${DB_TOKEN}\"\n",
145 " fs:\n",
146 " command: [\"mcp-server-filesystem\",\"/workspace\"]\n",
147 )
148 );
149 }
150
151 #[test]
152 fn render_inspect_omits_absent_metadata_and_mcp() {
153 let labels = StandaloneImageLabels {
154 description: None,
155 version: None,
156 tags: Vec::new(),
157 mcp: None,
158 };
159
160 assert_eq!(render_inspect("plain", &labels), "image: plain\n");
161 }
162}