1use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub struct ResourceConfig {
12 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub cpu: Option<CpuConfig>,
15
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub memory: Option<MemoryConfig>,
19
20 #[serde(default)]
22 pub network: NetworkConfig,
23
24 #[serde(default)]
26 pub filesystem: FilesystemConfig,
27
28 #[serde(default)]
30 pub execution: ExecutionLimits,
31}
32
33impl ResourceConfig {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn with_cpu(mut self, cpu: CpuConfig) -> Self {
41 self.cpu = Some(cpu);
42 self
43 }
44
45 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
47 self.memory = Some(memory);
48 self
49 }
50
51 pub fn with_network(mut self, network: NetworkConfig) -> Self {
53 self.network = network;
54 self
55 }
56
57 pub fn with_filesystem(mut self, filesystem: FilesystemConfig) -> Self {
59 self.filesystem = filesystem;
60 self
61 }
62
63 pub fn with_execution(mut self, execution: ExecutionLimits) -> Self {
65 self.execution = execution;
66 self
67 }
68
69 pub fn with_network_enabled(mut self) -> Self {
71 self.network.enabled = true;
72 self
73 }
74
75 pub fn with_network_disabled(mut self) -> Self {
77 self.network.enabled = false;
78 self
79 }
80
81 pub fn with_timeout(mut self, seconds: u64) -> Self {
83 self.execution.timeout_seconds = Some(seconds);
84 self
85 }
86
87 pub fn with_memory_limit(mut self, limit: impl Into<String>) -> Self {
89 self.memory = Some(MemoryConfig {
90 limit: limit.into(),
91 swap: None,
92 reservation: None,
93 });
94 self
95 }
96
97 pub fn with_cpu_limit(mut self, limit: impl Into<String>) -> Self {
99 self.cpu = Some(CpuConfig {
100 limit: limit.into(),
101 shares: None,
102 });
103 self
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub struct CpuConfig {
111 pub limit: String,
113
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub shares: Option<u32>,
117}
118
119impl CpuConfig {
120 pub fn new(limit: impl Into<String>) -> Self {
122 Self {
123 limit: limit.into(),
124 shares: None,
125 }
126 }
127
128 pub fn with_shares(mut self, shares: u32) -> Self {
130 self.shares = Some(shares);
131 self
132 }
133
134 pub fn limit_as_cores(&self) -> Option<f64> {
136 self.limit.parse().ok()
137 }
138
139 pub fn as_docker_quota(&self) -> Option<i64> {
141 self.limit_as_cores().map(|cores| (cores * 100_000.0) as i64)
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub struct MemoryConfig {
149 pub limit: String,
151
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub swap: Option<String>,
155
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub reservation: Option<String>,
159}
160
161impl MemoryConfig {
162 pub fn new(limit: impl Into<String>) -> Self {
164 Self {
165 limit: limit.into(),
166 swap: None,
167 reservation: None,
168 }
169 }
170
171 pub fn with_swap(mut self, swap: impl Into<String>) -> Self {
173 self.swap = Some(swap.into());
174 self
175 }
176
177 pub fn without_swap(mut self) -> Self {
179 self.swap = Some("0".to_string());
180 self
181 }
182
183 pub fn with_reservation(mut self, reservation: impl Into<String>) -> Self {
185 self.reservation = Some(reservation.into());
186 self
187 }
188
189 pub fn limit_as_bytes(&self) -> Option<u64> {
191 parse_size(&self.limit)
192 }
193
194 pub fn swap_as_bytes(&self) -> Option<u64> {
196 self.swap.as_ref().and_then(|s| parse_size(s))
197 }
198
199 pub fn reservation_as_bytes(&self) -> Option<u64> {
201 self.reservation.as_ref().and_then(|s| parse_size(s))
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "snake_case")]
208pub struct NetworkConfig {
209 #[serde(default)]
211 pub enabled: bool,
212
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub mode: Option<String>,
216
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub allowed_hosts: Option<Vec<String>>,
220
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub blocked_hosts: Option<Vec<String>>,
224
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub dns: Option<Vec<String>>,
228}
229
230impl Default for NetworkConfig {
231 fn default() -> Self {
232 Self {
233 enabled: false,
234 mode: None,
235 allowed_hosts: None,
236 blocked_hosts: None,
237 dns: None,
238 }
239 }
240}
241
242impl NetworkConfig {
243 pub fn enabled() -> Self {
245 Self {
246 enabled: true,
247 ..Default::default()
248 }
249 }
250
251 pub fn disabled() -> Self {
253 Self::default()
254 }
255
256 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
258 self.mode = Some(mode.into());
259 self
260 }
261
262 pub fn with_allowed_hosts(mut self, hosts: Vec<String>) -> Self {
264 self.allowed_hosts = Some(hosts);
265 self
266 }
267
268 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
270 self.allowed_hosts
271 .get_or_insert_with(Vec::new)
272 .push(host.into());
273 self
274 }
275
276 pub fn with_blocked_hosts(mut self, hosts: Vec<String>) -> Self {
278 self.blocked_hosts = Some(hosts);
279 self
280 }
281
282 pub fn block_host(mut self, host: impl Into<String>) -> Self {
284 self.blocked_hosts
285 .get_or_insert_with(Vec::new)
286 .push(host.into());
287 self
288 }
289
290 pub fn with_dns(mut self, servers: Vec<String>) -> Self {
292 self.dns = Some(servers);
293 self
294 }
295
296 pub fn is_host_allowed(&self, host: &str) -> bool {
298 if !self.enabled {
299 return false;
300 }
301
302 if let Some(ref blocked) = self.blocked_hosts {
304 if blocked.iter().any(|b| host_matches(host, b)) {
305 return false;
306 }
307 }
308
309 if let Some(ref allowed) = self.allowed_hosts {
311 return allowed.iter().any(|a| host_matches(host, a));
312 }
313
314 true
316 }
317}
318
319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "snake_case")]
322pub struct FilesystemConfig {
323 #[serde(default)]
325 pub read_only_root: bool,
326
327 #[serde(default, skip_serializing_if = "Vec::is_empty")]
329 pub writable_paths: Vec<String>,
330
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub max_file_size: Option<String>,
334
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub max_disk_usage: Option<String>,
338}
339
340impl FilesystemConfig {
341 pub fn new() -> Self {
343 Self::default()
344 }
345
346 pub fn read_only(mut self) -> Self {
348 self.read_only_root = true;
349 self
350 }
351
352 pub fn with_writable_path(mut self, path: impl Into<String>) -> Self {
354 self.writable_paths.push(path.into());
355 self
356 }
357
358 pub fn with_max_file_size(mut self, size: impl Into<String>) -> Self {
360 self.max_file_size = Some(size.into());
361 self
362 }
363
364 pub fn with_max_disk_usage(mut self, size: impl Into<String>) -> Self {
366 self.max_disk_usage = Some(size.into());
367 self
368 }
369
370 pub fn max_file_size_bytes(&self) -> Option<u64> {
372 self.max_file_size.as_ref().and_then(|s| parse_size(s))
373 }
374
375 pub fn max_disk_usage_bytes(&self) -> Option<u64> {
377 self.max_disk_usage.as_ref().and_then(|s| parse_size(s))
378 }
379}
380
381#[derive(Debug, Clone, Default, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub struct ExecutionLimits {
385 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub timeout_seconds: Option<u64>,
388
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub max_concurrent: Option<u32>,
392
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub rate_limit: Option<RateLimit>,
396}
397
398impl ExecutionLimits {
399 pub fn new() -> Self {
401 Self::default()
402 }
403
404 pub fn with_timeout(mut self, seconds: u64) -> Self {
406 self.timeout_seconds = Some(seconds);
407 self
408 }
409
410 pub fn with_max_concurrent(mut self, max: u32) -> Self {
412 self.max_concurrent = Some(max);
413 self
414 }
415
416 pub fn with_rate_limit(mut self, requests: u32, window_seconds: u32) -> Self {
418 self.rate_limit = Some(RateLimit {
419 requests,
420 window_seconds,
421 });
422 self
423 }
424
425 pub fn timeout(&self) -> Option<Duration> {
427 self.timeout_seconds.map(Duration::from_secs)
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433#[serde(rename_all = "snake_case")]
434pub struct RateLimit {
435 pub requests: u32,
437
438 pub window_seconds: u32,
440}
441
442impl RateLimit {
443 pub fn new(requests: u32, window_seconds: u32) -> Self {
445 Self {
446 requests,
447 window_seconds,
448 }
449 }
450
451 pub fn window(&self) -> Duration {
453 Duration::from_secs(self.window_seconds as u64)
454 }
455
456 pub fn requests_per_second(&self) -> f64 {
458 if self.window_seconds == 0 {
459 0.0
460 } else {
461 self.requests as f64 / self.window_seconds as f64
462 }
463 }
464}
465
466pub fn parse_size(s: &str) -> Option<u64> {
468 let s = s.trim().to_lowercase();
469 if s.is_empty() {
470 return None;
471 }
472
473 let (num_str, multiplier) = if s.ends_with("gb") || s.ends_with("g") {
474 let num = s.trim_end_matches(|c| c == 'g' || c == 'b');
475 (num, 1024 * 1024 * 1024)
476 } else if s.ends_with("mb") || s.ends_with("m") {
477 let num = s.trim_end_matches(|c| c == 'm' || c == 'b');
478 (num, 1024 * 1024)
479 } else if s.ends_with("kb") || s.ends_with("k") {
480 let num = s.trim_end_matches(|c| c == 'k' || c == 'b');
481 (num, 1024)
482 } else if s.ends_with('b') {
483 let num = s.trim_end_matches('b');
484 (num, 1)
485 } else {
486 (s.as_str(), 1)
488 };
489
490 num_str.trim().parse::<u64>().ok().map(|n| n * multiplier)
491}
492
493fn host_matches(host: &str, pattern: &str) -> bool {
495 if pattern.starts_with("*.") {
496 let suffix = &pattern[1..]; host.ends_with(suffix) || host == &pattern[2..]
498 } else {
499 host == pattern
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn test_resource_config_builder() {
509 let config = ResourceConfig::new()
510 .with_cpu_limit("2")
511 .with_memory_limit("1g")
512 .with_network_enabled()
513 .with_timeout(300);
514
515 assert!(config.cpu.is_some());
516 assert!(config.memory.is_some());
517 assert!(config.network.enabled);
518 assert_eq!(config.execution.timeout_seconds, Some(300));
519 }
520
521 #[test]
522 fn test_cpu_config() {
523 let cpu = CpuConfig::new("2.5").with_shares(1024);
524
525 assert_eq!(cpu.limit_as_cores(), Some(2.5));
526 assert_eq!(cpu.shares, Some(1024));
527 assert_eq!(cpu.as_docker_quota(), Some(250_000));
528 }
529
530 #[test]
531 fn test_memory_config() {
532 let mem = MemoryConfig::new("512m")
533 .with_swap("1g")
534 .with_reservation("256m");
535
536 assert_eq!(mem.limit_as_bytes(), Some(512 * 1024 * 1024));
537 assert_eq!(mem.swap_as_bytes(), Some(1024 * 1024 * 1024));
538 assert_eq!(mem.reservation_as_bytes(), Some(256 * 1024 * 1024));
539 }
540
541 #[test]
542 fn test_network_config() {
543 let net = NetworkConfig::enabled()
544 .with_mode("bridge")
545 .allow_host("api.example.com")
546 .allow_host("*.amazonaws.com")
547 .block_host("blocked.example.com");
548
549 assert!(net.enabled);
550 assert!(net.is_host_allowed("api.example.com"));
551 assert!(net.is_host_allowed("s3.amazonaws.com"));
552 assert!(!net.is_host_allowed("blocked.example.com"));
553 assert!(!net.is_host_allowed("other.com"));
554 }
555
556 #[test]
557 fn test_network_disabled() {
558 let net = NetworkConfig::disabled();
559 assert!(!net.is_host_allowed("any.com"));
560 }
561
562 #[test]
563 fn test_filesystem_config() {
564 let fs = FilesystemConfig::new()
565 .read_only()
566 .with_writable_path("/tmp")
567 .with_max_file_size("100m");
568
569 assert!(fs.read_only_root);
570 assert!(fs.writable_paths.contains(&"/tmp".to_string()));
571 assert_eq!(fs.max_file_size_bytes(), Some(100 * 1024 * 1024));
572 }
573
574 #[test]
575 fn test_execution_limits() {
576 let limits = ExecutionLimits::new()
577 .with_timeout(60)
578 .with_max_concurrent(10)
579 .with_rate_limit(100, 60);
580
581 assert_eq!(limits.timeout(), Some(Duration::from_secs(60)));
582 assert_eq!(limits.max_concurrent, Some(10));
583
584 let rate = limits.rate_limit.unwrap();
585 assert_eq!(rate.requests_per_second(), 100.0 / 60.0);
586 }
587
588 #[test]
589 fn test_parse_size() {
590 assert_eq!(parse_size("1024"), Some(1024));
591 assert_eq!(parse_size("1k"), Some(1024));
592 assert_eq!(parse_size("1kb"), Some(1024));
593 assert_eq!(parse_size("1m"), Some(1024 * 1024));
594 assert_eq!(parse_size("1mb"), Some(1024 * 1024));
595 assert_eq!(parse_size("1g"), Some(1024 * 1024 * 1024));
596 assert_eq!(parse_size("1gb"), Some(1024 * 1024 * 1024));
597 assert_eq!(parse_size("512M"), Some(512 * 1024 * 1024));
598 assert_eq!(parse_size(""), None);
599 assert_eq!(parse_size("invalid"), None);
600 }
601
602 #[test]
603 fn test_host_matches() {
604 assert!(host_matches("api.example.com", "api.example.com"));
605 assert!(host_matches("api.example.com", "*.example.com"));
606 assert!(host_matches("sub.api.example.com", "*.example.com"));
607 assert!(host_matches("example.com", "*.example.com"));
608 assert!(!host_matches("other.com", "*.example.com"));
609 }
610
611 #[test]
612 fn test_resource_config_serialization() {
613 let config = ResourceConfig::new()
614 .with_cpu_limit("2")
615 .with_memory_limit("1g")
616 .with_network_enabled();
617
618 let json = serde_json::to_string(&config).unwrap();
619 let deserialized: ResourceConfig = serde_json::from_str(&json).unwrap();
620
621 assert!(deserialized.cpu.is_some());
622 assert!(deserialized.memory.is_some());
623 assert!(deserialized.network.enabled);
624 }
625}