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