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 fn new(network: Identifier, aliases: Vec<Sourced<ProtectedString>>) -> Self {
2396 Self {
2397 network,
2398 aliases: aliases.iter().map(|alias| alias.value().expose().to_owned()).collect(),
2399 alias_sensitivities: aliases.iter().map(|alias| alias.value().is_sensitive()).collect(),
2400 alias_origins: aliases.into_iter().map(|alias| alias.origins().to_vec()).collect(),
2401 ipv4_address: None,
2402 ipv6_address: None,
2403 }
2404 }
2405
2406 #[must_use]
2408 pub const fn network(&self) -> &Identifier {
2409 &self.network
2410 }
2411
2412 #[must_use]
2414 pub fn aliases(&self) -> &[String] {
2415 &self.aliases
2416 }
2417
2418 #[must_use]
2420 pub fn alias_origins(&self) -> &[Vec<Provenance>] {
2421 &self.alias_origins
2422 }
2423
2424 #[must_use]
2429 pub fn alias_sensitivities(&self) -> &[bool] {
2430 &self.alias_sensitivities
2431 }
2432
2433 pub fn set_aliases_with_provenance(&mut self, aliases: Vec<Sourced<ProtectedString>>) {
2435 self.aliases = aliases.iter().map(|alias| alias.value().expose().to_owned()).collect();
2436 self.alias_sensitivities = aliases.iter().map(|alias| alias.value().is_sensitive()).collect();
2437 self.alias_origins = aliases.into_iter().map(|alias| alias.origins().to_vec()).collect();
2438 }
2439
2440 pub fn add_alias(&mut self, alias: &Sourced<ProtectedString>) {
2442 self.aliases.push(alias.value().expose().to_owned());
2443 self.alias_sensitivities.push(alias.value().is_sensitive());
2444 self.alias_origins.push(alias.origins().to_vec());
2445 }
2446
2447 pub fn set_ipv4_address(&mut self, address: Sourced<ProtectedString>) {
2449 self.ipv4_address = Some(address);
2450 }
2451
2452 #[must_use]
2454 pub const fn ipv4_address(&self) -> Option<&Sourced<ProtectedString>> {
2455 self.ipv4_address.as_ref()
2456 }
2457
2458 pub fn set_ipv6_address(&mut self, address: Sourced<ProtectedString>) {
2460 self.ipv6_address = Some(address);
2461 }
2462
2463 #[must_use]
2465 pub const fn ipv6_address(&self) -> Option<&Sourced<ProtectedString>> {
2466 self.ipv6_address.as_ref()
2467 }
2468}
2469
2470impl fmt::Debug for NetworkAttachment {
2471 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2472 let aliases = self
2473 .aliases
2474 .iter()
2475 .enumerate()
2476 .map(|(index, alias)| {
2477 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
2478 "[REDACTED]"
2479 } else {
2480 alias.as_str()
2481 }
2482 })
2483 .collect::<Vec<_>>();
2484 formatter
2485 .debug_struct("NetworkAttachment")
2486 .field("network", &self.network)
2487 .field("aliases", &aliases)
2488 .field("alias_origins", &self.alias_origins)
2489 .field("ipv4_address", &self.ipv4_address)
2490 .field("ipv6_address", &self.ipv6_address)
2491 .finish()
2492 }
2493}
2494
2495#[derive(Clone, Debug, Eq, PartialEq)]
2497#[non_exhaustive]
2498pub enum ServiceDependencyCondition {
2499 Started,
2501 Healthy,
2503 CompletedSuccessfully,
2505 Other(ProtectedString),
2507}
2508
2509#[derive(Clone, Debug, Eq, PartialEq)]
2515pub struct ServiceDependency {
2516 service: Identifier,
2517 condition: Option<Sourced<ServiceDependencyCondition>>,
2518 restart: Option<Sourced<bool>>,
2519 required: Option<Sourced<bool>>,
2520}
2521
2522#[derive(Clone, Debug, Eq, PartialEq)]
2524pub struct KernelParameter {
2525 name: ProtectedString,
2526 value: ProtectedString,
2527}
2528
2529impl KernelParameter {
2530 #[must_use]
2532 pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
2533 Self { name, value }
2534 }
2535
2536 #[must_use]
2538 pub const fn name(&self) -> &ProtectedString {
2539 &self.name
2540 }
2541
2542 #[must_use]
2544 pub const fn value(&self) -> &ProtectedString {
2545 &self.value
2546 }
2547}
2548
2549#[derive(Clone, Debug, Eq, PartialEq)]
2551pub struct ResourceLimit {
2552 name: ProtectedString,
2553 soft: Option<Sourced<ProtectedString>>,
2554 hard: Option<Sourced<ProtectedString>>,
2555}
2556
2557impl ResourceLimit {
2558 #[must_use]
2560 pub const fn new(
2561 name: ProtectedString,
2562 soft: Option<Sourced<ProtectedString>>,
2563 hard: Option<Sourced<ProtectedString>>,
2564 ) -> Self {
2565 Self { name, soft, hard }
2566 }
2567
2568 #[must_use]
2570 pub const fn name(&self) -> &ProtectedString {
2571 &self.name
2572 }
2573
2574 #[must_use]
2576 pub const fn soft(&self) -> Option<&Sourced<ProtectedString>> {
2577 self.soft.as_ref()
2578 }
2579
2580 #[must_use]
2582 pub const fn hard(&self) -> Option<&Sourced<ProtectedString>> {
2583 self.hard.as_ref()
2584 }
2585}
2586
2587#[derive(Clone, Debug, Eq, PartialEq)]
2589#[non_exhaustive]
2590pub enum Device {
2591 Short(ProtectedString),
2593 Long {
2595 source: Option<Sourced<ProtectedString>>,
2597 target: Option<Sourced<ProtectedString>>,
2599 permissions: Option<Sourced<ProtectedString>>,
2601 },
2602}
2603
2604#[derive(Clone, Debug, Eq, PartialEq)]
2609#[non_exhaustive]
2610pub enum SecurityOption {
2611 AppArmor(ProtectedString),
2613 NoNewPrivileges(bool),
2615 SeccompProfile(ProtectedString),
2617 SecurityLabelDisable(bool),
2619 SecurityLabelFileType(ProtectedString),
2621 SecurityLabelLevel(ProtectedString),
2623 SecurityLabelNested(bool),
2625 SecurityLabelType(ProtectedString),
2627 Mask(ProtectedString),
2629 Unmask(ProtectedString),
2631}
2632
2633impl ServiceDependency {
2634 #[must_use]
2637 pub const fn new(service: Identifier) -> Self {
2638 Self {
2639 service,
2640 condition: None,
2641 restart: None,
2642 required: None,
2643 }
2644 }
2645
2646 #[must_use]
2648 pub const fn service(&self) -> &Identifier {
2649 &self.service
2650 }
2651
2652 pub fn set_condition(&mut self, condition: Sourced<ServiceDependencyCondition>) {
2654 self.condition = Some(condition);
2655 }
2656
2657 #[must_use]
2659 pub const fn condition(&self) -> Option<&Sourced<ServiceDependencyCondition>> {
2660 self.condition.as_ref()
2661 }
2662
2663 pub fn set_restart(&mut self, restart: Sourced<bool>) {
2665 self.restart = Some(restart);
2666 }
2667
2668 #[must_use]
2670 pub const fn restart(&self) -> Option<&Sourced<bool>> {
2671 self.restart.as_ref()
2672 }
2673
2674 pub fn set_required(&mut self, required: Sourced<bool>) {
2676 self.required = Some(required);
2677 }
2678
2679 #[must_use]
2681 pub const fn required(&self) -> Option<&Sourced<bool>> {
2682 self.required.as_ref()
2683 }
2684
2685 #[must_use]
2687 pub fn is_required(&self) -> bool {
2688 self.required.as_ref().is_none_or(|required| *required.value())
2689 }
2690}
2691
2692#[derive(Clone, Debug, Eq, PartialEq)]
2694pub struct Service {
2695 name: Identifier,
2696 runtime_name: Option<Sourced<ProtectedString>>,
2697 rootfs: Option<Sourced<ProtectedString>>,
2698 image: Option<Sourced<ImageReference>>,
2699 image_acquisition: Option<Sourced<Identifier>>,
2700 image_build: Option<Sourced<Identifier>>,
2701 command: Option<Sourced<Command>>,
2702 startup_notification: Option<Sourced<StartupNotification>>,
2703 entrypoint: Option<Sourced<Entrypoint>>,
2704 run_init: Option<Sourced<bool>>,
2705 stop_timeout: Option<Sourced<StopTimeout>>,
2706 pull_policy: Option<Sourced<PullPolicy>>,
2707 memory_limit: Option<Sourced<ProtectedString>>,
2708 exposed_ports: Option<Vec<Sourced<ExposedPort>>>,
2709 exposed_ports_origins: Vec<Provenance>,
2710 restart_policy: Option<Sourced<RestartPolicy>>,
2711 healthcheck: Option<Sourced<Healthcheck>>,
2712 labels: Vec<Sourced<MetadataLabel>>,
2713 annotations: Option<Vec<Sourced<Annotation>>>,
2714 annotations_origins: Vec<Provenance>,
2715 logging: Option<Sourced<Logging>>,
2716 reload_action: Option<Sourced<ReloadAction>>,
2717 user: Option<Sourced<ProtectedString>>,
2718 group: Option<Sourced<ProtectedString>>,
2719 user_namespace: Option<Sourced<ProtectedString>>,
2720 supplementary_groups: Vec<Sourced<ProtectedString>>,
2721 working_directory: Option<Sourced<ProtectedString>>,
2722 read_only_root_filesystem: Option<Sourced<bool>>,
2723 hostname: Option<Sourced<ProtectedString>>,
2724 dns_servers: Option<Vec<Sourced<ProtectedString>>>,
2725 dns_servers_origins: Vec<Provenance>,
2726 dns_options: Option<Vec<Sourced<ProtectedString>>>,
2727 dns_options_origins: Vec<Provenance>,
2728 dns_search_domains: Option<Vec<Sourced<ProtectedString>>>,
2729 dns_search_domains_origins: Vec<Provenance>,
2730 security_options: Option<Vec<Sourced<SecurityOption>>>,
2731 security_options_origins: Vec<Provenance>,
2732 pids_limit: Option<Sourced<ProtectedString>>,
2733 shm_size: Option<Sourced<ProtectedString>>,
2734 cap_add: Option<Vec<Sourced<ProtectedString>>>,
2735 cap_add_origins: Vec<Provenance>,
2736 cap_drop: Option<Vec<Sourced<ProtectedString>>>,
2737 cap_drop_origins: Vec<Provenance>,
2738 tmpfs: Option<Vec<Sourced<ProtectedString>>>,
2739 tmpfs_origins: Vec<Provenance>,
2740 sysctls: Option<Vec<Sourced<KernelParameter>>>,
2741 sysctls_origins: Vec<Provenance>,
2742 ulimits: Option<Vec<Sourced<ResourceLimit>>>,
2743 ulimits_origins: Vec<Provenance>,
2744 devices: Option<Vec<Sourced<Device>>>,
2745 devices_origins: Vec<Provenance>,
2746 stop_signal: Option<Sourced<ProtectedString>>,
2747 podman_args: Option<Vec<Sourced<ProtectedString>>>,
2748 podman_args_origins: Vec<Provenance>,
2749 environment: Vec<Sourced<EnvironmentVariable>>,
2750 environment_files: Vec<Sourced<EnvironmentFile>>,
2751 host_mappings: Vec<Sourced<HostMapping>>,
2752 ports: Vec<Sourced<Port>>,
2753 mounts: Vec<Sourced<Mount>>,
2754 config_grants: Vec<Sourced<ResourceGrant>>,
2755 secret_grants: Vec<Sourced<ResourceGrant>>,
2756 networks: Vec<Sourced<NetworkAttachment>>,
2757 dependencies: Vec<Sourced<ServiceDependency>>,
2758}
2759
2760impl Service {
2761 #[must_use]
2763 pub const fn new(name: Identifier) -> Self {
2764 Self {
2765 name,
2766 runtime_name: None,
2767 rootfs: None,
2768 image: None,
2769 image_acquisition: None,
2770 image_build: None,
2771 command: None,
2772 startup_notification: None,
2773 entrypoint: None,
2774 run_init: None,
2775 stop_timeout: None,
2776 pull_policy: None,
2777 memory_limit: None,
2778 exposed_ports: None,
2779 exposed_ports_origins: Vec::new(),
2780 restart_policy: None,
2781 healthcheck: None,
2782 labels: Vec::new(),
2783 annotations: None,
2784 annotations_origins: Vec::new(),
2785 logging: None,
2786 reload_action: None,
2787 user: None,
2788 group: None,
2789 user_namespace: None,
2790 supplementary_groups: Vec::new(),
2791 working_directory: None,
2792 read_only_root_filesystem: None,
2793 hostname: None,
2794 dns_servers: None,
2795 dns_servers_origins: Vec::new(),
2796 dns_options: None,
2797 dns_options_origins: Vec::new(),
2798 dns_search_domains: None,
2799 dns_search_domains_origins: Vec::new(),
2800 security_options: None,
2801 security_options_origins: Vec::new(),
2802 pids_limit: None,
2803 shm_size: None,
2804 cap_add: None,
2805 cap_add_origins: Vec::new(),
2806 cap_drop: None,
2807 cap_drop_origins: Vec::new(),
2808 tmpfs: None,
2809 tmpfs_origins: Vec::new(),
2810 sysctls: None,
2811 sysctls_origins: Vec::new(),
2812 ulimits: None,
2813 ulimits_origins: Vec::new(),
2814 devices: None,
2815 devices_origins: Vec::new(),
2816 stop_signal: None,
2817 podman_args: None,
2818 podman_args_origins: Vec::new(),
2819 environment: Vec::new(),
2820 environment_files: Vec::new(),
2821 host_mappings: Vec::new(),
2822 ports: Vec::new(),
2823 mounts: Vec::new(),
2824 config_grants: Vec::new(),
2825 secret_grants: Vec::new(),
2826 networks: Vec::new(),
2827 dependencies: Vec::new(),
2828 }
2829 }
2830
2831 #[must_use]
2833 pub const fn name(&self) -> &Identifier {
2834 &self.name
2835 }
2836
2837 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
2839 self.runtime_name = Some(name);
2840 }
2841
2842 #[must_use]
2844 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
2845 self.runtime_name.as_ref()
2846 }
2847
2848 pub fn set_rootfs(&mut self, rootfs: Sourced<ProtectedString>) -> Result<(), ModelError> {
2855 self.ensure_rootfs_is_compatible()?;
2856 self.rootfs = Some(rootfs);
2857 Ok(())
2858 }
2859
2860 #[must_use]
2862 pub const fn rootfs(&self) -> Option<&Sourced<ProtectedString>> {
2863 self.rootfs.as_ref()
2864 }
2865
2866 pub fn set_image(&mut self, image: Sourced<ImageReference>) {
2868 self.image = Some(image);
2869 }
2870
2871 #[must_use]
2873 pub const fn image(&self) -> Option<&Sourced<ImageReference>> {
2874 self.image.as_ref()
2875 }
2876
2877 pub fn set_image_acquisition(&mut self, acquisition: Sourced<Identifier>) {
2881 self.image_acquisition = Some(acquisition);
2882 }
2883
2884 #[must_use]
2886 pub const fn image_acquisition(&self) -> Option<&Sourced<Identifier>> {
2887 self.image_acquisition.as_ref()
2888 }
2889
2890 pub fn set_image_build(&mut self, build: Sourced<Identifier>) {
2894 self.image_build = Some(build);
2895 }
2896
2897 #[must_use]
2899 pub const fn image_build(&self) -> Option<&Sourced<Identifier>> {
2900 self.image_build.as_ref()
2901 }
2902
2903 pub fn set_command(&mut self, command: Sourced<Command>) {
2905 self.command = Some(command);
2906 }
2907
2908 #[must_use]
2910 pub const fn command(&self) -> Option<&Sourced<Command>> {
2911 self.command.as_ref()
2912 }
2913
2914 pub fn set_startup_notification(&mut self, notification: Sourced<StartupNotification>) {
2916 self.startup_notification = Some(notification);
2917 }
2918
2919 #[must_use]
2921 pub const fn startup_notification(&self) -> Option<&Sourced<StartupNotification>> {
2922 self.startup_notification.as_ref()
2923 }
2924
2925 pub fn set_entrypoint(&mut self, entrypoint: Sourced<Entrypoint>) {
2927 self.entrypoint = Some(entrypoint);
2928 }
2929
2930 #[must_use]
2932 pub const fn entrypoint(&self) -> Option<&Sourced<Entrypoint>> {
2933 self.entrypoint.as_ref()
2934 }
2935
2936 pub fn set_run_init(&mut self, run_init: Sourced<bool>) {
2938 self.run_init = Some(run_init);
2939 }
2940
2941 #[must_use]
2943 pub const fn run_init(&self) -> Option<&Sourced<bool>> {
2944 self.run_init.as_ref()
2945 }
2946
2947 pub fn set_stop_timeout(&mut self, timeout: Sourced<StopTimeout>) {
2949 self.stop_timeout = Some(timeout);
2950 }
2951
2952 #[must_use]
2954 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
2955 self.stop_timeout.as_ref()
2956 }
2957
2958 pub fn set_pull_policy(&mut self, policy: Sourced<PullPolicy>) {
2960 self.pull_policy = Some(policy);
2961 }
2962
2963 #[must_use]
2965 pub const fn pull_policy(&self) -> Option<&Sourced<PullPolicy>> {
2966 self.pull_policy.as_ref()
2967 }
2968
2969 pub fn set_memory_limit(&mut self, limit: Sourced<ProtectedString>) {
2971 self.memory_limit = Some(limit);
2972 }
2973
2974 #[must_use]
2976 pub const fn memory_limit(&self) -> Option<&Sourced<ProtectedString>> {
2977 self.memory_limit.as_ref()
2978 }
2979
2980 pub fn set_exposed_ports(&mut self, ports: Vec<Sourced<ExposedPort>>) {
2982 self.exposed_ports = Some(ports);
2983 self.exposed_ports_origins.clear();
2984 }
2985
2986 pub fn set_exposed_ports_with_origins(&mut self, ports: Vec<Sourced<ExposedPort>>, origins: Vec<Provenance>) {
2988 self.exposed_ports = Some(ports);
2989 self.exposed_ports_origins = origins;
2990 }
2991
2992 pub fn add_exposed_port(&mut self, port: Sourced<ExposedPort>) {
2994 self.exposed_ports.get_or_insert_default().push(port);
2995 }
2996
2997 #[must_use]
2999 pub fn exposed_ports(&self) -> Option<&[Sourced<ExposedPort>]> {
3000 self.exposed_ports.as_deref()
3001 }
3002
3003 #[must_use]
3005 pub fn exposed_ports_origins(&self) -> &[Provenance] {
3006 &self.exposed_ports_origins
3007 }
3008
3009 pub fn set_restart_policy(&mut self, restart_policy: Sourced<RestartPolicy>) {
3011 self.restart_policy = Some(restart_policy);
3012 }
3013
3014 #[must_use]
3016 pub const fn restart_policy(&self) -> Option<&Sourced<RestartPolicy>> {
3017 self.restart_policy.as_ref()
3018 }
3019
3020 pub fn set_healthcheck(&mut self, healthcheck: Sourced<Healthcheck>) {
3022 self.healthcheck = Some(healthcheck);
3023 }
3024
3025 #[must_use]
3027 pub const fn healthcheck(&self) -> Option<&Sourced<Healthcheck>> {
3028 self.healthcheck.as_ref()
3029 }
3030
3031 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
3033 self.labels.push(label);
3034 }
3035
3036 #[must_use]
3038 pub fn labels(&self) -> &[Sourced<MetadataLabel>] {
3039 &self.labels
3040 }
3041
3042 pub fn set_annotations(&mut self, annotations: Vec<Sourced<Annotation>>) {
3044 self.annotations = Some(annotations);
3045 self.annotations_origins.clear();
3046 }
3047
3048 pub fn add_annotation(&mut self, annotation: Sourced<Annotation>) {
3050 self.annotations.get_or_insert_default().push(annotation);
3051 }
3052
3053 pub fn set_annotations_with_origins(&mut self, annotations: Vec<Sourced<Annotation>>, origins: Vec<Provenance>) {
3055 self.annotations = Some(annotations);
3056 self.annotations_origins = origins;
3057 }
3058
3059 #[must_use]
3061 pub fn annotations(&self) -> Option<&[Sourced<Annotation>]> {
3062 self.annotations.as_deref()
3063 }
3064
3065 #[must_use]
3067 pub fn annotations_origins(&self) -> &[Provenance] {
3068 &self.annotations_origins
3069 }
3070
3071 pub fn set_logging(&mut self, logging: Sourced<Logging>) {
3073 self.logging = Some(logging);
3074 }
3075
3076 #[must_use]
3078 pub const fn logging(&self) -> Option<&Sourced<Logging>> {
3079 self.logging.as_ref()
3080 }
3081
3082 pub fn set_reload_action(&mut self, reload_action: Sourced<ReloadAction>) {
3084 self.reload_action = Some(reload_action);
3085 }
3086
3087 #[must_use]
3089 pub const fn reload_action(&self) -> Option<&Sourced<ReloadAction>> {
3090 self.reload_action.as_ref()
3091 }
3092
3093 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
3095 self.user = Some(user);
3096 }
3097
3098 #[must_use]
3100 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
3101 self.user.as_ref()
3102 }
3103
3104 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
3106 self.group = Some(group);
3107 }
3108
3109 #[must_use]
3111 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
3112 self.group.as_ref()
3113 }
3114
3115 pub fn set_user_namespace(&mut self, user_namespace: Sourced<ProtectedString>) {
3117 self.user_namespace = Some(user_namespace);
3118 }
3119
3120 #[must_use]
3122 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
3123 self.user_namespace.as_ref()
3124 }
3125
3126 pub fn add_supplementary_group(&mut self, group: Sourced<ProtectedString>) {
3128 self.supplementary_groups.push(group);
3129 }
3130
3131 #[must_use]
3133 pub fn supplementary_groups(&self) -> &[Sourced<ProtectedString>] {
3134 &self.supplementary_groups
3135 }
3136
3137 pub fn set_working_directory(&mut self, working_directory: Sourced<ProtectedString>) {
3139 self.working_directory = Some(working_directory);
3140 }
3141
3142 #[must_use]
3144 pub const fn working_directory(&self) -> Option<&Sourced<ProtectedString>> {
3145 self.working_directory.as_ref()
3146 }
3147
3148 pub fn set_read_only_root_filesystem(&mut self, read_only: Sourced<bool>) {
3150 self.read_only_root_filesystem = Some(read_only);
3151 }
3152
3153 #[must_use]
3155 pub const fn read_only_root_filesystem(&self) -> Option<&Sourced<bool>> {
3156 self.read_only_root_filesystem.as_ref()
3157 }
3158
3159 pub fn set_hostname(&mut self, hostname: Sourced<ProtectedString>) {
3161 self.hostname = Some(hostname);
3162 }
3163
3164 #[must_use]
3166 pub const fn hostname(&self) -> Option<&Sourced<ProtectedString>> {
3167 self.hostname.as_ref()
3168 }
3169
3170 pub fn set_dns_servers_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3172 self.dns_servers = Some(values);
3173 self.dns_servers_origins = origins;
3174 }
3175
3176 pub fn set_dns_servers(&mut self, values: Vec<Sourced<ProtectedString>>) {
3178 self.set_dns_servers_with_origins(values, Vec::new());
3179 }
3180
3181 #[must_use]
3183 pub fn dns_servers(&self) -> Option<&[Sourced<ProtectedString>]> {
3184 self.dns_servers.as_deref()
3185 }
3186
3187 #[must_use]
3189 pub fn dns_servers_origins(&self) -> &[Provenance] {
3190 &self.dns_servers_origins
3191 }
3192
3193 pub fn set_dns_options_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3195 self.dns_options = Some(values);
3196 self.dns_options_origins = origins;
3197 }
3198
3199 pub fn set_dns_options(&mut self, values: Vec<Sourced<ProtectedString>>) {
3201 self.set_dns_options_with_origins(values, Vec::new());
3202 }
3203
3204 #[must_use]
3206 pub fn dns_options(&self) -> Option<&[Sourced<ProtectedString>]> {
3207 self.dns_options.as_deref()
3208 }
3209
3210 #[must_use]
3212 pub fn dns_options_origins(&self) -> &[Provenance] {
3213 &self.dns_options_origins
3214 }
3215
3216 pub fn set_dns_search_domains_with_origins(
3218 &mut self,
3219 values: Vec<Sourced<ProtectedString>>,
3220 origins: Vec<Provenance>,
3221 ) {
3222 self.dns_search_domains = Some(values);
3223 self.dns_search_domains_origins = origins;
3224 }
3225
3226 pub fn set_dns_search_domains(&mut self, values: Vec<Sourced<ProtectedString>>) {
3228 self.set_dns_search_domains_with_origins(values, Vec::new());
3229 }
3230
3231 #[must_use]
3233 pub fn dns_search_domains(&self) -> Option<&[Sourced<ProtectedString>]> {
3234 self.dns_search_domains.as_deref()
3235 }
3236
3237 #[must_use]
3239 pub fn dns_search_domains_origins(&self) -> &[Provenance] {
3240 &self.dns_search_domains_origins
3241 }
3242
3243 pub fn set_security_options_with_origins(
3245 &mut self,
3246 values: Vec<Sourced<SecurityOption>>,
3247 origins: Vec<Provenance>,
3248 ) {
3249 self.security_options = Some(values);
3250 self.security_options_origins = origins;
3251 }
3252
3253 pub fn set_security_options(&mut self, values: Vec<Sourced<SecurityOption>>) {
3255 self.set_security_options_with_origins(values, Vec::new());
3256 }
3257
3258 #[must_use]
3260 pub fn security_options(&self) -> Option<&[Sourced<SecurityOption>]> {
3261 self.security_options.as_deref()
3262 }
3263
3264 #[must_use]
3266 pub fn security_options_origins(&self) -> &[Provenance] {
3267 &self.security_options_origins
3268 }
3269
3270 pub fn set_pids_limit(&mut self, limit: Sourced<ProtectedString>) {
3272 self.pids_limit = Some(limit);
3273 }
3274
3275 #[must_use]
3277 pub const fn pids_limit(&self) -> Option<&Sourced<ProtectedString>> {
3278 self.pids_limit.as_ref()
3279 }
3280
3281 pub fn set_shm_size(&mut self, size: Sourced<ProtectedString>) {
3283 self.shm_size = Some(size);
3284 }
3285
3286 #[must_use]
3288 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
3289 self.shm_size.as_ref()
3290 }
3291
3292 pub fn set_cap_add(&mut self, values: Vec<Sourced<ProtectedString>>) {
3294 self.cap_add = Some(values);
3295 self.cap_add_origins.clear();
3296 }
3297
3298 pub fn set_cap_add_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3300 self.cap_add = Some(values);
3301 self.cap_add_origins = origins;
3302 }
3303
3304 #[must_use]
3306 pub fn cap_add(&self) -> Option<&[Sourced<ProtectedString>]> {
3307 self.cap_add.as_deref()
3308 }
3309
3310 #[must_use]
3312 pub fn cap_add_origins(&self) -> &[Provenance] {
3313 &self.cap_add_origins
3314 }
3315
3316 pub fn set_cap_drop(&mut self, values: Vec<Sourced<ProtectedString>>) {
3318 self.cap_drop = Some(values);
3319 self.cap_drop_origins.clear();
3320 }
3321
3322 pub fn set_cap_drop_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3324 self.cap_drop = Some(values);
3325 self.cap_drop_origins = origins;
3326 }
3327
3328 #[must_use]
3330 pub fn cap_drop(&self) -> Option<&[Sourced<ProtectedString>]> {
3331 self.cap_drop.as_deref()
3332 }
3333
3334 #[must_use]
3336 pub fn cap_drop_origins(&self) -> &[Provenance] {
3337 &self.cap_drop_origins
3338 }
3339
3340 pub fn set_tmpfs(&mut self, values: Vec<Sourced<ProtectedString>>) {
3342 self.tmpfs = Some(values);
3343 self.tmpfs_origins.clear();
3344 }
3345
3346 pub fn set_tmpfs_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3348 self.tmpfs = Some(values);
3349 self.tmpfs_origins = origins;
3350 }
3351
3352 #[must_use]
3354 pub fn tmpfs(&self) -> Option<&[Sourced<ProtectedString>]> {
3355 self.tmpfs.as_deref()
3356 }
3357
3358 #[must_use]
3360 pub fn tmpfs_origins(&self) -> &[Provenance] {
3361 &self.tmpfs_origins
3362 }
3363
3364 pub fn set_sysctls(&mut self, values: Vec<Sourced<KernelParameter>>) {
3366 self.sysctls = Some(values);
3367 self.sysctls_origins.clear();
3368 }
3369
3370 pub fn set_sysctls_with_origins(&mut self, values: Vec<Sourced<KernelParameter>>, origins: Vec<Provenance>) {
3372 self.sysctls = Some(values);
3373 self.sysctls_origins = origins;
3374 }
3375
3376 #[must_use]
3378 pub fn sysctls(&self) -> Option<&[Sourced<KernelParameter>]> {
3379 self.sysctls.as_deref()
3380 }
3381
3382 #[must_use]
3384 pub fn sysctls_origins(&self) -> &[Provenance] {
3385 &self.sysctls_origins
3386 }
3387
3388 pub fn set_ulimits(&mut self, values: Vec<Sourced<ResourceLimit>>) {
3390 self.ulimits = Some(values);
3391 self.ulimits_origins.clear();
3392 }
3393
3394 pub fn set_ulimits_with_origins(&mut self, values: Vec<Sourced<ResourceLimit>>, origins: Vec<Provenance>) {
3396 self.ulimits = Some(values);
3397 self.ulimits_origins = origins;
3398 }
3399
3400 #[must_use]
3402 pub fn ulimits(&self) -> Option<&[Sourced<ResourceLimit>]> {
3403 self.ulimits.as_deref()
3404 }
3405
3406 #[must_use]
3408 pub fn ulimits_origins(&self) -> &[Provenance] {
3409 &self.ulimits_origins
3410 }
3411
3412 pub fn set_devices(&mut self, values: Vec<Sourced<Device>>) {
3414 self.devices = Some(values);
3415 self.devices_origins.clear();
3416 }
3417
3418 pub fn set_devices_with_origins(&mut self, values: Vec<Sourced<Device>>, origins: Vec<Provenance>) {
3420 self.devices = Some(values);
3421 self.devices_origins = origins;
3422 }
3423
3424 #[must_use]
3426 pub fn devices(&self) -> Option<&[Sourced<Device>]> {
3427 self.devices.as_deref()
3428 }
3429
3430 #[must_use]
3432 pub fn devices_origins(&self) -> &[Provenance] {
3433 &self.devices_origins
3434 }
3435
3436 pub fn set_stop_signal(&mut self, signal: Sourced<ProtectedString>) {
3438 self.stop_signal = Some(signal);
3439 }
3440
3441 #[must_use]
3443 pub const fn stop_signal(&self) -> Option<&Sourced<ProtectedString>> {
3444 self.stop_signal.as_ref()
3445 }
3446
3447 pub fn set_podman_args(&mut self, values: Vec<Sourced<ProtectedString>>) {
3449 self.set_podman_args_with_origins(values, Vec::new());
3450 }
3451
3452 pub fn set_podman_args_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3454 self.podman_args = Some(values);
3455 self.podman_args_origins = origins;
3456 }
3457
3458 pub fn add_podman_arg(&mut self, value: Sourced<ProtectedString>) {
3460 self.podman_args.get_or_insert_default().push(value);
3461 }
3462
3463 #[must_use]
3465 pub fn podman_args(&self) -> Option<&[Sourced<ProtectedString>]> {
3466 self.podman_args.as_deref()
3467 }
3468
3469 #[must_use]
3471 pub fn podman_args_origins(&self) -> &[Provenance] {
3472 &self.podman_args_origins
3473 }
3474
3475 pub fn add_environment(&mut self, value: Sourced<EnvironmentVariable>) {
3477 self.environment.push(value);
3478 }
3479
3480 #[must_use]
3482 pub fn environment(&self) -> &[Sourced<EnvironmentVariable>] {
3483 &self.environment
3484 }
3485
3486 pub fn add_environment_file(&mut self, value: Sourced<EnvironmentFile>) {
3488 self.environment_files.push(value);
3489 }
3490
3491 #[must_use]
3493 pub fn environment_files(&self) -> &[Sourced<EnvironmentFile>] {
3494 &self.environment_files
3495 }
3496
3497 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
3499 self.host_mappings.push(value);
3500 }
3501
3502 #[must_use]
3504 pub fn host_mappings(&self) -> &[Sourced<HostMapping>] {
3505 &self.host_mappings
3506 }
3507
3508 pub fn add_port(&mut self, value: Sourced<Port>) {
3510 self.ports.push(value);
3511 }
3512
3513 #[must_use]
3515 pub fn ports(&self) -> &[Sourced<Port>] {
3516 &self.ports
3517 }
3518
3519 pub fn add_mount(&mut self, value: Sourced<Mount>) {
3521 self.mounts.push(value);
3522 }
3523
3524 #[must_use]
3526 pub fn mounts(&self) -> &[Sourced<Mount>] {
3527 &self.mounts
3528 }
3529
3530 pub fn add_config_grant(&mut self, value: Sourced<ResourceGrant>) {
3532 self.config_grants.push(value);
3533 }
3534
3535 #[must_use]
3537 pub fn config_grants(&self) -> &[Sourced<ResourceGrant>] {
3538 &self.config_grants
3539 }
3540
3541 pub fn add_secret_grant(&mut self, value: Sourced<ResourceGrant>) {
3543 self.secret_grants.push(value);
3544 }
3545
3546 #[must_use]
3548 pub fn secret_grants(&self) -> &[Sourced<ResourceGrant>] {
3549 &self.secret_grants
3550 }
3551
3552 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
3554 self.networks.push(value);
3555 }
3556
3557 pub fn replace_network(
3567 &mut self,
3568 index: usize,
3569 value: Sourced<NetworkAttachment>,
3570 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
3571 let len = self.networks.len();
3572 let Some(slot) = self.networks.get_mut(index) else {
3573 return Err(ModelError::UnknownNetworkAttachmentIndex { index, len });
3574 };
3575 Ok(std::mem::replace(slot, value))
3576 }
3577
3578 #[must_use]
3580 pub fn networks(&self) -> &[Sourced<NetworkAttachment>] {
3581 &self.networks
3582 }
3583
3584 pub fn add_dependency(&mut self, value: Sourced<ServiceDependency>) {
3586 self.dependencies.push(value);
3587 }
3588
3589 #[must_use]
3591 pub fn dependencies(&self) -> &[Sourced<ServiceDependency>] {
3592 &self.dependencies
3593 }
3594
3595 pub fn validate_image_source_exclusivity(&self) -> Result<(), ModelError> {
3604 if self.rootfs.is_some() {
3605 self.ensure_rootfs_is_compatible()?;
3606 }
3607 Ok(())
3608 }
3609
3610 fn ensure_rootfs_is_compatible(&self) -> Result<(), ModelError> {
3611 let source = if self.image.is_some() {
3612 Some("image")
3613 } else if self.image_acquisition.is_some() {
3614 Some("image acquisition")
3615 } else if self.image_build.is_some() {
3616 Some("image build")
3617 } else {
3618 None
3619 };
3620 if let Some(source) = source {
3621 return Err(ModelError::RootfsImageSourceConflict {
3622 service: self.name.as_str().to_owned(),
3623 source,
3624 });
3625 }
3626 Ok(())
3627 }
3628}
3629
3630#[derive(Clone, Debug, Eq, PartialEq)]
3632pub struct Application {
3633 name: Identifier,
3634 image_acquisitions: Vec<Sourced<ImageAcquisition>>,
3635 image_builds: Vec<Sourced<ImageBuild>>,
3636 services: Vec<Sourced<Service>>,
3637 service_groups: Vec<Sourced<ServiceGroup>>,
3638 volumes: Vec<Sourced<Volume>>,
3639 networks: Vec<Sourced<Network>>,
3640 configs: Vec<Sourced<Config>>,
3641 secrets: Vec<Sourced<Secret>>,
3642}
3643
3644impl Application {
3645 #[must_use]
3647 pub const fn new(name: Identifier) -> Self {
3648 Self {
3649 name,
3650 image_acquisitions: Vec::new(),
3651 image_builds: Vec::new(),
3652 services: Vec::new(),
3653 service_groups: Vec::new(),
3654 volumes: Vec::new(),
3655 networks: Vec::new(),
3656 configs: Vec::new(),
3657 secrets: Vec::new(),
3658 }
3659 }
3660
3661 #[must_use]
3663 pub const fn name(&self) -> &Identifier {
3664 &self.name
3665 }
3666
3667 pub fn add_image_acquisition(&mut self, acquisition: Sourced<ImageAcquisition>) -> Result<(), ModelError> {
3673 ensure_unique(
3674 "image acquisition",
3675 acquisition.value().name(),
3676 self.image_acquisitions.iter().map(|candidate| candidate.value().name()),
3677 )?;
3678 self.image_acquisitions.push(acquisition);
3679 Ok(())
3680 }
3681
3682 #[must_use]
3684 pub fn image_acquisitions(&self) -> &[Sourced<ImageAcquisition>] {
3685 &self.image_acquisitions
3686 }
3687
3688 pub fn add_image_build(&mut self, build: Sourced<ImageBuild>) -> Result<(), ModelError> {
3694 ensure_unique(
3695 "image build",
3696 build.value().name(),
3697 self.image_builds.iter().map(|candidate| candidate.value().name()),
3698 )?;
3699 self.image_builds.push(build);
3700 Ok(())
3701 }
3702
3703 #[must_use]
3705 pub fn image_builds(&self) -> &[Sourced<ImageBuild>] {
3706 &self.image_builds
3707 }
3708
3709 pub fn validate_image_artifact_references(&self) -> Result<(), ModelError> {
3719 for service in &self.services {
3720 if let Some(acquisition) = service.value().image_acquisition() {
3721 if !self.contains_image_acquisition(acquisition.value()) {
3722 return Err(ModelError::UnknownImageAcquisitionReference {
3723 service: service.value().name().as_str().to_owned(),
3724 acquisition: acquisition.value().as_str().to_owned(),
3725 });
3726 }
3727 }
3728 if let Some(build) = service.value().image_build() {
3729 if !self.contains_image_build(build.value()) {
3730 return Err(ModelError::UnknownImageBuildReference {
3731 service: service.value().name().as_str().to_owned(),
3732 build: build.value().as_str().to_owned(),
3733 });
3734 }
3735 }
3736 }
3737 for volume in &self.volumes {
3738 let Some(source) = volume.value().image_source() else {
3739 continue;
3740 };
3741 match source.value() {
3742 VolumeImageSource::Literal(_) => {}
3743 VolumeImageSource::ImageAcquisition(acquisition) => {
3744 if !self.contains_image_acquisition(acquisition) {
3745 return Err(ModelError::UnknownVolumeImageAcquisitionReference {
3746 volume: volume.value().name().as_str().to_owned(),
3747 acquisition: acquisition.as_str().to_owned(),
3748 });
3749 }
3750 }
3751 VolumeImageSource::ImageBuild(build) => {
3752 if !self.contains_image_build(build) {
3753 return Err(ModelError::UnknownVolumeImageBuildReference {
3754 volume: volume.value().name().as_str().to_owned(),
3755 build: build.as_str().to_owned(),
3756 });
3757 }
3758 }
3759 }
3760 }
3761 Ok(())
3762 }
3763
3764 pub fn validate_image_artifact_dependencies(
3775 &self,
3776 dependencies: &[Sourced<ArtifactDependency>],
3777 ) -> Result<(), ModelError> {
3778 self.validate_image_artifact_references()?;
3779
3780 let mut graph = BTreeMap::<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>::new();
3781 for dependency in dependencies {
3782 let source = dependency.value().source().value();
3783 let target = dependency.value().target().value();
3784 self.validate_artifact_dependency_node(source)?;
3785 self.validate_artifact_dependency_node(target)?;
3786 graph.entry(source.clone()).or_default().insert(target.clone());
3787 graph.entry(target.clone()).or_default();
3788 }
3789
3790 let mut state = BTreeMap::<ArtifactDependencyNode, VisitState>::new();
3791 let mut path = Vec::new();
3792 for node in graph.keys() {
3793 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
3794 continue;
3795 }
3796 if let Some(cycle) = detect_artifact_cycle(node, &graph, &mut state, &mut path) {
3797 return Err(ModelError::ImageArtifactDependencyCycle {
3798 nodes: cycle.into_iter().map(|node| node.display_name()).collect(),
3799 });
3800 }
3801 }
3802 Ok(())
3803 }
3804
3805 fn contains_image_acquisition(&self, name: &Identifier) -> bool {
3806 self.image_acquisitions
3807 .iter()
3808 .any(|candidate| candidate.value().name() == name)
3809 }
3810
3811 fn contains_image_build(&self, name: &Identifier) -> bool {
3812 self.image_builds
3813 .iter()
3814 .any(|candidate| candidate.value().name() == name)
3815 }
3816
3817 fn validate_artifact_dependency_node(&self, node: &ArtifactDependencyNode) -> Result<(), ModelError> {
3818 let (kind, name) = node.kind_and_name();
3819 let exists = match node {
3820 ArtifactDependencyNode::Volume(_) => self.volumes.iter().any(|volume| volume.value().name() == name),
3821 ArtifactDependencyNode::ImageAcquisition(_) => self.contains_image_acquisition(name),
3822 ArtifactDependencyNode::ImageBuild(_) => self.contains_image_build(name),
3823 };
3824 if exists {
3825 Ok(())
3826 } else {
3827 Err(ModelError::UnknownArtifactDependencyNode {
3828 kind,
3829 name: name.as_str().to_owned(),
3830 })
3831 }
3832 }
3833
3834 pub fn add_service(&mut self, service: Sourced<Service>) -> Result<(), ModelError> {
3842 ensure_unique(
3843 "service",
3844 service.value().name(),
3845 self.services.iter().map(|candidate| candidate.value().name()),
3846 )?;
3847 service.value().validate_image_source_exclusivity()?;
3848 if let Some(acquisition) = service.value().image_acquisition() {
3849 if !self
3850 .image_acquisitions
3851 .iter()
3852 .any(|candidate| candidate.value().name() == acquisition.value())
3853 {
3854 return Err(ModelError::UnknownImageAcquisitionReference {
3855 service: service.value().name().as_str().to_owned(),
3856 acquisition: acquisition.value().as_str().to_owned(),
3857 });
3858 }
3859 }
3860 if let Some(build) = service.value().image_build() {
3861 if !self
3862 .image_builds
3863 .iter()
3864 .any(|candidate| candidate.value().name() == build.value())
3865 {
3866 return Err(ModelError::UnknownImageBuildReference {
3867 service: service.value().name().as_str().to_owned(),
3868 build: build.value().as_str().to_owned(),
3869 });
3870 }
3871 }
3872 self.services.push(service);
3873 Ok(())
3874 }
3875
3876 #[must_use]
3878 pub fn services(&self) -> &[Sourced<Service>] {
3879 &self.services
3880 }
3881
3882 pub fn add_service_group(&mut self, group: Sourced<ServiceGroup>) -> Result<(), ModelError> {
3892 ensure_unique(
3893 "service group",
3894 group.value().name(),
3895 self.service_groups.iter().map(|candidate| candidate.value().name()),
3896 )?;
3897 for member in group.value().members() {
3898 if !self
3899 .services
3900 .iter()
3901 .any(|service| service.value().name() == member.value())
3902 {
3903 return Err(ModelError::UnknownServiceGroupMember {
3904 group: group.value().name().as_str().to_owned(),
3905 service: member.value().as_str().to_owned(),
3906 });
3907 }
3908 if let Some(existing) = self.service_groups.iter().find(|candidate| {
3909 candidate
3910 .value()
3911 .members()
3912 .iter()
3913 .any(|candidate_member| candidate_member.value() == member.value())
3914 }) {
3915 return Err(ModelError::ServiceInMultipleGroups {
3916 service: member.value().as_str().to_owned(),
3917 existing: existing.value().name().as_str().to_owned(),
3918 replacement: group.value().name().as_str().to_owned(),
3919 });
3920 }
3921 }
3922 self.service_groups.push(group);
3923 Ok(())
3924 }
3925
3926 #[must_use]
3928 pub fn service_groups(&self) -> &[Sourced<ServiceGroup>] {
3929 &self.service_groups
3930 }
3931
3932 pub fn add_volume(&mut self, volume: Sourced<Volume>) -> Result<(), ModelError> {
3938 ensure_unique(
3939 "volume",
3940 volume.value().name(),
3941 self.volumes.iter().map(|candidate| candidate.value().name()),
3942 )?;
3943 self.volumes.push(volume);
3944 Ok(())
3945 }
3946
3947 #[must_use]
3949 pub fn volumes(&self) -> &[Sourced<Volume>] {
3950 &self.volumes
3951 }
3952
3953 pub fn add_network(&mut self, network: Sourced<Network>) -> Result<(), ModelError> {
3959 ensure_unique(
3960 "network",
3961 network.value().name(),
3962 self.networks.iter().map(|candidate| candidate.value().name()),
3963 )?;
3964 self.networks.push(network);
3965 Ok(())
3966 }
3967
3968 #[must_use]
3970 pub fn networks(&self) -> &[Sourced<Network>] {
3971 &self.networks
3972 }
3973
3974 pub fn add_config(&mut self, config: Sourced<Config>) -> Result<(), ModelError> {
3980 ensure_unique(
3981 "config",
3982 config.value().name(),
3983 self.configs.iter().map(|candidate| candidate.value().name()),
3984 )?;
3985 self.configs.push(config);
3986 Ok(())
3987 }
3988
3989 #[must_use]
3991 pub fn configs(&self) -> &[Sourced<Config>] {
3992 &self.configs
3993 }
3994
3995 pub fn add_secret(&mut self, secret: Sourced<Secret>) -> Result<(), ModelError> {
4001 ensure_unique(
4002 "secret",
4003 secret.value().name(),
4004 self.secrets.iter().map(|candidate| candidate.value().name()),
4005 )?;
4006 self.secrets.push(secret);
4007 Ok(())
4008 }
4009
4010 #[must_use]
4012 pub fn secrets(&self) -> &[Sourced<Secret>] {
4013 &self.secrets
4014 }
4015}
4016
4017#[derive(Clone, Copy, Eq, PartialEq)]
4018enum VisitState {
4019 Visiting,
4020 Finished,
4021}
4022
4023fn detect_artifact_cycle(
4024 node: &ArtifactDependencyNode,
4025 graph: &BTreeMap<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>,
4026 state: &mut BTreeMap<ArtifactDependencyNode, VisitState>,
4027 path: &mut Vec<ArtifactDependencyNode>,
4028) -> Option<Vec<ArtifactDependencyNode>> {
4029 if state.get(node).is_some_and(|state| *state == VisitState::Visiting) {
4030 let index = path.iter().position(|candidate| candidate == node)?;
4031 let mut cycle = path[index..].to_vec();
4032 cycle.push(node.clone());
4033 return Some(cycle);
4034 }
4035 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
4036 return None;
4037 }
4038
4039 state.insert(node.clone(), VisitState::Visiting);
4040 path.push(node.clone());
4041 if let Some(targets) = graph.get(node) {
4042 for target in targets {
4043 if let Some(cycle) = detect_artifact_cycle(target, graph, state, path) {
4044 return Some(cycle);
4045 }
4046 }
4047 }
4048 path.pop();
4049 state.insert(node.clone(), VisitState::Finished);
4050 None
4051}
4052
4053fn ensure_unique<'a>(
4054 kind: &'static str,
4055 name: &Identifier,
4056 existing: impl Iterator<Item = &'a Identifier>,
4057) -> Result<(), ModelError> {
4058 if existing.into_iter().any(|candidate| candidate == name) {
4059 return Err(ModelError::DuplicateResource {
4060 kind,
4061 name: name.as_str().to_owned(),
4062 });
4063 }
4064 Ok(())
4065}
4066
4067fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
4068 if value.is_empty() {
4069 return Err(ModelError::EmptyValue(kind));
4070 }
4071 validate_no_nul(kind, value)
4072}
4073
4074fn validate_no_nul(kind: &'static str, value: &str) -> Result<(), ModelError> {
4075 if value.contains('\0') {
4076 return Err(ModelError::ContainsNul(kind));
4077 }
4078 Ok(())
4079}
4080
4081#[cfg(test)]
4082mod tests {
4083 use super::{
4084 Annotation, Application, ArtifactDependency, ArtifactDependencyNode, Command, Config, ConfigMaterial, Device,
4085 Entrypoint, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileSyntax, ExposedPort, GroupExitPolicy,
4086 HealthcheckDuration, HealthcheckRetries, HostAddress, HostAddressKind, HostMapping, Identifier,
4087 KernelParameter, Logging, LoggingOption, MetadataLabel, ModelError, Mount, MountSource, Network,
4088 NetworkAttachment, NetworkDriverOption, NetworkIpamConfig, Protocol, PullPolicy, ReloadAction, ResourceGrant,
4089 ResourceGrantSyntax, ResourceLimit, ResourceOwnership, RestartPolicy, Secret, SecretMaterial, SecurityOption,
4090 Service, ServiceDependency, ServiceDependencyCondition, ServiceGroup, ServiceGroupRuntime, StartupNotification,
4091 StopTimeout, Volume, VolumeImageSource,
4092 };
4093 use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Sourced};
4094
4095 #[test]
4096 fn preserves_service_order_and_rejects_duplicate_names() -> Result<(), String> {
4097 let mut application = Application::new(id("example")?);
4098 application
4099 .add_service(Sourced::generated(Service::new(id("web")?)))
4100 .map_err(|error| error.to_string())?;
4101 application
4102 .add_service(Sourced::generated(Service::new(id("database")?)))
4103 .map_err(|error| error.to_string())?;
4104
4105 let names: Vec<_> = application
4106 .services()
4107 .iter()
4108 .map(|service| service.value().name().as_str())
4109 .collect();
4110 assert_eq!(names, ["web", "database"]);
4111
4112 let duplicate = application.add_service(Sourced::generated(Service::new(id("web")?)));
4113 assert!(matches!(duplicate, Err(ModelError::DuplicateResource { .. })));
4114 Ok(())
4115 }
4116
4117 #[test]
4118 fn keeps_the_service_key_and_explicit_runtime_name_distinct() -> Result<(), String> {
4119 let mut service = Service::new(id("web")?);
4120 service.set_runtime_name(Sourced::generated(ProtectedString::plain("production-web")));
4121
4122 assert_eq!(service.name().as_str(), "web");
4123 assert_eq!(
4124 service.runtime_name().map(|name| name.value().expose()),
4125 Some("production-web")
4126 );
4127 Ok(())
4128 }
4129
4130 #[test]
4131 fn network_keeps_logical_and_runtime_names_and_literal_flags_distinct() -> Result<(), String> {
4132 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4133 let origin = crate::Provenance::source(source);
4134 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4135 network.set_runtime_name(Sourced::from_source(
4136 ProtectedString::plain("production-frontend"),
4137 origin.clone(),
4138 ));
4139 network.set_driver(Sourced::from_source(ProtectedString::plain("bridge"), origin.clone()));
4140 network.set_internal(Sourced::from_source(true, origin.clone()));
4141 network.set_ipv6(Sourced::from_source(false, origin.clone()));
4142 network.set_ipam_driver(Sourced::from_source(ProtectedString::plain("default"), origin));
4143
4144 assert_eq!(network.name().as_str(), "frontend");
4145 assert_eq!(
4146 network.runtime_name().map(|value| value.value().expose()),
4147 Some("production-frontend")
4148 );
4149 assert_eq!(network.driver().map(|value| value.value().expose()), Some("bridge"));
4150 assert_eq!(network.internal().map(Sourced::value), Some(&true));
4151 assert_eq!(network.ipv6().map(Sourced::value), Some(&false));
4152 assert_eq!(
4153 network.ipam_driver().map(|value| value.value().expose()),
4154 Some("default")
4155 );
4156 Ok(())
4157 }
4158
4159 #[test]
4160 fn network_collections_retain_resets_provenance_and_redact_protected_values() -> Result<(), String> {
4161 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4162 let origin = crate::Provenance::source(source);
4163 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4164 let option = NetworkDriverOption::new(
4165 Sourced::from_source(id("com.example.token")?, origin.clone()),
4166 Sourced::from_source(ProtectedString::sensitive("never-print-this"), origin.clone()),
4167 )
4168 .map_err(|error| error.to_string())?;
4169 let label = MetadataLabel::new(id("com.example.label")?, ProtectedString::sensitive("also-private"));
4170
4171 network
4172 .set_driver_options_with_origins(vec![Sourced::from_source(option, origin.clone())], vec![origin.clone()]);
4173 network.set_labels_with_origins(vec![Sourced::from_source(label, origin.clone())], vec![origin.clone()]);
4174 network.set_ipam_configs_with_origins(Vec::new(), vec![origin]);
4175
4176 assert_eq!(network.driver_options().map(<[_]>::len), Some(1));
4177 assert_eq!(network.labels().map(<[_]>::len), Some(1));
4178 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4179 assert_eq!(network.driver_options_origins().len(), 1);
4180 assert_eq!(network.labels_origins().len(), 1);
4181 assert_eq!(network.ipam_configs_origins().len(), 1);
4182 let debug = format!("{network:?}");
4183 assert!(!debug.contains("never-print-this"));
4184 assert!(!debug.contains("also-private"));
4185 assert!(debug.contains("[REDACTED]"));
4186
4187 network.set_driver_options(Vec::new());
4188 network.set_labels(Vec::new());
4189 network.set_ipam_configs(Vec::new());
4190 assert_eq!(network.driver_options().map(<[_]>::len), Some(0));
4191 assert_eq!(network.labels().map(<[_]>::len), Some(0));
4192 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4193 assert!(network.driver_options_origins().is_empty());
4194 assert!(network.labels_origins().is_empty());
4195 assert!(network.ipam_configs_origins().is_empty());
4196 Ok(())
4197 }
4198
4199 #[test]
4200 fn network_ipam_rows_preserve_association_order_and_reject_subnetless_values() -> Result<(), String> {
4201 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4202 let origin = crate::Provenance::source(source);
4203 let mut first = NetworkIpamConfig::new(Sourced::from_source(
4204 ProtectedString::plain("10.10.0.0/24"),
4205 origin.clone(),
4206 ))
4207 .map_err(|error| error.to_string())?;
4208 first
4209 .set_gateway(Sourced::from_source(
4210 ProtectedString::plain("10.10.0.1"),
4211 origin.clone(),
4212 ))
4213 .map_err(|error| error.to_string())?;
4214 let mut second = NetworkIpamConfig::new(Sourced::from_source(
4215 ProtectedString::plain("fd00:10::/64"),
4216 origin.clone(),
4217 ))
4218 .map_err(|error| error.to_string())?;
4219 second
4220 .set_ip_range(Sourced::from_source(
4221 ProtectedString::plain("fd00:10::100/120"),
4222 origin.clone(),
4223 ))
4224 .map_err(|error| error.to_string())?;
4225
4226 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4227 network.set_ipam_configs_with_origins(
4228 vec![
4229 Sourced::from_source(first, origin.clone()),
4230 Sourced::from_source(second, origin),
4231 ],
4232 Vec::new(),
4233 );
4234 let rows = network
4235 .ipam_configs()
4236 .ok_or_else(|| "IPAM configs were omitted".to_owned())?;
4237 assert_eq!(rows.len(), 2);
4238 assert_eq!(rows[0].value().subnet().value().expose(), "10.10.0.0/24");
4239 assert_eq!(
4240 rows[0].value().gateway().map(|value| value.value().expose()),
4241 Some("10.10.0.1")
4242 );
4243 assert_eq!(rows[0].value().ip_range(), None);
4244 assert_eq!(rows[1].value().subnet().value().expose(), "fd00:10::/64");
4245 assert_eq!(rows[1].value().gateway(), None);
4246 assert_eq!(
4247 rows[1].value().ip_range().map(|value| value.value().expose()),
4248 Some("fd00:10::100/120")
4249 );
4250
4251 assert!(matches!(
4252 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain(""))),
4253 Err(ModelError::EmptyValue("network IPAM subnet"))
4254 ));
4255 assert!(matches!(
4256 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain("10.0.0.0/24\0bad"))),
4257 Err(ModelError::ContainsNul("network IPAM subnet"))
4258 ));
4259 assert!(matches!(
4260 NetworkDriverOption::new(
4261 Sourced::generated(id("option")?),
4262 Sourced::generated(ProtectedString::plain("bad\0value")),
4263 ),
4264 Err(ModelError::ContainsNul("network driver option value"))
4265 ));
4266 Ok(())
4267 }
4268
4269 #[test]
4270 fn image_artifact_resources_are_ordered_unique_and_referenced_explicitly() -> Result<(), String> {
4271 let mut application = Application::new(id("example")?);
4272 application
4273 .add_image_acquisition(Sourced::generated(ImageAcquisition::new(id("base-image")?)))
4274 .map_err(|error| error.to_string())?;
4275 application
4276 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4277 .map_err(|error| error.to_string())?;
4278
4279 let mut web = Service::new(id("web")?);
4280 web.set_image_acquisition(Sourced::generated(id("base-image")?));
4281 web.set_image_build(Sourced::generated(id("web-build")?));
4282 application
4283 .add_service(Sourced::generated(web))
4284 .map_err(|error| error.to_string())?;
4285
4286 assert_eq!(
4287 application.image_acquisitions()[0].value().name().as_str(),
4288 "base-image"
4289 );
4290 assert_eq!(application.image_builds()[0].value().name().as_str(), "web-build");
4291 assert!(matches!(
4292 application.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?))),
4293 Err(ModelError::DuplicateResource {
4294 kind: "image build",
4295 ..
4296 })
4297 ));
4298
4299 let mut missing = Service::new(id("missing")?);
4300 missing.set_image_build(Sourced::generated(id("absent-build")?));
4301 assert!(matches!(
4302 application.add_service(Sourced::generated(missing)),
4303 Err(ModelError::UnknownImageBuildReference { .. })
4304 ));
4305 Ok(())
4306 }
4307
4308 #[test]
4309 fn volume_keeps_logical_runtime_and_service_names_and_local_fields_distinct() -> Result<(), String> {
4310 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4311 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4312 volume.set_runtime_name(Sourced::from_source(
4313 ProtectedString::plain("production-data"),
4314 origin.clone(),
4315 ));
4316 volume.set_service_name(Sourced::from_source(
4317 ProtectedString::plain("data-volume.service"),
4318 origin.clone(),
4319 ));
4320 volume.set_driver(Sourced::from_source(ProtectedString::plain("local"), origin.clone()));
4321 volume.set_device(Sourced::from_source(
4322 ProtectedString::plain("/srv/data"),
4323 origin.clone(),
4324 ));
4325 volume.set_volume_type(Sourced::from_source(ProtectedString::plain("none"), origin.clone()));
4326 volume.set_options(Sourced::from_source(ProtectedString::plain("bind"), origin.clone()));
4327
4328 assert_eq!(volume.name().as_str(), "data");
4329 assert_eq!(
4330 volume.runtime_name().map(|name| name.value().expose()),
4331 Some("production-data")
4332 );
4333 assert_eq!(
4334 volume.service_name().map(|name| name.value().expose()),
4335 Some("data-volume.service")
4336 );
4337 assert_eq!(volume.driver().map(|value| value.value().expose()), Some("local"));
4338 assert_eq!(volume.device().map(|value| value.value().expose()), Some("/srv/data"));
4339 assert_eq!(volume.volume_type().map(|value| value.value().expose()), Some("none"));
4340 assert_eq!(volume.options().map(|value| value.value().expose()), Some("bind"));
4341 assert_eq!(
4342 volume.options().map(Sourced::origins),
4343 Some(std::slice::from_ref(&origin))
4344 );
4345 Ok(())
4346 }
4347
4348 #[test]
4349 fn volume_preserves_resets_order_protected_values_and_identity_dimensions() -> Result<(), String> {
4350 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4351 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4352 assert!(volume.labels().is_none());
4353 assert!(volume.containers_conf_modules().is_none());
4354 assert!(volume.global_args().is_none());
4355 assert!(volume.podman_args().is_none());
4356 volume.set_labels_with_origins(Vec::new(), vec![origin.clone()]);
4357 volume.set_containers_conf_modules_with_origins(Vec::new(), vec![origin.clone()]);
4358 volume.set_global_args_with_origins(
4359 vec![
4360 Sourced::from_source(ProtectedString::plain("--first"), origin.clone()),
4361 Sourced::from_source(ProtectedString::sensitive("--token=never-print"), origin.clone()),
4362 ],
4363 vec![origin.clone()],
4364 );
4365 volume.set_podman_args_with_origins(
4366 vec![
4367 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4368 Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
4369 ],
4370 vec![origin.clone()],
4371 );
4372 volume.set_user(Sourced::from_source(
4373 ProtectedString::plain("named-user"),
4374 origin.clone(),
4375 ));
4376 volume.set_group(Sourced::from_source(
4377 ProtectedString::plain("named-group"),
4378 origin.clone(),
4379 ));
4380 volume.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4381 volume.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin));
4382
4383 assert_eq!(volume.labels().map(<[_]>::len), Some(0));
4384 assert_eq!(volume.containers_conf_modules().map(<[_]>::len), Some(0));
4385 assert_eq!(volume.global_args().map(<[_]>::len), Some(2));
4386 assert_eq!(volume.podman_args().map(<[_]>::len), Some(2));
4387 assert_eq!(volume.user().map(|value| value.value().expose()), Some("named-user"));
4388 assert_eq!(volume.group().map(|value| value.value().expose()), Some("named-group"));
4389 assert_eq!(volume.uid().map(|value| value.value().expose()), Some("1001"));
4390 assert_eq!(volume.gid().map(|value| value.value().expose()), Some("1002"));
4391 let debug = format!("{volume:?}");
4392 assert!(!debug.contains("never-print"));
4393 assert!(debug.contains("[REDACTED]"));
4394 Ok(())
4395 }
4396
4397 #[test]
4398 fn volume_copy_and_image_sources_preserve_absence_and_typed_distinctions() -> Result<(), String> {
4399 let origin =
4400 crate::Provenance::source(crate::SourceId::new("cache.volume").map_err(|error| error.to_string())?);
4401 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4402 assert_eq!(volume.copy(), None);
4403 volume.set_copy(Sourced::from_source(false, origin.clone()));
4404 assert_eq!(volume.copy().map(Sourced::value), Some(&false));
4405 volume.set_copy(Sourced::from_source(true, origin.clone()));
4406 assert_eq!(volume.copy().map(Sourced::value), Some(&true));
4407
4408 volume
4409 .set_image_source(Sourced::from_source(
4410 VolumeImageSource::Literal(ProtectedString::sensitive("registry.example/private:1")),
4411 origin.clone(),
4412 ))
4413 .map_err(|error| error.to_string())?;
4414 assert!(matches!(
4415 volume.image_source().map(Sourced::value),
4416 Some(VolumeImageSource::Literal(_))
4417 ));
4418 assert!(!format!("{volume:?}").contains("registry.example/private:1"));
4419
4420 volume
4421 .set_image_source(Sourced::from_source(
4422 VolumeImageSource::ImageAcquisition(id("cache-image")?),
4423 origin.clone(),
4424 ))
4425 .map_err(|error| error.to_string())?;
4426 assert!(matches!(
4427 volume.image_source().map(Sourced::value),
4428 Some(VolumeImageSource::ImageAcquisition(name)) if name.as_str() == "cache-image"
4429 ));
4430 volume
4431 .set_image_source(Sourced::from_source(
4432 VolumeImageSource::ImageBuild(id("cache-build")?),
4433 origin,
4434 ))
4435 .map_err(|error| error.to_string())?;
4436 assert!(matches!(
4437 volume.image_source().map(Sourced::value),
4438 Some(VolumeImageSource::ImageBuild(name)) if name.as_str() == "cache-build"
4439 ));
4440 Ok(())
4441 }
4442
4443 #[test]
4444 fn volume_artifact_validation_is_deferred_and_explicit_edges_find_cycles() -> Result<(), String> {
4445 let mut application = Application::new(id("example")?);
4446 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4447 volume
4448 .set_image_source(Sourced::generated(VolumeImageSource::ImageBuild(id("cache-build")?)))
4449 .map_err(|error| error.to_string())?;
4450 application
4451 .add_volume(Sourced::generated(volume))
4452 .map_err(|error| error.to_string())?;
4453 assert!(matches!(
4454 application.validate_image_artifact_references(),
4455 Err(ModelError::UnknownVolumeImageBuildReference { .. })
4456 ));
4457
4458 application
4459 .add_image_build(Sourced::generated(ImageBuild::new(id("cache-build")?)))
4460 .map_err(|error| error.to_string())?;
4461 application
4462 .validate_image_artifact_references()
4463 .map_err(|error| error.to_string())?;
4464
4465 let volume_node = ArtifactDependencyNode::Volume(id("cache")?);
4466 let build_node = ArtifactDependencyNode::ImageBuild(id("cache-build")?);
4467 let dependencies = vec![
4468 Sourced::generated(ArtifactDependency::new(
4469 Sourced::generated(volume_node.clone()),
4470 Sourced::generated(build_node.clone()),
4471 )),
4472 Sourced::generated(ArtifactDependency::new(
4473 Sourced::generated(build_node),
4474 Sourced::generated(volume_node),
4475 )),
4476 ];
4477 assert!(matches!(
4478 application.validate_image_artifact_dependencies(&dependencies),
4479 Err(ModelError::ImageArtifactDependencyCycle { .. })
4480 ));
4481 let missing = vec![Sourced::generated(ArtifactDependency::new(
4482 Sourced::generated(ArtifactDependencyNode::ImageBuild(id("cache-build")?)),
4483 Sourced::generated(ArtifactDependencyNode::Volume(id("missing")?)),
4484 ))];
4485 assert!(matches!(
4486 application.validate_image_artifact_dependencies(&missing),
4487 Err(ModelError::UnknownArtifactDependencyNode { kind: "volume", .. })
4488 ));
4489 Ok(())
4490 }
4491
4492 #[test]
4493 fn volume_rejects_invalid_literal_image_values() -> Result<(), String> {
4494 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4495 assert!(matches!(
4496 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4497 ""
4498 )))),
4499 Err(ModelError::EmptyValue("volume image"))
4500 ));
4501 assert!(matches!(
4502 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4503 "bad\0image"
4504 )))),
4505 Err(ModelError::ContainsNul("volume image"))
4506 ));
4507 Ok(())
4508 }
4509
4510 #[test]
4511 fn collection_resets_retain_explicit_emptiness_and_clear_stale_origins() -> Result<(), String> {
4512 let origin =
4513 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
4514 let mut service = Service::new(id("web")?);
4515
4516 service.set_cap_add_with_origins(Vec::new(), vec![origin.clone()]);
4517 service.set_cap_drop_with_origins(Vec::new(), vec![origin.clone()]);
4518 service.set_tmpfs_with_origins(Vec::new(), vec![origin.clone()]);
4519 service.set_sysctls_with_origins(Vec::new(), vec![origin.clone()]);
4520 service.set_ulimits_with_origins(Vec::new(), vec![origin.clone()]);
4521 service.set_devices_with_origins(Vec::new(), vec![origin]);
4522
4523 assert_eq!(service.cap_add().map(<[_]>::len), Some(0));
4524 assert_eq!(service.cap_drop().map(<[_]>::len), Some(0));
4525 assert_eq!(service.tmpfs().map(<[_]>::len), Some(0));
4526 assert_eq!(service.sysctls().map(<[_]>::len), Some(0));
4527 assert_eq!(service.ulimits().map(<[_]>::len), Some(0));
4528 assert_eq!(service.devices().map(<[_]>::len), Some(0));
4529 assert_eq!(service.cap_add_origins().len(), 1);
4530 assert_eq!(service.cap_drop_origins().len(), 1);
4531 assert_eq!(service.tmpfs_origins().len(), 1);
4532 assert_eq!(service.sysctls_origins().len(), 1);
4533 assert_eq!(service.ulimits_origins().len(), 1);
4534 assert_eq!(service.devices_origins().len(), 1);
4535
4536 service.set_cap_add(Vec::new());
4537 service.set_cap_drop(Vec::new());
4538 service.set_tmpfs(Vec::new());
4539 service.set_sysctls(Vec::<Sourced<KernelParameter>>::new());
4540 service.set_ulimits(Vec::<Sourced<ResourceLimit>>::new());
4541 service.set_devices(Vec::<Sourced<Device>>::new());
4542
4543 assert!(service.cap_add_origins().is_empty());
4544 assert!(service.cap_drop_origins().is_empty());
4545 assert!(service.tmpfs_origins().is_empty());
4546 assert!(service.sysctls_origins().is_empty());
4547 assert!(service.ulimits_origins().is_empty());
4548 assert!(service.devices_origins().is_empty());
4549 Ok(())
4550 }
4551
4552 #[test]
4553 fn restart_policy_keeps_unlimited_and_finite_on_failure_distinct() {
4554 let finite = std::num::NonZeroU64::new(4);
4555 assert_eq!(RestartPolicy::on_failure(None).maximum_retries(), None);
4556 assert_eq!(RestartPolicy::on_failure(finite).maximum_retries(), finite);
4557 assert_eq!(RestartPolicy::Always.maximum_retries(), None);
4558 }
4559
4560 #[test]
4561 fn metadata_labels_preserve_empty_and_protected_values() -> Result<(), String> {
4562 let empty = MetadataLabel::new(id("com.example.empty")?, ProtectedString::plain(""));
4563 let protected = MetadataLabel::new(id("com.example.token")?, ProtectedString::sensitive("never-print-this"));
4564 let mut service = Service::new(id("web")?);
4565 service.add_label(Sourced::generated(empty));
4566 service.add_label(Sourced::generated(protected));
4567
4568 assert_eq!(service.labels()[0].value().value().expose(), "");
4569 let debug = format!("{:?}", service.labels()[1]);
4570 assert!(!debug.contains("never-print-this"));
4571 assert!(debug.contains("[REDACTED]"));
4572 Ok(())
4573 }
4574
4575 #[test]
4576 fn environment_files_preserve_order_options_provenance_and_redaction() -> Result<(), String> {
4577 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4578 let origin = crate::Provenance::source(source);
4579 let mut service = Service::new(id("web")?);
4580 service.add_environment_file(Sourced::from_source(
4581 EnvironmentFile::new(ProtectedString::plain("./base.env"), EnvironmentFileSyntax::Short)
4582 .map_err(|error| error.to_string())?,
4583 origin.clone(),
4584 ));
4585 let mut local = EnvironmentFile::new(ProtectedString::sensitive("./private.env"), EnvironmentFileSyntax::Long)
4586 .map_err(|error| error.to_string())?;
4587 local.set_required(Sourced::from_source(false, origin.clone()));
4588 local.set_format(Sourced::from_source(EnvironmentFileFormat::Raw, origin.clone()));
4589 service.add_environment_file(Sourced::from_source(local, origin));
4590
4591 assert_eq!(service.environment_files().len(), 2);
4592 assert_eq!(service.environment_files()[0].value().path().expose(), "./base.env");
4593 assert_eq!(
4594 service.environment_files()[0].value().syntax(),
4595 EnvironmentFileSyntax::Short
4596 );
4597 assert!(service.environment_files()[0].value().is_required());
4598 let local = service.environment_files()[1].value();
4599 assert_eq!(local.syntax(), EnvironmentFileSyntax::Long);
4600 assert!(!local.is_required());
4601 assert_eq!(local.required().map_or(0, |value| value.origins().len()), 1);
4602 assert!(matches!(
4603 local.format().map(Sourced::value),
4604 Some(EnvironmentFileFormat::Raw)
4605 ));
4606 let debug = format!("{service:?}");
4607 assert!(!debug.contains("private.env"));
4608 assert!(debug.contains("[REDACTED]"));
4609 assert!(matches!(
4610 EnvironmentFile::new(ProtectedString::plain(""), EnvironmentFileSyntax::Short),
4611 Err(ModelError::EmptyValue("environment-file path"))
4612 ));
4613 Ok(())
4614 }
4615
4616 #[test]
4617 fn service_groups_preserve_order_and_reject_ambiguous_membership() -> Result<(), String> {
4618 let mut application = Application::new(id("example")?);
4619 for name in ["web", "worker"] {
4620 application
4621 .add_service(Sourced::generated(Service::new(id(name)?)))
4622 .map_err(|error| error.to_string())?;
4623 }
4624
4625 let mut frontend = ServiceGroup::new(id("frontend")?, ResourceOwnership::Uncertain);
4626 frontend
4627 .add_member(Sourced::generated(id("web")?))
4628 .map_err(|error| error.to_string())?;
4629 assert!(matches!(
4630 frontend.add_member(Sourced::generated(id("web")?)),
4631 Err(ModelError::DuplicateServiceGroupMember { .. })
4632 ));
4633 application
4634 .add_service_group(Sourced::generated(frontend))
4635 .map_err(|error| error.to_string())?;
4636
4637 assert_eq!(application.service_groups()[0].value().name().as_str(), "frontend");
4638 assert_eq!(
4639 application.service_groups()[0].value().members()[0].value().as_str(),
4640 "web"
4641 );
4642
4643 let mut conflicting = ServiceGroup::new(id("backend")?, ResourceOwnership::Application);
4644 conflicting
4645 .add_member(Sourced::generated(id("web")?))
4646 .map_err(|error| error.to_string())?;
4647 assert!(matches!(
4648 application.add_service_group(Sourced::generated(conflicting)),
4649 Err(ModelError::ServiceInMultipleGroups { .. })
4650 ));
4651
4652 let mut missing = ServiceGroup::new(id("missing")?, ResourceOwnership::External);
4653 missing
4654 .add_member(Sourced::generated(id("database")?))
4655 .map_err(|error| error.to_string())?;
4656 assert!(matches!(
4657 application.add_service_group(Sourced::generated(missing)),
4658 Err(ModelError::UnknownServiceGroupMember { .. })
4659 ));
4660 Ok(())
4661 }
4662
4663 #[test]
4664 fn group_runtime_keeps_group_names_and_pod_settings_distinct() -> Result<(), String> {
4665 let source = crate::SourceId::new("pod.pod").map_err(|error| error.to_string())?;
4666 let origin = crate::Provenance::source(source);
4667 let mut group = ServiceGroup::new(id("frontend")?, ResourceOwnership::Application);
4668 let mut runtime = ServiceGroupRuntime::new();
4669 runtime.set_runtime_name(Sourced::from_source(
4670 ProtectedString::plain("production-frontend"),
4671 origin.clone(),
4672 ));
4673 runtime.set_service_name(Sourced::from_source(
4674 ProtectedString::plain("frontend-pod"),
4675 origin.clone(),
4676 ));
4677 runtime.set_host_mappings_with_origins(
4678 vec![Sourced::from_source(
4679 HostMapping::new(
4680 id("host.docker.internal")?,
4681 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
4682 ),
4683 origin.clone(),
4684 )],
4685 vec![origin.clone()],
4686 );
4687 runtime.set_ports_with_origins(Vec::new(), vec![origin.clone()]);
4688 runtime.set_networks_with_origins(
4689 vec![Sourced::from_source(
4690 NetworkAttachment::new(
4691 id("edge")?,
4692 vec![Sourced::from_source(
4693 ProtectedString::sensitive("private-alias"),
4694 origin.clone(),
4695 )],
4696 ),
4697 origin.clone(),
4698 )],
4699 vec![origin.clone()],
4700 );
4701 runtime.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4702 runtime.set_mounts_with_origins(
4703 vec![Sourced::from_source(
4704 Mount::new(MountSource::Anonymous, "/cache", false).map_err(|error| error.to_string())?,
4705 origin.clone(),
4706 )],
4707 vec![origin.clone()],
4708 );
4709 runtime.set_shm_size(Sourced::from_source(ProtectedString::sensitive("64m"), origin.clone()));
4710 runtime.set_exit_policy(Sourced::from_source(
4711 GroupExitPolicy::Raw(ProtectedString::sensitive("preserve-this")),
4712 origin.clone(),
4713 ));
4714 runtime.set_stop_timeout(Sourced::from_source(
4715 StopTimeout::new("30s").map_err(|error| error.to_string())?,
4716 origin.clone(),
4717 ));
4718 assert!(matches!(
4719 runtime.replace_network(1, Sourced::generated(NetworkAttachment::new(id("other")?, Vec::new()))),
4720 Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index: 1, len: 1 })
4721 ));
4722 group.set_runtime(Sourced::from_source(runtime, origin));
4723
4724 let runtime = group
4725 .runtime()
4726 .ok_or_else(|| "group runtime was omitted".to_owned())?
4727 .value();
4728 assert_eq!(group.name().as_str(), "frontend");
4729 assert_eq!(
4730 runtime.runtime_name().map(|name| name.value().expose()),
4731 Some("production-frontend")
4732 );
4733 assert_eq!(
4734 runtime.service_name().map(|name| name.value().expose()),
4735 Some("frontend-pod")
4736 );
4737 assert_eq!(runtime.host_mappings().map(<[_]>::len), Some(1));
4738 assert_eq!(runtime.ports().map(<[_]>::len), Some(0));
4739 assert_eq!(runtime.networks_origins().len(), 1);
4740 assert_eq!(runtime.mounts().map(<[_]>::len), Some(1));
4741 assert!(matches!(
4742 runtime.exit_policy().map(Sourced::value),
4743 Some(GroupExitPolicy::Raw(_))
4744 ));
4745 let debug = format!("{group:?}");
4746 for sensitive in ["private-alias", "64m", "preserve-this"] {
4747 assert!(!debug.contains(sensitive));
4748 }
4749 assert!(debug.contains("[REDACTED]"));
4750 Ok(())
4751 }
4752
4753 #[test]
4754 fn rootfs_startup_notification_and_podman_args_preserve_safe_contracts() -> Result<(), String> {
4755 let source = crate::SourceId::new("web.container").map_err(|error| error.to_string())?;
4756 let origin = crate::Provenance::source(source);
4757 let mut service = Service::new(id("web")?);
4758 service.set_startup_notification(Sourced::from_source(StartupNotification::Healthy, origin.clone()));
4759 service.set_podman_args_with_origins(
4760 vec![
4761 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4762 Sourced::from_source(ProtectedString::sensitive("--secret=never-print"), origin.clone()),
4763 Sourced::from_source(ProtectedString::plain("--replace"), origin.clone()),
4764 ],
4765 vec![origin.clone()],
4766 );
4767 assert_eq!(service.podman_args().map(<[_]>::len), Some(3));
4768 assert_eq!(service.podman_args_origins(), std::slice::from_ref(&origin));
4769 assert!(matches!(
4770 service.startup_notification().map(Sourced::value),
4771 Some(StartupNotification::Healthy)
4772 ));
4773 assert!(!format!("{service:?}").contains("never-print"));
4774
4775 let mut with_image = Service::new(id("image-first")?);
4776 with_image.set_image(Sourced::generated(
4777 ImageReference::parse("example.invalid/web:1").map_err(|error| error.to_string())?,
4778 ));
4779 assert!(matches!(
4780 with_image.set_rootfs(Sourced::generated(ProtectedString::plain("/srv/rootfs"))),
4781 Err(ModelError::RootfsImageSourceConflict { source: "image", .. })
4782 ));
4783
4784 let mut with_rootfs = Service::new(id("rootfs-first")?);
4785 with_rootfs
4786 .set_rootfs(Sourced::generated(ProtectedString::sensitive("/private/rootfs")))
4787 .map_err(|error| error.to_string())?;
4788 with_rootfs.set_image_build(Sourced::generated(id("web-build")?));
4789 let mut application = Application::new(id("example")?);
4790 application
4791 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4792 .map_err(|error| error.to_string())?;
4793 assert!(matches!(
4794 application.add_service(Sourced::generated(with_rootfs)),
4795 Err(ModelError::RootfsImageSourceConflict {
4796 source: "image build",
4797 ..
4798 })
4799 ));
4800 Ok(())
4801 }
4802
4803 #[test]
4804 fn validates_raw_preserving_healthcheck_scalars() -> Result<(), String> {
4805 let duration = HealthcheckDuration::new("1m30s").map_err(|error| error.to_string())?;
4806 let retries = HealthcheckRetries::new("003").map_err(|error| error.to_string())?;
4807 assert_eq!(duration.as_str(), "1m30s");
4808 assert_eq!(retries.as_str(), "003");
4809 assert_eq!(
4810 HealthcheckRetries::new("three"),
4811 Err(ModelError::InvalidHealthcheckRetries)
4812 );
4813 assert!(matches!(
4814 HealthcheckDuration::new(""),
4815 Err(ModelError::EmptyValue("health-check duration"))
4816 ));
4817 Ok(())
4818 }
4819
4820 #[test]
4821 fn preserves_ordered_dependency_edges_and_field_provenance() -> Result<(), String> {
4822 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4823 let origin = crate::Provenance::source(source);
4824 let mut service = Service::new(id("web")?);
4825
4826 let mut database = ServiceDependency::new(id("database")?);
4827 database.set_condition(Sourced::from_source(
4828 ServiceDependencyCondition::Healthy,
4829 origin.clone(),
4830 ));
4831 database.set_required(Sourced::from_source(true, origin.clone()));
4832 service.add_dependency(Sourced::from_source(database, origin.clone()));
4833
4834 let cache = ServiceDependency::new(id("cache")?);
4835 assert!(cache.is_required());
4836 service.add_dependency(Sourced::from_source(cache, origin));
4837
4838 assert_eq!(
4839 service
4840 .dependencies()
4841 .iter()
4842 .map(|dependency| dependency.value().service().as_str())
4843 .collect::<Vec<_>>(),
4844 ["database", "cache"]
4845 );
4846 assert!(matches!(
4847 service.dependencies()[0].value().condition().map(Sourced::value),
4848 Some(ServiceDependencyCondition::Healthy)
4849 ));
4850 assert_eq!(service.dependencies()[0].origins().len(), 1);
4851 assert_eq!(
4852 service.dependencies()[0]
4853 .value()
4854 .condition()
4855 .map_or(0, |condition| condition.origins().len()),
4856 1
4857 );
4858 Ok(())
4859 }
4860
4861 #[test]
4862 fn retains_execution_identity_context_order_provenance_and_redaction() -> Result<(), String> {
4863 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4864 let origin = crate::Provenance::source(source);
4865 let mut service = Service::new(id("web")?);
4866
4867 service.set_user(Sourced::from_source(ProtectedString::sensitive("1001"), origin.clone()));
4868 service.set_group(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4869 service.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4870 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("audio"), origin.clone()));
4871 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("44"), origin.clone()));
4872 service.set_working_directory(Sourced::from_source(ProtectedString::plain("/srv/app"), origin.clone()));
4873 service.set_read_only_root_filesystem(Sourced::from_source(true, origin));
4874
4875 assert_eq!(service.user().map(|value| value.value().expose()), Some("1001"));
4876 assert_eq!(service.group().map(|value| value.value().expose()), Some("1002"));
4877 assert_eq!(
4878 service.user_namespace().map(|value| value.value().expose()),
4879 Some("keep-id")
4880 );
4881 assert_eq!(
4882 service
4883 .supplementary_groups()
4884 .iter()
4885 .map(|group| group.value().expose())
4886 .collect::<Vec<_>>(),
4887 ["audio", "44"]
4888 );
4889 assert_eq!(
4890 service.working_directory().map(|value| value.value().expose()),
4891 Some("/srv/app")
4892 );
4893 assert_eq!(service.read_only_root_filesystem().map(Sourced::value), Some(&true));
4894 assert_eq!(service.user().map_or(0, |value| value.origins().len()), 1);
4895 let debug = format!("{service:?}");
4896 assert!(!debug.contains("1001"));
4897 assert!(debug.contains("[REDACTED]"));
4898 Ok(())
4899 }
4900
4901 #[test]
4902 fn retains_config_secret_resources_grants_provenance_and_redaction() -> Result<(), String> {
4903 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4904 let origin = crate::Provenance::source(source);
4905 let mut application = Application::new(id("example")?);
4906
4907 let mut config = Config::new(id("settings")?, ResourceOwnership::Application);
4908 config.set_material(Sourced::from_source(
4909 ConfigMaterial::Content(ProtectedString::sensitive("private-config")),
4910 origin.clone(),
4911 ));
4912 application
4913 .add_config(Sourced::from_source(config, origin.clone()))
4914 .map_err(|error| error.to_string())?;
4915
4916 let mut secret = Secret::new(id("password")?, ResourceOwnership::External);
4917 secret.set_runtime_name(Sourced::from_source(
4918 ProtectedString::plain("production-password"),
4919 origin.clone(),
4920 ));
4921 secret.set_material(Sourced::from_source(
4922 SecretMaterial::Environment(ProtectedString::sensitive("private-environment-name")),
4923 origin.clone(),
4924 ));
4925 application
4926 .add_secret(Sourced::from_source(secret, origin.clone()))
4927 .map_err(|error| error.to_string())?;
4928
4929 let mut service = Service::new(id("web")?);
4930 service.add_config_grant(Sourced::from_source(
4931 ResourceGrant::new(ProtectedString::plain("settings"), ResourceGrantSyntax::Short)
4932 .map_err(|error| error.to_string())?,
4933 origin.clone(),
4934 ));
4935 let mut secret_grant = ResourceGrant::new(
4936 ProtectedString::sensitive("private-grant-source"),
4937 ResourceGrantSyntax::Long,
4938 )
4939 .map_err(|error| error.to_string())?;
4940 secret_grant.set_target(Sourced::from_source(
4941 ProtectedString::plain("database-password"),
4942 origin.clone(),
4943 ));
4944 secret_grant.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4945 secret_grant.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4946 secret_grant.set_mode(Sourced::from_source(ProtectedString::plain("0440"), origin.clone()));
4947 service.add_secret_grant(Sourced::from_source(secret_grant, origin.clone()));
4948 application
4949 .add_service(Sourced::from_source(service, origin))
4950 .map_err(|error| error.to_string())?;
4951
4952 assert_eq!(application.configs().len(), 1);
4953 assert_eq!(application.secrets().len(), 1);
4954 assert_eq!(application.services()[0].value().config_grants().len(), 1);
4955 let grant = &application.services()[0].value().secret_grants()[0];
4956 assert_eq!(grant.value().syntax(), ResourceGrantSyntax::Long);
4957 assert_eq!(
4958 grant.value().target().map(|value| value.value().expose()),
4959 Some("database-password")
4960 );
4961 assert_eq!(grant.value().uid().map_or(0, |value| value.origins().len()), 1);
4962 assert_eq!(grant.origins().len(), 1);
4963 let debug = format!("{application:?}");
4964 for secret in ["private-config", "private-environment-name", "private-grant-source"] {
4965 assert!(!debug.contains(secret));
4966 }
4967 assert!(debug.contains("[REDACTED]"));
4968
4969 assert!(matches!(
4970 ResourceGrant::new(ProtectedString::plain(""), ResourceGrantSyntax::Short),
4971 Err(ModelError::EmptyValue("resource grant source"))
4972 ));
4973 assert!(matches!(
4974 application.add_config(Sourced::generated(Config::new(
4975 id("settings")?,
4976 ResourceOwnership::External,
4977 ))),
4978 Err(ModelError::DuplicateResource { kind: "config", .. })
4979 ));
4980 assert!(matches!(
4981 application.add_secret(Sourced::generated(Secret::new(
4982 id("password")?,
4983 ResourceOwnership::External,
4984 ))),
4985 Err(ModelError::DuplicateResource { kind: "secret", .. })
4986 ));
4987 Ok(())
4988 }
4989
4990 #[test]
4991 fn host_mappings_preserve_order_spelling_and_runtime_tokens() -> Result<(), String> {
4992 let mut service = Service::new(id("web")?);
4993 service.add_host_mapping(Sourced::generated(HostMapping::new(
4994 id("host.docker.internal")?,
4995 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
4996 )));
4997 service.add_host_mapping(Sourced::generated(HostMapping::new(
4998 id("ipv6")?,
4999 HostAddress::new("[::1]").map_err(|error| error.to_string())?,
5000 )));
5001
5002 assert_eq!(service.host_mappings().len(), 2);
5003 assert_eq!(
5004 service.host_mappings()[0].value().address().kind(),
5005 HostAddressKind::HostGateway
5006 );
5007 assert_eq!(service.host_mappings()[1].value().address().raw(), "[::1]");
5008 assert_eq!(
5009 service.host_mappings()[1].value().address().kind(),
5010 HostAddressKind::Ipv6 { bracketed: true }
5011 );
5012 assert!(matches!(HostAddress::new(""), Err(ModelError::EmptyValue(_))));
5013 Ok(())
5014 }
5015
5016 #[test]
5017 fn dns_collections_preserve_order_provenance_and_explicit_empty_state() -> Result<(), String> {
5018 let mut service = Service::new(id("web")?);
5019 assert!(service.dns_servers().is_none());
5020 service.set_dns_servers(Vec::new());
5021 assert!(matches!(service.dns_servers(), Some(values) if values.is_empty()));
5022 service.set_dns_options(vec![
5023 Sourced::generated(ProtectedString::plain("ndots:5")),
5024 Sourced::generated(ProtectedString::sensitive("rotate")),
5025 ]);
5026 service.set_dns_search_domains(vec![Sourced::generated(ProtectedString::plain("example.test"))]);
5027 assert_eq!(
5028 service
5029 .dns_options()
5030 .unwrap_or_default()
5031 .iter()
5032 .map(|value| value.value().expose())
5033 .collect::<Vec<_>>(),
5034 ["ndots:5", "rotate"]
5035 );
5036 assert!(!format!("{service:?}").contains("rotate"));
5037 Ok(())
5038 }
5039
5040 #[test]
5041 fn security_options_preserve_empty_order_duplicates_provenance_and_redaction() -> Result<(), String> {
5042 let origin =
5043 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5044 let mut service = Service::new(id("web")?);
5045
5046 assert!(service.security_options().is_none());
5047 service.set_security_options_with_origins(Vec::new(), vec![origin.clone()]);
5048 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5049 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5050
5051 service.set_security_options_with_origins(
5052 vec![
5053 Sourced::from_source(
5054 SecurityOption::AppArmor(ProtectedString::sensitive("apparmor-secret")),
5055 origin.clone(),
5056 ),
5057 Sourced::from_source(SecurityOption::NoNewPrivileges(true), origin.clone()),
5058 Sourced::from_source(
5059 SecurityOption::SeccompProfile(ProtectedString::sensitive("seccomp-secret")),
5060 origin.clone(),
5061 ),
5062 Sourced::from_source(SecurityOption::SecurityLabelDisable(false), origin.clone()),
5063 Sourced::from_source(
5064 SecurityOption::SecurityLabelFileType(ProtectedString::sensitive("file-type-secret")),
5065 origin.clone(),
5066 ),
5067 Sourced::from_source(
5068 SecurityOption::SecurityLabelLevel(ProtectedString::sensitive("level-secret")),
5069 origin.clone(),
5070 ),
5071 Sourced::from_source(SecurityOption::SecurityLabelNested(true), origin.clone()),
5072 Sourced::from_source(
5073 SecurityOption::SecurityLabelType(ProtectedString::sensitive("type-secret")),
5074 origin.clone(),
5075 ),
5076 Sourced::from_source(
5077 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5078 origin.clone(),
5079 ),
5080 Sourced::from_source(
5081 SecurityOption::Unmask(ProtectedString::sensitive("unmask-secret")),
5082 origin.clone(),
5083 ),
5084 Sourced::from_source(
5085 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5086 origin.clone(),
5087 ),
5088 ],
5089 vec![origin.clone()],
5090 );
5091
5092 let options = service.security_options().unwrap_or_default();
5093 assert_eq!(options.len(), 11);
5094 assert!(
5095 matches!(options[0].value(), SecurityOption::AppArmor(profile) if profile.expose() == "apparmor-secret")
5096 );
5097 assert!(matches!(options[1].value(), SecurityOption::NoNewPrivileges(true)));
5098 assert!(
5099 matches!(options[2].value(), SecurityOption::SeccompProfile(profile) if profile.expose() == "seccomp-secret")
5100 );
5101 assert!(matches!(
5102 options[3].value(),
5103 SecurityOption::SecurityLabelDisable(false)
5104 ));
5105 assert!(
5106 matches!(options[4].value(), SecurityOption::SecurityLabelFileType(profile) if profile.expose() == "file-type-secret")
5107 );
5108 assert!(
5109 matches!(options[5].value(), SecurityOption::SecurityLabelLevel(profile) if profile.expose() == "level-secret")
5110 );
5111 assert!(matches!(options[6].value(), SecurityOption::SecurityLabelNested(true)));
5112 assert!(
5113 matches!(options[7].value(), SecurityOption::SecurityLabelType(profile) if profile.expose() == "type-secret")
5114 );
5115 assert!(matches!(options[8].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5116 assert!(matches!(options[9].value(), SecurityOption::Unmask(path) if path.expose() == "unmask-secret"));
5117 assert!(matches!(options[10].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5118 assert_eq!(options[0].origins(), std::slice::from_ref(&origin));
5119 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5120
5121 let debug = format!("{service:?}");
5122 for secret in [
5123 "apparmor-secret",
5124 "seccomp-secret",
5125 "file-type-secret",
5126 "level-secret",
5127 "type-secret",
5128 "mask-secret",
5129 "unmask-secret",
5130 ] {
5131 assert!(!debug.contains(secret));
5132 }
5133 assert!(debug.contains("[REDACTED]"));
5134
5135 service.set_security_options(Vec::new());
5136 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5137 assert!(service.security_options_origins().is_empty());
5138 Ok(())
5139 }
5140
5141 #[test]
5142 fn retains_entrypoint_run_init_stop_pull_memory_and_exposed_port_intent() -> Result<(), String> {
5143 let origin =
5144 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5145 let mut service = Service::new(id("web")?);
5146 service.set_command(Sourced::from_source(
5147 Command::Exec(vec![ProtectedString::plain("serve")]),
5148 origin.clone(),
5149 ));
5150 service.set_entrypoint(Sourced::from_source(
5151 Entrypoint::Shell(ProtectedString::sensitive("/bin/sh -c private-entrypoint")),
5152 origin.clone(),
5153 ));
5154 service.set_run_init(Sourced::from_source(true, origin.clone()));
5155 service.set_stop_timeout(Sourced::from_source(
5156 StopTimeout::new("01m30s").map_err(|error| error.to_string())?,
5157 origin.clone(),
5158 ));
5159 service.set_pull_policy(Sourced::from_source(
5160 PullPolicy::Every(ProtectedString::sensitive("12h")),
5161 origin.clone(),
5162 ));
5163 service.set_memory_limit(Sourced::from_source(
5164 ProtectedString::sensitive("512MiB"),
5165 origin.clone(),
5166 ));
5167 assert!(service.exposed_ports().is_none());
5168 service.set_exposed_ports_with_origins(Vec::new(), vec![origin.clone()]);
5169 assert_eq!(service.exposed_ports().map(<[_]>::len), Some(0));
5170 assert_eq!(service.exposed_ports_origins(), std::slice::from_ref(&origin));
5171 service.add_exposed_port(Sourced::from_source(
5172 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5173 origin.clone(),
5174 ));
5175 service.add_exposed_port(Sourced::from_source(
5176 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5177 origin,
5178 ));
5179
5180 assert!(matches!(service.command().map(Sourced::value), Some(Command::Exec(_))));
5181 assert!(matches!(
5182 service.entrypoint().map(Sourced::value),
5183 Some(Entrypoint::Shell(_))
5184 ));
5185 assert_eq!(service.run_init().map(Sourced::value), Some(&true));
5186 assert_eq!(
5187 service.stop_timeout().map(|timeout| timeout.value().as_str()),
5188 Some("01m30s")
5189 );
5190 assert!(matches!(
5191 service.pull_policy().map(Sourced::value),
5192 Some(PullPolicy::Every(_))
5193 ));
5194 assert_eq!(
5195 service.memory_limit().map(|limit| limit.value().expose()),
5196 Some("512MiB")
5197 );
5198 let exposed_ports = service.exposed_ports().ok_or("missing exposed ports")?;
5199 assert_eq!(exposed_ports.len(), 2);
5200 assert_eq!(exposed_ports[0].value().container(), 8080);
5201 assert_eq!(exposed_ports[0].value().protocol(), &Protocol::Tcp);
5202 assert!(matches!(
5203 ExposedPort::new(0, Protocol::Udp),
5204 Err(ModelError::ZeroContainerPort)
5205 ));
5206 assert!(matches!(
5207 StopTimeout::new(""),
5208 Err(ModelError::EmptyValue("stop timeout"))
5209 ));
5210
5211 let debug = format!("{service:?}");
5212 for secret in ["private-entrypoint", "512MiB", "12h"] {
5213 assert!(!debug.contains(secret));
5214 }
5215 assert!(debug.contains("[REDACTED]"));
5216 Ok(())
5217 }
5218
5219 #[test]
5220 fn annotations_and_logging_preserve_empty_order_field_provenance_and_redaction() -> Result<(), String> {
5221 let origin =
5222 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5223 let mut service = Service::new(id("web")?);
5224
5225 assert!(service.annotations().is_none());
5226 service.set_annotations_with_origins(Vec::new(), vec![origin.clone()]);
5227 assert_eq!(service.annotations().map(<[_]>::len), Some(0));
5228 assert_eq!(service.annotations_origins(), std::slice::from_ref(&origin));
5229
5230 service.set_annotations_with_origins(
5231 vec![
5232 Sourced::from_source(
5233 Annotation::new(
5234 Sourced::from_source(id("io.example.first")?, origin.clone()),
5235 Sourced::from_source(ProtectedString::sensitive("annotation-secret"), origin.clone()),
5236 ),
5237 origin.clone(),
5238 ),
5239 Sourced::from_source(
5240 Annotation::new(
5241 Sourced::from_source(id("io.example.second")?, origin.clone()),
5242 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5243 ),
5244 origin.clone(),
5245 ),
5246 ],
5247 vec![origin.clone()],
5248 );
5249 let annotations = service.annotations().unwrap_or_default();
5250 assert_eq!(annotations.len(), 2);
5251 assert_eq!(annotations[0].value().name().value().as_str(), "io.example.first");
5252 assert_eq!(annotations[1].value().value().value().expose(), "");
5253 assert_eq!(annotations[0].value().name().origins(), std::slice::from_ref(&origin));
5254 assert_eq!(annotations[0].value().value().origins(), std::slice::from_ref(&origin));
5255
5256 let mut logging = Logging::new();
5257 assert!(logging.options().is_none());
5258 logging.set_driver(Sourced::from_source(ProtectedString::plain("journald"), origin.clone()));
5259 logging.set_options_with_origins(
5260 vec![
5261 Sourced::from_source(
5262 LoggingOption::new(
5263 Sourced::from_source(id("tag")?, origin.clone()),
5264 Sourced::from_source(ProtectedString::sensitive("logging-secret"), origin.clone()),
5265 ),
5266 origin.clone(),
5267 ),
5268 Sourced::from_source(
5269 LoggingOption::new(
5270 Sourced::from_source(id("labels")?, origin.clone()),
5271 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5272 ),
5273 origin.clone(),
5274 ),
5275 ],
5276 vec![origin.clone()],
5277 );
5278 service.set_logging(Sourced::from_source(logging, origin));
5279
5280 let logging = service.logging().map(Sourced::value).ok_or("missing logging")?;
5281 assert_eq!(logging.driver().map(|driver| driver.value().expose()), Some("journald"));
5282 assert_eq!(logging.options().map(<[_]>::len), Some(2));
5283 assert_eq!(
5284 logging.options().unwrap_or_default()[0].value().name().value().as_str(),
5285 "tag"
5286 );
5287 assert_eq!(logging.options_origins().len(), 1);
5288 let debug = format!("{service:?}");
5289 assert!(!debug.contains("annotation-secret"));
5290 assert!(!debug.contains("logging-secret"));
5291 assert!(debug.contains("[REDACTED]"));
5292 Ok(())
5293 }
5294
5295 #[test]
5296 fn network_attachments_retain_alias_provenance_and_redact_sensitive_values() -> Result<(), String> {
5297 let origin =
5298 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5299 let mut attachment = NetworkAttachment::new(
5300 id("frontend")?,
5301 vec![
5302 Sourced::from_source(ProtectedString::plain("web"), origin.clone()),
5303 Sourced::from_source(ProtectedString::sensitive("private-alias"), origin.clone()),
5304 ],
5305 );
5306 attachment.set_ipv4_address(Sourced::from_source(
5307 ProtectedString::plain("192.0.2.10"),
5308 origin.clone(),
5309 ));
5310 attachment.set_ipv6_address(Sourced::from_source(
5311 ProtectedString::plain("2001:db8::10"),
5312 origin.clone(),
5313 ));
5314 let metrics = Sourced::generated(ProtectedString::plain("metrics"));
5315 attachment.add_alias(&metrics);
5316
5317 assert_eq!(attachment.aliases(), ["web", "private-alias", "metrics"]);
5318 assert_eq!(attachment.alias_sensitivities(), [false, true, false]);
5319 assert_eq!(attachment.alias_origins().len(), 3);
5320 assert_eq!(attachment.alias_origins()[0].len(), 1);
5321 assert_eq!(attachment.alias_origins()[1], std::slice::from_ref(&origin));
5322 assert!(attachment.alias_origins()[2].is_empty());
5323 assert_eq!(
5324 attachment.ipv4_address().map(|address| address.value().expose()),
5325 Some("192.0.2.10")
5326 );
5327 assert_eq!(
5328 attachment.ipv6_address().map(|address| address.value().expose()),
5329 Some("2001:db8::10")
5330 );
5331 let debug = format!("{attachment:?}");
5332 assert!(!debug.contains("private-alias"));
5333 assert!(debug.contains("[REDACTED]"));
5334
5335 let mut service = Service::new(id("web")?);
5336 service.add_network(Sourced::generated(NetworkAttachment::new(
5337 id("previous")?,
5338 vec![Sourced::generated(ProtectedString::plain("previous-alias"))],
5339 )));
5340 let previous = service
5341 .replace_network(0, Sourced::generated(attachment))
5342 .map_err(|error| error.to_string())?;
5343 assert_eq!(previous.value().network().as_str(), "previous");
5344 assert_eq!(service.networks()[0].value().network().as_str(), "frontend");
5345 assert!(matches!(
5346 service.replace_network(1, Sourced::generated(NetworkAttachment::new(id("unused")?, Vec::new()))),
5347 Err(ModelError::UnknownNetworkAttachmentIndex { index: 1, len: 1 })
5348 ));
5349 Ok(())
5350 }
5351
5352 #[test]
5353 fn reload_action_is_one_explicit_command_or_signal() -> Result<(), String> {
5354 let origin =
5355 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5356 let mut service = Service::new(id("web")?);
5357 service.set_reload_action(Sourced::from_source(
5358 ReloadAction::Command(Command::Exec(vec![ProtectedString::plain("reload")])),
5359 origin.clone(),
5360 ));
5361 assert!(matches!(
5362 service.reload_action().map(Sourced::value),
5363 Some(ReloadAction::Command(Command::Exec(_)))
5364 ));
5365
5366 service.set_reload_action(Sourced::from_source(
5367 ReloadAction::Signal(ProtectedString::sensitive("SIGHUP")),
5368 origin,
5369 ));
5370 assert!(matches!(
5371 service.reload_action().map(Sourced::value),
5372 Some(ReloadAction::Signal(_))
5373 ));
5374 let debug = format!("{service:?}");
5375 assert!(!debug.contains("SIGHUP"));
5376 assert!(debug.contains("[REDACTED]"));
5377 Ok(())
5378 }
5379
5380 fn id(value: &str) -> Result<Identifier, String> {
5381 Identifier::new(value).map_err(|error| error.to_string())
5382 }
5383}