docker_wrapper/command/manifest/
inspect.rs

1//! Docker manifest inspect command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8/// Platform information in a manifest
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ManifestPlatform {
11    /// Architecture (e.g., "amd64", "arm64")
12    pub architecture: Option<String>,
13    /// Operating system (e.g., "linux", "windows")
14    pub os: Option<String>,
15    /// OS version
16    #[serde(rename = "os.version")]
17    pub os_version: Option<String>,
18    /// Architecture variant (e.g., "v8")
19    pub variant: Option<String>,
20}
21
22/// Information about a manifest or manifest list
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ManifestInfo {
25    /// Schema version
26    #[serde(rename = "schemaVersion")]
27    pub schema_version: Option<i32>,
28    /// Media type
29    #[serde(rename = "mediaType")]
30    pub media_type: Option<String>,
31    /// Digest of the manifest
32    pub digest: Option<String>,
33    /// Size of the manifest in bytes
34    pub size: Option<i64>,
35    /// Platform information
36    pub platform: Option<ManifestPlatform>,
37    /// Manifests in the list (for manifest lists)
38    pub manifests: Option<Vec<ManifestInfo>>,
39    /// Raw JSON output
40    #[serde(skip)]
41    pub raw_json: String,
42}
43
44impl ManifestInfo {
45    /// Parse the manifest inspect output
46    fn parse(output: &CommandOutput) -> Self {
47        let stdout = output.stdout.trim();
48        if stdout.is_empty() {
49            return Self {
50                schema_version: None,
51                media_type: None,
52                digest: None,
53                size: None,
54                platform: None,
55                manifests: None,
56                raw_json: String::new(),
57            };
58        }
59
60        let mut info: ManifestInfo =
61            serde_json::from_str(stdout).unwrap_or_else(|_| ManifestInfo {
62                schema_version: None,
63                media_type: None,
64                digest: None,
65                size: None,
66                platform: None,
67                manifests: None,
68                raw_json: String::new(),
69            });
70        info.raw_json = stdout.to_string();
71        info
72    }
73}
74
75/// Docker manifest inspect command builder
76///
77/// Displays an image manifest or manifest list.
78///
79/// # Example
80///
81/// ```rust,no_run
82/// use docker_wrapper::{DockerCommand, ManifestInspectCommand};
83///
84/// # #[tokio::main]
85/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
86/// let info = ManifestInspectCommand::new("myapp:latest")
87///     .verbose()
88///     .execute()
89///     .await?;
90///
91/// if let Some(manifests) = &info.manifests {
92///     for manifest in manifests {
93///         if let Some(platform) = &manifest.platform {
94///             println!("Platform: {:?}/{:?}",
95///                 platform.os, platform.architecture);
96///         }
97///     }
98/// }
99/// # Ok(())
100/// # }
101/// ```
102#[derive(Debug, Clone)]
103pub struct ManifestInspectCommand {
104    /// The manifest list name (optional)
105    manifest_list: Option<String>,
106    /// The manifest to inspect
107    manifest: String,
108    /// Allow communication with an insecure registry
109    insecure: bool,
110    /// Output additional info including layers and platform
111    verbose: bool,
112    /// Command executor
113    pub executor: CommandExecutor,
114}
115
116impl ManifestInspectCommand {
117    /// Create a new manifest inspect command
118    ///
119    /// # Arguments
120    ///
121    /// * `manifest` - The manifest to inspect (e.g., "myapp:latest")
122    #[must_use]
123    pub fn new(manifest: impl Into<String>) -> Self {
124        Self {
125            manifest_list: None,
126            manifest: manifest.into(),
127            insecure: false,
128            verbose: false,
129            executor: CommandExecutor::new(),
130        }
131    }
132
133    /// Set the manifest list to inspect from
134    #[must_use]
135    pub fn manifest_list(mut self, manifest_list: impl Into<String>) -> Self {
136        self.manifest_list = Some(manifest_list.into());
137        self
138    }
139
140    /// Allow communication with an insecure registry
141    #[must_use]
142    pub fn insecure(mut self) -> Self {
143        self.insecure = true;
144        self
145    }
146
147    /// Output additional info including layers and platform
148    #[must_use]
149    pub fn verbose(mut self) -> Self {
150        self.verbose = true;
151        self
152    }
153
154    /// Build the command arguments
155    fn build_args(&self) -> Vec<String> {
156        let mut args = vec!["manifest".to_string(), "inspect".to_string()];
157
158        if self.insecure {
159            args.push("--insecure".to_string());
160        }
161
162        if self.verbose {
163            args.push("--verbose".to_string());
164        }
165
166        if let Some(ref list) = self.manifest_list {
167            args.push(list.clone());
168        }
169
170        args.push(self.manifest.clone());
171
172        args.extend(self.executor.raw_args.clone());
173
174        args
175    }
176}
177
178#[async_trait]
179impl DockerCommand for ManifestInspectCommand {
180    type Output = ManifestInfo;
181
182    fn get_executor(&self) -> &CommandExecutor {
183        &self.executor
184    }
185
186    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
187        &mut self.executor
188    }
189
190    fn build_command_args(&self) -> Vec<String> {
191        self.build_args()
192    }
193
194    async fn execute(&self) -> Result<Self::Output> {
195        let args = self.build_args();
196        let output = self.execute_command(args).await?;
197        Ok(ManifestInfo::parse(&output))
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_manifest_inspect_basic() {
207        let cmd = ManifestInspectCommand::new("myapp:latest");
208        let args = cmd.build_args();
209        assert_eq!(args, vec!["manifest", "inspect", "myapp:latest"]);
210    }
211
212    #[test]
213    fn test_manifest_inspect_with_list() {
214        let cmd = ManifestInspectCommand::new("myapp:latest-amd64").manifest_list("myapp:latest");
215        let args = cmd.build_args();
216        assert_eq!(
217            args,
218            vec!["manifest", "inspect", "myapp:latest", "myapp:latest-amd64"]
219        );
220    }
221
222    #[test]
223    fn test_manifest_inspect_with_insecure() {
224        let cmd = ManifestInspectCommand::new("myapp:latest").insecure();
225        let args = cmd.build_args();
226        assert!(args.contains(&"--insecure".to_string()));
227    }
228
229    #[test]
230    fn test_manifest_inspect_with_verbose() {
231        let cmd = ManifestInspectCommand::new("myapp:latest").verbose();
232        let args = cmd.build_args();
233        assert!(args.contains(&"--verbose".to_string()));
234    }
235
236    #[test]
237    fn test_manifest_inspect_all_options() {
238        let cmd = ManifestInspectCommand::new("myapp:latest")
239            .insecure()
240            .verbose();
241        let args = cmd.build_args();
242        assert!(args.contains(&"--insecure".to_string()));
243        assert!(args.contains(&"--verbose".to_string()));
244    }
245
246    #[test]
247    fn test_manifest_info_parse_empty() {
248        let output = CommandOutput {
249            stdout: String::new(),
250            stderr: String::new(),
251            exit_code: 0,
252            success: true,
253        };
254        let info = ManifestInfo::parse(&output);
255        assert!(info.schema_version.is_none());
256    }
257
258    #[test]
259    fn test_manifest_info_parse_json() {
260        let json = r#"{"schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json"}"#;
261        let output = CommandOutput {
262            stdout: json.to_string(),
263            stderr: String::new(),
264            exit_code: 0,
265            success: true,
266        };
267        let info = ManifestInfo::parse(&output);
268        assert_eq!(info.schema_version, Some(2));
269        assert_eq!(
270            info.media_type,
271            Some("application/vnd.docker.distribution.manifest.list.v2+json".to_string())
272        );
273    }
274}