1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ContainerdConfig {
8 pub image: String,
10 pub command: Option<Vec<String>>,
12 pub run: Option<String>,
14 #[serde(default)]
15 pub env: HashMap<String, String>,
17 #[serde(default)]
18 pub volumes: Vec<VolumeMountConfig>,
20 pub working_dir: Option<String>,
22 #[serde(default = "default_user")]
23 pub user: String,
25 #[serde(default = "default_network")]
26 pub network: String,
28 pub memory: Option<String>,
30 pub cpu: Option<String>,
32 #[serde(default = "default_pull")]
33 pub pull: String,
35 #[serde(default = "default_containerd_addr")]
36 pub containerd_addr: String,
38 #[serde(default = "default_cli")]
40 pub cli: String,
41 #[serde(default)]
42 pub tls: TlsConfig,
44 #[serde(default)]
45 pub registry_auth: HashMap<String, RegistryAuth>,
47 pub inputs: Option<HashMap<String, String>>,
50 pub timeout_ms: Option<u64>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VolumeMountConfig {
57 pub source: String,
59 pub target: String,
61 #[serde(default)]
62 pub readonly: bool,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default)]
67pub struct TlsConfig {
69 pub ca: Option<String>,
71 pub cert: Option<String>,
73 pub key: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct RegistryAuth {
80 pub username: String,
82 pub password: String,
84}
85
86fn default_user() -> String {
87 "65534:65534".to_string()
88}
89
90fn default_network() -> String {
91 "none".to_string()
92}
93
94fn default_pull() -> String {
95 "if-not-present".to_string()
96}
97
98fn default_containerd_addr() -> String {
99 "/run/containerd/containerd.sock".to_string()
100}
101
102fn default_cli() -> String {
103 "nerdctl".to_string()
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use pretty_assertions::assert_eq;
110
111 #[test]
112 fn serde_round_trip_full_config() {
113 let config = ContainerdConfig {
114 image: "alpine:3.18".to_string(),
115 command: Some(vec!["echo".to_string(), "hello".to_string()]),
116 run: None,
117 env: HashMap::from([("FOO".to_string(), "bar".to_string())]),
118 volumes: vec![VolumeMountConfig {
119 source: "/host/path".to_string(),
120 target: "/container/path".to_string(),
121 readonly: true,
122 }],
123 working_dir: Some("/app".to_string()),
124 user: "1000:1000".to_string(),
125 network: "host".to_string(),
126 memory: Some("512m".to_string()),
127 cpu: Some("1.0".to_string()),
128 pull: "always".to_string(),
129 containerd_addr: "/custom/containerd.sock".to_string(),
130 cli: "nerdctl".to_string(),
131 tls: TlsConfig {
132 ca: Some("/ca.pem".to_string()),
133 cert: Some("/cert.pem".to_string()),
134 key: Some("/key.pem".to_string()),
135 },
136 registry_auth: HashMap::from([(
137 "registry.example.com".to_string(),
138 RegistryAuth {
139 username: "user".to_string(),
140 password: "pass".to_string(),
141 },
142 )]),
143 inputs: None,
144 timeout_ms: Some(30000),
145 };
146
147 let json = serde_json::to_string(&config).unwrap();
148 let deserialized: ContainerdConfig = serde_json::from_str(&json).unwrap();
149
150 assert_eq!(deserialized.image, config.image);
151 assert_eq!(deserialized.command, config.command);
152 assert_eq!(deserialized.run, config.run);
153 assert_eq!(deserialized.env, config.env);
154 assert_eq!(deserialized.volumes.len(), 1);
155 assert_eq!(deserialized.volumes[0].source, "/host/path");
156 assert_eq!(deserialized.volumes[0].readonly, true);
157 assert_eq!(deserialized.working_dir, Some("/app".to_string()));
158 assert_eq!(deserialized.user, "1000:1000");
159 assert_eq!(deserialized.network, "host");
160 assert_eq!(deserialized.memory, Some("512m".to_string()));
161 assert_eq!(deserialized.cpu, Some("1.0".to_string()));
162 assert_eq!(deserialized.pull, "always");
163 assert_eq!(deserialized.containerd_addr, "/custom/containerd.sock");
164 assert_eq!(deserialized.tls.ca, Some("/ca.pem".to_string()));
165 assert_eq!(deserialized.tls.cert, Some("/cert.pem".to_string()));
166 assert_eq!(deserialized.tls.key, Some("/key.pem".to_string()));
167 assert!(
168 deserialized
169 .registry_auth
170 .contains_key("registry.example.com")
171 );
172 assert_eq!(deserialized.timeout_ms, Some(30000));
173 }
174
175 #[test]
176 fn serde_round_trip_minimal_config() {
177 let json = r#"{"image": "alpine:latest"}"#;
178 let config: ContainerdConfig = serde_json::from_str(json).unwrap();
179
180 assert_eq!(config.image, "alpine:latest");
181 assert_eq!(config.command, None);
182 assert_eq!(config.run, None);
183 assert!(config.env.is_empty());
184 assert!(config.volumes.is_empty());
185 assert_eq!(config.working_dir, None);
186 assert_eq!(config.user, "65534:65534");
187 assert_eq!(config.network, "none");
188 assert_eq!(config.memory, None);
189 assert_eq!(config.cpu, None);
190 assert_eq!(config.pull, "if-not-present");
191 assert_eq!(config.containerd_addr, "/run/containerd/containerd.sock");
192 assert_eq!(config.timeout_ms, None);
193
194 let serialized = serde_json::to_string(&config).unwrap();
196 let deserialized: ContainerdConfig = serde_json::from_str(&serialized).unwrap();
197 assert_eq!(deserialized.image, "alpine:latest");
198 assert_eq!(deserialized.user, "65534:65534");
199 }
200
201 #[test]
202 fn default_values() {
203 let json = r#"{"image": "busybox"}"#;
204 let config: ContainerdConfig = serde_json::from_str(json).unwrap();
205
206 assert_eq!(config.user, "65534:65534");
207 assert_eq!(config.network, "none");
208 assert_eq!(config.pull, "if-not-present");
209 assert_eq!(config.containerd_addr, "/run/containerd/containerd.sock");
210 }
211
212 #[test]
213 fn volume_mount_serde() {
214 let vol = VolumeMountConfig {
215 source: "/data".to_string(),
216 target: "/mnt/data".to_string(),
217 readonly: false,
218 };
219 let json = serde_json::to_string(&vol).unwrap();
220 let deserialized: VolumeMountConfig = serde_json::from_str(&json).unwrap();
221 assert_eq!(deserialized.source, "/data");
222 assert_eq!(deserialized.target, "/mnt/data");
223 assert_eq!(deserialized.readonly, false);
224
225 let vol_ro = VolumeMountConfig {
227 source: "/src".to_string(),
228 target: "/dest".to_string(),
229 readonly: true,
230 };
231 let json_ro = serde_json::to_string(&vol_ro).unwrap();
232 let deserialized_ro: VolumeMountConfig = serde_json::from_str(&json_ro).unwrap();
233 assert_eq!(deserialized_ro.readonly, true);
234 }
235
236 #[test]
237 fn tls_config_defaults() {
238 let tls = TlsConfig::default();
239 assert_eq!(tls.ca, None);
240 assert_eq!(tls.cert, None);
241 assert_eq!(tls.key, None);
242
243 let json = r#"{}"#;
244 let deserialized: TlsConfig = serde_json::from_str(json).unwrap();
245 assert_eq!(deserialized.ca, None);
246 assert_eq!(deserialized.cert, None);
247 assert_eq!(deserialized.key, None);
248 }
249
250 #[test]
251 fn registry_auth_serde() {
252 let auth = RegistryAuth {
253 username: "admin".to_string(),
254 password: "secret123".to_string(),
255 };
256 let json = serde_json::to_string(&auth).unwrap();
257 let deserialized: RegistryAuth = serde_json::from_str(&json).unwrap();
258 assert_eq!(deserialized.username, "admin");
259 assert_eq!(deserialized.password, "secret123");
260 }
261}