1use std::{
4 collections::{BTreeMap, BTreeSet},
5 error::Error,
6 fmt,
7 net::IpAddr,
8};
9
10use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Provenance, Sourced};
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14#[non_exhaustive]
15pub enum ModelError {
16 EmptyValue(&'static str),
18 ContainsNul(&'static str),
20 ReversedSpan {
22 start: usize,
24 end: usize,
26 },
27 DuplicateResource {
29 kind: &'static str,
31 name: String,
33 },
34 DuplicateServiceGroupMember {
36 group: String,
38 service: String,
40 },
41 UnknownServiceGroupMember {
43 group: String,
45 service: String,
47 },
48 ServiceInMultipleGroups {
50 service: String,
52 existing: String,
54 replacement: String,
56 },
57 UnknownImageAcquisitionReference {
59 service: String,
61 acquisition: String,
63 },
64 UnknownImageBuildReference {
66 service: String,
68 build: String,
70 },
71 UnknownVolumeImageAcquisitionReference {
73 volume: String,
75 acquisition: String,
77 },
78 UnknownVolumeImageBuildReference {
80 volume: String,
82 build: String,
84 },
85 UnknownArtifactDependencyNode {
87 kind: &'static str,
89 name: String,
91 },
92 ImageArtifactDependencyCycle {
94 nodes: Vec<String>,
96 },
97 InvalidImageReference(&'static str),
99 ZeroContainerPort,
101 UnknownNetworkAttachmentIndex {
103 index: usize,
105 len: usize,
107 },
108 UnknownServiceGroupRuntimeNetworkIndex {
110 index: usize,
112 len: usize,
114 },
115 RootfsImageSourceConflict {
117 service: String,
119 source: &'static str,
121 },
122 InvalidHealthcheckRetries,
124}
125
126impl fmt::Display for ModelError {
127 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::EmptyValue(kind) => write!(formatter, "{kind} must not be empty"),
130 Self::ContainsNul(kind) => write!(formatter, "{kind} must not contain a NUL byte"),
131 Self::ReversedSpan { start, end } => {
132 write!(formatter, "source span end {end} is before start {start}")
133 }
134 Self::DuplicateResource { kind, name } => {
135 write!(formatter, "duplicate {kind} `{name}`")
136 }
137 Self::DuplicateServiceGroupMember { group, service } => {
138 write!(
139 formatter,
140 "service group `{group}` contains duplicate member `{service}`"
141 )
142 }
143 Self::UnknownServiceGroupMember { group, service } => {
144 write!(
145 formatter,
146 "service group `{group}` references unknown service `{service}`"
147 )
148 }
149 Self::ServiceInMultipleGroups {
150 service,
151 existing,
152 replacement,
153 } => write!(
154 formatter,
155 "service `{service}` belongs to both service groups `{existing}` and `{replacement}`"
156 ),
157 Self::UnknownImageAcquisitionReference { service, acquisition } => write!(
158 formatter,
159 "service `{service}` references unknown image acquisition `{acquisition}`"
160 ),
161 Self::UnknownImageBuildReference { service, build } => {
162 write!(
163 formatter,
164 "service `{service}` references unknown image build `{build}`"
165 )
166 }
167 Self::UnknownVolumeImageAcquisitionReference { volume, acquisition } => write!(
168 formatter,
169 "volume `{volume}` references unknown image acquisition `{acquisition}`"
170 ),
171 Self::UnknownVolumeImageBuildReference { volume, build } => {
172 write!(formatter, "volume `{volume}` references unknown image build `{build}`")
173 }
174 Self::UnknownArtifactDependencyNode { kind, name } => {
175 write!(formatter, "artifact dependency references unknown {kind} `{name}`")
176 }
177 Self::ImageArtifactDependencyCycle { nodes } => {
178 write!(formatter, "image-artifact dependency cycle: {}", nodes.join(" -> "))
179 }
180 Self::InvalidImageReference(reason) => write!(formatter, "invalid image reference: {reason}"),
181 Self::ZeroContainerPort => formatter.write_str("container port must not be zero"),
182 Self::UnknownNetworkAttachmentIndex { index, len } => {
183 write!(
184 formatter,
185 "network attachment index {index} is outside collection length {len}"
186 )
187 }
188 Self::UnknownServiceGroupRuntimeNetworkIndex { index, len } => {
189 write!(
190 formatter,
191 "group-runtime network attachment index {index} is outside collection length {len}"
192 )
193 }
194 Self::RootfsImageSourceConflict { service, source } => write!(
195 formatter,
196 "service `{service}` combines rootfs with image source `{source}`"
197 ),
198 Self::InvalidHealthcheckRetries => {
199 formatter.write_str("health-check retries must be a non-negative decimal integer")
200 }
201 }
202 }
203}
204
205impl Error for ModelError {}
206
207#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
209pub struct Identifier(String);
210
211impl Identifier {
212 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
218 let value = value.into();
219 validate_text("identifier", &value)?;
220 Ok(Self(value))
221 }
222
223 #[must_use]
225 pub fn as_str(&self) -> &str {
226 &self.0
227 }
228}
229
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232#[non_exhaustive]
233pub enum ResourceOwnership {
234 Application,
236 External,
238 Implicit,
240 Uncertain,
242}
243
244#[derive(Clone, Debug, Eq, PartialEq)]
246pub struct Volume {
247 name: Identifier,
248 ownership: ResourceOwnership,
249 runtime_name: Option<Sourced<ProtectedString>>,
250 service_name: Option<Sourced<ProtectedString>>,
251 driver: Option<Sourced<ProtectedString>>,
252 device: Option<Sourced<ProtectedString>>,
253 type_spelling: Option<Sourced<ProtectedString>>,
254 options: Option<Sourced<ProtectedString>>,
255 labels: Option<Vec<Sourced<MetadataLabel>>>,
256 labels_origins: Vec<Provenance>,
257 copy: Option<Sourced<bool>>,
258 containers_conf_modules: Option<Vec<Sourced<ProtectedString>>>,
259 containers_conf_modules_origins: Vec<Provenance>,
260 global_args: Option<Vec<Sourced<ProtectedString>>>,
261 global_args_origins: Vec<Provenance>,
262 podman_args: Option<Vec<Sourced<ProtectedString>>>,
263 podman_args_origins: Vec<Provenance>,
264 user: Option<Sourced<ProtectedString>>,
265 group: Option<Sourced<ProtectedString>>,
266 uid: Option<Sourced<ProtectedString>>,
267 gid: Option<Sourced<ProtectedString>>,
268 image_source: Option<Sourced<VolumeImageSource>>,
269}
270
271impl Volume {
272 #[must_use]
274 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
275 Self {
276 name,
277 ownership,
278 runtime_name: None,
279 service_name: None,
280 driver: None,
281 device: None,
282 type_spelling: None,
283 options: None,
284 labels: None,
285 labels_origins: Vec::new(),
286 copy: None,
287 containers_conf_modules: None,
288 containers_conf_modules_origins: Vec::new(),
289 global_args: None,
290 global_args_origins: Vec::new(),
291 podman_args: None,
292 podman_args_origins: Vec::new(),
293 user: None,
294 group: None,
295 uid: None,
296 gid: None,
297 image_source: None,
298 }
299 }
300
301 #[must_use]
303 pub const fn name(&self) -> &Identifier {
304 &self.name
305 }
306
307 #[must_use]
309 pub const fn ownership(&self) -> ResourceOwnership {
310 self.ownership
311 }
312
313 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
315 self.runtime_name = Some(name);
316 }
317
318 #[must_use]
320 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
321 self.runtime_name.as_ref()
322 }
323
324 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
326 self.service_name = Some(name);
327 }
328
329 #[must_use]
331 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
332 self.service_name.as_ref()
333 }
334
335 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
337 self.driver = Some(driver);
338 }
339
340 #[must_use]
342 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
343 self.driver.as_ref()
344 }
345
346 pub fn set_device(&mut self, device: Sourced<ProtectedString>) {
348 self.device = Some(device);
349 }
350
351 #[must_use]
353 pub const fn device(&self) -> Option<&Sourced<ProtectedString>> {
354 self.device.as_ref()
355 }
356
357 pub fn set_volume_type(&mut self, volume_type: Sourced<ProtectedString>) {
359 self.type_spelling = Some(volume_type);
360 }
361
362 #[must_use]
364 pub const fn volume_type(&self) -> Option<&Sourced<ProtectedString>> {
365 self.type_spelling.as_ref()
366 }
367
368 pub fn set_options(&mut self, options: Sourced<ProtectedString>) {
373 self.options = Some(options);
374 }
375
376 #[must_use]
378 pub const fn options(&self) -> Option<&Sourced<ProtectedString>> {
379 self.options.as_ref()
380 }
381
382 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
384 self.set_labels_with_origins(labels, Vec::new());
385 }
386
387 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
389 self.labels = Some(labels);
390 self.labels_origins = origins;
391 }
392
393 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
395 self.labels.get_or_insert_default().push(label);
396 }
397
398 #[must_use]
400 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
401 self.labels.as_deref()
402 }
403
404 #[must_use]
406 pub fn labels_origins(&self) -> &[Provenance] {
407 &self.labels_origins
408 }
409
410 pub fn set_copy(&mut self, copy: Sourced<bool>) {
412 self.copy = Some(copy);
413 }
414
415 #[must_use]
417 pub const fn copy(&self) -> Option<&Sourced<bool>> {
418 self.copy.as_ref()
419 }
420
421 pub fn set_containers_conf_modules(&mut self, values: Vec<Sourced<ProtectedString>>) {
423 self.set_containers_conf_modules_with_origins(values, Vec::new());
424 }
425
426 pub fn set_containers_conf_modules_with_origins(
428 &mut self,
429 values: Vec<Sourced<ProtectedString>>,
430 origins: Vec<Provenance>,
431 ) {
432 self.containers_conf_modules = Some(values);
433 self.containers_conf_modules_origins = origins;
434 }
435
436 #[must_use]
438 pub fn containers_conf_modules(&self) -> Option<&[Sourced<ProtectedString>]> {
439 self.containers_conf_modules.as_deref()
440 }
441
442 #[must_use]
444 pub fn containers_conf_modules_origins(&self) -> &[Provenance] {
445 &self.containers_conf_modules_origins
446 }
447
448 pub fn set_global_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
450 self.set_global_args_with_origins(values, Vec::new());
451 }
452
453 pub fn set_global_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
455 self.global_args = Some(values);
456 self.global_args_origins = origins;
457 }
458
459 #[must_use]
461 pub fn global_args(&self) -> Option<&[Sourced<ProtectedString>]> {
462 self.global_args.as_deref()
463 }
464
465 #[must_use]
467 pub fn global_args_origins(&self) -> &[Provenance] {
468 &self.global_args_origins
469 }
470
471 pub fn set_podman_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
475 self.set_podman_args_with_origins(values, Vec::new());
476 }
477
478 pub fn set_podman_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
480 self.podman_args = Some(values);
481 self.podman_args_origins = origins;
482 }
483
484 #[must_use]
486 pub fn podman_args(&self) -> Option<&[Sourced<ProtectedString>]> {
487 self.podman_args.as_deref()
488 }
489
490 #[must_use]
492 pub fn podman_args_origins(&self) -> &[Provenance] {
493 &self.podman_args_origins
494 }
495
496 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
498 self.user = Some(user);
499 }
500
501 #[must_use]
503 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
504 self.user.as_ref()
505 }
506
507 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
509 self.group = Some(group);
510 }
511
512 #[must_use]
514 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
515 self.group.as_ref()
516 }
517
518 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
520 self.uid = Some(uid);
521 }
522
523 #[must_use]
525 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
526 self.uid.as_ref()
527 }
528
529 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
531 self.gid = Some(gid);
532 }
533
534 #[must_use]
536 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
537 self.gid.as_ref()
538 }
539
540 pub fn set_image_source(&mut self, image_source: Sourced<VolumeImageSource>) -> Result<(), ModelError> {
550 image_source.value().validate()?;
551 self.image_source = Some(image_source);
552 Ok(())
553 }
554
555 #[must_use]
557 pub const fn image_source(&self) -> Option<&Sourced<VolumeImageSource>> {
558 self.image_source.as_ref()
559 }
560}
561
562#[derive(Clone, Debug, Eq, PartialEq)]
567#[non_exhaustive]
568pub enum VolumeImageSource {
569 Literal(ProtectedString),
571 ImageAcquisition(Identifier),
573 ImageBuild(Identifier),
575}
576
577impl VolumeImageSource {
578 fn validate(&self) -> Result<(), ModelError> {
579 if let Self::Literal(image) = self {
580 validate_text("volume image", image.expose())?;
581 }
582 Ok(())
583 }
584}
585
586#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
591#[non_exhaustive]
592pub enum ArtifactDependencyNode {
593 Volume(Identifier),
595 ImageAcquisition(Identifier),
597 ImageBuild(Identifier),
599}
600
601impl ArtifactDependencyNode {
602 fn kind_and_name(&self) -> (&'static str, &Identifier) {
603 match self {
604 Self::Volume(name) => ("volume", name),
605 Self::ImageAcquisition(name) => ("image acquisition", name),
606 Self::ImageBuild(name) => ("image build", name),
607 }
608 }
609
610 fn display_name(&self) -> String {
611 let (kind, name) = self.kind_and_name();
612 format!("{kind}:{}", name.as_str())
613 }
614}
615
616#[derive(Clone, Debug, Eq, PartialEq)]
621pub struct ArtifactDependency {
622 source: Sourced<ArtifactDependencyNode>,
623 target: Sourced<ArtifactDependencyNode>,
624}
625
626impl ArtifactDependency {
627 #[must_use]
629 pub const fn new(source: Sourced<ArtifactDependencyNode>, target: Sourced<ArtifactDependencyNode>) -> Self {
630 Self { source, target }
631 }
632
633 #[must_use]
635 pub const fn source(&self) -> &Sourced<ArtifactDependencyNode> {
636 &self.source
637 }
638
639 #[must_use]
641 pub const fn target(&self) -> &Sourced<ArtifactDependencyNode> {
642 &self.target
643 }
644}
645
646#[derive(Clone, Debug, Eq, PartialEq)]
648pub struct Network {
649 name: Identifier,
650 ownership: ResourceOwnership,
651 runtime_name: Option<Sourced<ProtectedString>>,
652 driver: Option<Sourced<ProtectedString>>,
653 driver_options: Option<Vec<Sourced<NetworkDriverOption>>>,
654 driver_options_origins: Vec<Provenance>,
655 labels: Option<Vec<Sourced<MetadataLabel>>>,
656 labels_origins: Vec<Provenance>,
657 internal: Option<Sourced<bool>>,
658 ipv6: Option<Sourced<bool>>,
659 ipam_driver: Option<Sourced<ProtectedString>>,
660 ipam_configs: Option<Vec<Sourced<NetworkIpamConfig>>>,
661 ipam_configs_origins: Vec<Provenance>,
662}
663
664#[derive(Clone, Debug, Eq, PartialEq)]
670pub struct NetworkDriverOption {
671 name: Sourced<Identifier>,
672 value: Sourced<ProtectedString>,
673}
674
675impl NetworkDriverOption {
676 pub fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Result<Self, ModelError> {
683 validate_no_nul("network driver option value", value.value().expose())?;
684 Ok(Self { name, value })
685 }
686
687 #[must_use]
689 pub const fn name(&self) -> &Sourced<Identifier> {
690 &self.name
691 }
692
693 #[must_use]
695 pub const fn value(&self) -> &Sourced<ProtectedString> {
696 &self.value
697 }
698}
699
700#[derive(Clone, Debug, Eq, PartialEq)]
706pub struct NetworkIpamConfig {
707 subnet: Sourced<ProtectedString>,
708 gateway: Option<Sourced<ProtectedString>>,
709 ip_range: Option<Sourced<ProtectedString>>,
710}
711
712impl NetworkIpamConfig {
713 pub fn new(subnet: Sourced<ProtectedString>) -> Result<Self, ModelError> {
719 validate_text("network IPAM subnet", subnet.value().expose())?;
720 Ok(Self {
721 subnet,
722 gateway: None,
723 ip_range: None,
724 })
725 }
726
727 #[must_use]
729 pub const fn subnet(&self) -> &Sourced<ProtectedString> {
730 &self.subnet
731 }
732
733 pub fn set_gateway(&mut self, gateway: Sourced<ProtectedString>) -> Result<(), ModelError> {
739 validate_text("network IPAM gateway", gateway.value().expose())?;
740 self.gateway = Some(gateway);
741 Ok(())
742 }
743
744 #[must_use]
746 pub const fn gateway(&self) -> Option<&Sourced<ProtectedString>> {
747 self.gateway.as_ref()
748 }
749
750 pub fn set_ip_range(&mut self, ip_range: Sourced<ProtectedString>) -> Result<(), ModelError> {
756 validate_text("network IPAM IP range", ip_range.value().expose())?;
757 self.ip_range = Some(ip_range);
758 Ok(())
759 }
760
761 #[must_use]
763 pub const fn ip_range(&self) -> Option<&Sourced<ProtectedString>> {
764 self.ip_range.as_ref()
765 }
766}
767
768#[derive(Clone, Debug, Eq, PartialEq)]
770#[non_exhaustive]
771pub enum ConfigMaterial {
772 File(ProtectedString),
774 Environment(ProtectedString),
776 Content(ProtectedString),
778}
779
780#[derive(Clone, Debug, Eq, PartialEq)]
782pub struct Config {
783 name: Identifier,
784 ownership: ResourceOwnership,
785 runtime_name: Option<Sourced<ProtectedString>>,
786 material: Option<Sourced<ConfigMaterial>>,
787}
788
789impl Config {
790 #[must_use]
792 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
793 Self {
794 name,
795 ownership,
796 runtime_name: None,
797 material: None,
798 }
799 }
800
801 #[must_use]
803 pub const fn name(&self) -> &Identifier {
804 &self.name
805 }
806
807 #[must_use]
809 pub const fn ownership(&self) -> ResourceOwnership {
810 self.ownership
811 }
812
813 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
815 self.runtime_name = Some(name);
816 }
817
818 #[must_use]
820 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
821 self.runtime_name.as_ref()
822 }
823
824 pub fn set_material(&mut self, material: Sourced<ConfigMaterial>) {
826 self.material = Some(material);
827 }
828
829 #[must_use]
831 pub const fn material(&self) -> Option<&Sourced<ConfigMaterial>> {
832 self.material.as_ref()
833 }
834}
835
836#[derive(Clone, Debug, Eq, PartialEq)]
838#[non_exhaustive]
839pub enum SecretMaterial {
840 File(ProtectedString),
842 Environment(ProtectedString),
844}
845
846#[derive(Clone, Debug, Eq, PartialEq)]
848pub struct Secret {
849 name: Identifier,
850 ownership: ResourceOwnership,
851 runtime_name: Option<Sourced<ProtectedString>>,
852 material: Option<Sourced<SecretMaterial>>,
853}
854
855impl Secret {
856 #[must_use]
858 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
859 Self {
860 name,
861 ownership,
862 runtime_name: None,
863 material: None,
864 }
865 }
866
867 #[must_use]
869 pub const fn name(&self) -> &Identifier {
870 &self.name
871 }
872
873 #[must_use]
875 pub const fn ownership(&self) -> ResourceOwnership {
876 self.ownership
877 }
878
879 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
881 self.runtime_name = Some(name);
882 }
883
884 #[must_use]
886 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
887 self.runtime_name.as_ref()
888 }
889
890 pub fn set_material(&mut self, material: Sourced<SecretMaterial>) {
892 self.material = Some(material);
893 }
894
895 #[must_use]
897 pub const fn material(&self) -> Option<&Sourced<SecretMaterial>> {
898 self.material.as_ref()
899 }
900}
901
902#[derive(Clone, Copy, Debug, Eq, PartialEq)]
904#[non_exhaustive]
905pub enum ResourceGrantSyntax {
906 Short,
908 Long,
910}
911
912#[derive(Clone, Debug, Eq, PartialEq)]
918pub struct ResourceGrant {
919 source: ProtectedString,
920 syntax: ResourceGrantSyntax,
921 target: Option<Sourced<ProtectedString>>,
922 uid: Option<Sourced<ProtectedString>>,
923 gid: Option<Sourced<ProtectedString>>,
924 mode: Option<Sourced<ProtectedString>>,
925}
926
927impl ResourceGrant {
928 pub fn new(source: ProtectedString, syntax: ResourceGrantSyntax) -> Result<Self, ModelError> {
934 validate_text("resource grant source", source.expose())?;
935 Ok(Self {
936 source,
937 syntax,
938 target: None,
939 uid: None,
940 gid: None,
941 mode: None,
942 })
943 }
944
945 #[must_use]
947 pub const fn source(&self) -> &ProtectedString {
948 &self.source
949 }
950
951 #[must_use]
953 pub const fn syntax(&self) -> ResourceGrantSyntax {
954 self.syntax
955 }
956
957 pub fn set_target(&mut self, target: Sourced<ProtectedString>) {
959 self.target = Some(target);
960 }
961
962 #[must_use]
964 pub const fn target(&self) -> Option<&Sourced<ProtectedString>> {
965 self.target.as_ref()
966 }
967
968 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
970 self.uid = Some(uid);
971 }
972
973 #[must_use]
975 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
976 self.uid.as_ref()
977 }
978
979 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
981 self.gid = Some(gid);
982 }
983
984 #[must_use]
986 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
987 self.gid.as_ref()
988 }
989
990 pub fn set_mode(&mut self, mode: Sourced<ProtectedString>) {
992 self.mode = Some(mode);
993 }
994
995 #[must_use]
997 pub const fn mode(&self) -> Option<&Sourced<ProtectedString>> {
998 self.mode.as_ref()
999 }
1000}
1001
1002impl Network {
1003 #[must_use]
1005 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1006 Self {
1007 name,
1008 ownership,
1009 runtime_name: None,
1010 driver: None,
1011 driver_options: None,
1012 driver_options_origins: Vec::new(),
1013 labels: None,
1014 labels_origins: Vec::new(),
1015 internal: None,
1016 ipv6: None,
1017 ipam_driver: None,
1018 ipam_configs: None,
1019 ipam_configs_origins: Vec::new(),
1020 }
1021 }
1022
1023 #[must_use]
1025 pub const fn name(&self) -> &Identifier {
1026 &self.name
1027 }
1028
1029 #[must_use]
1031 pub const fn ownership(&self) -> ResourceOwnership {
1032 self.ownership
1033 }
1034
1035 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1037 self.runtime_name = Some(name);
1038 }
1039
1040 #[must_use]
1042 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1043 self.runtime_name.as_ref()
1044 }
1045
1046 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
1048 self.driver = Some(driver);
1049 }
1050
1051 #[must_use]
1053 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
1054 self.driver.as_ref()
1055 }
1056
1057 pub fn set_driver_options(&mut self, options: Vec<Sourced<NetworkDriverOption>>) {
1059 self.driver_options = Some(options);
1060 self.driver_options_origins.clear();
1061 }
1062
1063 pub fn set_driver_options_with_origins(
1065 &mut self,
1066 options: Vec<Sourced<NetworkDriverOption>>,
1067 origins: Vec<Provenance>,
1068 ) {
1069 self.driver_options = Some(options);
1070 self.driver_options_origins = origins;
1071 }
1072
1073 pub fn add_driver_option(&mut self, option: Sourced<NetworkDriverOption>) {
1075 self.driver_options.get_or_insert_default().push(option);
1076 }
1077
1078 #[must_use]
1080 pub fn driver_options(&self) -> Option<&[Sourced<NetworkDriverOption>]> {
1081 self.driver_options.as_deref()
1082 }
1083
1084 #[must_use]
1086 pub fn driver_options_origins(&self) -> &[Provenance] {
1087 &self.driver_options_origins
1088 }
1089
1090 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
1092 self.labels = Some(labels);
1093 self.labels_origins.clear();
1094 }
1095
1096 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
1098 self.labels = Some(labels);
1099 self.labels_origins = origins;
1100 }
1101
1102 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
1104 self.labels.get_or_insert_default().push(label);
1105 }
1106
1107 #[must_use]
1109 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
1110 self.labels.as_deref()
1111 }
1112
1113 #[must_use]
1115 pub fn labels_origins(&self) -> &[Provenance] {
1116 &self.labels_origins
1117 }
1118
1119 pub fn set_internal(&mut self, internal: Sourced<bool>) {
1121 self.internal = Some(internal);
1122 }
1123
1124 #[must_use]
1126 pub const fn internal(&self) -> Option<&Sourced<bool>> {
1127 self.internal.as_ref()
1128 }
1129
1130 pub fn set_ipv6(&mut self, ipv6: Sourced<bool>) {
1132 self.ipv6 = Some(ipv6);
1133 }
1134
1135 #[must_use]
1137 pub const fn ipv6(&self) -> Option<&Sourced<bool>> {
1138 self.ipv6.as_ref()
1139 }
1140
1141 pub fn set_ipam_driver(&mut self, driver: Sourced<ProtectedString>) {
1143 self.ipam_driver = Some(driver);
1144 }
1145
1146 #[must_use]
1148 pub const fn ipam_driver(&self) -> Option<&Sourced<ProtectedString>> {
1149 self.ipam_driver.as_ref()
1150 }
1151
1152 pub fn set_ipam_configs(&mut self, configs: Vec<Sourced<NetworkIpamConfig>>) {
1154 self.ipam_configs = Some(configs);
1155 self.ipam_configs_origins.clear();
1156 }
1157
1158 pub fn set_ipam_configs_with_origins(
1160 &mut self,
1161 configs: Vec<Sourced<NetworkIpamConfig>>,
1162 origins: Vec<Provenance>,
1163 ) {
1164 self.ipam_configs = Some(configs);
1165 self.ipam_configs_origins = origins;
1166 }
1167
1168 pub fn add_ipam_config(&mut self, config: Sourced<NetworkIpamConfig>) {
1170 self.ipam_configs.get_or_insert_default().push(config);
1171 }
1172
1173 #[must_use]
1175 pub fn ipam_configs(&self) -> Option<&[Sourced<NetworkIpamConfig>]> {
1176 self.ipam_configs.as_deref()
1177 }
1178
1179 #[must_use]
1181 pub fn ipam_configs_origins(&self) -> &[Provenance] {
1182 &self.ipam_configs_origins
1183 }
1184}
1185
1186#[derive(Clone, Debug, Eq, PartialEq)]
1191pub struct ServiceGroup {
1192 name: Identifier,
1193 ownership: ResourceOwnership,
1194 members: Vec<Sourced<Identifier>>,
1195 runtime: Option<Sourced<ServiceGroupRuntime>>,
1196}
1197
1198impl ServiceGroup {
1199 #[must_use]
1201 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1202 Self {
1203 name,
1204 ownership,
1205 members: Vec::new(),
1206 runtime: None,
1207 }
1208 }
1209
1210 #[must_use]
1212 pub const fn name(&self) -> &Identifier {
1213 &self.name
1214 }
1215
1216 #[must_use]
1218 pub const fn ownership(&self) -> ResourceOwnership {
1219 self.ownership
1220 }
1221
1222 pub fn add_member(&mut self, member: Sourced<Identifier>) -> Result<(), ModelError> {
1228 if self.members.iter().any(|candidate| candidate.value() == member.value()) {
1229 return Err(ModelError::DuplicateServiceGroupMember {
1230 group: self.name.as_str().to_owned(),
1231 service: member.value().as_str().to_owned(),
1232 });
1233 }
1234 self.members.push(member);
1235 Ok(())
1236 }
1237
1238 #[must_use]
1240 pub fn members(&self) -> &[Sourced<Identifier>] {
1241 &self.members
1242 }
1243
1244 pub fn set_runtime(&mut self, runtime: Sourced<ServiceGroupRuntime>) {
1249 self.runtime = Some(runtime);
1250 }
1251
1252 #[must_use]
1254 pub const fn runtime(&self) -> Option<&Sourced<ServiceGroupRuntime>> {
1255 self.runtime.as_ref()
1256 }
1257}
1258
1259#[derive(Clone, Debug, Eq, PartialEq)]
1261#[non_exhaustive]
1262pub enum GroupExitPolicy {
1263 Stop,
1265 Continue,
1267 Raw(ProtectedString),
1269}
1270
1271#[derive(Clone, Debug, Default, Eq, PartialEq)]
1277pub struct ServiceGroupRuntime {
1278 runtime_name: Option<Sourced<ProtectedString>>,
1279 service_name: Option<Sourced<ProtectedString>>,
1280 host_mappings: Option<Vec<Sourced<HostMapping>>>,
1281 host_mappings_origins: Vec<Provenance>,
1282 ports: Option<Vec<Sourced<Port>>>,
1283 ports_origins: Vec<Provenance>,
1284 networks: Option<Vec<Sourced<NetworkAttachment>>>,
1285 networks_origins: Vec<Provenance>,
1286 user_namespace: Option<Sourced<ProtectedString>>,
1287 mounts: Option<Vec<Sourced<Mount>>>,
1288 mounts_origins: Vec<Provenance>,
1289 shm_size: Option<Sourced<ProtectedString>>,
1290 exit_policy: Option<Sourced<GroupExitPolicy>>,
1291 stop_timeout: Option<Sourced<StopTimeout>>,
1292}
1293
1294impl ServiceGroupRuntime {
1295 #[must_use]
1297 pub const fn new() -> Self {
1298 Self {
1299 runtime_name: None,
1300 service_name: None,
1301 host_mappings: None,
1302 host_mappings_origins: Vec::new(),
1303 ports: None,
1304 ports_origins: Vec::new(),
1305 networks: None,
1306 networks_origins: Vec::new(),
1307 user_namespace: None,
1308 mounts: None,
1309 mounts_origins: Vec::new(),
1310 shm_size: None,
1311 exit_policy: None,
1312 stop_timeout: None,
1313 }
1314 }
1315
1316 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1318 self.runtime_name = Some(name);
1319 }
1320
1321 #[must_use]
1323 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1324 self.runtime_name.as_ref()
1325 }
1326
1327 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
1329 self.service_name = Some(name);
1330 }
1331
1332 #[must_use]
1334 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
1335 self.service_name.as_ref()
1336 }
1337
1338 pub fn set_host_mappings(&mut self, values: Vec<Sourced<HostMapping>>) {
1340 self.set_host_mappings_with_origins(values, Vec::new());
1341 }
1342
1343 pub fn set_host_mappings_with_origins(&mut self, values: Vec<Sourced<HostMapping>>, origins: Vec<Provenance>) {
1345 self.host_mappings = Some(values);
1346 self.host_mappings_origins = origins;
1347 }
1348
1349 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
1351 self.host_mappings.get_or_insert_default().push(value);
1352 }
1353
1354 #[must_use]
1356 pub fn host_mappings(&self) -> Option<&[Sourced<HostMapping>]> {
1357 self.host_mappings.as_deref()
1358 }
1359
1360 #[must_use]
1362 pub fn host_mappings_origins(&self) -> &[Provenance] {
1363 &self.host_mappings_origins
1364 }
1365
1366 pub fn set_ports(&mut self, values: Vec<Sourced<Port>>) {
1368 self.set_ports_with_origins(values, Vec::new());
1369 }
1370
1371 pub fn set_ports_with_origins(&mut self, values: Vec<Sourced<Port>>, origins: Vec<Provenance>) {
1373 self.ports = Some(values);
1374 self.ports_origins = origins;
1375 }
1376
1377 pub fn add_port(&mut self, value: Sourced<Port>) {
1379 self.ports.get_or_insert_default().push(value);
1380 }
1381
1382 #[must_use]
1384 pub fn ports(&self) -> Option<&[Sourced<Port>]> {
1385 self.ports.as_deref()
1386 }
1387
1388 #[must_use]
1390 pub fn ports_origins(&self) -> &[Provenance] {
1391 &self.ports_origins
1392 }
1393
1394 pub fn set_networks(&mut self, values: Vec<Sourced<NetworkAttachment>>) {
1396 self.set_networks_with_origins(values, Vec::new());
1397 }
1398
1399 pub fn set_networks_with_origins(&mut self, values: Vec<Sourced<NetworkAttachment>>, origins: Vec<Provenance>) {
1401 self.networks = Some(values);
1402 self.networks_origins = origins;
1403 }
1404
1405 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
1407 self.networks.get_or_insert_default().push(value);
1408 }
1409
1410 pub fn replace_network(
1417 &mut self,
1418 index: usize,
1419 value: Sourced<NetworkAttachment>,
1420 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
1421 let len = self.networks.as_ref().map_or(0, Vec::len);
1422 let Some(networks) = self.networks.as_mut() else {
1423 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1424 };
1425 let Some(slot) = networks.get_mut(index) else {
1426 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1427 };
1428 Ok(std::mem::replace(slot, value))
1429 }
1430
1431 #[must_use]
1433 pub fn networks(&self) -> Option<&[Sourced<NetworkAttachment>]> {
1434 self.networks.as_deref()
1435 }
1436
1437 #[must_use]
1439 pub fn networks_origins(&self) -> &[Provenance] {
1440 &self.networks_origins
1441 }
1442
1443 pub fn set_user_namespace(&mut self, value: Sourced<ProtectedString>) {
1445 self.user_namespace = Some(value);
1446 }
1447
1448 #[must_use]
1450 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
1451 self.user_namespace.as_ref()
1452 }
1453
1454 pub fn set_mounts(&mut self, values: Vec<Sourced<Mount>>) {
1456 self.set_mounts_with_origins(values, Vec::new());
1457 }
1458
1459 pub fn set_mounts_with_origins(&mut self, values: Vec<Sourced<Mount>>, origins: Vec<Provenance>) {
1461 self.mounts = Some(values);
1462 self.mounts_origins = origins;
1463 }
1464
1465 pub fn add_mount(&mut self, value: Sourced<Mount>) {
1467 self.mounts.get_or_insert_default().push(value);
1468 }
1469
1470 #[must_use]
1472 pub fn mounts(&self) -> Option<&[Sourced<Mount>]> {
1473 self.mounts.as_deref()
1474 }
1475
1476 #[must_use]
1478 pub fn mounts_origins(&self) -> &[Provenance] {
1479 &self.mounts_origins
1480 }
1481
1482 pub fn set_shm_size(&mut self, value: Sourced<ProtectedString>) {
1484 self.shm_size = Some(value);
1485 }
1486
1487 #[must_use]
1489 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
1490 self.shm_size.as_ref()
1491 }
1492
1493 pub fn set_exit_policy(&mut self, value: Sourced<GroupExitPolicy>) {
1495 self.exit_policy = Some(value);
1496 }
1497
1498 #[must_use]
1500 pub const fn exit_policy(&self) -> Option<&Sourced<GroupExitPolicy>> {
1501 self.exit_policy.as_ref()
1502 }
1503
1504 pub fn set_stop_timeout(&mut self, value: Sourced<StopTimeout>) {
1506 self.stop_timeout = Some(value);
1507 }
1508
1509 #[must_use]
1511 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
1512 self.stop_timeout.as_ref()
1513 }
1514}
1515
1516#[derive(Clone, Debug, Eq, PartialEq)]
1518#[non_exhaustive]
1519pub enum Command {
1520 Exec(Vec<ProtectedString>),
1522 Shell(ProtectedString),
1524 Empty,
1526}
1527
1528#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1533#[non_exhaustive]
1534pub enum StartupNotification {
1535 Runtime,
1537 Application,
1539 Healthy,
1541}
1542
1543#[derive(Clone, Debug, Eq, PartialEq)]
1548#[non_exhaustive]
1549pub enum Entrypoint {
1550 Exec(Vec<ProtectedString>),
1552 Shell(ProtectedString),
1554 Empty,
1556}
1557
1558#[derive(Clone, Debug, Eq, PartialEq)]
1563#[non_exhaustive]
1564pub enum PullPolicy {
1565 Always,
1567 Missing,
1569 Never,
1571 IfNotPresent,
1573 Build,
1575 Daily,
1577 Weekly,
1579 Every(ProtectedString),
1581 Raw(ProtectedString),
1583}
1584
1585#[derive(Clone, Debug, Eq, PartialEq)]
1590pub struct StopTimeout(String);
1591
1592impl StopTimeout {
1593 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1599 let value = value.into();
1600 validate_text("stop timeout", &value)?;
1601 Ok(Self(value))
1602 }
1603
1604 #[must_use]
1606 pub fn as_str(&self) -> &str {
1607 &self.0
1608 }
1609}
1610
1611#[derive(Clone, Debug, Eq, PartialEq)]
1613pub struct ExposedPort {
1614 container: u16,
1615 protocol: Protocol,
1616}
1617
1618impl ExposedPort {
1619 pub fn new(container: u16, protocol: Protocol) -> Result<Self, ModelError> {
1625 if container == 0 {
1626 return Err(ModelError::ZeroContainerPort);
1627 }
1628 Ok(Self { container, protocol })
1629 }
1630
1631 #[must_use]
1633 pub const fn container(&self) -> u16 {
1634 self.container
1635 }
1636
1637 #[must_use]
1639 pub const fn protocol(&self) -> &Protocol {
1640 &self.protocol
1641 }
1642}
1643
1644#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1650#[non_exhaustive]
1651pub enum RestartPolicy {
1652 Never,
1654 Always,
1656 OnFailure {
1658 maximum_retries: Option<std::num::NonZeroU64>,
1660 },
1661 UnlessStopped,
1663}
1664
1665impl RestartPolicy {
1666 #[must_use]
1668 pub const fn on_failure(maximum_retries: Option<std::num::NonZeroU64>) -> Self {
1669 Self::OnFailure { maximum_retries }
1670 }
1671
1672 #[must_use]
1674 pub const fn maximum_retries(self) -> Option<std::num::NonZeroU64> {
1675 match self {
1676 Self::OnFailure { maximum_retries } => maximum_retries,
1677 Self::Never | Self::Always | Self::UnlessStopped => None,
1678 }
1679 }
1680}
1681
1682#[derive(Clone, Debug, Eq, PartialEq)]
1684#[non_exhaustive]
1685pub enum HealthcheckCommand {
1686 Exec(Vec<ProtectedString>),
1688 Shell(ProtectedString),
1690}
1691
1692#[derive(Clone, Debug, Eq, PartialEq)]
1694pub struct HealthcheckDuration(String);
1695
1696impl HealthcheckDuration {
1697 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1706 let value = value.into();
1707 validate_text("health-check duration", &value)?;
1708 Ok(Self(value))
1709 }
1710
1711 #[must_use]
1713 pub fn as_str(&self) -> &str {
1714 &self.0
1715 }
1716}
1717
1718#[derive(Clone, Debug, Eq, PartialEq)]
1720pub struct HealthcheckRetries(String);
1721
1722impl HealthcheckRetries {
1723 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1730 let value = value.into();
1731 validate_text("health-check retries", &value)?;
1732 if !value.bytes().all(|byte| byte.is_ascii_digit()) {
1733 return Err(ModelError::InvalidHealthcheckRetries);
1734 }
1735 Ok(Self(value))
1736 }
1737
1738 #[must_use]
1740 pub fn as_str(&self) -> &str {
1741 &self.0
1742 }
1743}
1744
1745#[derive(Clone, Debug, Default, Eq, PartialEq)]
1747pub struct Healthcheck {
1748 command: Option<Sourced<HealthcheckCommand>>,
1749 disabled: Option<Sourced<bool>>,
1750 interval: Option<Sourced<HealthcheckDuration>>,
1751 timeout: Option<Sourced<HealthcheckDuration>>,
1752 retries: Option<Sourced<HealthcheckRetries>>,
1753 start_period: Option<Sourced<HealthcheckDuration>>,
1754 start_interval: Option<Sourced<HealthcheckDuration>>,
1755}
1756
1757impl Healthcheck {
1758 #[must_use]
1760 pub const fn new() -> Self {
1761 Self {
1762 command: None,
1763 disabled: None,
1764 interval: None,
1765 timeout: None,
1766 retries: None,
1767 start_period: None,
1768 start_interval: None,
1769 }
1770 }
1771
1772 pub fn set_command(&mut self, command: Sourced<HealthcheckCommand>) {
1774 self.command = Some(command);
1775 }
1776
1777 #[must_use]
1779 pub const fn command(&self) -> Option<&Sourced<HealthcheckCommand>> {
1780 self.command.as_ref()
1781 }
1782
1783 pub fn set_disabled(&mut self, disabled: Sourced<bool>) {
1785 self.disabled = Some(disabled);
1786 }
1787
1788 #[must_use]
1790 pub const fn disabled(&self) -> Option<&Sourced<bool>> {
1791 self.disabled.as_ref()
1792 }
1793
1794 pub fn set_interval(&mut self, interval: Sourced<HealthcheckDuration>) {
1796 self.interval = Some(interval);
1797 }
1798
1799 #[must_use]
1801 pub const fn interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1802 self.interval.as_ref()
1803 }
1804
1805 pub fn set_timeout(&mut self, timeout: Sourced<HealthcheckDuration>) {
1807 self.timeout = Some(timeout);
1808 }
1809
1810 #[must_use]
1812 pub const fn timeout(&self) -> Option<&Sourced<HealthcheckDuration>> {
1813 self.timeout.as_ref()
1814 }
1815
1816 pub fn set_retries(&mut self, retries: Sourced<HealthcheckRetries>) {
1818 self.retries = Some(retries);
1819 }
1820
1821 #[must_use]
1823 pub const fn retries(&self) -> Option<&Sourced<HealthcheckRetries>> {
1824 self.retries.as_ref()
1825 }
1826
1827 pub fn set_start_period(&mut self, start_period: Sourced<HealthcheckDuration>) {
1829 self.start_period = Some(start_period);
1830 }
1831
1832 #[must_use]
1834 pub const fn start_period(&self) -> Option<&Sourced<HealthcheckDuration>> {
1835 self.start_period.as_ref()
1836 }
1837
1838 pub fn set_start_interval(&mut self, start_interval: Sourced<HealthcheckDuration>) {
1840 self.start_interval = Some(start_interval);
1841 }
1842
1843 #[must_use]
1845 pub const fn start_interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1846 self.start_interval.as_ref()
1847 }
1848}
1849
1850#[derive(Clone, Debug, Eq, PartialEq)]
1852#[non_exhaustive]
1853pub enum EnvironmentValue {
1854 Literal(ProtectedString),
1856 Host,
1858 Unset,
1860}
1861
1862#[derive(Clone, Debug, Eq, PartialEq)]
1864pub struct EnvironmentVariable {
1865 name: Identifier,
1866 value: EnvironmentValue,
1867}
1868
1869#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1871#[non_exhaustive]
1872pub enum EnvironmentFileSyntax {
1873 Short,
1875 Long,
1877}
1878
1879#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1881#[non_exhaustive]
1882pub enum EnvironmentFileFormat {
1883 Raw,
1885}
1886
1887#[derive(Clone, Debug, Eq, PartialEq)]
1893pub struct EnvironmentFile {
1894 path: ProtectedString,
1895 syntax: EnvironmentFileSyntax,
1896 required: Option<Sourced<bool>>,
1897 format: Option<Sourced<EnvironmentFileFormat>>,
1898}
1899
1900impl EnvironmentFile {
1901 pub fn new(path: ProtectedString, syntax: EnvironmentFileSyntax) -> Result<Self, ModelError> {
1907 validate_text("environment-file path", path.expose())?;
1908 Ok(Self {
1909 path,
1910 syntax,
1911 required: None,
1912 format: None,
1913 })
1914 }
1915
1916 #[must_use]
1918 pub const fn path(&self) -> &ProtectedString {
1919 &self.path
1920 }
1921
1922 #[must_use]
1924 pub const fn syntax(&self) -> EnvironmentFileSyntax {
1925 self.syntax
1926 }
1927
1928 pub fn set_required(&mut self, required: Sourced<bool>) {
1930 self.required = Some(required);
1931 }
1932
1933 #[must_use]
1935 pub const fn required(&self) -> Option<&Sourced<bool>> {
1936 self.required.as_ref()
1937 }
1938
1939 #[must_use]
1941 pub fn is_required(&self) -> bool {
1942 self.required.as_ref().is_none_or(|required| *required.value())
1943 }
1944
1945 pub fn set_format(&mut self, format: Sourced<EnvironmentFileFormat>) {
1947 self.format = Some(format);
1948 }
1949
1950 #[must_use]
1952 pub const fn format(&self) -> Option<&Sourced<EnvironmentFileFormat>> {
1953 self.format.as_ref()
1954 }
1955}
1956
1957#[derive(Clone, Debug, Eq, PartialEq)]
1963pub struct MetadataLabel {
1964 name: Identifier,
1965 value: ProtectedString,
1966}
1967
1968impl MetadataLabel {
1969 #[must_use]
1971 pub const fn new(name: Identifier, value: ProtectedString) -> Self {
1972 Self { name, value }
1973 }
1974
1975 #[must_use]
1977 pub const fn name(&self) -> &Identifier {
1978 &self.name
1979 }
1980
1981 #[must_use]
1983 pub const fn value(&self) -> &ProtectedString {
1984 &self.value
1985 }
1986}
1987
1988#[derive(Clone, Debug, Eq, PartialEq)]
1993pub struct Annotation {
1994 name: Sourced<Identifier>,
1995 value: Sourced<ProtectedString>,
1996}
1997
1998impl Annotation {
1999 #[must_use]
2001 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2002 Self { name, value }
2003 }
2004
2005 #[must_use]
2007 pub const fn name(&self) -> &Sourced<Identifier> {
2008 &self.name
2009 }
2010
2011 #[must_use]
2013 pub const fn value(&self) -> &Sourced<ProtectedString> {
2014 &self.value
2015 }
2016}
2017
2018#[derive(Clone, Debug, Eq, PartialEq)]
2020pub struct LoggingOption {
2021 name: Sourced<Identifier>,
2022 value: Sourced<ProtectedString>,
2023}
2024
2025impl LoggingOption {
2026 #[must_use]
2028 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2029 Self { name, value }
2030 }
2031
2032 #[must_use]
2034 pub const fn name(&self) -> &Sourced<Identifier> {
2035 &self.name
2036 }
2037
2038 #[must_use]
2040 pub const fn value(&self) -> &Sourced<ProtectedString> {
2041 &self.value
2042 }
2043}
2044
2045#[derive(Clone, Debug, Default, Eq, PartialEq)]
2050pub struct Logging {
2051 driver: Option<Sourced<ProtectedString>>,
2052 options: Option<Vec<Sourced<LoggingOption>>>,
2053 options_origins: Vec<Provenance>,
2054}
2055
2056impl Logging {
2057 #[must_use]
2059 pub const fn new() -> Self {
2060 Self {
2061 driver: None,
2062 options: None,
2063 options_origins: Vec::new(),
2064 }
2065 }
2066
2067 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
2069 self.driver = Some(driver);
2070 }
2071
2072 #[must_use]
2074 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
2075 self.driver.as_ref()
2076 }
2077
2078 pub fn set_options(&mut self, options: Vec<Sourced<LoggingOption>>) {
2080 self.options = Some(options);
2081 self.options_origins.clear();
2082 }
2083
2084 pub fn add_option(&mut self, option: Sourced<LoggingOption>) {
2086 self.options.get_or_insert_default().push(option);
2087 }
2088
2089 pub fn set_options_with_origins(&mut self, options: Vec<Sourced<LoggingOption>>, origins: Vec<Provenance>) {
2091 self.options = Some(options);
2092 self.options_origins = origins;
2093 }
2094
2095 #[must_use]
2097 pub fn options(&self) -> Option<&[Sourced<LoggingOption>]> {
2098 self.options.as_deref()
2099 }
2100
2101 #[must_use]
2103 pub fn options_origins(&self) -> &[Provenance] {
2104 &self.options_origins
2105 }
2106}
2107
2108#[derive(Clone, Debug, Eq, PartialEq)]
2113#[non_exhaustive]
2114pub enum ReloadAction {
2115 Command(Command),
2117 Signal(ProtectedString),
2119}
2120
2121impl EnvironmentVariable {
2122 #[must_use]
2124 pub const fn new(name: Identifier, value: EnvironmentValue) -> Self {
2125 Self { name, value }
2126 }
2127
2128 #[must_use]
2130 pub const fn name(&self) -> &Identifier {
2131 &self.name
2132 }
2133
2134 #[must_use]
2136 pub const fn value(&self) -> &EnvironmentValue {
2137 &self.value
2138 }
2139}
2140
2141#[derive(Clone, Debug, Eq, PartialEq)]
2143pub struct HostAddress {
2144 raw: String,
2145 kind: HostAddressKind,
2146}
2147
2148impl HostAddress {
2149 pub fn new(raw: impl Into<String>) -> Result<Self, ModelError> {
2155 let raw = raw.into();
2156 validate_text("host mapping address", &raw)?;
2157 let unbracketed = raw
2158 .strip_prefix('[')
2159 .and_then(|value| value.strip_suffix(']'))
2160 .unwrap_or(&raw);
2161 let kind = if raw == "host-gateway" {
2162 HostAddressKind::HostGateway
2163 } else {
2164 match unbracketed.parse::<IpAddr>() {
2165 Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
2166 Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
2167 bracketed: raw.starts_with('[') && raw.ends_with(']'),
2168 },
2169 Err(_) => HostAddressKind::Other,
2170 }
2171 };
2172 Ok(Self { raw, kind })
2173 }
2174
2175 #[must_use]
2177 pub fn raw(&self) -> &str {
2178 &self.raw
2179 }
2180
2181 #[must_use]
2183 pub const fn kind(&self) -> HostAddressKind {
2184 self.kind
2185 }
2186}
2187
2188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2190#[non_exhaustive]
2191pub enum HostAddressKind {
2192 Ipv4,
2194 Ipv6 {
2196 bracketed: bool,
2198 },
2199 HostGateway,
2201 Other,
2203}
2204
2205#[derive(Clone, Debug, Eq, PartialEq)]
2207pub struct HostMapping {
2208 hostname: Identifier,
2209 address: HostAddress,
2210}
2211
2212impl HostMapping {
2213 #[must_use]
2215 pub const fn new(hostname: Identifier, address: HostAddress) -> Self {
2216 Self { hostname, address }
2217 }
2218
2219 #[must_use]
2221 pub const fn hostname(&self) -> &Identifier {
2222 &self.hostname
2223 }
2224
2225 #[must_use]
2227 pub const fn address(&self) -> &HostAddress {
2228 &self.address
2229 }
2230}
2231
2232#[derive(Clone, Debug, Eq, PartialEq)]
2234#[non_exhaustive]
2235pub enum Protocol {
2236 Tcp,
2238 Udp,
2240 Sctp,
2242 Other(String),
2244}
2245
2246#[derive(Clone, Debug, Eq, PartialEq)]
2248pub struct Port {
2249 container: u16,
2250 published: Option<u16>,
2251 host_address: Option<String>,
2252 protocol: Protocol,
2253}
2254
2255impl Port {
2256 pub fn new(
2262 container: u16,
2263 published: Option<u16>,
2264 host_address: Option<String>,
2265 protocol: Protocol,
2266 ) -> Result<Self, ModelError> {
2267 if container == 0 {
2268 return Err(ModelError::ZeroContainerPort);
2269 }
2270 Ok(Self {
2271 container,
2272 published,
2273 host_address,
2274 protocol,
2275 })
2276 }
2277
2278 #[must_use]
2280 pub const fn container(&self) -> u16 {
2281 self.container
2282 }
2283
2284 #[must_use]
2286 pub const fn published(&self) -> Option<u16> {
2287 self.published
2288 }
2289
2290 #[must_use]
2292 pub fn host_address(&self) -> Option<&str> {
2293 self.host_address.as_deref()
2294 }
2295
2296 #[must_use]
2298 pub const fn protocol(&self) -> &Protocol {
2299 &self.protocol
2300 }
2301}
2302
2303#[derive(Clone, Debug, Eq, PartialEq)]
2305#[non_exhaustive]
2306pub enum MountSource {
2307 Volume(Identifier),
2309 HostPath(String),
2311 Anonymous,
2313}
2314
2315#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2317#[non_exhaustive]
2318pub enum SelinuxRelabel {
2319 Shared,
2321 Private,
2323}
2324
2325#[derive(Clone, Debug, Eq, PartialEq)]
2327pub struct Mount {
2328 source: MountSource,
2329 target: String,
2330 read_only: bool,
2331 selinux_relabel: Option<SelinuxRelabel>,
2332}
2333
2334impl Mount {
2335 pub fn new(source: MountSource, target: impl Into<String>, read_only: bool) -> Result<Self, ModelError> {
2341 let target = target.into();
2342 validate_text("mount target", &target)?;
2343 Ok(Self {
2344 source,
2345 target,
2346 read_only,
2347 selinux_relabel: None,
2348 })
2349 }
2350
2351 #[must_use]
2353 pub const fn source(&self) -> &MountSource {
2354 &self.source
2355 }
2356
2357 #[must_use]
2359 pub fn target(&self) -> &str {
2360 &self.target
2361 }
2362
2363 #[must_use]
2365 pub const fn read_only(&self) -> bool {
2366 self.read_only
2367 }
2368
2369 pub fn set_selinux_relabel(&mut self, relabel: SelinuxRelabel) {
2371 self.selinux_relabel = Some(relabel);
2372 }
2373
2374 #[must_use]
2376 pub const fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
2377 self.selinux_relabel
2378 }
2379}
2380
2381#[derive(Clone, Eq, PartialEq)]
2383pub struct NetworkAttachment {
2384 network: Identifier,
2385 aliases: Vec<String>,
2386 alias_sensitivities: Vec<bool>,
2387 alias_origins: Vec<Vec<Provenance>>,
2388 ipv4_address: Option<Sourced<ProtectedString>>,
2389 ipv6_address: Option<Sourced<ProtectedString>>,
2390}
2391
2392impl NetworkAttachment {
2393 #[must_use]
2395 pub const fn new(network: Identifier, aliases: Vec<String>) -> Self {
2396 Self {
2397 network,
2398 aliases,
2399 alias_sensitivities: Vec::new(),
2400 alias_origins: Vec::new(),
2401 ipv4_address: None,
2402 ipv6_address: None,
2403 }
2404 }
2405
2406 #[must_use]
2408 pub fn with_sourced_aliases(network: Identifier, aliases: Vec<Sourced<ProtectedString>>) -> Self {
2409 let mut attachment = Self::new(network, Vec::new());
2410 attachment.set_aliases_with_provenance(aliases);
2411 attachment
2412 }
2413
2414 #[must_use]
2416 pub const fn network(&self) -> &Identifier {
2417 &self.network
2418 }
2419
2420 #[must_use]
2422 pub fn aliases(&self) -> &[String] {
2423 &self.aliases
2424 }
2425
2426 #[must_use]
2431 pub fn alias_origins(&self) -> &[Vec<Provenance>] {
2432 &self.alias_origins
2433 }
2434
2435 #[must_use]
2441 pub fn alias_sensitivities(&self) -> &[bool] {
2442 &self.alias_sensitivities
2443 }
2444
2445 pub fn set_aliases_with_provenance(&mut self, aliases: Vec<Sourced<ProtectedString>>) {
2447 self.aliases = aliases.iter().map(|alias| alias.value().expose().to_owned()).collect();
2448 self.alias_sensitivities = aliases.iter().map(|alias| alias.value().is_sensitive()).collect();
2449 self.alias_origins = aliases.into_iter().map(|alias| alias.origins().to_vec()).collect();
2450 }
2451
2452 pub fn add_alias(&mut self, alias: &Sourced<ProtectedString>) {
2454 self.aliases.push(alias.value().expose().to_owned());
2455 self.alias_sensitivities.push(alias.value().is_sensitive());
2456 self.alias_origins.push(alias.origins().to_vec());
2457 }
2458
2459 pub fn set_ipv4_address(&mut self, address: Sourced<ProtectedString>) {
2461 self.ipv4_address = Some(address);
2462 }
2463
2464 #[must_use]
2466 pub const fn ipv4_address(&self) -> Option<&Sourced<ProtectedString>> {
2467 self.ipv4_address.as_ref()
2468 }
2469
2470 pub fn set_ipv6_address(&mut self, address: Sourced<ProtectedString>) {
2472 self.ipv6_address = Some(address);
2473 }
2474
2475 #[must_use]
2477 pub const fn ipv6_address(&self) -> Option<&Sourced<ProtectedString>> {
2478 self.ipv6_address.as_ref()
2479 }
2480}
2481
2482impl fmt::Debug for NetworkAttachment {
2483 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2484 let aliases = self
2485 .aliases
2486 .iter()
2487 .enumerate()
2488 .map(|(index, alias)| {
2489 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
2490 "[REDACTED]"
2491 } else {
2492 alias.as_str()
2493 }
2494 })
2495 .collect::<Vec<_>>();
2496 formatter
2497 .debug_struct("NetworkAttachment")
2498 .field("network", &self.network)
2499 .field("aliases", &aliases)
2500 .field("alias_origins", &self.alias_origins)
2501 .field("ipv4_address", &self.ipv4_address)
2502 .field("ipv6_address", &self.ipv6_address)
2503 .finish()
2504 }
2505}
2506
2507#[derive(Clone, Debug, Eq, PartialEq)]
2509#[non_exhaustive]
2510pub enum ServiceDependencyCondition {
2511 Started,
2513 Healthy,
2515 CompletedSuccessfully,
2517 Other(ProtectedString),
2519}
2520
2521#[derive(Clone, Debug, Eq, PartialEq)]
2527pub struct ServiceDependency {
2528 service: Identifier,
2529 condition: Option<Sourced<ServiceDependencyCondition>>,
2530 restart: Option<Sourced<bool>>,
2531 required: Option<Sourced<bool>>,
2532}
2533
2534#[derive(Clone, Debug, Eq, PartialEq)]
2536pub struct KernelParameter {
2537 name: ProtectedString,
2538 value: ProtectedString,
2539}
2540
2541impl KernelParameter {
2542 #[must_use]
2544 pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
2545 Self { name, value }
2546 }
2547
2548 #[must_use]
2550 pub const fn name(&self) -> &ProtectedString {
2551 &self.name
2552 }
2553
2554 #[must_use]
2556 pub const fn value(&self) -> &ProtectedString {
2557 &self.value
2558 }
2559}
2560
2561#[derive(Clone, Debug, Eq, PartialEq)]
2563pub struct ResourceLimit {
2564 name: ProtectedString,
2565 soft: Option<Sourced<ProtectedString>>,
2566 hard: Option<Sourced<ProtectedString>>,
2567}
2568
2569impl ResourceLimit {
2570 #[must_use]
2572 pub const fn new(
2573 name: ProtectedString,
2574 soft: Option<Sourced<ProtectedString>>,
2575 hard: Option<Sourced<ProtectedString>>,
2576 ) -> Self {
2577 Self { name, soft, hard }
2578 }
2579
2580 #[must_use]
2582 pub const fn name(&self) -> &ProtectedString {
2583 &self.name
2584 }
2585
2586 #[must_use]
2588 pub const fn soft(&self) -> Option<&Sourced<ProtectedString>> {
2589 self.soft.as_ref()
2590 }
2591
2592 #[must_use]
2594 pub const fn hard(&self) -> Option<&Sourced<ProtectedString>> {
2595 self.hard.as_ref()
2596 }
2597}
2598
2599#[derive(Clone, Debug, Eq, PartialEq)]
2601#[non_exhaustive]
2602pub enum Device {
2603 Short(ProtectedString),
2605 Long {
2607 source: Option<Sourced<ProtectedString>>,
2609 target: Option<Sourced<ProtectedString>>,
2611 permissions: Option<Sourced<ProtectedString>>,
2613 },
2614}
2615
2616#[derive(Clone, Debug, Eq, PartialEq)]
2621#[non_exhaustive]
2622pub enum SecurityOption {
2623 AppArmor(ProtectedString),
2625 NoNewPrivileges(bool),
2627 SeccompProfile(ProtectedString),
2629 SecurityLabelDisable(bool),
2631 SecurityLabelFileType(ProtectedString),
2633 SecurityLabelLevel(ProtectedString),
2635 SecurityLabelNested(bool),
2637 SecurityLabelType(ProtectedString),
2639 Mask(ProtectedString),
2641 Unmask(ProtectedString),
2643}
2644
2645impl ServiceDependency {
2646 #[must_use]
2649 pub const fn new(service: Identifier) -> Self {
2650 Self {
2651 service,
2652 condition: None,
2653 restart: None,
2654 required: None,
2655 }
2656 }
2657
2658 #[must_use]
2660 pub const fn service(&self) -> &Identifier {
2661 &self.service
2662 }
2663
2664 pub fn set_condition(&mut self, condition: Sourced<ServiceDependencyCondition>) {
2666 self.condition = Some(condition);
2667 }
2668
2669 #[must_use]
2671 pub const fn condition(&self) -> Option<&Sourced<ServiceDependencyCondition>> {
2672 self.condition.as_ref()
2673 }
2674
2675 pub fn set_restart(&mut self, restart: Sourced<bool>) {
2677 self.restart = Some(restart);
2678 }
2679
2680 #[must_use]
2682 pub const fn restart(&self) -> Option<&Sourced<bool>> {
2683 self.restart.as_ref()
2684 }
2685
2686 pub fn set_required(&mut self, required: Sourced<bool>) {
2688 self.required = Some(required);
2689 }
2690
2691 #[must_use]
2693 pub const fn required(&self) -> Option<&Sourced<bool>> {
2694 self.required.as_ref()
2695 }
2696
2697 #[must_use]
2699 pub fn is_required(&self) -> bool {
2700 self.required.as_ref().is_none_or(|required| *required.value())
2701 }
2702}
2703
2704#[derive(Clone, Debug, Eq, PartialEq)]
2706pub struct Service {
2707 name: Identifier,
2708 runtime_name: Option<Sourced<ProtectedString>>,
2709 rootfs: Option<Sourced<ProtectedString>>,
2710 image: Option<Sourced<ImageReference>>,
2711 image_acquisition: Option<Sourced<Identifier>>,
2712 image_build: Option<Sourced<Identifier>>,
2713 command: Option<Sourced<Command>>,
2714 startup_notification: Option<Sourced<StartupNotification>>,
2715 entrypoint: Option<Sourced<Entrypoint>>,
2716 run_init: Option<Sourced<bool>>,
2717 stop_timeout: Option<Sourced<StopTimeout>>,
2718 pull_policy: Option<Sourced<PullPolicy>>,
2719 memory_limit: Option<Sourced<ProtectedString>>,
2720 exposed_ports: Option<Vec<Sourced<ExposedPort>>>,
2721 exposed_ports_origins: Vec<Provenance>,
2722 restart_policy: Option<Sourced<RestartPolicy>>,
2723 healthcheck: Option<Sourced<Healthcheck>>,
2724 labels: Vec<Sourced<MetadataLabel>>,
2725 annotations: Option<Vec<Sourced<Annotation>>>,
2726 annotations_origins: Vec<Provenance>,
2727 logging: Option<Sourced<Logging>>,
2728 reload_action: Option<Sourced<ReloadAction>>,
2729 user: Option<Sourced<ProtectedString>>,
2730 group: Option<Sourced<ProtectedString>>,
2731 user_namespace: Option<Sourced<ProtectedString>>,
2732 supplementary_groups: Vec<Sourced<ProtectedString>>,
2733 working_directory: Option<Sourced<ProtectedString>>,
2734 read_only_root_filesystem: Option<Sourced<bool>>,
2735 hostname: Option<Sourced<ProtectedString>>,
2736 dns_servers: Option<Vec<Sourced<ProtectedString>>>,
2737 dns_servers_origins: Vec<Provenance>,
2738 dns_options: Option<Vec<Sourced<ProtectedString>>>,
2739 dns_options_origins: Vec<Provenance>,
2740 dns_search_domains: Option<Vec<Sourced<ProtectedString>>>,
2741 dns_search_domains_origins: Vec<Provenance>,
2742 security_options: Option<Vec<Sourced<SecurityOption>>>,
2743 security_options_origins: Vec<Provenance>,
2744 pids_limit: Option<Sourced<ProtectedString>>,
2745 shm_size: Option<Sourced<ProtectedString>>,
2746 cap_add: Option<Vec<Sourced<ProtectedString>>>,
2747 cap_add_origins: Vec<Provenance>,
2748 cap_drop: Option<Vec<Sourced<ProtectedString>>>,
2749 cap_drop_origins: Vec<Provenance>,
2750 tmpfs: Option<Vec<Sourced<ProtectedString>>>,
2751 tmpfs_origins: Vec<Provenance>,
2752 sysctls: Option<Vec<Sourced<KernelParameter>>>,
2753 sysctls_origins: Vec<Provenance>,
2754 ulimits: Option<Vec<Sourced<ResourceLimit>>>,
2755 ulimits_origins: Vec<Provenance>,
2756 devices: Option<Vec<Sourced<Device>>>,
2757 devices_origins: Vec<Provenance>,
2758 stop_signal: Option<Sourced<ProtectedString>>,
2759 podman_args: Option<Vec<Sourced<ProtectedString>>>,
2760 podman_args_origins: Vec<Provenance>,
2761 environment: Vec<Sourced<EnvironmentVariable>>,
2762 environment_files: Vec<Sourced<EnvironmentFile>>,
2763 host_mappings: Vec<Sourced<HostMapping>>,
2764 ports: Vec<Sourced<Port>>,
2765 mounts: Vec<Sourced<Mount>>,
2766 config_grants: Vec<Sourced<ResourceGrant>>,
2767 secret_grants: Vec<Sourced<ResourceGrant>>,
2768 networks: Vec<Sourced<NetworkAttachment>>,
2769 dependencies: Vec<Sourced<ServiceDependency>>,
2770}
2771
2772impl Service {
2773 #[must_use]
2775 pub const fn new(name: Identifier) -> Self {
2776 Self {
2777 name,
2778 runtime_name: None,
2779 rootfs: None,
2780 image: None,
2781 image_acquisition: None,
2782 image_build: None,
2783 command: None,
2784 startup_notification: None,
2785 entrypoint: None,
2786 run_init: None,
2787 stop_timeout: None,
2788 pull_policy: None,
2789 memory_limit: None,
2790 exposed_ports: None,
2791 exposed_ports_origins: Vec::new(),
2792 restart_policy: None,
2793 healthcheck: None,
2794 labels: Vec::new(),
2795 annotations: None,
2796 annotations_origins: Vec::new(),
2797 logging: None,
2798 reload_action: None,
2799 user: None,
2800 group: None,
2801 user_namespace: None,
2802 supplementary_groups: Vec::new(),
2803 working_directory: None,
2804 read_only_root_filesystem: None,
2805 hostname: None,
2806 dns_servers: None,
2807 dns_servers_origins: Vec::new(),
2808 dns_options: None,
2809 dns_options_origins: Vec::new(),
2810 dns_search_domains: None,
2811 dns_search_domains_origins: Vec::new(),
2812 security_options: None,
2813 security_options_origins: Vec::new(),
2814 pids_limit: None,
2815 shm_size: None,
2816 cap_add: None,
2817 cap_add_origins: Vec::new(),
2818 cap_drop: None,
2819 cap_drop_origins: Vec::new(),
2820 tmpfs: None,
2821 tmpfs_origins: Vec::new(),
2822 sysctls: None,
2823 sysctls_origins: Vec::new(),
2824 ulimits: None,
2825 ulimits_origins: Vec::new(),
2826 devices: None,
2827 devices_origins: Vec::new(),
2828 stop_signal: None,
2829 podman_args: None,
2830 podman_args_origins: Vec::new(),
2831 environment: Vec::new(),
2832 environment_files: Vec::new(),
2833 host_mappings: Vec::new(),
2834 ports: Vec::new(),
2835 mounts: Vec::new(),
2836 config_grants: Vec::new(),
2837 secret_grants: Vec::new(),
2838 networks: Vec::new(),
2839 dependencies: Vec::new(),
2840 }
2841 }
2842
2843 #[must_use]
2845 pub const fn name(&self) -> &Identifier {
2846 &self.name
2847 }
2848
2849 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
2851 self.runtime_name = Some(name);
2852 }
2853
2854 #[must_use]
2856 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
2857 self.runtime_name.as_ref()
2858 }
2859
2860 pub fn set_rootfs(&mut self, rootfs: Sourced<ProtectedString>) -> Result<(), ModelError> {
2867 self.ensure_rootfs_is_compatible()?;
2868 self.rootfs = Some(rootfs);
2869 Ok(())
2870 }
2871
2872 #[must_use]
2874 pub const fn rootfs(&self) -> Option<&Sourced<ProtectedString>> {
2875 self.rootfs.as_ref()
2876 }
2877
2878 pub fn set_image(&mut self, image: Sourced<ImageReference>) {
2880 self.image = Some(image);
2881 }
2882
2883 #[must_use]
2885 pub const fn image(&self) -> Option<&Sourced<ImageReference>> {
2886 self.image.as_ref()
2887 }
2888
2889 pub fn set_image_acquisition(&mut self, acquisition: Sourced<Identifier>) {
2893 self.image_acquisition = Some(acquisition);
2894 }
2895
2896 #[must_use]
2898 pub const fn image_acquisition(&self) -> Option<&Sourced<Identifier>> {
2899 self.image_acquisition.as_ref()
2900 }
2901
2902 pub fn set_image_build(&mut self, build: Sourced<Identifier>) {
2906 self.image_build = Some(build);
2907 }
2908
2909 #[must_use]
2911 pub const fn image_build(&self) -> Option<&Sourced<Identifier>> {
2912 self.image_build.as_ref()
2913 }
2914
2915 pub fn set_command(&mut self, command: Sourced<Command>) {
2917 self.command = Some(command);
2918 }
2919
2920 #[must_use]
2922 pub const fn command(&self) -> Option<&Sourced<Command>> {
2923 self.command.as_ref()
2924 }
2925
2926 pub fn set_startup_notification(&mut self, notification: Sourced<StartupNotification>) {
2928 self.startup_notification = Some(notification);
2929 }
2930
2931 #[must_use]
2933 pub const fn startup_notification(&self) -> Option<&Sourced<StartupNotification>> {
2934 self.startup_notification.as_ref()
2935 }
2936
2937 pub fn set_entrypoint(&mut self, entrypoint: Sourced<Entrypoint>) {
2939 self.entrypoint = Some(entrypoint);
2940 }
2941
2942 #[must_use]
2944 pub const fn entrypoint(&self) -> Option<&Sourced<Entrypoint>> {
2945 self.entrypoint.as_ref()
2946 }
2947
2948 pub fn set_run_init(&mut self, run_init: Sourced<bool>) {
2950 self.run_init = Some(run_init);
2951 }
2952
2953 #[must_use]
2955 pub const fn run_init(&self) -> Option<&Sourced<bool>> {
2956 self.run_init.as_ref()
2957 }
2958
2959 pub fn set_stop_timeout(&mut self, timeout: Sourced<StopTimeout>) {
2961 self.stop_timeout = Some(timeout);
2962 }
2963
2964 #[must_use]
2966 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
2967 self.stop_timeout.as_ref()
2968 }
2969
2970 pub fn set_pull_policy(&mut self, policy: Sourced<PullPolicy>) {
2972 self.pull_policy = Some(policy);
2973 }
2974
2975 #[must_use]
2977 pub const fn pull_policy(&self) -> Option<&Sourced<PullPolicy>> {
2978 self.pull_policy.as_ref()
2979 }
2980
2981 pub fn set_memory_limit(&mut self, limit: Sourced<ProtectedString>) {
2983 self.memory_limit = Some(limit);
2984 }
2985
2986 #[must_use]
2988 pub const fn memory_limit(&self) -> Option<&Sourced<ProtectedString>> {
2989 self.memory_limit.as_ref()
2990 }
2991
2992 pub fn set_exposed_ports(&mut self, ports: Vec<Sourced<ExposedPort>>) {
2994 self.exposed_ports = Some(ports);
2995 self.exposed_ports_origins.clear();
2996 }
2997
2998 pub fn set_exposed_ports_with_origins(&mut self, ports: Vec<Sourced<ExposedPort>>, origins: Vec<Provenance>) {
3000 self.exposed_ports = Some(ports);
3001 self.exposed_ports_origins = origins;
3002 }
3003
3004 pub fn add_exposed_port(&mut self, port: Sourced<ExposedPort>) {
3006 self.exposed_ports.get_or_insert_default().push(port);
3007 }
3008
3009 #[must_use]
3011 pub fn exposed_ports(&self) -> Option<&[Sourced<ExposedPort>]> {
3012 self.exposed_ports.as_deref()
3013 }
3014
3015 #[must_use]
3017 pub fn exposed_ports_origins(&self) -> &[Provenance] {
3018 &self.exposed_ports_origins
3019 }
3020
3021 pub fn set_restart_policy(&mut self, restart_policy: Sourced<RestartPolicy>) {
3023 self.restart_policy = Some(restart_policy);
3024 }
3025
3026 #[must_use]
3028 pub const fn restart_policy(&self) -> Option<&Sourced<RestartPolicy>> {
3029 self.restart_policy.as_ref()
3030 }
3031
3032 pub fn set_healthcheck(&mut self, healthcheck: Sourced<Healthcheck>) {
3034 self.healthcheck = Some(healthcheck);
3035 }
3036
3037 #[must_use]
3039 pub const fn healthcheck(&self) -> Option<&Sourced<Healthcheck>> {
3040 self.healthcheck.as_ref()
3041 }
3042
3043 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
3045 self.labels.push(label);
3046 }
3047
3048 #[must_use]
3050 pub fn labels(&self) -> &[Sourced<MetadataLabel>] {
3051 &self.labels
3052 }
3053
3054 pub fn set_annotations(&mut self, annotations: Vec<Sourced<Annotation>>) {
3056 self.annotations = Some(annotations);
3057 self.annotations_origins.clear();
3058 }
3059
3060 pub fn add_annotation(&mut self, annotation: Sourced<Annotation>) {
3062 self.annotations.get_or_insert_default().push(annotation);
3063 }
3064
3065 pub fn set_annotations_with_origins(&mut self, annotations: Vec<Sourced<Annotation>>, origins: Vec<Provenance>) {
3067 self.annotations = Some(annotations);
3068 self.annotations_origins = origins;
3069 }
3070
3071 #[must_use]
3073 pub fn annotations(&self) -> Option<&[Sourced<Annotation>]> {
3074 self.annotations.as_deref()
3075 }
3076
3077 #[must_use]
3079 pub fn annotations_origins(&self) -> &[Provenance] {
3080 &self.annotations_origins
3081 }
3082
3083 pub fn set_logging(&mut self, logging: Sourced<Logging>) {
3085 self.logging = Some(logging);
3086 }
3087
3088 #[must_use]
3090 pub const fn logging(&self) -> Option<&Sourced<Logging>> {
3091 self.logging.as_ref()
3092 }
3093
3094 pub fn set_reload_action(&mut self, reload_action: Sourced<ReloadAction>) {
3096 self.reload_action = Some(reload_action);
3097 }
3098
3099 #[must_use]
3101 pub const fn reload_action(&self) -> Option<&Sourced<ReloadAction>> {
3102 self.reload_action.as_ref()
3103 }
3104
3105 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
3107 self.user = Some(user);
3108 }
3109
3110 #[must_use]
3112 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
3113 self.user.as_ref()
3114 }
3115
3116 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
3118 self.group = Some(group);
3119 }
3120
3121 #[must_use]
3123 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
3124 self.group.as_ref()
3125 }
3126
3127 pub fn set_user_namespace(&mut self, user_namespace: Sourced<ProtectedString>) {
3129 self.user_namespace = Some(user_namespace);
3130 }
3131
3132 #[must_use]
3134 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
3135 self.user_namespace.as_ref()
3136 }
3137
3138 pub fn add_supplementary_group(&mut self, group: Sourced<ProtectedString>) {
3140 self.supplementary_groups.push(group);
3141 }
3142
3143 #[must_use]
3145 pub fn supplementary_groups(&self) -> &[Sourced<ProtectedString>] {
3146 &self.supplementary_groups
3147 }
3148
3149 pub fn set_working_directory(&mut self, working_directory: Sourced<ProtectedString>) {
3151 self.working_directory = Some(working_directory);
3152 }
3153
3154 #[must_use]
3156 pub const fn working_directory(&self) -> Option<&Sourced<ProtectedString>> {
3157 self.working_directory.as_ref()
3158 }
3159
3160 pub fn set_read_only_root_filesystem(&mut self, read_only: Sourced<bool>) {
3162 self.read_only_root_filesystem = Some(read_only);
3163 }
3164
3165 #[must_use]
3167 pub const fn read_only_root_filesystem(&self) -> Option<&Sourced<bool>> {
3168 self.read_only_root_filesystem.as_ref()
3169 }
3170
3171 pub fn set_hostname(&mut self, hostname: Sourced<ProtectedString>) {
3173 self.hostname = Some(hostname);
3174 }
3175
3176 #[must_use]
3178 pub const fn hostname(&self) -> Option<&Sourced<ProtectedString>> {
3179 self.hostname.as_ref()
3180 }
3181
3182 pub fn set_dns_servers_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3184 self.dns_servers = Some(values);
3185 self.dns_servers_origins = origins;
3186 }
3187
3188 pub fn set_dns_servers(&mut self, values: Vec<Sourced<ProtectedString>>) {
3190 self.set_dns_servers_with_origins(values, Vec::new());
3191 }
3192
3193 #[must_use]
3195 pub fn dns_servers(&self) -> Option<&[Sourced<ProtectedString>]> {
3196 self.dns_servers.as_deref()
3197 }
3198
3199 #[must_use]
3201 pub fn dns_servers_origins(&self) -> &[Provenance] {
3202 &self.dns_servers_origins
3203 }
3204
3205 pub fn set_dns_options_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3207 self.dns_options = Some(values);
3208 self.dns_options_origins = origins;
3209 }
3210
3211 pub fn set_dns_options(&mut self, values: Vec<Sourced<ProtectedString>>) {
3213 self.set_dns_options_with_origins(values, Vec::new());
3214 }
3215
3216 #[must_use]
3218 pub fn dns_options(&self) -> Option<&[Sourced<ProtectedString>]> {
3219 self.dns_options.as_deref()
3220 }
3221
3222 #[must_use]
3224 pub fn dns_options_origins(&self) -> &[Provenance] {
3225 &self.dns_options_origins
3226 }
3227
3228 pub fn set_dns_search_domains_with_origins(
3230 &mut self,
3231 values: Vec<Sourced<ProtectedString>>,
3232 origins: Vec<Provenance>,
3233 ) {
3234 self.dns_search_domains = Some(values);
3235 self.dns_search_domains_origins = origins;
3236 }
3237
3238 pub fn set_dns_search_domains(&mut self, values: Vec<Sourced<ProtectedString>>) {
3240 self.set_dns_search_domains_with_origins(values, Vec::new());
3241 }
3242
3243 #[must_use]
3245 pub fn dns_search_domains(&self) -> Option<&[Sourced<ProtectedString>]> {
3246 self.dns_search_domains.as_deref()
3247 }
3248
3249 #[must_use]
3251 pub fn dns_search_domains_origins(&self) -> &[Provenance] {
3252 &self.dns_search_domains_origins
3253 }
3254
3255 pub fn set_security_options_with_origins(
3257 &mut self,
3258 values: Vec<Sourced<SecurityOption>>,
3259 origins: Vec<Provenance>,
3260 ) {
3261 self.security_options = Some(values);
3262 self.security_options_origins = origins;
3263 }
3264
3265 pub fn set_security_options(&mut self, values: Vec<Sourced<SecurityOption>>) {
3267 self.set_security_options_with_origins(values, Vec::new());
3268 }
3269
3270 #[must_use]
3272 pub fn security_options(&self) -> Option<&[Sourced<SecurityOption>]> {
3273 self.security_options.as_deref()
3274 }
3275
3276 #[must_use]
3278 pub fn security_options_origins(&self) -> &[Provenance] {
3279 &self.security_options_origins
3280 }
3281
3282 pub fn set_pids_limit(&mut self, limit: Sourced<ProtectedString>) {
3284 self.pids_limit = Some(limit);
3285 }
3286
3287 #[must_use]
3289 pub const fn pids_limit(&self) -> Option<&Sourced<ProtectedString>> {
3290 self.pids_limit.as_ref()
3291 }
3292
3293 pub fn set_shm_size(&mut self, size: Sourced<ProtectedString>) {
3295 self.shm_size = Some(size);
3296 }
3297
3298 #[must_use]
3300 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
3301 self.shm_size.as_ref()
3302 }
3303
3304 pub fn set_cap_add(&mut self, values: Vec<Sourced<ProtectedString>>) {
3306 self.cap_add = Some(values);
3307 self.cap_add_origins.clear();
3308 }
3309
3310 pub fn set_cap_add_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3312 self.cap_add = Some(values);
3313 self.cap_add_origins = origins;
3314 }
3315
3316 #[must_use]
3318 pub fn cap_add(&self) -> Option<&[Sourced<ProtectedString>]> {
3319 self.cap_add.as_deref()
3320 }
3321
3322 #[must_use]
3324 pub fn cap_add_origins(&self) -> &[Provenance] {
3325 &self.cap_add_origins
3326 }
3327
3328 pub fn set_cap_drop(&mut self, values: Vec<Sourced<ProtectedString>>) {
3330 self.cap_drop = Some(values);
3331 self.cap_drop_origins.clear();
3332 }
3333
3334 pub fn set_cap_drop_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3336 self.cap_drop = Some(values);
3337 self.cap_drop_origins = origins;
3338 }
3339
3340 #[must_use]
3342 pub fn cap_drop(&self) -> Option<&[Sourced<ProtectedString>]> {
3343 self.cap_drop.as_deref()
3344 }
3345
3346 #[must_use]
3348 pub fn cap_drop_origins(&self) -> &[Provenance] {
3349 &self.cap_drop_origins
3350 }
3351
3352 pub fn set_tmpfs(&mut self, values: Vec<Sourced<ProtectedString>>) {
3354 self.tmpfs = Some(values);
3355 self.tmpfs_origins.clear();
3356 }
3357
3358 pub fn set_tmpfs_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3360 self.tmpfs = Some(values);
3361 self.tmpfs_origins = origins;
3362 }
3363
3364 #[must_use]
3366 pub fn tmpfs(&self) -> Option<&[Sourced<ProtectedString>]> {
3367 self.tmpfs.as_deref()
3368 }
3369
3370 #[must_use]
3372 pub fn tmpfs_origins(&self) -> &[Provenance] {
3373 &self.tmpfs_origins
3374 }
3375
3376 pub fn set_sysctls(&mut self, values: Vec<Sourced<KernelParameter>>) {
3378 self.sysctls = Some(values);
3379 self.sysctls_origins.clear();
3380 }
3381
3382 pub fn set_sysctls_with_origins(&mut self, values: Vec<Sourced<KernelParameter>>, origins: Vec<Provenance>) {
3384 self.sysctls = Some(values);
3385 self.sysctls_origins = origins;
3386 }
3387
3388 #[must_use]
3390 pub fn sysctls(&self) -> Option<&[Sourced<KernelParameter>]> {
3391 self.sysctls.as_deref()
3392 }
3393
3394 #[must_use]
3396 pub fn sysctls_origins(&self) -> &[Provenance] {
3397 &self.sysctls_origins
3398 }
3399
3400 pub fn set_ulimits(&mut self, values: Vec<Sourced<ResourceLimit>>) {
3402 self.ulimits = Some(values);
3403 self.ulimits_origins.clear();
3404 }
3405
3406 pub fn set_ulimits_with_origins(&mut self, values: Vec<Sourced<ResourceLimit>>, origins: Vec<Provenance>) {
3408 self.ulimits = Some(values);
3409 self.ulimits_origins = origins;
3410 }
3411
3412 #[must_use]
3414 pub fn ulimits(&self) -> Option<&[Sourced<ResourceLimit>]> {
3415 self.ulimits.as_deref()
3416 }
3417
3418 #[must_use]
3420 pub fn ulimits_origins(&self) -> &[Provenance] {
3421 &self.ulimits_origins
3422 }
3423
3424 pub fn set_devices(&mut self, values: Vec<Sourced<Device>>) {
3426 self.devices = Some(values);
3427 self.devices_origins.clear();
3428 }
3429
3430 pub fn set_devices_with_origins(&mut self, values: Vec<Sourced<Device>>, origins: Vec<Provenance>) {
3432 self.devices = Some(values);
3433 self.devices_origins = origins;
3434 }
3435
3436 #[must_use]
3438 pub fn devices(&self) -> Option<&[Sourced<Device>]> {
3439 self.devices.as_deref()
3440 }
3441
3442 #[must_use]
3444 pub fn devices_origins(&self) -> &[Provenance] {
3445 &self.devices_origins
3446 }
3447
3448 pub fn set_stop_signal(&mut self, signal: Sourced<ProtectedString>) {
3450 self.stop_signal = Some(signal);
3451 }
3452
3453 #[must_use]
3455 pub const fn stop_signal(&self) -> Option<&Sourced<ProtectedString>> {
3456 self.stop_signal.as_ref()
3457 }
3458
3459 pub fn set_podman_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
3461 self.set_podman_args_with_origins(values, Vec::new());
3462 }
3463
3464 pub fn set_podman_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3466 self.podman_args = Some(values);
3467 self.podman_args_origins = origins;
3468 }
3469
3470 pub fn add_podman_arg(&mut self, value: Sourced<ProtectedString>) {
3472 self.podman_args.get_or_insert_default().push(value);
3473 }
3474
3475 #[must_use]
3477 pub fn podman_args(&self) -> Option<&[Sourced<ProtectedString>]> {
3478 self.podman_args.as_deref()
3479 }
3480
3481 #[must_use]
3483 pub fn podman_args_origins(&self) -> &[Provenance] {
3484 &self.podman_args_origins
3485 }
3486
3487 pub fn add_environment(&mut self, value: Sourced<EnvironmentVariable>) {
3489 self.environment.push(value);
3490 }
3491
3492 #[must_use]
3494 pub fn environment(&self) -> &[Sourced<EnvironmentVariable>] {
3495 &self.environment
3496 }
3497
3498 pub fn add_environment_file(&mut self, value: Sourced<EnvironmentFile>) {
3500 self.environment_files.push(value);
3501 }
3502
3503 #[must_use]
3505 pub fn environment_files(&self) -> &[Sourced<EnvironmentFile>] {
3506 &self.environment_files
3507 }
3508
3509 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
3511 self.host_mappings.push(value);
3512 }
3513
3514 #[must_use]
3516 pub fn host_mappings(&self) -> &[Sourced<HostMapping>] {
3517 &self.host_mappings
3518 }
3519
3520 pub fn add_port(&mut self, value: Sourced<Port>) {
3522 self.ports.push(value);
3523 }
3524
3525 #[must_use]
3527 pub fn ports(&self) -> &[Sourced<Port>] {
3528 &self.ports
3529 }
3530
3531 pub fn add_mount(&mut self, value: Sourced<Mount>) {
3533 self.mounts.push(value);
3534 }
3535
3536 #[must_use]
3538 pub fn mounts(&self) -> &[Sourced<Mount>] {
3539 &self.mounts
3540 }
3541
3542 pub fn add_config_grant(&mut self, value: Sourced<ResourceGrant>) {
3544 self.config_grants.push(value);
3545 }
3546
3547 #[must_use]
3549 pub fn config_grants(&self) -> &[Sourced<ResourceGrant>] {
3550 &self.config_grants
3551 }
3552
3553 pub fn add_secret_grant(&mut self, value: Sourced<ResourceGrant>) {
3555 self.secret_grants.push(value);
3556 }
3557
3558 #[must_use]
3560 pub fn secret_grants(&self) -> &[Sourced<ResourceGrant>] {
3561 &self.secret_grants
3562 }
3563
3564 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
3566 self.networks.push(value);
3567 }
3568
3569 pub fn replace_network(
3579 &mut self,
3580 index: usize,
3581 value: Sourced<NetworkAttachment>,
3582 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
3583 let len = self.networks.len();
3584 let Some(slot) = self.networks.get_mut(index) else {
3585 return Err(ModelError::UnknownNetworkAttachmentIndex { index, len });
3586 };
3587 Ok(std::mem::replace(slot, value))
3588 }
3589
3590 #[must_use]
3592 pub fn networks(&self) -> &[Sourced<NetworkAttachment>] {
3593 &self.networks
3594 }
3595
3596 pub fn add_dependency(&mut self, value: Sourced<ServiceDependency>) {
3598 self.dependencies.push(value);
3599 }
3600
3601 #[must_use]
3603 pub fn dependencies(&self) -> &[Sourced<ServiceDependency>] {
3604 &self.dependencies
3605 }
3606
3607 pub fn validate_image_source_exclusivity(&self) -> Result<(), ModelError> {
3616 if self.rootfs.is_some() {
3617 self.ensure_rootfs_is_compatible()?;
3618 }
3619 Ok(())
3620 }
3621
3622 fn ensure_rootfs_is_compatible(&self) -> Result<(), ModelError> {
3623 let source = if self.image.is_some() {
3624 Some("image")
3625 } else if self.image_acquisition.is_some() {
3626 Some("image acquisition")
3627 } else if self.image_build.is_some() {
3628 Some("image build")
3629 } else {
3630 None
3631 };
3632 if let Some(source) = source {
3633 return Err(ModelError::RootfsImageSourceConflict {
3634 service: self.name.as_str().to_owned(),
3635 source,
3636 });
3637 }
3638 Ok(())
3639 }
3640}
3641
3642#[derive(Clone, Debug, Eq, PartialEq)]
3644pub struct Application {
3645 name: Identifier,
3646 image_acquisitions: Vec<Sourced<ImageAcquisition>>,
3647 image_builds: Vec<Sourced<ImageBuild>>,
3648 services: Vec<Sourced<Service>>,
3649 service_groups: Vec<Sourced<ServiceGroup>>,
3650 volumes: Vec<Sourced<Volume>>,
3651 networks: Vec<Sourced<Network>>,
3652 configs: Vec<Sourced<Config>>,
3653 secrets: Vec<Sourced<Secret>>,
3654}
3655
3656impl Application {
3657 #[must_use]
3659 pub const fn new(name: Identifier) -> Self {
3660 Self {
3661 name,
3662 image_acquisitions: Vec::new(),
3663 image_builds: Vec::new(),
3664 services: Vec::new(),
3665 service_groups: Vec::new(),
3666 volumes: Vec::new(),
3667 networks: Vec::new(),
3668 configs: Vec::new(),
3669 secrets: Vec::new(),
3670 }
3671 }
3672
3673 #[must_use]
3675 pub const fn name(&self) -> &Identifier {
3676 &self.name
3677 }
3678
3679 pub fn add_image_acquisition(&mut self, acquisition: Sourced<ImageAcquisition>) -> Result<(), ModelError> {
3685 ensure_unique(
3686 "image acquisition",
3687 acquisition.value().name(),
3688 self.image_acquisitions.iter().map(|candidate| candidate.value().name()),
3689 )?;
3690 self.image_acquisitions.push(acquisition);
3691 Ok(())
3692 }
3693
3694 #[must_use]
3696 pub fn image_acquisitions(&self) -> &[Sourced<ImageAcquisition>] {
3697 &self.image_acquisitions
3698 }
3699
3700 pub fn add_image_build(&mut self, build: Sourced<ImageBuild>) -> Result<(), ModelError> {
3706 ensure_unique(
3707 "image build",
3708 build.value().name(),
3709 self.image_builds.iter().map(|candidate| candidate.value().name()),
3710 )?;
3711 self.image_builds.push(build);
3712 Ok(())
3713 }
3714
3715 #[must_use]
3717 pub fn image_builds(&self) -> &[Sourced<ImageBuild>] {
3718 &self.image_builds
3719 }
3720
3721 pub fn validate_image_artifact_references(&self) -> Result<(), ModelError> {
3731 for service in &self.services {
3732 if let Some(acquisition) = service.value().image_acquisition() {
3733 if !self.contains_image_acquisition(acquisition.value()) {
3734 return Err(ModelError::UnknownImageAcquisitionReference {
3735 service: service.value().name().as_str().to_owned(),
3736 acquisition: acquisition.value().as_str().to_owned(),
3737 });
3738 }
3739 }
3740 if let Some(build) = service.value().image_build() {
3741 if !self.contains_image_build(build.value()) {
3742 return Err(ModelError::UnknownImageBuildReference {
3743 service: service.value().name().as_str().to_owned(),
3744 build: build.value().as_str().to_owned(),
3745 });
3746 }
3747 }
3748 }
3749 for volume in &self.volumes {
3750 let Some(source) = volume.value().image_source() else {
3751 continue;
3752 };
3753 match source.value() {
3754 VolumeImageSource::Literal(_) => {}
3755 VolumeImageSource::ImageAcquisition(acquisition) => {
3756 if !self.contains_image_acquisition(acquisition) {
3757 return Err(ModelError::UnknownVolumeImageAcquisitionReference {
3758 volume: volume.value().name().as_str().to_owned(),
3759 acquisition: acquisition.as_str().to_owned(),
3760 });
3761 }
3762 }
3763 VolumeImageSource::ImageBuild(build) => {
3764 if !self.contains_image_build(build) {
3765 return Err(ModelError::UnknownVolumeImageBuildReference {
3766 volume: volume.value().name().as_str().to_owned(),
3767 build: build.as_str().to_owned(),
3768 });
3769 }
3770 }
3771 }
3772 }
3773 Ok(())
3774 }
3775
3776 pub fn validate_image_artifact_dependencies(
3787 &self,
3788 dependencies: &[Sourced<ArtifactDependency>],
3789 ) -> Result<(), ModelError> {
3790 self.validate_image_artifact_references()?;
3791
3792 let mut graph = BTreeMap::<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>::new();
3793 for dependency in dependencies {
3794 let source = dependency.value().source().value();
3795 let target = dependency.value().target().value();
3796 self.validate_artifact_dependency_node(source)?;
3797 self.validate_artifact_dependency_node(target)?;
3798 graph.entry(source.clone()).or_default().insert(target.clone());
3799 graph.entry(target.clone()).or_default();
3800 }
3801
3802 let mut state = BTreeMap::<ArtifactDependencyNode, VisitState>::new();
3803 let mut path = Vec::new();
3804 for node in graph.keys() {
3805 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
3806 continue;
3807 }
3808 if let Some(cycle) = detect_artifact_cycle(node, &graph, &mut state, &mut path) {
3809 return Err(ModelError::ImageArtifactDependencyCycle {
3810 nodes: cycle.into_iter().map(|node| node.display_name()).collect(),
3811 });
3812 }
3813 }
3814 Ok(())
3815 }
3816
3817 fn contains_image_acquisition(&self, name: &Identifier) -> bool {
3818 self.image_acquisitions
3819 .iter()
3820 .any(|candidate| candidate.value().name() == name)
3821 }
3822
3823 fn contains_image_build(&self, name: &Identifier) -> bool {
3824 self.image_builds
3825 .iter()
3826 .any(|candidate| candidate.value().name() == name)
3827 }
3828
3829 fn validate_artifact_dependency_node(&self, node: &ArtifactDependencyNode) -> Result<(), ModelError> {
3830 let (kind, name) = node.kind_and_name();
3831 let exists = match node {
3832 ArtifactDependencyNode::Volume(_) => self.volumes.iter().any(|volume| volume.value().name() == name),
3833 ArtifactDependencyNode::ImageAcquisition(_) => self.contains_image_acquisition(name),
3834 ArtifactDependencyNode::ImageBuild(_) => self.contains_image_build(name),
3835 };
3836 if exists {
3837 Ok(())
3838 } else {
3839 Err(ModelError::UnknownArtifactDependencyNode {
3840 kind,
3841 name: name.as_str().to_owned(),
3842 })
3843 }
3844 }
3845
3846 pub fn add_service(&mut self, service: Sourced<Service>) -> Result<(), ModelError> {
3854 ensure_unique(
3855 "service",
3856 service.value().name(),
3857 self.services.iter().map(|candidate| candidate.value().name()),
3858 )?;
3859 service.value().validate_image_source_exclusivity()?;
3860 if let Some(acquisition) = service.value().image_acquisition() {
3861 if !self
3862 .image_acquisitions
3863 .iter()
3864 .any(|candidate| candidate.value().name() == acquisition.value())
3865 {
3866 return Err(ModelError::UnknownImageAcquisitionReference {
3867 service: service.value().name().as_str().to_owned(),
3868 acquisition: acquisition.value().as_str().to_owned(),
3869 });
3870 }
3871 }
3872 if let Some(build) = service.value().image_build() {
3873 if !self
3874 .image_builds
3875 .iter()
3876 .any(|candidate| candidate.value().name() == build.value())
3877 {
3878 return Err(ModelError::UnknownImageBuildReference {
3879 service: service.value().name().as_str().to_owned(),
3880 build: build.value().as_str().to_owned(),
3881 });
3882 }
3883 }
3884 self.services.push(service);
3885 Ok(())
3886 }
3887
3888 #[must_use]
3890 pub fn services(&self) -> &[Sourced<Service>] {
3891 &self.services
3892 }
3893
3894 pub fn add_service_group(&mut self, group: Sourced<ServiceGroup>) -> Result<(), ModelError> {
3904 ensure_unique(
3905 "service group",
3906 group.value().name(),
3907 self.service_groups.iter().map(|candidate| candidate.value().name()),
3908 )?;
3909 for member in group.value().members() {
3910 if !self
3911 .services
3912 .iter()
3913 .any(|service| service.value().name() == member.value())
3914 {
3915 return Err(ModelError::UnknownServiceGroupMember {
3916 group: group.value().name().as_str().to_owned(),
3917 service: member.value().as_str().to_owned(),
3918 });
3919 }
3920 if let Some(existing) = self.service_groups.iter().find(|candidate| {
3921 candidate
3922 .value()
3923 .members()
3924 .iter()
3925 .any(|candidate_member| candidate_member.value() == member.value())
3926 }) {
3927 return Err(ModelError::ServiceInMultipleGroups {
3928 service: member.value().as_str().to_owned(),
3929 existing: existing.value().name().as_str().to_owned(),
3930 replacement: group.value().name().as_str().to_owned(),
3931 });
3932 }
3933 }
3934 self.service_groups.push(group);
3935 Ok(())
3936 }
3937
3938 #[must_use]
3940 pub fn service_groups(&self) -> &[Sourced<ServiceGroup>] {
3941 &self.service_groups
3942 }
3943
3944 pub fn add_volume(&mut self, volume: Sourced<Volume>) -> Result<(), ModelError> {
3950 ensure_unique(
3951 "volume",
3952 volume.value().name(),
3953 self.volumes.iter().map(|candidate| candidate.value().name()),
3954 )?;
3955 self.volumes.push(volume);
3956 Ok(())
3957 }
3958
3959 #[must_use]
3961 pub fn volumes(&self) -> &[Sourced<Volume>] {
3962 &self.volumes
3963 }
3964
3965 pub fn add_network(&mut self, network: Sourced<Network>) -> Result<(), ModelError> {
3971 ensure_unique(
3972 "network",
3973 network.value().name(),
3974 self.networks.iter().map(|candidate| candidate.value().name()),
3975 )?;
3976 self.networks.push(network);
3977 Ok(())
3978 }
3979
3980 #[must_use]
3982 pub fn networks(&self) -> &[Sourced<Network>] {
3983 &self.networks
3984 }
3985
3986 pub fn add_config(&mut self, config: Sourced<Config>) -> Result<(), ModelError> {
3992 ensure_unique(
3993 "config",
3994 config.value().name(),
3995 self.configs.iter().map(|candidate| candidate.value().name()),
3996 )?;
3997 self.configs.push(config);
3998 Ok(())
3999 }
4000
4001 #[must_use]
4003 pub fn configs(&self) -> &[Sourced<Config>] {
4004 &self.configs
4005 }
4006
4007 pub fn add_secret(&mut self, secret: Sourced<Secret>) -> Result<(), ModelError> {
4013 ensure_unique(
4014 "secret",
4015 secret.value().name(),
4016 self.secrets.iter().map(|candidate| candidate.value().name()),
4017 )?;
4018 self.secrets.push(secret);
4019 Ok(())
4020 }
4021
4022 #[must_use]
4024 pub fn secrets(&self) -> &[Sourced<Secret>] {
4025 &self.secrets
4026 }
4027}
4028
4029#[derive(Clone, Copy, Eq, PartialEq)]
4030enum VisitState {
4031 Visiting,
4032 Finished,
4033}
4034
4035fn detect_artifact_cycle(
4036 node: &ArtifactDependencyNode,
4037 graph: &BTreeMap<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>,
4038 state: &mut BTreeMap<ArtifactDependencyNode, VisitState>,
4039 path: &mut Vec<ArtifactDependencyNode>,
4040) -> Option<Vec<ArtifactDependencyNode>> {
4041 if state.get(node).is_some_and(|state| *state == VisitState::Visiting) {
4042 let index = path.iter().position(|candidate| candidate == node)?;
4043 let mut cycle = path[index..].to_vec();
4044 cycle.push(node.clone());
4045 return Some(cycle);
4046 }
4047 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
4048 return None;
4049 }
4050
4051 state.insert(node.clone(), VisitState::Visiting);
4052 path.push(node.clone());
4053 if let Some(targets) = graph.get(node) {
4054 for target in targets {
4055 if let Some(cycle) = detect_artifact_cycle(target, graph, state, path) {
4056 return Some(cycle);
4057 }
4058 }
4059 }
4060 path.pop();
4061 state.insert(node.clone(), VisitState::Finished);
4062 None
4063}
4064
4065fn ensure_unique<'a>(
4066 kind: &'static str,
4067 name: &Identifier,
4068 existing: impl Iterator<Item = &'a Identifier>,
4069) -> Result<(), ModelError> {
4070 if existing.into_iter().any(|candidate| candidate == name) {
4071 return Err(ModelError::DuplicateResource {
4072 kind,
4073 name: name.as_str().to_owned(),
4074 });
4075 }
4076 Ok(())
4077}
4078
4079fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
4080 if value.is_empty() {
4081 return Err(ModelError::EmptyValue(kind));
4082 }
4083 validate_no_nul(kind, value)
4084}
4085
4086fn validate_no_nul(kind: &'static str, value: &str) -> Result<(), ModelError> {
4087 if value.contains('\0') {
4088 return Err(ModelError::ContainsNul(kind));
4089 }
4090 Ok(())
4091}
4092
4093#[cfg(test)]
4094mod tests {
4095 use super::{
4096 Annotation, Application, ArtifactDependency, ArtifactDependencyNode, Command, Config, ConfigMaterial, Device,
4097 Entrypoint, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileSyntax, ExposedPort, GroupExitPolicy,
4098 HealthcheckDuration, HealthcheckRetries, HostAddress, HostAddressKind, HostMapping, Identifier,
4099 KernelParameter, Logging, LoggingOption, MetadataLabel, ModelError, Mount, MountSource, Network,
4100 NetworkAttachment, NetworkDriverOption, NetworkIpamConfig, Protocol, PullPolicy, ReloadAction, ResourceGrant,
4101 ResourceGrantSyntax, ResourceLimit, ResourceOwnership, RestartPolicy, Secret, SecretMaterial, SecurityOption,
4102 Service, ServiceDependency, ServiceDependencyCondition, ServiceGroup, ServiceGroupRuntime, StartupNotification,
4103 StopTimeout, Volume, VolumeImageSource,
4104 };
4105 use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Sourced};
4106
4107 #[test]
4108 fn preserves_service_order_and_rejects_duplicate_names() -> Result<(), String> {
4109 let mut application = Application::new(id("example")?);
4110 application
4111 .add_service(Sourced::generated(Service::new(id("web")?)))
4112 .map_err(|error| error.to_string())?;
4113 application
4114 .add_service(Sourced::generated(Service::new(id("database")?)))
4115 .map_err(|error| error.to_string())?;
4116
4117 let names: Vec<_> = application
4118 .services()
4119 .iter()
4120 .map(|service| service.value().name().as_str())
4121 .collect();
4122 assert_eq!(names, ["web", "database"]);
4123
4124 let duplicate = application.add_service(Sourced::generated(Service::new(id("web")?)));
4125 assert!(matches!(duplicate, Err(ModelError::DuplicateResource { .. })));
4126 Ok(())
4127 }
4128
4129 #[test]
4130 fn keeps_the_service_key_and_explicit_runtime_name_distinct() -> Result<(), String> {
4131 let mut service = Service::new(id("web")?);
4132 service.set_runtime_name(Sourced::generated(ProtectedString::plain("production-web")));
4133
4134 assert_eq!(service.name().as_str(), "web");
4135 assert_eq!(
4136 service.runtime_name().map(|name| name.value().expose()),
4137 Some("production-web")
4138 );
4139 Ok(())
4140 }
4141
4142 #[test]
4143 fn network_keeps_logical_and_runtime_names_and_literal_flags_distinct() -> Result<(), String> {
4144 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4145 let origin = crate::Provenance::source(source);
4146 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4147 network.set_runtime_name(Sourced::from_source(
4148 ProtectedString::plain("production-frontend"),
4149 origin.clone(),
4150 ));
4151 network.set_driver(Sourced::from_source(ProtectedString::plain("bridge"), origin.clone()));
4152 network.set_internal(Sourced::from_source(true, origin.clone()));
4153 network.set_ipv6(Sourced::from_source(false, origin.clone()));
4154 network.set_ipam_driver(Sourced::from_source(ProtectedString::plain("default"), origin));
4155
4156 assert_eq!(network.name().as_str(), "frontend");
4157 assert_eq!(
4158 network.runtime_name().map(|value| value.value().expose()),
4159 Some("production-frontend")
4160 );
4161 assert_eq!(network.driver().map(|value| value.value().expose()), Some("bridge"));
4162 assert_eq!(network.internal().map(Sourced::value), Some(&true));
4163 assert_eq!(network.ipv6().map(Sourced::value), Some(&false));
4164 assert_eq!(
4165 network.ipam_driver().map(|value| value.value().expose()),
4166 Some("default")
4167 );
4168 Ok(())
4169 }
4170
4171 #[test]
4172 fn network_collections_retain_resets_provenance_and_redact_protected_values() -> Result<(), String> {
4173 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4174 let origin = crate::Provenance::source(source);
4175 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4176 let option = NetworkDriverOption::new(
4177 Sourced::from_source(id("com.example.token")?, origin.clone()),
4178 Sourced::from_source(ProtectedString::sensitive("never-print-this"), origin.clone()),
4179 )
4180 .map_err(|error| error.to_string())?;
4181 let label = MetadataLabel::new(id("com.example.label")?, ProtectedString::sensitive("also-private"));
4182
4183 network
4184 .set_driver_options_with_origins(vec![Sourced::from_source(option, origin.clone())], vec![origin.clone()]);
4185 network.set_labels_with_origins(vec![Sourced::from_source(label, origin.clone())], vec![origin.clone()]);
4186 network.set_ipam_configs_with_origins(Vec::new(), vec![origin]);
4187
4188 assert_eq!(network.driver_options().map(<[_]>::len), Some(1));
4189 assert_eq!(network.labels().map(<[_]>::len), Some(1));
4190 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4191 assert_eq!(network.driver_options_origins().len(), 1);
4192 assert_eq!(network.labels_origins().len(), 1);
4193 assert_eq!(network.ipam_configs_origins().len(), 1);
4194 let debug = format!("{network:?}");
4195 assert!(!debug.contains("never-print-this"));
4196 assert!(!debug.contains("also-private"));
4197 assert!(debug.contains("[REDACTED]"));
4198
4199 network.set_driver_options(Vec::new());
4200 network.set_labels(Vec::new());
4201 network.set_ipam_configs(Vec::new());
4202 assert_eq!(network.driver_options().map(<[_]>::len), Some(0));
4203 assert_eq!(network.labels().map(<[_]>::len), Some(0));
4204 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4205 assert!(network.driver_options_origins().is_empty());
4206 assert!(network.labels_origins().is_empty());
4207 assert!(network.ipam_configs_origins().is_empty());
4208 Ok(())
4209 }
4210
4211 #[test]
4212 fn network_ipam_rows_preserve_association_order_and_reject_subnetless_values() -> Result<(), String> {
4213 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4214 let origin = crate::Provenance::source(source);
4215 let mut first = NetworkIpamConfig::new(Sourced::from_source(
4216 ProtectedString::plain("10.10.0.0/24"),
4217 origin.clone(),
4218 ))
4219 .map_err(|error| error.to_string())?;
4220 first
4221 .set_gateway(Sourced::from_source(
4222 ProtectedString::plain("10.10.0.1"),
4223 origin.clone(),
4224 ))
4225 .map_err(|error| error.to_string())?;
4226 let mut second = NetworkIpamConfig::new(Sourced::from_source(
4227 ProtectedString::plain("fd00:10::/64"),
4228 origin.clone(),
4229 ))
4230 .map_err(|error| error.to_string())?;
4231 second
4232 .set_ip_range(Sourced::from_source(
4233 ProtectedString::plain("fd00:10::100/120"),
4234 origin.clone(),
4235 ))
4236 .map_err(|error| error.to_string())?;
4237
4238 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4239 network.set_ipam_configs_with_origins(
4240 vec![
4241 Sourced::from_source(first, origin.clone()),
4242 Sourced::from_source(second, origin),
4243 ],
4244 Vec::new(),
4245 );
4246 let rows = network
4247 .ipam_configs()
4248 .ok_or_else(|| "IPAM configs were omitted".to_owned())?;
4249 assert_eq!(rows.len(), 2);
4250 assert_eq!(rows[0].value().subnet().value().expose(), "10.10.0.0/24");
4251 assert_eq!(
4252 rows[0].value().gateway().map(|value| value.value().expose()),
4253 Some("10.10.0.1")
4254 );
4255 assert_eq!(rows[0].value().ip_range(), None);
4256 assert_eq!(rows[1].value().subnet().value().expose(), "fd00:10::/64");
4257 assert_eq!(rows[1].value().gateway(), None);
4258 assert_eq!(
4259 rows[1].value().ip_range().map(|value| value.value().expose()),
4260 Some("fd00:10::100/120")
4261 );
4262
4263 assert!(matches!(
4264 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain(""))),
4265 Err(ModelError::EmptyValue("network IPAM subnet"))
4266 ));
4267 assert!(matches!(
4268 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain("10.0.0.0/24\0bad"))),
4269 Err(ModelError::ContainsNul("network IPAM subnet"))
4270 ));
4271 assert!(matches!(
4272 NetworkDriverOption::new(
4273 Sourced::generated(id("option")?),
4274 Sourced::generated(ProtectedString::plain("bad\0value")),
4275 ),
4276 Err(ModelError::ContainsNul("network driver option value"))
4277 ));
4278 Ok(())
4279 }
4280
4281 #[test]
4282 fn image_artifact_resources_are_ordered_unique_and_referenced_explicitly() -> Result<(), String> {
4283 let mut application = Application::new(id("example")?);
4284 application
4285 .add_image_acquisition(Sourced::generated(ImageAcquisition::new(id("base-image")?)))
4286 .map_err(|error| error.to_string())?;
4287 application
4288 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4289 .map_err(|error| error.to_string())?;
4290
4291 let mut web = Service::new(id("web")?);
4292 web.set_image_acquisition(Sourced::generated(id("base-image")?));
4293 web.set_image_build(Sourced::generated(id("web-build")?));
4294 application
4295 .add_service(Sourced::generated(web))
4296 .map_err(|error| error.to_string())?;
4297
4298 assert_eq!(
4299 application.image_acquisitions()[0].value().name().as_str(),
4300 "base-image"
4301 );
4302 assert_eq!(application.image_builds()[0].value().name().as_str(), "web-build");
4303 assert!(matches!(
4304 application.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?))),
4305 Err(ModelError::DuplicateResource {
4306 kind: "image build",
4307 ..
4308 })
4309 ));
4310
4311 let mut missing = Service::new(id("missing")?);
4312 missing.set_image_build(Sourced::generated(id("absent-build")?));
4313 assert!(matches!(
4314 application.add_service(Sourced::generated(missing)),
4315 Err(ModelError::UnknownImageBuildReference { .. })
4316 ));
4317 Ok(())
4318 }
4319
4320 #[test]
4321 fn volume_keeps_logical_runtime_and_service_names_and_local_fields_distinct() -> Result<(), String> {
4322 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4323 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4324 volume.set_runtime_name(Sourced::from_source(
4325 ProtectedString::plain("production-data"),
4326 origin.clone(),
4327 ));
4328 volume.set_service_name(Sourced::from_source(
4329 ProtectedString::plain("data-volume.service"),
4330 origin.clone(),
4331 ));
4332 volume.set_driver(Sourced::from_source(ProtectedString::plain("local"), origin.clone()));
4333 volume.set_device(Sourced::from_source(
4334 ProtectedString::plain("/srv/data"),
4335 origin.clone(),
4336 ));
4337 volume.set_volume_type(Sourced::from_source(ProtectedString::plain("none"), origin.clone()));
4338 volume.set_options(Sourced::from_source(ProtectedString::plain("bind"), origin.clone()));
4339
4340 assert_eq!(volume.name().as_str(), "data");
4341 assert_eq!(
4342 volume.runtime_name().map(|name| name.value().expose()),
4343 Some("production-data")
4344 );
4345 assert_eq!(
4346 volume.service_name().map(|name| name.value().expose()),
4347 Some("data-volume.service")
4348 );
4349 assert_eq!(volume.driver().map(|value| value.value().expose()), Some("local"));
4350 assert_eq!(volume.device().map(|value| value.value().expose()), Some("/srv/data"));
4351 assert_eq!(volume.volume_type().map(|value| value.value().expose()), Some("none"));
4352 assert_eq!(volume.options().map(|value| value.value().expose()), Some("bind"));
4353 assert_eq!(
4354 volume.options().map(Sourced::origins),
4355 Some(std::slice::from_ref(&origin))
4356 );
4357 Ok(())
4358 }
4359
4360 #[test]
4361 fn volume_preserves_resets_order_protected_values_and_identity_dimensions() -> Result<(), String> {
4362 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4363 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4364 assert!(volume.labels().is_none());
4365 assert!(volume.containers_conf_modules().is_none());
4366 assert!(volume.global_args().is_none());
4367 assert!(volume.podman_args().is_none());
4368 volume.set_labels_with_origins(Vec::new(), vec![origin.clone()]);
4369 volume.set_containers_conf_modules_with_origins(Vec::new(), vec![origin.clone()]);
4370 volume.set_global_args_with_origins(
4371 vec![
4372 Sourced::from_source(ProtectedString::plain("--first"), origin.clone()),
4373 Sourced::from_source(ProtectedString::sensitive("--token=never-print"), origin.clone()),
4374 ],
4375 vec![origin.clone()],
4376 );
4377 volume.set_podman_args_with_origins(
4378 vec![
4379 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4380 Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
4381 ],
4382 vec![origin.clone()],
4383 );
4384 volume.set_user(Sourced::from_source(
4385 ProtectedString::plain("named-user"),
4386 origin.clone(),
4387 ));
4388 volume.set_group(Sourced::from_source(
4389 ProtectedString::plain("named-group"),
4390 origin.clone(),
4391 ));
4392 volume.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4393 volume.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin));
4394
4395 assert_eq!(volume.labels().map(<[_]>::len), Some(0));
4396 assert_eq!(volume.containers_conf_modules().map(<[_]>::len), Some(0));
4397 assert_eq!(volume.global_args().map(<[_]>::len), Some(2));
4398 assert_eq!(volume.podman_args().map(<[_]>::len), Some(2));
4399 assert_eq!(volume.user().map(|value| value.value().expose()), Some("named-user"));
4400 assert_eq!(volume.group().map(|value| value.value().expose()), Some("named-group"));
4401 assert_eq!(volume.uid().map(|value| value.value().expose()), Some("1001"));
4402 assert_eq!(volume.gid().map(|value| value.value().expose()), Some("1002"));
4403 let debug = format!("{volume:?}");
4404 assert!(!debug.contains("never-print"));
4405 assert!(debug.contains("[REDACTED]"));
4406 Ok(())
4407 }
4408
4409 #[test]
4410 fn volume_copy_and_image_sources_preserve_absence_and_typed_distinctions() -> Result<(), String> {
4411 let origin =
4412 crate::Provenance::source(crate::SourceId::new("cache.volume").map_err(|error| error.to_string())?);
4413 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4414 assert_eq!(volume.copy(), None);
4415 volume.set_copy(Sourced::from_source(false, origin.clone()));
4416 assert_eq!(volume.copy().map(Sourced::value), Some(&false));
4417 volume.set_copy(Sourced::from_source(true, origin.clone()));
4418 assert_eq!(volume.copy().map(Sourced::value), Some(&true));
4419
4420 volume
4421 .set_image_source(Sourced::from_source(
4422 VolumeImageSource::Literal(ProtectedString::sensitive("registry.example/private:1")),
4423 origin.clone(),
4424 ))
4425 .map_err(|error| error.to_string())?;
4426 assert!(matches!(
4427 volume.image_source().map(Sourced::value),
4428 Some(VolumeImageSource::Literal(_))
4429 ));
4430 assert!(!format!("{volume:?}").contains("registry.example/private:1"));
4431
4432 volume
4433 .set_image_source(Sourced::from_source(
4434 VolumeImageSource::ImageAcquisition(id("cache-image")?),
4435 origin.clone(),
4436 ))
4437 .map_err(|error| error.to_string())?;
4438 assert!(matches!(
4439 volume.image_source().map(Sourced::value),
4440 Some(VolumeImageSource::ImageAcquisition(name)) if name.as_str() == "cache-image"
4441 ));
4442 volume
4443 .set_image_source(Sourced::from_source(
4444 VolumeImageSource::ImageBuild(id("cache-build")?),
4445 origin,
4446 ))
4447 .map_err(|error| error.to_string())?;
4448 assert!(matches!(
4449 volume.image_source().map(Sourced::value),
4450 Some(VolumeImageSource::ImageBuild(name)) if name.as_str() == "cache-build"
4451 ));
4452 Ok(())
4453 }
4454
4455 #[test]
4456 fn volume_artifact_validation_is_deferred_and_explicit_edges_find_cycles() -> Result<(), String> {
4457 let mut application = Application::new(id("example")?);
4458 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4459 volume
4460 .set_image_source(Sourced::generated(VolumeImageSource::ImageBuild(id("cache-build")?)))
4461 .map_err(|error| error.to_string())?;
4462 application
4463 .add_volume(Sourced::generated(volume))
4464 .map_err(|error| error.to_string())?;
4465 assert!(matches!(
4466 application.validate_image_artifact_references(),
4467 Err(ModelError::UnknownVolumeImageBuildReference { .. })
4468 ));
4469
4470 application
4471 .add_image_build(Sourced::generated(ImageBuild::new(id("cache-build")?)))
4472 .map_err(|error| error.to_string())?;
4473 application
4474 .validate_image_artifact_references()
4475 .map_err(|error| error.to_string())?;
4476
4477 let volume_node = ArtifactDependencyNode::Volume(id("cache")?);
4478 let build_node = ArtifactDependencyNode::ImageBuild(id("cache-build")?);
4479 let dependencies = vec![
4480 Sourced::generated(ArtifactDependency::new(
4481 Sourced::generated(volume_node.clone()),
4482 Sourced::generated(build_node.clone()),
4483 )),
4484 Sourced::generated(ArtifactDependency::new(
4485 Sourced::generated(build_node),
4486 Sourced::generated(volume_node),
4487 )),
4488 ];
4489 assert!(matches!(
4490 application.validate_image_artifact_dependencies(&dependencies),
4491 Err(ModelError::ImageArtifactDependencyCycle { .. })
4492 ));
4493 let missing = vec![Sourced::generated(ArtifactDependency::new(
4494 Sourced::generated(ArtifactDependencyNode::ImageBuild(id("cache-build")?)),
4495 Sourced::generated(ArtifactDependencyNode::Volume(id("missing")?)),
4496 ))];
4497 assert!(matches!(
4498 application.validate_image_artifact_dependencies(&missing),
4499 Err(ModelError::UnknownArtifactDependencyNode { kind: "volume", .. })
4500 ));
4501 Ok(())
4502 }
4503
4504 #[test]
4505 fn volume_rejects_invalid_literal_image_values() -> Result<(), String> {
4506 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4507 assert!(matches!(
4508 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4509 ""
4510 )))),
4511 Err(ModelError::EmptyValue("volume image"))
4512 ));
4513 assert!(matches!(
4514 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4515 "bad\0image"
4516 )))),
4517 Err(ModelError::ContainsNul("volume image"))
4518 ));
4519 Ok(())
4520 }
4521
4522 #[test]
4523 fn collection_resets_retain_explicit_emptiness_and_clear_stale_origins() -> Result<(), String> {
4524 let origin =
4525 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
4526 let mut service = Service::new(id("web")?);
4527
4528 service.set_cap_add_with_origins(Vec::new(), vec![origin.clone()]);
4529 service.set_cap_drop_with_origins(Vec::new(), vec![origin.clone()]);
4530 service.set_tmpfs_with_origins(Vec::new(), vec![origin.clone()]);
4531 service.set_sysctls_with_origins(Vec::new(), vec![origin.clone()]);
4532 service.set_ulimits_with_origins(Vec::new(), vec![origin.clone()]);
4533 service.set_devices_with_origins(Vec::new(), vec![origin]);
4534
4535 assert_eq!(service.cap_add().map(<[_]>::len), Some(0));
4536 assert_eq!(service.cap_drop().map(<[_]>::len), Some(0));
4537 assert_eq!(service.tmpfs().map(<[_]>::len), Some(0));
4538 assert_eq!(service.sysctls().map(<[_]>::len), Some(0));
4539 assert_eq!(service.ulimits().map(<[_]>::len), Some(0));
4540 assert_eq!(service.devices().map(<[_]>::len), Some(0));
4541 assert_eq!(service.cap_add_origins().len(), 1);
4542 assert_eq!(service.cap_drop_origins().len(), 1);
4543 assert_eq!(service.tmpfs_origins().len(), 1);
4544 assert_eq!(service.sysctls_origins().len(), 1);
4545 assert_eq!(service.ulimits_origins().len(), 1);
4546 assert_eq!(service.devices_origins().len(), 1);
4547
4548 service.set_cap_add(Vec::new());
4549 service.set_cap_drop(Vec::new());
4550 service.set_tmpfs(Vec::new());
4551 service.set_sysctls(Vec::<Sourced<KernelParameter>>::new());
4552 service.set_ulimits(Vec::<Sourced<ResourceLimit>>::new());
4553 service.set_devices(Vec::<Sourced<Device>>::new());
4554
4555 assert!(service.cap_add_origins().is_empty());
4556 assert!(service.cap_drop_origins().is_empty());
4557 assert!(service.tmpfs_origins().is_empty());
4558 assert!(service.sysctls_origins().is_empty());
4559 assert!(service.ulimits_origins().is_empty());
4560 assert!(service.devices_origins().is_empty());
4561 Ok(())
4562 }
4563
4564 #[test]
4565 fn restart_policy_keeps_unlimited_and_finite_on_failure_distinct() {
4566 let finite = std::num::NonZeroU64::new(4);
4567 assert_eq!(RestartPolicy::on_failure(None).maximum_retries(), None);
4568 assert_eq!(RestartPolicy::on_failure(finite).maximum_retries(), finite);
4569 assert_eq!(RestartPolicy::Always.maximum_retries(), None);
4570 }
4571
4572 #[test]
4573 fn metadata_labels_preserve_empty_and_protected_values() -> Result<(), String> {
4574 let empty = MetadataLabel::new(id("com.example.empty")?, ProtectedString::plain(""));
4575 let protected = MetadataLabel::new(id("com.example.token")?, ProtectedString::sensitive("never-print-this"));
4576 let mut service = Service::new(id("web")?);
4577 service.add_label(Sourced::generated(empty));
4578 service.add_label(Sourced::generated(protected));
4579
4580 assert_eq!(service.labels()[0].value().value().expose(), "");
4581 let debug = format!("{:?}", service.labels()[1]);
4582 assert!(!debug.contains("never-print-this"));
4583 assert!(debug.contains("[REDACTED]"));
4584 Ok(())
4585 }
4586
4587 #[test]
4588 fn environment_files_preserve_order_options_provenance_and_redaction() -> Result<(), String> {
4589 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4590 let origin = crate::Provenance::source(source);
4591 let mut service = Service::new(id("web")?);
4592 service.add_environment_file(Sourced::from_source(
4593 EnvironmentFile::new(ProtectedString::plain("./base.env"), EnvironmentFileSyntax::Short)
4594 .map_err(|error| error.to_string())?,
4595 origin.clone(),
4596 ));
4597 let mut local = EnvironmentFile::new(ProtectedString::sensitive("./private.env"), EnvironmentFileSyntax::Long)
4598 .map_err(|error| error.to_string())?;
4599 local.set_required(Sourced::from_source(false, origin.clone()));
4600 local.set_format(Sourced::from_source(EnvironmentFileFormat::Raw, origin.clone()));
4601 service.add_environment_file(Sourced::from_source(local, origin));
4602
4603 assert_eq!(service.environment_files().len(), 2);
4604 assert_eq!(service.environment_files()[0].value().path().expose(), "./base.env");
4605 assert_eq!(
4606 service.environment_files()[0].value().syntax(),
4607 EnvironmentFileSyntax::Short
4608 );
4609 assert!(service.environment_files()[0].value().is_required());
4610 let local = service.environment_files()[1].value();
4611 assert_eq!(local.syntax(), EnvironmentFileSyntax::Long);
4612 assert!(!local.is_required());
4613 assert_eq!(local.required().map_or(0, |value| value.origins().len()), 1);
4614 assert!(matches!(
4615 local.format().map(Sourced::value),
4616 Some(EnvironmentFileFormat::Raw)
4617 ));
4618 let debug = format!("{service:?}");
4619 assert!(!debug.contains("private.env"));
4620 assert!(debug.contains("[REDACTED]"));
4621 assert!(matches!(
4622 EnvironmentFile::new(ProtectedString::plain(""), EnvironmentFileSyntax::Short),
4623 Err(ModelError::EmptyValue("environment-file path"))
4624 ));
4625 Ok(())
4626 }
4627
4628 #[test]
4629 fn service_groups_preserve_order_and_reject_ambiguous_membership() -> Result<(), String> {
4630 let mut application = Application::new(id("example")?);
4631 for name in ["web", "worker"] {
4632 application
4633 .add_service(Sourced::generated(Service::new(id(name)?)))
4634 .map_err(|error| error.to_string())?;
4635 }
4636
4637 let mut frontend = ServiceGroup::new(id("frontend")?, ResourceOwnership::Uncertain);
4638 frontend
4639 .add_member(Sourced::generated(id("web")?))
4640 .map_err(|error| error.to_string())?;
4641 assert!(matches!(
4642 frontend.add_member(Sourced::generated(id("web")?)),
4643 Err(ModelError::DuplicateServiceGroupMember { .. })
4644 ));
4645 application
4646 .add_service_group(Sourced::generated(frontend))
4647 .map_err(|error| error.to_string())?;
4648
4649 assert_eq!(application.service_groups()[0].value().name().as_str(), "frontend");
4650 assert_eq!(
4651 application.service_groups()[0].value().members()[0].value().as_str(),
4652 "web"
4653 );
4654
4655 let mut conflicting = ServiceGroup::new(id("backend")?, ResourceOwnership::Application);
4656 conflicting
4657 .add_member(Sourced::generated(id("web")?))
4658 .map_err(|error| error.to_string())?;
4659 assert!(matches!(
4660 application.add_service_group(Sourced::generated(conflicting)),
4661 Err(ModelError::ServiceInMultipleGroups { .. })
4662 ));
4663
4664 let mut missing = ServiceGroup::new(id("missing")?, ResourceOwnership::External);
4665 missing
4666 .add_member(Sourced::generated(id("database")?))
4667 .map_err(|error| error.to_string())?;
4668 assert!(matches!(
4669 application.add_service_group(Sourced::generated(missing)),
4670 Err(ModelError::UnknownServiceGroupMember { .. })
4671 ));
4672 Ok(())
4673 }
4674
4675 #[test]
4676 fn group_runtime_keeps_group_names_and_pod_settings_distinct() -> Result<(), String> {
4677 let source = crate::SourceId::new("pod.pod").map_err(|error| error.to_string())?;
4678 let origin = crate::Provenance::source(source);
4679 let mut group = ServiceGroup::new(id("frontend")?, ResourceOwnership::Application);
4680 let mut runtime = ServiceGroupRuntime::new();
4681 runtime.set_runtime_name(Sourced::from_source(
4682 ProtectedString::plain("production-frontend"),
4683 origin.clone(),
4684 ));
4685 runtime.set_service_name(Sourced::from_source(
4686 ProtectedString::plain("frontend-pod"),
4687 origin.clone(),
4688 ));
4689 runtime.set_host_mappings_with_origins(
4690 vec![Sourced::from_source(
4691 HostMapping::new(
4692 id("host.docker.internal")?,
4693 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
4694 ),
4695 origin.clone(),
4696 )],
4697 vec![origin.clone()],
4698 );
4699 runtime.set_ports_with_origins(Vec::new(), vec![origin.clone()]);
4700 runtime.set_networks_with_origins(
4701 vec![Sourced::from_source(
4702 NetworkAttachment::with_sourced_aliases(
4703 id("edge")?,
4704 vec![Sourced::from_source(
4705 ProtectedString::sensitive("private-alias"),
4706 origin.clone(),
4707 )],
4708 ),
4709 origin.clone(),
4710 )],
4711 vec![origin.clone()],
4712 );
4713 runtime.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4714 runtime.set_mounts_with_origins(
4715 vec![Sourced::from_source(
4716 Mount::new(MountSource::Anonymous, "/cache", false).map_err(|error| error.to_string())?,
4717 origin.clone(),
4718 )],
4719 vec![origin.clone()],
4720 );
4721 runtime.set_shm_size(Sourced::from_source(ProtectedString::sensitive("64m"), origin.clone()));
4722 runtime.set_exit_policy(Sourced::from_source(
4723 GroupExitPolicy::Raw(ProtectedString::sensitive("preserve-this")),
4724 origin.clone(),
4725 ));
4726 runtime.set_stop_timeout(Sourced::from_source(
4727 StopTimeout::new("30s").map_err(|error| error.to_string())?,
4728 origin.clone(),
4729 ));
4730 assert!(matches!(
4731 runtime.replace_network(1, Sourced::generated(NetworkAttachment::new(id("other")?, Vec::new()))),
4732 Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index: 1, len: 1 })
4733 ));
4734 group.set_runtime(Sourced::from_source(runtime, origin));
4735
4736 let runtime = group
4737 .runtime()
4738 .ok_or_else(|| "group runtime was omitted".to_owned())?
4739 .value();
4740 assert_eq!(group.name().as_str(), "frontend");
4741 assert_eq!(
4742 runtime.runtime_name().map(|name| name.value().expose()),
4743 Some("production-frontend")
4744 );
4745 assert_eq!(
4746 runtime.service_name().map(|name| name.value().expose()),
4747 Some("frontend-pod")
4748 );
4749 assert_eq!(runtime.host_mappings().map(<[_]>::len), Some(1));
4750 assert_eq!(runtime.ports().map(<[_]>::len), Some(0));
4751 assert_eq!(runtime.networks_origins().len(), 1);
4752 assert_eq!(runtime.mounts().map(<[_]>::len), Some(1));
4753 assert!(matches!(
4754 runtime.exit_policy().map(Sourced::value),
4755 Some(GroupExitPolicy::Raw(_))
4756 ));
4757 let debug = format!("{group:?}");
4758 for sensitive in ["private-alias", "64m", "preserve-this"] {
4759 assert!(!debug.contains(sensitive));
4760 }
4761 assert!(debug.contains("[REDACTED]"));
4762 Ok(())
4763 }
4764
4765 #[test]
4766 fn rootfs_startup_notification_and_podman_args_preserve_safe_contracts() -> Result<(), String> {
4767 let source = crate::SourceId::new("web.container").map_err(|error| error.to_string())?;
4768 let origin = crate::Provenance::source(source);
4769 let mut service = Service::new(id("web")?);
4770 service.set_startup_notification(Sourced::from_source(StartupNotification::Healthy, origin.clone()));
4771 service.set_podman_args_with_origins(
4772 vec![
4773 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4774 Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
4775 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4776 ],
4777 vec![origin.clone()],
4778 );
4779 assert_eq!(service.podman_args().map(<[_]>::len), Some(3));
4780 assert_eq!(service.podman_args_origins(), std::slice::from_ref(&origin));
4781 assert!(matches!(
4782 service.startup_notification().map(Sourced::value),
4783 Some(StartupNotification::Healthy)
4784 ));
4785 assert!(!format!("{service:?}").contains("never-print"));
4786
4787 let mut with_image = Service::new(id("image-first")?);
4788 with_image.set_image(Sourced::generated(
4789 ImageReference::parse("example.invalid/web:1").map_err(|error| error.to_string())?,
4790 ));
4791 assert!(matches!(
4792 with_image.set_rootfs(Sourced::generated(ProtectedString::plain("/srv/rootfs"))),
4793 Err(ModelError::RootfsImageSourceConflict { source: "image", .. })
4794 ));
4795
4796 let mut with_rootfs = Service::new(id("rootfs-first")?);
4797 with_rootfs
4798 .set_rootfs(Sourced::generated(ProtectedString::sensitive("/private/rootfs")))
4799 .map_err(|error| error.to_string())?;
4800 with_rootfs.set_image_build(Sourced::generated(id("web-build")?));
4801 let mut application = Application::new(id("example")?);
4802 application
4803 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4804 .map_err(|error| error.to_string())?;
4805 assert!(matches!(
4806 application.add_service(Sourced::generated(with_rootfs)),
4807 Err(ModelError::RootfsImageSourceConflict {
4808 source: "image build",
4809 ..
4810 })
4811 ));
4812 Ok(())
4813 }
4814
4815 #[test]
4816 fn validates_raw_preserving_healthcheck_scalars() -> Result<(), String> {
4817 let duration = HealthcheckDuration::new("1m30s").map_err(|error| error.to_string())?;
4818 let retries = HealthcheckRetries::new("003").map_err(|error| error.to_string())?;
4819 assert_eq!(duration.as_str(), "1m30s");
4820 assert_eq!(retries.as_str(), "003");
4821 assert_eq!(
4822 HealthcheckRetries::new("three"),
4823 Err(ModelError::InvalidHealthcheckRetries)
4824 );
4825 assert!(matches!(
4826 HealthcheckDuration::new(""),
4827 Err(ModelError::EmptyValue("health-check duration"))
4828 ));
4829 Ok(())
4830 }
4831
4832 #[test]
4833 fn preserves_ordered_dependency_edges_and_field_provenance() -> Result<(), String> {
4834 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4835 let origin = crate::Provenance::source(source);
4836 let mut service = Service::new(id("web")?);
4837
4838 let mut database = ServiceDependency::new(id("database")?);
4839 database.set_condition(Sourced::from_source(
4840 ServiceDependencyCondition::Healthy,
4841 origin.clone(),
4842 ));
4843 database.set_required(Sourced::from_source(true, origin.clone()));
4844 service.add_dependency(Sourced::from_source(database, origin.clone()));
4845
4846 let cache = ServiceDependency::new(id("cache")?);
4847 assert!(cache.is_required());
4848 service.add_dependency(Sourced::from_source(cache, origin));
4849
4850 assert_eq!(
4851 service
4852 .dependencies()
4853 .iter()
4854 .map(|dependency| dependency.value().service().as_str())
4855 .collect::<Vec<_>>(),
4856 ["database", "cache"]
4857 );
4858 assert!(matches!(
4859 service.dependencies()[0].value().condition().map(Sourced::value),
4860 Some(ServiceDependencyCondition::Healthy)
4861 ));
4862 assert_eq!(service.dependencies()[0].origins().len(), 1);
4863 assert_eq!(
4864 service.dependencies()[0]
4865 .value()
4866 .condition()
4867 .map_or(0, |condition| condition.origins().len()),
4868 1
4869 );
4870 Ok(())
4871 }
4872
4873 #[test]
4874 fn retains_execution_identity_context_order_provenance_and_redaction() -> Result<(), String> {
4875 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4876 let origin = crate::Provenance::source(source);
4877 let mut service = Service::new(id("web")?);
4878
4879 service.set_user(Sourced::from_source(ProtectedString::sensitive("1001"), origin.clone()));
4880 service.set_group(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4881 service.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4882 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("audio"), origin.clone()));
4883 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("44"), origin.clone()));
4884 service.set_working_directory(Sourced::from_source(ProtectedString::plain("/srv/app"), origin.clone()));
4885 service.set_read_only_root_filesystem(Sourced::from_source(true, origin));
4886
4887 assert_eq!(service.user().map(|value| value.value().expose()), Some("1001"));
4888 assert_eq!(service.group().map(|value| value.value().expose()), Some("1002"));
4889 assert_eq!(
4890 service.user_namespace().map(|value| value.value().expose()),
4891 Some("keep-id")
4892 );
4893 assert_eq!(
4894 service
4895 .supplementary_groups()
4896 .iter()
4897 .map(|group| group.value().expose())
4898 .collect::<Vec<_>>(),
4899 ["audio", "44"]
4900 );
4901 assert_eq!(
4902 service.working_directory().map(|value| value.value().expose()),
4903 Some("/srv/app")
4904 );
4905 assert_eq!(service.read_only_root_filesystem().map(Sourced::value), Some(&true));
4906 assert_eq!(service.user().map_or(0, |value| value.origins().len()), 1);
4907 let debug = format!("{service:?}");
4908 assert!(!debug.contains("1001"));
4909 assert!(debug.contains("[REDACTED]"));
4910 Ok(())
4911 }
4912
4913 #[test]
4914 fn retains_config_secret_resources_grants_provenance_and_redaction() -> Result<(), String> {
4915 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4916 let origin = crate::Provenance::source(source);
4917 let mut application = Application::new(id("example")?);
4918
4919 let mut config = Config::new(id("settings")?, ResourceOwnership::Application);
4920 config.set_material(Sourced::from_source(
4921 ConfigMaterial::Content(ProtectedString::sensitive("private-config")),
4922 origin.clone(),
4923 ));
4924 application
4925 .add_config(Sourced::from_source(config, origin.clone()))
4926 .map_err(|error| error.to_string())?;
4927
4928 let mut secret = Secret::new(id("password")?, ResourceOwnership::External);
4929 secret.set_runtime_name(Sourced::from_source(
4930 ProtectedString::plain("production-password"),
4931 origin.clone(),
4932 ));
4933 secret.set_material(Sourced::from_source(
4934 SecretMaterial::Environment(ProtectedString::sensitive("private-environment-name")),
4935 origin.clone(),
4936 ));
4937 application
4938 .add_secret(Sourced::from_source(secret, origin.clone()))
4939 .map_err(|error| error.to_string())?;
4940
4941 let mut service = Service::new(id("web")?);
4942 service.add_config_grant(Sourced::from_source(
4943 ResourceGrant::new(ProtectedString::plain("settings"), ResourceGrantSyntax::Short)
4944 .map_err(|error| error.to_string())?,
4945 origin.clone(),
4946 ));
4947 let mut secret_grant = ResourceGrant::new(
4948 ProtectedString::sensitive("private-grant-source"),
4949 ResourceGrantSyntax::Long,
4950 )
4951 .map_err(|error| error.to_string())?;
4952 secret_grant.set_target(Sourced::from_source(
4953 ProtectedString::plain("database-password"),
4954 origin.clone(),
4955 ));
4956 secret_grant.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4957 secret_grant.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4958 secret_grant.set_mode(Sourced::from_source(ProtectedString::plain("0440"), origin.clone()));
4959 service.add_secret_grant(Sourced::from_source(secret_grant, origin.clone()));
4960 application
4961 .add_service(Sourced::from_source(service, origin))
4962 .map_err(|error| error.to_string())?;
4963
4964 assert_eq!(application.configs().len(), 1);
4965 assert_eq!(application.secrets().len(), 1);
4966 assert_eq!(application.services()[0].value().config_grants().len(), 1);
4967 let grant = &application.services()[0].value().secret_grants()[0];
4968 assert_eq!(grant.value().syntax(), ResourceGrantSyntax::Long);
4969 assert_eq!(
4970 grant.value().target().map(|value| value.value().expose()),
4971 Some("database-password")
4972 );
4973 assert_eq!(grant.value().uid().map_or(0, |value| value.origins().len()), 1);
4974 assert_eq!(grant.origins().len(), 1);
4975 let debug = format!("{application:?}");
4976 for secret in ["private-config", "private-environment-name", "private-grant-source"] {
4977 assert!(!debug.contains(secret));
4978 }
4979 assert!(debug.contains("[REDACTED]"));
4980
4981 assert!(matches!(
4982 ResourceGrant::new(ProtectedString::plain(""), ResourceGrantSyntax::Short),
4983 Err(ModelError::EmptyValue("resource grant source"))
4984 ));
4985 assert!(matches!(
4986 application.add_config(Sourced::generated(Config::new(
4987 id("settings")?,
4988 ResourceOwnership::External,
4989 ))),
4990 Err(ModelError::DuplicateResource { kind: "config", .. })
4991 ));
4992 assert!(matches!(
4993 application.add_secret(Sourced::generated(Secret::new(
4994 id("password")?,
4995 ResourceOwnership::External,
4996 ))),
4997 Err(ModelError::DuplicateResource { kind: "secret", .. })
4998 ));
4999 Ok(())
5000 }
5001
5002 #[test]
5003 fn host_mappings_preserve_order_spelling_and_runtime_tokens() -> Result<(), String> {
5004 let mut service = Service::new(id("web")?);
5005 service.add_host_mapping(Sourced::generated(HostMapping::new(
5006 id("host.docker.internal")?,
5007 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
5008 )));
5009 service.add_host_mapping(Sourced::generated(HostMapping::new(
5010 id("ipv6")?,
5011 HostAddress::new("[::1]").map_err(|error| error.to_string())?,
5012 )));
5013
5014 assert_eq!(service.host_mappings().len(), 2);
5015 assert_eq!(
5016 service.host_mappings()[0].value().address().kind(),
5017 HostAddressKind::HostGateway
5018 );
5019 assert_eq!(service.host_mappings()[1].value().address().raw(), "[::1]");
5020 assert_eq!(
5021 service.host_mappings()[1].value().address().kind(),
5022 HostAddressKind::Ipv6 { bracketed: true }
5023 );
5024 assert!(matches!(HostAddress::new(""), Err(ModelError::EmptyValue(_))));
5025 Ok(())
5026 }
5027
5028 #[test]
5029 fn dns_collections_preserve_order_provenance_and_explicit_empty_state() -> Result<(), String> {
5030 let mut service = Service::new(id("web")?);
5031 assert!(service.dns_servers().is_none());
5032 service.set_dns_servers(Vec::new());
5033 assert!(matches!(service.dns_servers(), Some(values) if values.is_empty()));
5034 service.set_dns_options(vec![
5035 Sourced::generated(ProtectedString::plain("ndots:5")),
5036 Sourced::generated(ProtectedString::sensitive("rotate")),
5037 ]);
5038 service.set_dns_search_domains(vec![Sourced::generated(ProtectedString::plain("example.test"))]);
5039 assert_eq!(
5040 service
5041 .dns_options()
5042 .unwrap_or_default()
5043 .iter()
5044 .map(|value| value.value().expose())
5045 .collect::<Vec<_>>(),
5046 ["ndots:5", "rotate"]
5047 );
5048 assert!(!format!("{service:?}").contains("rotate"));
5049 Ok(())
5050 }
5051
5052 #[test]
5053 fn security_options_preserve_empty_order_duplicates_provenance_and_redaction() -> Result<(), String> {
5054 let origin =
5055 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5056 let mut service = Service::new(id("web")?);
5057
5058 assert!(service.security_options().is_none());
5059 service.set_security_options_with_origins(Vec::new(), vec![origin.clone()]);
5060 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5061 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5062
5063 service.set_security_options_with_origins(
5064 vec![
5065 Sourced::from_source(
5066 SecurityOption::AppArmor(ProtectedString::sensitive("apparmor-secret")),
5067 origin.clone(),
5068 ),
5069 Sourced::from_source(SecurityOption::NoNewPrivileges(true), origin.clone()),
5070 Sourced::from_source(
5071 SecurityOption::SeccompProfile(ProtectedString::sensitive("seccomp-secret")),
5072 origin.clone(),
5073 ),
5074 Sourced::from_source(SecurityOption::SecurityLabelDisable(false), origin.clone()),
5075 Sourced::from_source(
5076 SecurityOption::SecurityLabelFileType(ProtectedString::sensitive("file-type-secret")),
5077 origin.clone(),
5078 ),
5079 Sourced::from_source(
5080 SecurityOption::SecurityLabelLevel(ProtectedString::sensitive("level-secret")),
5081 origin.clone(),
5082 ),
5083 Sourced::from_source(SecurityOption::SecurityLabelNested(true), origin.clone()),
5084 Sourced::from_source(
5085 SecurityOption::SecurityLabelType(ProtectedString::sensitive("type-secret")),
5086 origin.clone(),
5087 ),
5088 Sourced::from_source(
5089 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5090 origin.clone(),
5091 ),
5092 Sourced::from_source(
5093 SecurityOption::Unmask(ProtectedString::sensitive("unmask-secret")),
5094 origin.clone(),
5095 ),
5096 Sourced::from_source(
5097 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5098 origin.clone(),
5099 ),
5100 ],
5101 vec![origin.clone()],
5102 );
5103
5104 let options = service.security_options().unwrap_or_default();
5105 assert_eq!(options.len(), 11);
5106 assert!(
5107 matches!(options[0].value(), SecurityOption::AppArmor(profile) if profile.expose() == "apparmor-secret")
5108 );
5109 assert!(matches!(options[1].value(), SecurityOption::NoNewPrivileges(true)));
5110 assert!(
5111 matches!(options[2].value(), SecurityOption::SeccompProfile(profile) if profile.expose() == "seccomp-secret")
5112 );
5113 assert!(matches!(
5114 options[3].value(),
5115 SecurityOption::SecurityLabelDisable(false)
5116 ));
5117 assert!(
5118 matches!(options[4].value(), SecurityOption::SecurityLabelFileType(profile) if profile.expose() == "file-type-secret")
5119 );
5120 assert!(
5121 matches!(options[5].value(), SecurityOption::SecurityLabelLevel(profile) if profile.expose() == "level-secret")
5122 );
5123 assert!(matches!(options[6].value(), SecurityOption::SecurityLabelNested(true)));
5124 assert!(
5125 matches!(options[7].value(), SecurityOption::SecurityLabelType(profile) if profile.expose() == "type-secret")
5126 );
5127 assert!(matches!(options[8].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5128 assert!(matches!(options[9].value(), SecurityOption::Unmask(path) if path.expose() == "unmask-secret"));
5129 assert!(matches!(options[10].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5130 assert_eq!(options[0].origins(), std::slice::from_ref(&origin));
5131 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5132
5133 let debug = format!("{service:?}");
5134 for secret in [
5135 "apparmor-secret",
5136 "seccomp-secret",
5137 "file-type-secret",
5138 "level-secret",
5139 "type-secret",
5140 "mask-secret",
5141 "unmask-secret",
5142 ] {
5143 assert!(!debug.contains(secret));
5144 }
5145 assert!(debug.contains("[REDACTED]"));
5146
5147 service.set_security_options(Vec::new());
5148 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5149 assert!(service.security_options_origins().is_empty());
5150 Ok(())
5151 }
5152
5153 #[test]
5154 fn retains_entrypoint_run_init_stop_pull_memory_and_exposed_port_intent() -> Result<(), String> {
5155 let origin =
5156 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5157 let mut service = Service::new(id("web")?);
5158 service.set_command(Sourced::from_source(
5159 Command::Exec(vec![ProtectedString::plain("serve")]),
5160 origin.clone(),
5161 ));
5162 service.set_entrypoint(Sourced::from_source(
5163 Entrypoint::Shell(ProtectedString::sensitive("/bin/sh -c private-entrypoint")),
5164 origin.clone(),
5165 ));
5166 service.set_run_init(Sourced::from_source(true, origin.clone()));
5167 service.set_stop_timeout(Sourced::from_source(
5168 StopTimeout::new("01m30s").map_err(|error| error.to_string())?,
5169 origin.clone(),
5170 ));
5171 service.set_pull_policy(Sourced::from_source(
5172 PullPolicy::Every(ProtectedString::sensitive("12h")),
5173 origin.clone(),
5174 ));
5175 service.set_memory_limit(Sourced::from_source(
5176 ProtectedString::sensitive("512MiB"),
5177 origin.clone(),
5178 ));
5179 assert!(service.exposed_ports().is_none());
5180 service.set_exposed_ports_with_origins(Vec::new(), vec![origin.clone()]);
5181 assert_eq!(service.exposed_ports().map(<[_]>::len), Some(0));
5182 assert_eq!(service.exposed_ports_origins(), std::slice::from_ref(&origin));
5183 service.add_exposed_port(Sourced::from_source(
5184 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5185 origin.clone(),
5186 ));
5187 service.add_exposed_port(Sourced::from_source(
5188 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5189 origin,
5190 ));
5191
5192 assert!(matches!(service.command().map(Sourced::value), Some(Command::Exec(_))));
5193 assert!(matches!(
5194 service.entrypoint().map(Sourced::value),
5195 Some(Entrypoint::Shell(_))
5196 ));
5197 assert_eq!(service.run_init().map(Sourced::value), Some(&true));
5198 assert_eq!(
5199 service.stop_timeout().map(|timeout| timeout.value().as_str()),
5200 Some("01m30s")
5201 );
5202 assert!(matches!(
5203 service.pull_policy().map(Sourced::value),
5204 Some(PullPolicy::Every(_))
5205 ));
5206 assert_eq!(
5207 service.memory_limit().map(|limit| limit.value().expose()),
5208 Some("512MiB")
5209 );
5210 let exposed_ports = service.exposed_ports().ok_or("missing exposed ports")?;
5211 assert_eq!(exposed_ports.len(), 2);
5212 assert_eq!(exposed_ports[0].value().container(), 8080);
5213 assert_eq!(exposed_ports[0].value().protocol(), &Protocol::Tcp);
5214 assert!(matches!(
5215 ExposedPort::new(0, Protocol::Udp),
5216 Err(ModelError::ZeroContainerPort)
5217 ));
5218 assert!(matches!(
5219 StopTimeout::new(""),
5220 Err(ModelError::EmptyValue("stop timeout"))
5221 ));
5222
5223 let debug = format!("{service:?}");
5224 for secret in ["private-entrypoint", "512MiB", "12h"] {
5225 assert!(!debug.contains(secret));
5226 }
5227 assert!(debug.contains("[REDACTED]"));
5228 Ok(())
5229 }
5230
5231 #[test]
5232 fn annotations_and_logging_preserve_empty_order_field_provenance_and_redaction() -> Result<(), String> {
5233 let origin =
5234 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5235 let mut service = Service::new(id("web")?);
5236
5237 assert!(service.annotations().is_none());
5238 service.set_annotations_with_origins(Vec::new(), vec![origin.clone()]);
5239 assert_eq!(service.annotations().map(<[_]>::len), Some(0));
5240 assert_eq!(service.annotations_origins(), std::slice::from_ref(&origin));
5241
5242 service.set_annotations_with_origins(
5243 vec![
5244 Sourced::from_source(
5245 Annotation::new(
5246 Sourced::from_source(id("io.example.first")?, origin.clone()),
5247 Sourced::from_source(ProtectedString::sensitive("annotation-secret"), origin.clone()),
5248 ),
5249 origin.clone(),
5250 ),
5251 Sourced::from_source(
5252 Annotation::new(
5253 Sourced::from_source(id("io.example.second")?, origin.clone()),
5254 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5255 ),
5256 origin.clone(),
5257 ),
5258 ],
5259 vec![origin.clone()],
5260 );
5261 let annotations = service.annotations().unwrap_or_default();
5262 assert_eq!(annotations.len(), 2);
5263 assert_eq!(annotations[0].value().name().value().as_str(), "io.example.first");
5264 assert_eq!(annotations[1].value().value().value().expose(), "");
5265 assert_eq!(annotations[0].value().name().origins(), std::slice::from_ref(&origin));
5266 assert_eq!(annotations[0].value().value().origins(), std::slice::from_ref(&origin));
5267
5268 let mut logging = Logging::new();
5269 assert!(logging.options().is_none());
5270 logging.set_driver(Sourced::from_source(ProtectedString::plain("journald"), origin.clone()));
5271 logging.set_options_with_origins(
5272 vec![
5273 Sourced::from_source(
5274 LoggingOption::new(
5275 Sourced::from_source(id("tag")?, origin.clone()),
5276 Sourced::from_source(ProtectedString::sensitive("logging-secret"), origin.clone()),
5277 ),
5278 origin.clone(),
5279 ),
5280 Sourced::from_source(
5281 LoggingOption::new(
5282 Sourced::from_source(id("labels")?, origin.clone()),
5283 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5284 ),
5285 origin.clone(),
5286 ),
5287 ],
5288 vec![origin.clone()],
5289 );
5290 service.set_logging(Sourced::from_source(logging, origin));
5291
5292 let logging = service.logging().map(Sourced::value).ok_or("missing logging")?;
5293 assert_eq!(logging.driver().map(|driver| driver.value().expose()), Some("journald"));
5294 assert_eq!(logging.options().map(<[_]>::len), Some(2));
5295 assert_eq!(
5296 logging.options().unwrap_or_default()[0].value().name().value().as_str(),
5297 "tag"
5298 );
5299 assert_eq!(logging.options_origins().len(), 1);
5300 let debug = format!("{service:?}");
5301 assert!(!debug.contains("annotation-secret"));
5302 assert!(!debug.contains("logging-secret"));
5303 assert!(debug.contains("[REDACTED]"));
5304 Ok(())
5305 }
5306
5307 #[test]
5308 fn network_attachments_keep_legacy_constructor_and_add_source_aware_addresses_aliases() -> Result<(), String> {
5309 let origin =
5310 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5311 let legacy = NetworkAttachment::new(id("legacy")?, vec!["legacy.alias".to_owned()]);
5312 assert_eq!(legacy.aliases(), ["legacy.alias"]);
5313 assert!(legacy.alias_origins().is_empty());
5314
5315 let mut attachment = NetworkAttachment::with_sourced_aliases(
5316 id("frontend")?,
5317 vec![
5318 Sourced::from_source(ProtectedString::plain("web"), origin.clone()),
5319 Sourced::from_source(ProtectedString::sensitive("private-alias"), origin.clone()),
5320 ],
5321 );
5322 attachment.set_ipv4_address(Sourced::from_source(
5323 ProtectedString::plain("192.0.2.10"),
5324 origin.clone(),
5325 ));
5326 attachment.set_ipv6_address(Sourced::from_source(ProtectedString::plain("2001:db8::10"), origin));
5327 let metrics = Sourced::generated(ProtectedString::plain("metrics"));
5328 attachment.add_alias(&metrics);
5329
5330 assert_eq!(attachment.aliases(), ["web", "private-alias", "metrics"]);
5331 assert_eq!(attachment.alias_sensitivities(), [false, true, false]);
5332 assert_eq!(attachment.alias_origins().len(), 3);
5333 assert_eq!(attachment.alias_origins()[0].len(), 1);
5334 assert!(attachment.alias_origins()[2].is_empty());
5335 assert_eq!(
5336 attachment.ipv4_address().map(|address| address.value().expose()),
5337 Some("192.0.2.10")
5338 );
5339 assert_eq!(
5340 attachment.ipv6_address().map(|address| address.value().expose()),
5341 Some("2001:db8::10")
5342 );
5343 let debug = format!("{attachment:?}");
5344 assert!(!debug.contains("private-alias"));
5345 assert!(debug.contains("[REDACTED]"));
5346
5347 let mut service = Service::new(id("web")?);
5348 service.add_network(Sourced::generated(legacy));
5349 let previous = service
5350 .replace_network(0, Sourced::generated(attachment))
5351 .map_err(|error| error.to_string())?;
5352 assert_eq!(previous.value().network().as_str(), "legacy");
5353 assert_eq!(service.networks()[0].value().network().as_str(), "frontend");
5354 assert!(matches!(
5355 service.replace_network(1, Sourced::generated(NetworkAttachment::new(id("unused")?, Vec::new()))),
5356 Err(ModelError::UnknownNetworkAttachmentIndex { index: 1, len: 1 })
5357 ));
5358 Ok(())
5359 }
5360
5361 #[test]
5362 fn reload_action_is_one_explicit_command_or_signal() -> Result<(), String> {
5363 let origin =
5364 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5365 let mut service = Service::new(id("web")?);
5366 service.set_reload_action(Sourced::from_source(
5367 ReloadAction::Command(Command::Exec(vec![ProtectedString::plain("reload")])),
5368 origin.clone(),
5369 ));
5370 assert!(matches!(
5371 service.reload_action().map(Sourced::value),
5372 Some(ReloadAction::Command(Command::Exec(_)))
5373 ));
5374
5375 service.set_reload_action(Sourced::from_source(
5376 ReloadAction::Signal(ProtectedString::sensitive("SIGHUP")),
5377 origin,
5378 ));
5379 assert!(matches!(
5380 service.reload_action().map(Sourced::value),
5381 Some(ReloadAction::Signal(_))
5382 ));
5383 let debug = format!("{service:?}");
5384 assert!(!debug.contains("SIGHUP"));
5385 assert!(debug.contains("[REDACTED]"));
5386 Ok(())
5387 }
5388
5389 fn id(value: &str) -> Result<Identifier, String> {
5390 Identifier::new(value).map_err(|error| error.to_string())
5391 }
5392}