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