1mod error;
6mod types;
7mod validate;
8
9pub use error::*;
10pub use types::*;
11pub use validate::*;
12
13use validator::Validate;
14
15pub fn from_yaml_str(yaml: &str) -> Result<DeploymentSpec, SpecError> {
21 let spec: DeploymentSpec = serde_yaml::from_str(yaml)?;
22
23 spec.validate().map_err(|e| {
25 SpecError::Validation(ValidationError {
26 kind: ValidationErrorKind::Generic {
27 message: e.to_string(),
28 },
29 path: String::new(),
30 })
31 })?;
32
33 validate_dependencies(&spec)?;
35 validate_unique_service_endpoints(&spec)?;
36 validate_cron_schedules(&spec)?;
37 validate_tunnels(&spec)?;
38 validate_wasm_configs(&spec)?;
39
40 Ok(spec)
41}
42
43pub fn from_yaml_file(path: &std::path::Path) -> Result<DeploymentSpec, SpecError> {
49 let content = std::fs::read_to_string(path)?;
50 from_yaml_str(&content)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn test_parse_from_yaml_str() {
59 let yaml = r"
60version: v1
61deployment: test
62services:
63 hello:
64 rtype: service
65 image:
66 name: hello-world:latest
67";
68 let result = from_yaml_str(yaml);
69 assert!(result.is_ok());
70 let spec = result.unwrap();
71 assert_eq!(spec.version, "v1");
72 assert_eq!(spec.deployment, "test");
73 }
74
75 #[test]
80 fn test_invalid_version_rejected() {
81 let yaml = r"
82version: v2
83deployment: my-app
84services:
85 hello:
86 image:
87 name: hello-world:latest
88";
89 let result = from_yaml_str(yaml);
90 assert!(result.is_err());
91 let err = result.unwrap_err();
92 let err_str = err.to_string();
93 assert!(
94 err_str.contains("version") || err_str.contains("v1"),
95 "Error should mention version: {err_str}",
96 );
97 }
98
99 #[test]
100 fn test_empty_deployment_name_rejected() {
101 let yaml = r"
102version: v1
103deployment: ab
104services:
105 hello:
106 image:
107 name: hello-world:latest
108";
109 let result = from_yaml_str(yaml);
110 assert!(result.is_err());
111 let err = result.unwrap_err();
112 let err_str = err.to_string();
113 assert!(
114 err_str.contains("deployment") || err_str.contains("3-63"),
115 "Error should mention deployment name: {err_str}",
116 );
117 }
118
119 #[test]
120 fn test_invalid_port_zero_rejected() {
121 let yaml = r"
122version: v1
123deployment: my-app
124services:
125 hello:
126 image:
127 name: hello-world:latest
128 endpoints:
129 - name: http
130 protocol: http
131 port: 0
132";
133 let result = from_yaml_str(yaml);
134 assert!(result.is_err());
135 let err = result.unwrap_err();
136 let err_str = err.to_string();
137 assert!(
138 err_str.contains("port"),
139 "Error should mention port: {err_str}",
140 );
141 }
142
143 #[test]
144 fn test_unknown_dependency_rejected() {
145 let yaml = r"
146version: v1
147deployment: my-app
148services:
149 api:
150 image:
151 name: api:latest
152 depends:
153 - service: database
154";
155 let result = from_yaml_str(yaml);
156 assert!(result.is_err());
157 let err = result.unwrap_err();
158 let err_str = err.to_string();
159 assert!(
160 err_str.contains("database") || err_str.contains("unknown"),
161 "Error should mention unknown dependency: {err_str}",
162 );
163 }
164
165 #[test]
166 fn test_duplicate_endpoint_names_rejected() {
167 let yaml = r"
168version: v1
169deployment: my-app
170services:
171 api:
172 image:
173 name: api:latest
174 endpoints:
175 - name: http
176 protocol: http
177 port: 8080
178 - name: http
179 protocol: https
180 port: 8443
181";
182 let result = from_yaml_str(yaml);
183 assert!(result.is_err());
184 let err = result.unwrap_err();
185 let err_str = err.to_string();
186 assert!(
187 err_str.contains("http") || err_str.contains("duplicate"),
188 "Error should mention duplicate endpoint: {err_str}",
189 );
190 }
191
192 #[test]
193 fn test_invalid_scale_range_min_gt_max_rejected() {
194 let yaml = r"
195version: v1
196deployment: my-app
197services:
198 api:
199 image:
200 name: api:latest
201 scale:
202 mode: adaptive
203 min: 10
204 max: 5
205";
206 let result = from_yaml_str(yaml);
207 assert!(result.is_err());
208 let err = result.unwrap_err();
209 let err_str = err.to_string();
210 assert!(
211 err_str.contains("scale") || err_str.contains("min") || err_str.contains("max"),
212 "Error should mention scale range: {err_str}",
213 );
214 }
215
216 #[test]
217 fn test_invalid_cpu_zero_rejected() {
218 let yaml = r"
219version: v1
220deployment: my-app
221services:
222 api:
223 image:
224 name: api:latest
225 resources:
226 cpu: 0
227";
228 let result = from_yaml_str(yaml);
229 assert!(result.is_err());
230 let err = result.unwrap_err();
231 let err_str = err.to_string();
232 assert!(
233 err_str.contains("cpu") || err_str.contains("CPU"),
234 "Error should mention CPU: {err_str}",
235 );
236 }
237
238 #[test]
239 fn test_invalid_memory_format_rejected() {
240 let yaml = r"
241version: v1
242deployment: my-app
243services:
244 api:
245 image:
246 name: api:latest
247 resources:
248 memory: 512MB
249";
250 let result = from_yaml_str(yaml);
251 assert!(result.is_err());
252 let err = result.unwrap_err();
253 let err_str = err.to_string();
254 assert!(
255 err_str.contains("memory"),
256 "Error should mention memory format: {err_str}",
257 );
258 }
259
260 #[test]
261 fn test_empty_image_name_rejected() {
262 let yaml = r#"
263version: v1
264deployment: my-app
265services:
266 api:
267 image:
268 name: ""
269"#;
270 let result = from_yaml_str(yaml);
271 assert!(result.is_err());
272 let err = result.unwrap_err();
273 let err_str = err.to_string();
274 assert!(
275 err_str.contains("image") || err_str.contains("empty"),
276 "Error should mention empty image name: {err_str}",
277 );
278 }
279
280 #[test]
281 fn test_valid_spec_passes_validation() {
282 let yaml = r"
283version: v1
284deployment: my-production-app
285services:
286 api:
287 image:
288 name: ghcr.io/myorg/api:v1.2.3
289 resources:
290 cpu: 0.5
291 memory: 512Mi
292 endpoints:
293 - name: http
294 protocol: http
295 port: 8080
296 expose: public
297 - name: metrics
298 protocol: http
299 port: 9090
300 expose: internal
301 scale:
302 mode: adaptive
303 min: 2
304 max: 10
305 targets:
306 cpu: 70
307 depends:
308 - service: database
309 condition: healthy
310 database:
311 image:
312 name: postgres:15
313 endpoints:
314 - name: postgres
315 protocol: tcp
316 port: 5432
317 scale:
318 mode: fixed
319 replicas: 1
320";
321 let result = from_yaml_str(yaml);
322 assert!(result.is_ok(), "Valid spec should pass: {result:?}");
323 let spec = result.unwrap();
324 assert_eq!(spec.version, "v1");
325 assert_eq!(spec.deployment, "my-production-app");
326 assert_eq!(spec.services.len(), 2);
327 }
328
329 #[test]
330 fn test_valid_dependency_passes() {
331 let yaml = r"
332version: v1
333deployment: my-app
334services:
335 api:
336 image:
337 name: api:latest
338 depends:
339 - service: database
340 database:
341 image:
342 name: postgres:15
343";
344 let result = from_yaml_str(yaml);
345 assert!(result.is_ok(), "Valid dependency should pass: {result:?}");
346 }
347
348 #[test]
353 fn test_valid_cron_job_with_schedule() {
354 let yaml = r#"
355version: v1
356deployment: my-app
357services:
358 cleanup:
359 rtype: cron
360 image:
361 name: cleanup:latest
362 schedule: "0 0 0 * * * *"
363"#;
364 let result = from_yaml_str(yaml);
365 assert!(result.is_ok(), "Valid cron job should pass: {result:?}");
366 let spec = result.unwrap();
367 let cleanup = spec.services.get("cleanup").unwrap();
368 assert_eq!(cleanup.rtype, ResourceType::Cron);
369 assert_eq!(cleanup.schedule, Some("0 0 0 * * * *".to_string()));
370 }
371
372 #[test]
373 fn test_cron_without_schedule_rejected() {
374 let yaml = r"
375version: v1
376deployment: my-app
377services:
378 cleanup:
379 rtype: cron
380 image:
381 name: cleanup:latest
382";
383 let result = from_yaml_str(yaml);
384 assert!(result.is_err());
385 let err = result.unwrap_err();
386 let err_str = err.to_string();
387 assert!(
388 err_str.contains("schedule") || err_str.contains("cron"),
389 "Error should mention missing schedule: {err_str}",
390 );
391 }
392
393 #[test]
394 fn test_service_with_schedule_rejected() {
395 let yaml = r#"
396version: v1
397deployment: my-app
398services:
399 api:
400 rtype: service
401 image:
402 name: api:latest
403 schedule: "0 0 0 * * * *"
404"#;
405 let result = from_yaml_str(yaml);
406 assert!(result.is_err());
407 let err = result.unwrap_err();
408 let err_str = err.to_string();
409 assert!(
410 err_str.contains("schedule") || err_str.contains("cron"),
411 "Error should mention schedule/cron mismatch: {err_str}",
412 );
413 }
414
415 #[test]
416 fn test_job_with_schedule_rejected() {
417 let yaml = r#"
418version: v1
419deployment: my-app
420services:
421 backup:
422 rtype: job
423 image:
424 name: backup:latest
425 schedule: "0 0 0 * * * *"
426"#;
427 let result = from_yaml_str(yaml);
428 assert!(result.is_err());
429 let err = result.unwrap_err();
430 let err_str = err.to_string();
431 assert!(
432 err_str.contains("schedule") || err_str.contains("cron"),
433 "Error should mention schedule/cron mismatch: {err_str}",
434 );
435 }
436
437 #[test]
438 fn test_invalid_cron_expression_rejected() {
439 let yaml = r#"
440version: v1
441deployment: my-app
442services:
443 cleanup:
444 rtype: cron
445 image:
446 name: cleanup:latest
447 schedule: "not a valid cron"
448"#;
449 let result = from_yaml_str(yaml);
450 assert!(result.is_err());
451 let err = result.unwrap_err();
452 let err_str = err.to_string();
453 assert!(
454 err_str.contains("cron") || err_str.contains("schedule") || err_str.contains("invalid"),
455 "Error should mention invalid cron expression: {err_str}",
456 );
457 }
458
459 #[test]
460 fn test_valid_extended_cron_expression() {
461 let yaml = r#"
462version: v1
463deployment: my-app
464services:
465 cleanup:
466 rtype: cron
467 image:
468 name: cleanup:latest
469 schedule: "0 30 2 * * * *"
470"#;
471 let result = from_yaml_str(yaml);
472 assert!(
473 result.is_ok(),
474 "Extended cron expression should be valid: {result:?}"
475 );
476 }
477
478 #[test]
479 fn test_mixed_service_types_valid() {
480 let yaml = r#"
481version: v1
482deployment: my-app
483services:
484 api:
485 rtype: service
486 image:
487 name: api:latest
488 endpoints:
489 - name: http
490 protocol: http
491 port: 8080
492 backup:
493 rtype: job
494 image:
495 name: backup:latest
496 cleanup:
497 rtype: cron
498 image:
499 name: cleanup:latest
500 schedule: "0 0 0 * * * *"
501"#;
502 let result = from_yaml_str(yaml);
503 assert!(
504 result.is_ok(),
505 "Mixed service types should be valid: {result:?}"
506 );
507 let spec = result.unwrap();
508 assert_eq!(spec.services.len(), 3);
509 assert_eq!(spec.services["api"].rtype, ResourceType::Service);
510 assert_eq!(spec.services["backup"].rtype, ResourceType::Job);
511 assert_eq!(spec.services["cleanup"].rtype, ResourceType::Cron);
512 }
513
514 #[test]
519 fn test_valid_endpoint_tunnel() {
520 let yaml = r"
521version: v1
522deployment: my-app
523services:
524 api:
525 image:
526 name: api:latest
527 endpoints:
528 - name: http
529 protocol: http
530 port: 8080
531 tunnel:
532 enabled: true
533 remote_port: 8080
534";
535 let result = from_yaml_str(yaml);
536 assert!(
537 result.is_ok(),
538 "Valid endpoint tunnel should pass: {result:?}"
539 );
540 }
541
542 #[test]
543 fn test_valid_top_level_tunnel() {
544 let yaml = r"
545version: v1
546deployment: my-app
547services:
548 api:
549 image:
550 name: api:latest
551tunnels:
552 db-access:
553 from: api-node
554 to: db-node
555 local_port: 5432
556 remote_port: 5432
557";
558 let result = from_yaml_str(yaml);
559 assert!(
560 result.is_ok(),
561 "Valid top-level tunnel should pass: {result:?}"
562 );
563 let spec = result.unwrap();
564 assert!(spec.tunnels.contains_key("db-access"));
565 }
566
567 #[test]
568 fn test_invalid_tunnel_ttl_rejected() {
569 let yaml = r"
570version: v1
571deployment: my-app
572services:
573 api:
574 image:
575 name: api:latest
576 endpoints:
577 - name: http
578 protocol: http
579 port: 8080
580 tunnel:
581 enabled: true
582 access:
583 enabled: true
584 max_ttl: invalid
585";
586 let result = from_yaml_str(yaml);
587 assert!(result.is_err());
588 let err = result.unwrap_err();
589 let err_str = err.to_string();
590 assert!(
591 err_str.contains("ttl") || err_str.contains("TTL") || err_str.contains("invalid"),
592 "Error should mention invalid TTL: {err_str}",
593 );
594 }
595
596 #[test]
597 fn test_invalid_tunnel_local_port_zero_rejected() {
598 let yaml = r"
599version: v1
600deployment: my-app
601services: {}
602tunnels:
603 bad-tunnel:
604 from: node-a
605 to: node-b
606 local_port: 0
607 remote_port: 8080
608";
609 let result = from_yaml_str(yaml);
610 assert!(result.is_err());
611 let err = result.unwrap_err();
612 let err_str = err.to_string();
613 assert!(
614 err_str.contains("port") || err_str.contains("local"),
615 "Error should mention invalid port: {err_str}",
616 );
617 }
618
619 #[test]
620 fn test_invalid_tunnel_remote_port_zero_rejected() {
621 let yaml = r"
622version: v1
623deployment: my-app
624services: {}
625tunnels:
626 bad-tunnel:
627 from: node-a
628 to: node-b
629 local_port: 8080
630 remote_port: 0
631";
632 let result = from_yaml_str(yaml);
633 assert!(result.is_err());
634 let err = result.unwrap_err();
635 let err_str = err.to_string();
636 assert!(
637 err_str.contains("port") || err_str.contains("remote"),
638 "Error should mention invalid port: {err_str}",
639 );
640 }
641
642 #[test]
643 fn test_valid_tunnel_with_access_config() {
644 let yaml = r"
645version: v1
646deployment: my-app
647services:
648 api:
649 image:
650 name: api:latest
651 endpoints:
652 - name: http
653 protocol: http
654 port: 8080
655 tunnel:
656 enabled: true
657 remote_port: 0
658 access:
659 enabled: true
660 max_ttl: 4h
661 audit: true
662";
663 let result = from_yaml_str(yaml);
664 assert!(
665 result.is_ok(),
666 "Valid tunnel with access config should pass: {result:?}"
667 );
668 let spec = result.unwrap();
669 let tunnel = spec.services["api"].endpoints[0].tunnel.as_ref().unwrap();
670 let access = tunnel.access.as_ref().unwrap();
671 assert!(access.enabled);
672 assert_eq!(access.max_ttl, Some("4h".to_string()));
673 assert!(access.audit);
674 }
675
676 #[test]
681 fn test_wasm_config_on_non_wasm_type_rejected() {
682 let yaml = r"
683version: v1
684deployment: my-app
685services:
686 api:
687 image:
688 name: api:latest
689 service_type: standard
690 wasm:
691 min_instances: 1
692 max_instances: 4
693";
694 let result = from_yaml_str(yaml);
695 assert!(result.is_err());
696 let err_str = result.unwrap_err().to_string();
697 assert!(
698 err_str.contains("wasm") || err_str.contains("WASM"),
699 "Error should mention wasm config on non-wasm type: {err_str}",
700 );
701 }
702
703 #[test]
704 fn test_wasm_service_without_config_is_ok() {
705 let yaml = r"
707version: v1
708deployment: my-app
709services:
710 handler:
711 image:
712 name: handler:latest
713 service_type: wasm_http
714";
715 let result = from_yaml_str(yaml);
716 assert!(
717 result.is_ok(),
718 "WASM service without explicit config should pass: {result:?}"
719 );
720 }
721
722 #[test]
723 fn test_wasm_min_instances_gt_max_rejected() {
724 let yaml = r"
725version: v1
726deployment: my-app
727services:
728 handler:
729 image:
730 name: handler:latest
731 service_type: wasm_http
732 wasm:
733 min_instances: 10
734 max_instances: 2
735";
736 let result = from_yaml_str(yaml);
737 assert!(result.is_err());
738 let err_str = result.unwrap_err().to_string();
739 assert!(
740 err_str.contains("min_instances") || err_str.contains("max_instances"),
741 "Error should mention instance range: {err_str}",
742 );
743 }
744
745 #[test]
746 fn test_wasm_valid_instance_range() {
747 let yaml = r"
748version: v1
749deployment: my-app
750services:
751 handler:
752 image:
753 name: handler:latest
754 service_type: wasm_http
755 wasm:
756 min_instances: 2
757 max_instances: 10
758";
759 let result = from_yaml_str(yaml);
760 assert!(
761 result.is_ok(),
762 "Valid WASM instance range should pass: {result:?}"
763 );
764 }
765
766 #[test]
767 fn test_wasm_invalid_max_memory_format() {
768 let yaml = r#"
769version: v1
770deployment: my-app
771services:
772 handler:
773 image:
774 name: handler:latest
775 service_type: wasm_http
776 wasm:
777 max_memory: "512MB"
778"#;
779 let result = from_yaml_str(yaml);
780 assert!(result.is_err());
781 let err_str = result.unwrap_err().to_string();
782 assert!(
783 err_str.contains("memory") || err_str.contains("512MB"),
784 "Error should mention invalid memory format: {err_str}",
785 );
786 }
787
788 #[test]
789 fn test_wasm_valid_max_memory_format() {
790 let yaml = r#"
791version: v1
792deployment: my-app
793services:
794 handler:
795 image:
796 name: handler:latest
797 service_type: wasm_http
798 wasm:
799 max_memory: "256Mi"
800"#;
801 let result = from_yaml_str(yaml);
802 assert!(
803 result.is_ok(),
804 "Valid WASM max_memory should pass: {result:?}"
805 );
806 }
807
808 #[test]
809 fn test_wasm_capability_escalation_rejected() {
810 let yaml = r"
812version: v1
813deployment: my-app
814services:
815 transform:
816 image:
817 name: transform:latest
818 service_type: wasm_transformer
819 wasm:
820 capabilities:
821 secrets: true
822";
823 let result = from_yaml_str(yaml);
824 assert!(result.is_err());
825 let err_str = result.unwrap_err().to_string();
826 assert!(
827 err_str.contains("secrets") || err_str.contains("capability"),
828 "Error should mention capability escalation: {err_str}",
829 );
830 }
831
832 #[test]
833 fn test_wasm_capability_restriction_allowed() {
834 let yaml = r"
838version: v1
839deployment: my-app
840services:
841 handler:
842 image:
843 name: handler:latest
844 service_type: wasm_http
845 wasm:
846 capabilities:
847 config: false
848 keyvalue: true
849 logging: true
850 secrets: false
851 metrics: false
852 http_client: true
853 cli: false
854 filesystem: false
855 sockets: false
856";
857 let result = from_yaml_str(yaml);
858 assert!(
859 result.is_ok(),
860 "Restricting a default capability should pass: {result:?}"
861 );
862 }
863
864 #[test]
865 fn test_wasm_http_with_tcp_only_endpoints_rejected() {
866 let yaml = r"
868version: v1
869deployment: my-app
870services:
871 handler:
872 image:
873 name: handler:latest
874 service_type: wasm_http
875 wasm:
876 min_instances: 1
877 endpoints:
878 - name: raw
879 protocol: tcp
880 port: 9090
881";
882 let result = from_yaml_str(yaml);
883 assert!(result.is_err());
884 let err_str = result.unwrap_err().to_string();
885 assert!(
886 err_str.contains("HTTP") || err_str.contains("http") || err_str.contains("endpoint"),
887 "Error should mention missing HTTP endpoint: {err_str}",
888 );
889 }
890
891 #[test]
892 fn test_wasm_http_with_http_endpoint_passes() {
893 let yaml = r"
894version: v1
895deployment: my-app
896services:
897 handler:
898 image:
899 name: handler:latest
900 service_type: wasm_http
901 wasm:
902 min_instances: 1
903 endpoints:
904 - name: web
905 protocol: http
906 port: 8080
907";
908 let result = from_yaml_str(yaml);
909 assert!(
910 result.is_ok(),
911 "WasmHttp with HTTP endpoint should pass: {result:?}"
912 );
913 }
914
915 #[test]
916 fn test_wasm_http_with_no_endpoints_passes() {
917 let yaml = r"
919version: v1
920deployment: my-app
921services:
922 handler:
923 image:
924 name: handler:latest
925 service_type: wasm_http
926 wasm:
927 min_instances: 1
928";
929 let result = from_yaml_str(yaml);
930 assert!(
931 result.is_ok(),
932 "WasmHttp with no endpoints should pass: {result:?}"
933 );
934 }
935
936 #[test]
937 fn test_wasm_preopen_empty_source_rejected() {
938 let yaml = r#"
939version: v1
940deployment: my-app
941services:
942 handler:
943 image:
944 name: handler:latest
945 service_type: wasm_plugin
946 wasm:
947 preopens:
948 - source: ""
949 target: /data
950"#;
951 let result = from_yaml_str(yaml);
952 assert!(result.is_err());
953 let err_str = result.unwrap_err().to_string();
954 assert!(
955 err_str.contains("preopen") || err_str.contains("source") || err_str.contains("empty"),
956 "Error should mention empty preopen source: {err_str}",
957 );
958 }
959
960 #[test]
961 fn test_wasm_preopen_empty_target_rejected() {
962 let yaml = r#"
963version: v1
964deployment: my-app
965services:
966 handler:
967 image:
968 name: handler:latest
969 service_type: wasm_plugin
970 wasm:
971 preopens:
972 - source: /host/data
973 target: ""
974"#;
975 let result = from_yaml_str(yaml);
976 assert!(result.is_err());
977 let err_str = result.unwrap_err().to_string();
978 assert!(
979 err_str.contains("preopen") || err_str.contains("target") || err_str.contains("empty"),
980 "Error should mention empty preopen target: {err_str}",
981 );
982 }
983
984 #[test]
985 fn test_wasm_valid_preopen_passes() {
986 let yaml = r"
987version: v1
988deployment: my-app
989services:
990 handler:
991 image:
992 name: handler:latest
993 service_type: wasm_plugin
994 wasm:
995 preopens:
996 - source: /host/data
997 target: /data
998 readonly: true
999";
1000 let result = from_yaml_str(yaml);
1001 assert!(result.is_ok(), "Valid WASM preopen should pass: {result:?}");
1002 }
1003
1004 #[test]
1005 fn test_wasm_plugin_can_use_all_capabilities() {
1006 let yaml = r"
1008version: v1
1009deployment: my-app
1010services:
1011 plugin:
1012 image:
1013 name: plugin:latest
1014 service_type: wasm_plugin
1015 wasm:
1016 capabilities:
1017 config: true
1018 keyvalue: true
1019 logging: true
1020 secrets: true
1021 metrics: true
1022 http_client: true
1023 cli: true
1024 filesystem: true
1025";
1026 let result = from_yaml_str(yaml);
1027 assert!(
1028 result.is_ok(),
1029 "WasmPlugin with all its default capabilities should pass: {result:?}"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_wasm_plugin_cannot_use_sockets() {
1035 let yaml = r"
1037version: v1
1038deployment: my-app
1039services:
1040 plugin:
1041 image:
1042 name: plugin:latest
1043 service_type: wasm_plugin
1044 wasm:
1045 capabilities:
1046 sockets: true
1047";
1048 let result = from_yaml_str(yaml);
1049 assert!(result.is_err());
1050 let err_str = result.unwrap_err().to_string();
1051 assert!(
1052 err_str.contains("sockets") || err_str.contains("capability"),
1053 "Error should mention sockets capability escalation: {err_str}",
1054 );
1055 }
1056
1057 #[test]
1058 fn test_wasm_equal_min_max_instances_passes() {
1059 let yaml = r"
1060version: v1
1061deployment: my-app
1062services:
1063 handler:
1064 image:
1065 name: handler:latest
1066 service_type: wasm_http
1067 wasm:
1068 min_instances: 5
1069 max_instances: 5
1070";
1071 let result = from_yaml_str(yaml);
1072 assert!(
1073 result.is_ok(),
1074 "Equal min/max instances should pass: {result:?}"
1075 );
1076 }
1077}