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 UnknownNativeEvidenceOwner {
36 kind: &'static str,
38 name: String,
40 },
41 MissingNativeEvidenceProvenance {
43 component: &'static str,
45 },
46 EmptyNativeEvidenceEvent,
48 UnprotectedNativeEvidenceSegment,
50 DuplicateServiceGroupMember {
52 group: String,
54 service: String,
56 },
57 UnknownServiceGroupMember {
59 group: String,
61 service: String,
63 },
64 ServiceInMultipleGroups {
66 service: String,
68 existing: String,
70 replacement: String,
72 },
73 UnknownImageAcquisitionReference {
75 service: String,
77 acquisition: String,
79 },
80 UnknownImageBuildReference {
82 service: String,
84 build: String,
86 },
87 UnknownVolumeImageAcquisitionReference {
89 volume: String,
91 acquisition: String,
93 },
94 UnknownVolumeImageBuildReference {
96 volume: String,
98 build: String,
100 },
101 UnknownArtifactDependencyNode {
103 kind: &'static str,
105 name: String,
107 },
108 ImageArtifactDependencyCycle {
110 nodes: Vec<String>,
112 },
113 InvalidImageReference(&'static str),
115 ZeroContainerPort,
117 UnknownNetworkAttachmentIndex {
119 index: usize,
121 len: usize,
123 },
124 UnknownServiceGroupRuntimeNetworkIndex {
126 index: usize,
128 len: usize,
130 },
131 RootfsImageSourceConflict {
133 service: String,
135 source: &'static str,
137 },
138 InvalidHealthcheckRetries,
140}
141
142impl fmt::Display for ModelError {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::EmptyValue(kind) => write!(formatter, "{kind} must not be empty"),
146 Self::ContainsNul(kind) => write!(formatter, "{kind} must not contain a NUL byte"),
147 Self::ReversedSpan { start, end } => {
148 write!(formatter, "source span end {end} is before start {start}")
149 }
150 Self::DuplicateResource { kind, name } => {
151 write!(formatter, "duplicate {kind} `{name}`")
152 }
153 Self::UnknownNativeEvidenceOwner { kind, name } => {
154 write!(formatter, "retained native evidence references unknown {kind} `{name}`")
155 }
156 Self::MissingNativeEvidenceProvenance { component } => {
157 write!(
158 formatter,
159 "retained native evidence {component} must carry source provenance"
160 )
161 }
162 Self::EmptyNativeEvidenceEvent => {
163 formatter.write_str("retained native evidence must contain at least one physical source segment")
164 }
165 Self::UnprotectedNativeEvidenceSegment => {
166 formatter.write_str("retained native evidence physical segments must be sensitive")
167 }
168 Self::DuplicateServiceGroupMember { group, service } => {
169 write!(
170 formatter,
171 "service group `{group}` contains duplicate member `{service}`"
172 )
173 }
174 Self::UnknownServiceGroupMember { group, service } => {
175 write!(
176 formatter,
177 "service group `{group}` references unknown service `{service}`"
178 )
179 }
180 Self::ServiceInMultipleGroups {
181 service,
182 existing,
183 replacement,
184 } => write!(
185 formatter,
186 "service `{service}` belongs to both service groups `{existing}` and `{replacement}`"
187 ),
188 Self::UnknownImageAcquisitionReference { service, acquisition } => write!(
189 formatter,
190 "service `{service}` references unknown image acquisition `{acquisition}`"
191 ),
192 Self::UnknownImageBuildReference { service, build } => {
193 write!(
194 formatter,
195 "service `{service}` references unknown image build `{build}`"
196 )
197 }
198 Self::UnknownVolumeImageAcquisitionReference { volume, acquisition } => write!(
199 formatter,
200 "volume `{volume}` references unknown image acquisition `{acquisition}`"
201 ),
202 Self::UnknownVolumeImageBuildReference { volume, build } => {
203 write!(formatter, "volume `{volume}` references unknown image build `{build}`")
204 }
205 Self::UnknownArtifactDependencyNode { kind, name } => {
206 write!(formatter, "artifact dependency references unknown {kind} `{name}`")
207 }
208 Self::ImageArtifactDependencyCycle { nodes } => {
209 write!(formatter, "image-artifact dependency cycle: {}", nodes.join(" -> "))
210 }
211 Self::InvalidImageReference(reason) => write!(formatter, "invalid image reference: {reason}"),
212 Self::ZeroContainerPort => formatter.write_str("container port must not be zero"),
213 Self::UnknownNetworkAttachmentIndex { index, len } => {
214 write!(
215 formatter,
216 "network attachment index {index} is outside collection length {len}"
217 )
218 }
219 Self::UnknownServiceGroupRuntimeNetworkIndex { index, len } => {
220 write!(
221 formatter,
222 "group-runtime network attachment index {index} is outside collection length {len}"
223 )
224 }
225 Self::RootfsImageSourceConflict { service, source } => write!(
226 formatter,
227 "service `{service}` combines rootfs with image source `{source}`"
228 ),
229 Self::InvalidHealthcheckRetries => {
230 formatter.write_str("health-check retries must be a non-negative decimal integer")
231 }
232 }
233 }
234}
235
236impl Error for ModelError {}
237
238#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
240pub struct Identifier(String);
241
242impl Identifier {
243 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
249 let value = value.into();
250 validate_text("identifier", &value)?;
251 Ok(Self(value))
252 }
253
254 #[must_use]
256 pub fn as_str(&self) -> &str {
257 &self.0
258 }
259}
260
261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
263#[non_exhaustive]
264pub enum ResourceOwnership {
265 Application,
267 External,
269 Implicit,
271 Uncertain,
273}
274
275#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
280#[non_exhaustive]
281pub enum RetainedNativeEvidenceSubject {
282 QuadletServicePodmanArgs(Identifier),
284 QuadletVolumeContainersConfModules(Identifier),
286 QuadletVolumeGlobalArgs(Identifier),
288 QuadletVolumePodmanArgs(Identifier),
290}
291
292impl RetainedNativeEvidenceSubject {
293 #[must_use]
295 pub fn conversion_subject(&self) -> String {
296 match self {
297 Self::QuadletServicePodmanArgs(name) => {
298 format!("services.{}.podman_args", name.as_str())
299 }
300 Self::QuadletVolumeContainersConfModules(name) => {
301 format!("volumes.{}.containers_conf_modules", name.as_str())
302 }
303 Self::QuadletVolumeGlobalArgs(name) => {
304 format!("volumes.{}.global_args", name.as_str())
305 }
306 Self::QuadletVolumePodmanArgs(name) => {
307 format!("volumes.{}.podman_args", name.as_str())
308 }
309 }
310 }
311
312 const fn owner(&self) -> (&'static str, &Identifier) {
313 match self {
314 Self::QuadletServicePodmanArgs(name) => ("service", name),
315 Self::QuadletVolumeContainersConfModules(name)
316 | Self::QuadletVolumeGlobalArgs(name)
317 | Self::QuadletVolumePodmanArgs(name) => ("volume", name),
318 }
319 }
320}
321
322#[derive(Clone, Debug, Eq, PartialEq)]
324#[non_exhaustive]
325pub enum RetainedNativeEvidenceEvent {
326 Value(Vec<Sourced<ProtectedString>>),
328 Reset(Vec<Sourced<ProtectedString>>),
330}
331
332impl RetainedNativeEvidenceEvent {
333 #[must_use]
335 pub fn physical_segments(&self) -> &[Sourced<ProtectedString>] {
336 match self {
337 Self::Value(segments) | Self::Reset(segments) => segments,
338 }
339 }
340}
341
342#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct RetainedNativeEvidence {
349 subject: RetainedNativeEvidenceSubject,
350 event: Sourced<RetainedNativeEvidenceEvent>,
351}
352
353impl RetainedNativeEvidence {
354 pub fn new(
364 subject: RetainedNativeEvidenceSubject,
365 event: Sourced<RetainedNativeEvidenceEvent>,
366 ) -> Result<Self, ModelError> {
367 if event.origins().is_empty() {
368 return Err(ModelError::MissingNativeEvidenceProvenance { component: "event" });
369 }
370 let segments = event.value().physical_segments();
371 if segments.is_empty() {
372 return Err(ModelError::EmptyNativeEvidenceEvent);
373 }
374 if segments.iter().any(|segment| segment.origins().is_empty()) {
375 return Err(ModelError::MissingNativeEvidenceProvenance {
376 component: "physical segment",
377 });
378 }
379 if segments.iter().any(|segment| !segment.value().is_sensitive()) {
380 return Err(ModelError::UnprotectedNativeEvidenceSegment);
381 }
382 Ok(Self { subject, event })
383 }
384
385 #[must_use]
387 pub const fn subject(&self) -> &RetainedNativeEvidenceSubject {
388 &self.subject
389 }
390
391 #[must_use]
393 pub const fn event(&self) -> &Sourced<RetainedNativeEvidenceEvent> {
394 &self.event
395 }
396}
397
398#[derive(Clone, Debug, Eq, PartialEq)]
400pub struct Volume {
401 name: Identifier,
402 ownership: ResourceOwnership,
403 runtime_name: Option<Sourced<ProtectedString>>,
404 service_name: Option<Sourced<ProtectedString>>,
405 driver: Option<Sourced<ProtectedString>>,
406 device: Option<Sourced<ProtectedString>>,
407 type_spelling: Option<Sourced<ProtectedString>>,
408 options: Option<Sourced<ProtectedString>>,
409 labels: Option<Vec<Sourced<MetadataLabel>>>,
410 labels_origins: Vec<Provenance>,
411 copy: Option<Sourced<bool>>,
412 user: Option<Sourced<ProtectedString>>,
413 group: Option<Sourced<ProtectedString>>,
414 uid: Option<Sourced<ProtectedString>>,
415 gid: Option<Sourced<ProtectedString>>,
416 image_source: Option<Sourced<VolumeImageSource>>,
417}
418
419impl Volume {
420 #[must_use]
422 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
423 Self {
424 name,
425 ownership,
426 runtime_name: None,
427 service_name: None,
428 driver: None,
429 device: None,
430 type_spelling: None,
431 options: None,
432 labels: None,
433 labels_origins: Vec::new(),
434 copy: None,
435 user: None,
436 group: None,
437 uid: None,
438 gid: None,
439 image_source: None,
440 }
441 }
442
443 #[must_use]
445 pub const fn name(&self) -> &Identifier {
446 &self.name
447 }
448
449 #[must_use]
451 pub const fn ownership(&self) -> ResourceOwnership {
452 self.ownership
453 }
454
455 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
457 self.runtime_name = Some(name);
458 }
459
460 #[must_use]
462 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
463 self.runtime_name.as_ref()
464 }
465
466 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
468 self.service_name = Some(name);
469 }
470
471 #[must_use]
473 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
474 self.service_name.as_ref()
475 }
476
477 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
479 self.driver = Some(driver);
480 }
481
482 #[must_use]
484 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
485 self.driver.as_ref()
486 }
487
488 pub fn set_device(&mut self, device: Sourced<ProtectedString>) {
490 self.device = Some(device);
491 }
492
493 #[must_use]
495 pub const fn device(&self) -> Option<&Sourced<ProtectedString>> {
496 self.device.as_ref()
497 }
498
499 pub fn set_volume_type(&mut self, volume_type: Sourced<ProtectedString>) {
501 self.type_spelling = Some(volume_type);
502 }
503
504 #[must_use]
506 pub const fn volume_type(&self) -> Option<&Sourced<ProtectedString>> {
507 self.type_spelling.as_ref()
508 }
509
510 pub fn set_options(&mut self, options: Sourced<ProtectedString>) {
515 self.options = Some(options);
516 }
517
518 #[must_use]
520 pub const fn options(&self) -> Option<&Sourced<ProtectedString>> {
521 self.options.as_ref()
522 }
523
524 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
526 self.set_labels_with_origins(labels, Vec::new());
527 }
528
529 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
531 self.labels = Some(labels);
532 self.labels_origins = origins;
533 }
534
535 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
537 self.labels.get_or_insert_default().push(label);
538 }
539
540 #[must_use]
542 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
543 self.labels.as_deref()
544 }
545
546 #[must_use]
548 pub fn labels_origins(&self) -> &[Provenance] {
549 &self.labels_origins
550 }
551
552 pub fn set_copy(&mut self, copy: Sourced<bool>) {
554 self.copy = Some(copy);
555 }
556
557 #[must_use]
559 pub const fn copy(&self) -> Option<&Sourced<bool>> {
560 self.copy.as_ref()
561 }
562
563 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
565 self.user = Some(user);
566 }
567
568 #[must_use]
570 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
571 self.user.as_ref()
572 }
573
574 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
576 self.group = Some(group);
577 }
578
579 #[must_use]
581 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
582 self.group.as_ref()
583 }
584
585 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
587 self.uid = Some(uid);
588 }
589
590 #[must_use]
592 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
593 self.uid.as_ref()
594 }
595
596 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
598 self.gid = Some(gid);
599 }
600
601 #[must_use]
603 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
604 self.gid.as_ref()
605 }
606
607 pub fn set_image_source(&mut self, image_source: Sourced<VolumeImageSource>) -> Result<(), ModelError> {
617 image_source.value().validate()?;
618 self.image_source = Some(image_source);
619 Ok(())
620 }
621
622 #[must_use]
624 pub const fn image_source(&self) -> Option<&Sourced<VolumeImageSource>> {
625 self.image_source.as_ref()
626 }
627}
628
629#[derive(Clone, Debug, Eq, PartialEq)]
634#[non_exhaustive]
635pub enum VolumeImageSource {
636 Literal(ProtectedString),
638 ImageAcquisition(Identifier),
640 ImageBuild(Identifier),
642}
643
644impl VolumeImageSource {
645 fn validate(&self) -> Result<(), ModelError> {
646 if let Self::Literal(image) = self {
647 validate_text("volume image", image.expose())?;
648 }
649 Ok(())
650 }
651}
652
653#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
658#[non_exhaustive]
659pub enum ArtifactDependencyNode {
660 Volume(Identifier),
662 ImageAcquisition(Identifier),
664 ImageBuild(Identifier),
666}
667
668impl ArtifactDependencyNode {
669 fn kind_and_name(&self) -> (&'static str, &Identifier) {
670 match self {
671 Self::Volume(name) => ("volume", name),
672 Self::ImageAcquisition(name) => ("image acquisition", name),
673 Self::ImageBuild(name) => ("image build", name),
674 }
675 }
676
677 fn display_name(&self) -> String {
678 let (kind, name) = self.kind_and_name();
679 format!("{kind}:{}", name.as_str())
680 }
681}
682
683#[derive(Clone, Debug, Eq, PartialEq)]
688pub struct ArtifactDependency {
689 source: Sourced<ArtifactDependencyNode>,
690 target: Sourced<ArtifactDependencyNode>,
691}
692
693impl ArtifactDependency {
694 #[must_use]
696 pub const fn new(source: Sourced<ArtifactDependencyNode>, target: Sourced<ArtifactDependencyNode>) -> Self {
697 Self { source, target }
698 }
699
700 #[must_use]
702 pub const fn source(&self) -> &Sourced<ArtifactDependencyNode> {
703 &self.source
704 }
705
706 #[must_use]
708 pub const fn target(&self) -> &Sourced<ArtifactDependencyNode> {
709 &self.target
710 }
711}
712
713#[derive(Clone, Debug, Eq, PartialEq)]
715pub struct Network {
716 name: Identifier,
717 ownership: ResourceOwnership,
718 runtime_name: Option<Sourced<ProtectedString>>,
719 driver: Option<Sourced<ProtectedString>>,
720 driver_options: Option<Vec<Sourced<NetworkDriverOption>>>,
721 driver_options_origins: Vec<Provenance>,
722 labels: Option<Vec<Sourced<MetadataLabel>>>,
723 labels_origins: Vec<Provenance>,
724 internal: Option<Sourced<bool>>,
725 ipv6: Option<Sourced<bool>>,
726 ipam_driver: Option<Sourced<ProtectedString>>,
727 ipam_configs: Option<Vec<Sourced<NetworkIpamConfig>>>,
728 ipam_configs_origins: Vec<Provenance>,
729}
730
731#[derive(Clone, Debug, Eq, PartialEq)]
737pub struct NetworkDriverOption {
738 name: Sourced<Identifier>,
739 value: Sourced<ProtectedString>,
740}
741
742impl NetworkDriverOption {
743 pub fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Result<Self, ModelError> {
750 validate_no_nul("network driver option value", value.value().expose())?;
751 Ok(Self { name, value })
752 }
753
754 #[must_use]
756 pub const fn name(&self) -> &Sourced<Identifier> {
757 &self.name
758 }
759
760 #[must_use]
762 pub const fn value(&self) -> &Sourced<ProtectedString> {
763 &self.value
764 }
765}
766
767#[derive(Clone, Debug, Eq, PartialEq)]
773pub struct NetworkIpamConfig {
774 subnet: Sourced<ProtectedString>,
775 gateway: Option<Sourced<ProtectedString>>,
776 ip_range: Option<Sourced<ProtectedString>>,
777}
778
779impl NetworkIpamConfig {
780 pub fn new(subnet: Sourced<ProtectedString>) -> Result<Self, ModelError> {
786 validate_text("network IPAM subnet", subnet.value().expose())?;
787 Ok(Self {
788 subnet,
789 gateway: None,
790 ip_range: None,
791 })
792 }
793
794 #[must_use]
796 pub const fn subnet(&self) -> &Sourced<ProtectedString> {
797 &self.subnet
798 }
799
800 pub fn set_gateway(&mut self, gateway: Sourced<ProtectedString>) -> Result<(), ModelError> {
806 validate_text("network IPAM gateway", gateway.value().expose())?;
807 self.gateway = Some(gateway);
808 Ok(())
809 }
810
811 #[must_use]
813 pub const fn gateway(&self) -> Option<&Sourced<ProtectedString>> {
814 self.gateway.as_ref()
815 }
816
817 pub fn set_ip_range(&mut self, ip_range: Sourced<ProtectedString>) -> Result<(), ModelError> {
823 validate_text("network IPAM IP range", ip_range.value().expose())?;
824 self.ip_range = Some(ip_range);
825 Ok(())
826 }
827
828 #[must_use]
830 pub const fn ip_range(&self) -> Option<&Sourced<ProtectedString>> {
831 self.ip_range.as_ref()
832 }
833}
834
835#[derive(Clone, Debug, Eq, PartialEq)]
837#[non_exhaustive]
838pub enum ConfigMaterial {
839 File(ProtectedString),
841 Environment(ProtectedString),
843 Content(ProtectedString),
845}
846
847#[derive(Clone, Debug, Eq, PartialEq)]
849pub struct Config {
850 name: Identifier,
851 ownership: ResourceOwnership,
852 runtime_name: Option<Sourced<ProtectedString>>,
853 material: Option<Sourced<ConfigMaterial>>,
854}
855
856impl Config {
857 #[must_use]
859 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
860 Self {
861 name,
862 ownership,
863 runtime_name: None,
864 material: None,
865 }
866 }
867
868 #[must_use]
870 pub const fn name(&self) -> &Identifier {
871 &self.name
872 }
873
874 #[must_use]
876 pub const fn ownership(&self) -> ResourceOwnership {
877 self.ownership
878 }
879
880 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
882 self.runtime_name = Some(name);
883 }
884
885 #[must_use]
887 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
888 self.runtime_name.as_ref()
889 }
890
891 pub fn set_material(&mut self, material: Sourced<ConfigMaterial>) {
893 self.material = Some(material);
894 }
895
896 #[must_use]
898 pub const fn material(&self) -> Option<&Sourced<ConfigMaterial>> {
899 self.material.as_ref()
900 }
901}
902
903#[derive(Clone, Debug, Eq, PartialEq)]
905#[non_exhaustive]
906pub enum SecretMaterial {
907 File(ProtectedString),
909 Environment(ProtectedString),
911}
912
913#[derive(Clone, Debug, Eq, PartialEq)]
915pub struct Secret {
916 name: Identifier,
917 ownership: ResourceOwnership,
918 runtime_name: Option<Sourced<ProtectedString>>,
919 material: Option<Sourced<SecretMaterial>>,
920}
921
922impl Secret {
923 #[must_use]
925 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
926 Self {
927 name,
928 ownership,
929 runtime_name: None,
930 material: None,
931 }
932 }
933
934 #[must_use]
936 pub const fn name(&self) -> &Identifier {
937 &self.name
938 }
939
940 #[must_use]
942 pub const fn ownership(&self) -> ResourceOwnership {
943 self.ownership
944 }
945
946 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
948 self.runtime_name = Some(name);
949 }
950
951 #[must_use]
953 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
954 self.runtime_name.as_ref()
955 }
956
957 pub fn set_material(&mut self, material: Sourced<SecretMaterial>) {
959 self.material = Some(material);
960 }
961
962 #[must_use]
964 pub const fn material(&self) -> Option<&Sourced<SecretMaterial>> {
965 self.material.as_ref()
966 }
967}
968
969#[derive(Clone, Copy, Debug, Eq, PartialEq)]
971#[non_exhaustive]
972pub enum ResourceGrantSyntax {
973 Short,
975 Long,
977}
978
979#[derive(Clone, Debug, Eq, PartialEq)]
985pub struct ResourceGrant {
986 source: ProtectedString,
987 syntax: ResourceGrantSyntax,
988 target: Option<Sourced<ProtectedString>>,
989 uid: Option<Sourced<ProtectedString>>,
990 gid: Option<Sourced<ProtectedString>>,
991 mode: Option<Sourced<ProtectedString>>,
992}
993
994impl ResourceGrant {
995 pub fn new(source: ProtectedString, syntax: ResourceGrantSyntax) -> Result<Self, ModelError> {
1001 validate_text("resource grant source", source.expose())?;
1002 Ok(Self {
1003 source,
1004 syntax,
1005 target: None,
1006 uid: None,
1007 gid: None,
1008 mode: None,
1009 })
1010 }
1011
1012 #[must_use]
1014 pub const fn source(&self) -> &ProtectedString {
1015 &self.source
1016 }
1017
1018 #[must_use]
1020 pub const fn syntax(&self) -> ResourceGrantSyntax {
1021 self.syntax
1022 }
1023
1024 pub fn set_target(&mut self, target: Sourced<ProtectedString>) {
1026 self.target = Some(target);
1027 }
1028
1029 #[must_use]
1031 pub const fn target(&self) -> Option<&Sourced<ProtectedString>> {
1032 self.target.as_ref()
1033 }
1034
1035 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
1037 self.uid = Some(uid);
1038 }
1039
1040 #[must_use]
1042 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
1043 self.uid.as_ref()
1044 }
1045
1046 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
1048 self.gid = Some(gid);
1049 }
1050
1051 #[must_use]
1053 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
1054 self.gid.as_ref()
1055 }
1056
1057 pub fn set_mode(&mut self, mode: Sourced<ProtectedString>) {
1059 self.mode = Some(mode);
1060 }
1061
1062 #[must_use]
1064 pub const fn mode(&self) -> Option<&Sourced<ProtectedString>> {
1065 self.mode.as_ref()
1066 }
1067}
1068
1069impl Network {
1070 #[must_use]
1072 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1073 Self {
1074 name,
1075 ownership,
1076 runtime_name: None,
1077 driver: None,
1078 driver_options: None,
1079 driver_options_origins: Vec::new(),
1080 labels: None,
1081 labels_origins: Vec::new(),
1082 internal: None,
1083 ipv6: None,
1084 ipam_driver: None,
1085 ipam_configs: None,
1086 ipam_configs_origins: Vec::new(),
1087 }
1088 }
1089
1090 #[must_use]
1092 pub const fn name(&self) -> &Identifier {
1093 &self.name
1094 }
1095
1096 #[must_use]
1098 pub const fn ownership(&self) -> ResourceOwnership {
1099 self.ownership
1100 }
1101
1102 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1104 self.runtime_name = Some(name);
1105 }
1106
1107 #[must_use]
1109 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1110 self.runtime_name.as_ref()
1111 }
1112
1113 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
1115 self.driver = Some(driver);
1116 }
1117
1118 #[must_use]
1120 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
1121 self.driver.as_ref()
1122 }
1123
1124 pub fn set_driver_options(&mut self, options: Vec<Sourced<NetworkDriverOption>>) {
1126 self.driver_options = Some(options);
1127 self.driver_options_origins.clear();
1128 }
1129
1130 pub fn set_driver_options_with_origins(
1132 &mut self,
1133 options: Vec<Sourced<NetworkDriverOption>>,
1134 origins: Vec<Provenance>,
1135 ) {
1136 self.driver_options = Some(options);
1137 self.driver_options_origins = origins;
1138 }
1139
1140 pub fn add_driver_option(&mut self, option: Sourced<NetworkDriverOption>) {
1142 self.driver_options.get_or_insert_default().push(option);
1143 }
1144
1145 #[must_use]
1147 pub fn driver_options(&self) -> Option<&[Sourced<NetworkDriverOption>]> {
1148 self.driver_options.as_deref()
1149 }
1150
1151 #[must_use]
1153 pub fn driver_options_origins(&self) -> &[Provenance] {
1154 &self.driver_options_origins
1155 }
1156
1157 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
1159 self.labels = Some(labels);
1160 self.labels_origins.clear();
1161 }
1162
1163 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
1165 self.labels = Some(labels);
1166 self.labels_origins = origins;
1167 }
1168
1169 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
1171 self.labels.get_or_insert_default().push(label);
1172 }
1173
1174 #[must_use]
1176 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
1177 self.labels.as_deref()
1178 }
1179
1180 #[must_use]
1182 pub fn labels_origins(&self) -> &[Provenance] {
1183 &self.labels_origins
1184 }
1185
1186 pub fn set_internal(&mut self, internal: Sourced<bool>) {
1188 self.internal = Some(internal);
1189 }
1190
1191 #[must_use]
1193 pub const fn internal(&self) -> Option<&Sourced<bool>> {
1194 self.internal.as_ref()
1195 }
1196
1197 pub fn set_ipv6(&mut self, ipv6: Sourced<bool>) {
1199 self.ipv6 = Some(ipv6);
1200 }
1201
1202 #[must_use]
1204 pub const fn ipv6(&self) -> Option<&Sourced<bool>> {
1205 self.ipv6.as_ref()
1206 }
1207
1208 pub fn set_ipam_driver(&mut self, driver: Sourced<ProtectedString>) {
1210 self.ipam_driver = Some(driver);
1211 }
1212
1213 #[must_use]
1215 pub const fn ipam_driver(&self) -> Option<&Sourced<ProtectedString>> {
1216 self.ipam_driver.as_ref()
1217 }
1218
1219 pub fn set_ipam_configs(&mut self, configs: Vec<Sourced<NetworkIpamConfig>>) {
1221 self.ipam_configs = Some(configs);
1222 self.ipam_configs_origins.clear();
1223 }
1224
1225 pub fn set_ipam_configs_with_origins(
1227 &mut self,
1228 configs: Vec<Sourced<NetworkIpamConfig>>,
1229 origins: Vec<Provenance>,
1230 ) {
1231 self.ipam_configs = Some(configs);
1232 self.ipam_configs_origins = origins;
1233 }
1234
1235 pub fn add_ipam_config(&mut self, config: Sourced<NetworkIpamConfig>) {
1237 self.ipam_configs.get_or_insert_default().push(config);
1238 }
1239
1240 #[must_use]
1242 pub fn ipam_configs(&self) -> Option<&[Sourced<NetworkIpamConfig>]> {
1243 self.ipam_configs.as_deref()
1244 }
1245
1246 #[must_use]
1248 pub fn ipam_configs_origins(&self) -> &[Provenance] {
1249 &self.ipam_configs_origins
1250 }
1251}
1252
1253#[derive(Clone, Debug, Eq, PartialEq)]
1258pub struct ServiceGroup {
1259 name: Identifier,
1260 ownership: ResourceOwnership,
1261 members: Vec<Sourced<Identifier>>,
1262 runtime: Option<Sourced<ServiceGroupRuntime>>,
1263}
1264
1265impl ServiceGroup {
1266 #[must_use]
1268 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1269 Self {
1270 name,
1271 ownership,
1272 members: Vec::new(),
1273 runtime: None,
1274 }
1275 }
1276
1277 #[must_use]
1279 pub const fn name(&self) -> &Identifier {
1280 &self.name
1281 }
1282
1283 #[must_use]
1285 pub const fn ownership(&self) -> ResourceOwnership {
1286 self.ownership
1287 }
1288
1289 pub fn add_member(&mut self, member: Sourced<Identifier>) -> Result<(), ModelError> {
1295 if self.members.iter().any(|candidate| candidate.value() == member.value()) {
1296 return Err(ModelError::DuplicateServiceGroupMember {
1297 group: self.name.as_str().to_owned(),
1298 service: member.value().as_str().to_owned(),
1299 });
1300 }
1301 self.members.push(member);
1302 Ok(())
1303 }
1304
1305 #[must_use]
1307 pub fn members(&self) -> &[Sourced<Identifier>] {
1308 &self.members
1309 }
1310
1311 pub fn set_runtime(&mut self, runtime: Sourced<ServiceGroupRuntime>) {
1316 self.runtime = Some(runtime);
1317 }
1318
1319 #[must_use]
1321 pub const fn runtime(&self) -> Option<&Sourced<ServiceGroupRuntime>> {
1322 self.runtime.as_ref()
1323 }
1324}
1325
1326#[derive(Clone, Debug, Eq, PartialEq)]
1328#[non_exhaustive]
1329pub enum GroupExitPolicy {
1330 Stop,
1332 Continue,
1334 Raw(ProtectedString),
1336}
1337
1338#[derive(Clone, Debug, Default, Eq, PartialEq)]
1344pub struct ServiceGroupRuntime {
1345 runtime_name: Option<Sourced<ProtectedString>>,
1346 service_name: Option<Sourced<ProtectedString>>,
1347 host_mappings: Option<Vec<Sourced<HostMapping>>>,
1348 host_mappings_origins: Vec<Provenance>,
1349 ports: Option<Vec<Sourced<Port>>>,
1350 ports_origins: Vec<Provenance>,
1351 networks: Option<Vec<Sourced<NetworkAttachment>>>,
1352 networks_origins: Vec<Provenance>,
1353 user_namespace: Option<Sourced<ProtectedString>>,
1354 mounts: Option<Vec<Sourced<Mount>>>,
1355 mounts_origins: Vec<Provenance>,
1356 shm_size: Option<Sourced<ProtectedString>>,
1357 exit_policy: Option<Sourced<GroupExitPolicy>>,
1358 stop_timeout: Option<Sourced<StopTimeout>>,
1359}
1360
1361impl ServiceGroupRuntime {
1362 #[must_use]
1364 pub const fn new() -> Self {
1365 Self {
1366 runtime_name: None,
1367 service_name: None,
1368 host_mappings: None,
1369 host_mappings_origins: Vec::new(),
1370 ports: None,
1371 ports_origins: Vec::new(),
1372 networks: None,
1373 networks_origins: Vec::new(),
1374 user_namespace: None,
1375 mounts: None,
1376 mounts_origins: Vec::new(),
1377 shm_size: None,
1378 exit_policy: None,
1379 stop_timeout: None,
1380 }
1381 }
1382
1383 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1385 self.runtime_name = Some(name);
1386 }
1387
1388 #[must_use]
1390 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1391 self.runtime_name.as_ref()
1392 }
1393
1394 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
1396 self.service_name = Some(name);
1397 }
1398
1399 #[must_use]
1401 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
1402 self.service_name.as_ref()
1403 }
1404
1405 pub fn set_host_mappings(&mut self, values: Vec<Sourced<HostMapping>>) {
1407 self.set_host_mappings_with_origins(values, Vec::new());
1408 }
1409
1410 pub fn set_host_mappings_with_origins(&mut self, values: Vec<Sourced<HostMapping>>, origins: Vec<Provenance>) {
1412 self.host_mappings = Some(values);
1413 self.host_mappings_origins = origins;
1414 }
1415
1416 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
1418 self.host_mappings.get_or_insert_default().push(value);
1419 }
1420
1421 #[must_use]
1423 pub fn host_mappings(&self) -> Option<&[Sourced<HostMapping>]> {
1424 self.host_mappings.as_deref()
1425 }
1426
1427 #[must_use]
1429 pub fn host_mappings_origins(&self) -> &[Provenance] {
1430 &self.host_mappings_origins
1431 }
1432
1433 pub fn set_ports(&mut self, values: Vec<Sourced<Port>>) {
1435 self.set_ports_with_origins(values, Vec::new());
1436 }
1437
1438 pub fn set_ports_with_origins(&mut self, values: Vec<Sourced<Port>>, origins: Vec<Provenance>) {
1440 self.ports = Some(values);
1441 self.ports_origins = origins;
1442 }
1443
1444 pub fn add_port(&mut self, value: Sourced<Port>) {
1446 self.ports.get_or_insert_default().push(value);
1447 }
1448
1449 #[must_use]
1451 pub fn ports(&self) -> Option<&[Sourced<Port>]> {
1452 self.ports.as_deref()
1453 }
1454
1455 #[must_use]
1457 pub fn ports_origins(&self) -> &[Provenance] {
1458 &self.ports_origins
1459 }
1460
1461 pub fn set_networks(&mut self, values: Vec<Sourced<NetworkAttachment>>) {
1463 self.set_networks_with_origins(values, Vec::new());
1464 }
1465
1466 pub fn set_networks_with_origins(&mut self, values: Vec<Sourced<NetworkAttachment>>, origins: Vec<Provenance>) {
1468 self.networks = Some(values);
1469 self.networks_origins = origins;
1470 }
1471
1472 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
1474 self.networks.get_or_insert_default().push(value);
1475 }
1476
1477 pub fn replace_network(
1484 &mut self,
1485 index: usize,
1486 value: Sourced<NetworkAttachment>,
1487 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
1488 let len = self.networks.as_ref().map_or(0, Vec::len);
1489 let Some(networks) = self.networks.as_mut() else {
1490 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1491 };
1492 let Some(slot) = networks.get_mut(index) else {
1493 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1494 };
1495 Ok(std::mem::replace(slot, value))
1496 }
1497
1498 #[must_use]
1500 pub fn networks(&self) -> Option<&[Sourced<NetworkAttachment>]> {
1501 self.networks.as_deref()
1502 }
1503
1504 #[must_use]
1506 pub fn networks_origins(&self) -> &[Provenance] {
1507 &self.networks_origins
1508 }
1509
1510 pub fn set_user_namespace(&mut self, value: Sourced<ProtectedString>) {
1512 self.user_namespace = Some(value);
1513 }
1514
1515 #[must_use]
1517 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
1518 self.user_namespace.as_ref()
1519 }
1520
1521 pub fn set_mounts(&mut self, values: Vec<Sourced<Mount>>) {
1523 self.set_mounts_with_origins(values, Vec::new());
1524 }
1525
1526 pub fn set_mounts_with_origins(&mut self, values: Vec<Sourced<Mount>>, origins: Vec<Provenance>) {
1528 self.mounts = Some(values);
1529 self.mounts_origins = origins;
1530 }
1531
1532 pub fn add_mount(&mut self, value: Sourced<Mount>) {
1534 self.mounts.get_or_insert_default().push(value);
1535 }
1536
1537 #[must_use]
1539 pub fn mounts(&self) -> Option<&[Sourced<Mount>]> {
1540 self.mounts.as_deref()
1541 }
1542
1543 #[must_use]
1545 pub fn mounts_origins(&self) -> &[Provenance] {
1546 &self.mounts_origins
1547 }
1548
1549 pub fn set_shm_size(&mut self, value: Sourced<ProtectedString>) {
1551 self.shm_size = Some(value);
1552 }
1553
1554 #[must_use]
1556 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
1557 self.shm_size.as_ref()
1558 }
1559
1560 pub fn set_exit_policy(&mut self, value: Sourced<GroupExitPolicy>) {
1562 self.exit_policy = Some(value);
1563 }
1564
1565 #[must_use]
1567 pub const fn exit_policy(&self) -> Option<&Sourced<GroupExitPolicy>> {
1568 self.exit_policy.as_ref()
1569 }
1570
1571 pub fn set_stop_timeout(&mut self, value: Sourced<StopTimeout>) {
1573 self.stop_timeout = Some(value);
1574 }
1575
1576 #[must_use]
1578 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
1579 self.stop_timeout.as_ref()
1580 }
1581}
1582
1583#[derive(Clone, Debug, Eq, PartialEq)]
1585#[non_exhaustive]
1586pub enum Command {
1587 Exec(Vec<ProtectedString>),
1589 Shell(ProtectedString),
1591 Empty,
1593}
1594
1595#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1600#[non_exhaustive]
1601pub enum StartupNotification {
1602 Runtime,
1604 Application,
1606 Healthy,
1608}
1609
1610#[derive(Clone, Debug, Eq, PartialEq)]
1615#[non_exhaustive]
1616pub enum Entrypoint {
1617 Exec(Vec<ProtectedString>),
1619 Shell(ProtectedString),
1621 Empty,
1623}
1624
1625#[derive(Clone, Debug, Eq, PartialEq)]
1630#[non_exhaustive]
1631pub enum PullPolicy {
1632 Always,
1634 Missing,
1636 Never,
1638 IfNotPresent,
1640 Build,
1642 Daily,
1644 Weekly,
1646 Every(ProtectedString),
1648 Raw(ProtectedString),
1650}
1651
1652#[derive(Clone, Debug, Eq, PartialEq)]
1657pub struct StopTimeout(String);
1658
1659impl StopTimeout {
1660 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1666 let value = value.into();
1667 validate_text("stop timeout", &value)?;
1668 Ok(Self(value))
1669 }
1670
1671 #[must_use]
1673 pub fn as_str(&self) -> &str {
1674 &self.0
1675 }
1676}
1677
1678#[derive(Clone, Debug, Eq, PartialEq)]
1680pub struct ExposedPort {
1681 container: u16,
1682 protocol: Protocol,
1683}
1684
1685impl ExposedPort {
1686 pub fn new(container: u16, protocol: Protocol) -> Result<Self, ModelError> {
1692 if container == 0 {
1693 return Err(ModelError::ZeroContainerPort);
1694 }
1695 Ok(Self { container, protocol })
1696 }
1697
1698 #[must_use]
1700 pub const fn container(&self) -> u16 {
1701 self.container
1702 }
1703
1704 #[must_use]
1706 pub const fn protocol(&self) -> &Protocol {
1707 &self.protocol
1708 }
1709}
1710
1711#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1717#[non_exhaustive]
1718pub enum RestartPolicy {
1719 Never,
1721 Always,
1723 OnFailure {
1725 maximum_retries: Option<std::num::NonZeroU64>,
1727 },
1728 UnlessStopped,
1730}
1731
1732impl RestartPolicy {
1733 #[must_use]
1735 pub const fn on_failure(maximum_retries: Option<std::num::NonZeroU64>) -> Self {
1736 Self::OnFailure { maximum_retries }
1737 }
1738
1739 #[must_use]
1741 pub const fn maximum_retries(self) -> Option<std::num::NonZeroU64> {
1742 match self {
1743 Self::OnFailure { maximum_retries } => maximum_retries,
1744 Self::Never | Self::Always | Self::UnlessStopped => None,
1745 }
1746 }
1747}
1748
1749#[derive(Clone, Debug, Eq, PartialEq)]
1751#[non_exhaustive]
1752pub enum HealthcheckCommand {
1753 Exec(Vec<ProtectedString>),
1755 Shell(ProtectedString),
1757}
1758
1759#[derive(Clone, Debug, Eq, PartialEq)]
1761pub struct HealthcheckDuration(String);
1762
1763impl HealthcheckDuration {
1764 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1773 let value = value.into();
1774 validate_text("health-check duration", &value)?;
1775 Ok(Self(value))
1776 }
1777
1778 #[must_use]
1780 pub fn as_str(&self) -> &str {
1781 &self.0
1782 }
1783}
1784
1785#[derive(Clone, Debug, Eq, PartialEq)]
1787pub struct HealthcheckRetries(String);
1788
1789impl HealthcheckRetries {
1790 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1797 let value = value.into();
1798 validate_text("health-check retries", &value)?;
1799 if !value.bytes().all(|byte| byte.is_ascii_digit()) {
1800 return Err(ModelError::InvalidHealthcheckRetries);
1801 }
1802 Ok(Self(value))
1803 }
1804
1805 #[must_use]
1807 pub fn as_str(&self) -> &str {
1808 &self.0
1809 }
1810}
1811
1812#[derive(Clone, Debug, Default, Eq, PartialEq)]
1814pub struct Healthcheck {
1815 command: Option<Sourced<HealthcheckCommand>>,
1816 disabled: Option<Sourced<bool>>,
1817 interval: Option<Sourced<HealthcheckDuration>>,
1818 timeout: Option<Sourced<HealthcheckDuration>>,
1819 retries: Option<Sourced<HealthcheckRetries>>,
1820 start_period: Option<Sourced<HealthcheckDuration>>,
1821 start_interval: Option<Sourced<HealthcheckDuration>>,
1822}
1823
1824impl Healthcheck {
1825 #[must_use]
1827 pub const fn new() -> Self {
1828 Self {
1829 command: None,
1830 disabled: None,
1831 interval: None,
1832 timeout: None,
1833 retries: None,
1834 start_period: None,
1835 start_interval: None,
1836 }
1837 }
1838
1839 pub fn set_command(&mut self, command: Sourced<HealthcheckCommand>) {
1841 self.command = Some(command);
1842 }
1843
1844 #[must_use]
1846 pub const fn command(&self) -> Option<&Sourced<HealthcheckCommand>> {
1847 self.command.as_ref()
1848 }
1849
1850 pub fn set_disabled(&mut self, disabled: Sourced<bool>) {
1852 self.disabled = Some(disabled);
1853 }
1854
1855 #[must_use]
1857 pub const fn disabled(&self) -> Option<&Sourced<bool>> {
1858 self.disabled.as_ref()
1859 }
1860
1861 pub fn set_interval(&mut self, interval: Sourced<HealthcheckDuration>) {
1863 self.interval = Some(interval);
1864 }
1865
1866 #[must_use]
1868 pub const fn interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1869 self.interval.as_ref()
1870 }
1871
1872 pub fn set_timeout(&mut self, timeout: Sourced<HealthcheckDuration>) {
1874 self.timeout = Some(timeout);
1875 }
1876
1877 #[must_use]
1879 pub const fn timeout(&self) -> Option<&Sourced<HealthcheckDuration>> {
1880 self.timeout.as_ref()
1881 }
1882
1883 pub fn set_retries(&mut self, retries: Sourced<HealthcheckRetries>) {
1885 self.retries = Some(retries);
1886 }
1887
1888 #[must_use]
1890 pub const fn retries(&self) -> Option<&Sourced<HealthcheckRetries>> {
1891 self.retries.as_ref()
1892 }
1893
1894 pub fn set_start_period(&mut self, start_period: Sourced<HealthcheckDuration>) {
1896 self.start_period = Some(start_period);
1897 }
1898
1899 #[must_use]
1901 pub const fn start_period(&self) -> Option<&Sourced<HealthcheckDuration>> {
1902 self.start_period.as_ref()
1903 }
1904
1905 pub fn set_start_interval(&mut self, start_interval: Sourced<HealthcheckDuration>) {
1907 self.start_interval = Some(start_interval);
1908 }
1909
1910 #[must_use]
1912 pub const fn start_interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1913 self.start_interval.as_ref()
1914 }
1915}
1916
1917#[derive(Clone, Debug, Eq, PartialEq)]
1919#[non_exhaustive]
1920pub enum EnvironmentValue {
1921 Literal(ProtectedString),
1923 Host,
1925 Unset,
1927}
1928
1929#[derive(Clone, Debug, Eq, PartialEq)]
1931pub struct EnvironmentVariable {
1932 name: Identifier,
1933 value: EnvironmentValue,
1934}
1935
1936#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1938#[non_exhaustive]
1939pub enum EnvironmentFileSyntax {
1940 Short,
1942 Long,
1944}
1945
1946#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1948#[non_exhaustive]
1949pub enum EnvironmentFileFormat {
1950 Raw,
1952}
1953
1954#[derive(Clone, Debug, Eq, PartialEq)]
1960pub struct EnvironmentFile {
1961 path: ProtectedString,
1962 syntax: EnvironmentFileSyntax,
1963 required: Option<Sourced<bool>>,
1964 format: Option<Sourced<EnvironmentFileFormat>>,
1965}
1966
1967impl EnvironmentFile {
1968 pub fn new(path: ProtectedString, syntax: EnvironmentFileSyntax) -> Result<Self, ModelError> {
1974 validate_text("environment-file path", path.expose())?;
1975 Ok(Self {
1976 path,
1977 syntax,
1978 required: None,
1979 format: None,
1980 })
1981 }
1982
1983 #[must_use]
1985 pub const fn path(&self) -> &ProtectedString {
1986 &self.path
1987 }
1988
1989 #[must_use]
1991 pub const fn syntax(&self) -> EnvironmentFileSyntax {
1992 self.syntax
1993 }
1994
1995 pub fn set_required(&mut self, required: Sourced<bool>) {
1997 self.required = Some(required);
1998 }
1999
2000 #[must_use]
2002 pub const fn required(&self) -> Option<&Sourced<bool>> {
2003 self.required.as_ref()
2004 }
2005
2006 #[must_use]
2008 pub fn is_required(&self) -> bool {
2009 self.required.as_ref().is_none_or(|required| *required.value())
2010 }
2011
2012 pub fn set_format(&mut self, format: Sourced<EnvironmentFileFormat>) {
2014 self.format = Some(format);
2015 }
2016
2017 #[must_use]
2019 pub const fn format(&self) -> Option<&Sourced<EnvironmentFileFormat>> {
2020 self.format.as_ref()
2021 }
2022}
2023
2024#[derive(Clone, Debug, Eq, PartialEq)]
2030pub struct MetadataLabel {
2031 name: Identifier,
2032 value: ProtectedString,
2033}
2034
2035impl MetadataLabel {
2036 #[must_use]
2038 pub const fn new(name: Identifier, value: ProtectedString) -> Self {
2039 Self { name, value }
2040 }
2041
2042 #[must_use]
2044 pub const fn name(&self) -> &Identifier {
2045 &self.name
2046 }
2047
2048 #[must_use]
2050 pub const fn value(&self) -> &ProtectedString {
2051 &self.value
2052 }
2053}
2054
2055#[derive(Clone, Debug, Eq, PartialEq)]
2060pub struct Annotation {
2061 name: Sourced<Identifier>,
2062 value: Sourced<ProtectedString>,
2063}
2064
2065impl Annotation {
2066 #[must_use]
2068 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2069 Self { name, value }
2070 }
2071
2072 #[must_use]
2074 pub const fn name(&self) -> &Sourced<Identifier> {
2075 &self.name
2076 }
2077
2078 #[must_use]
2080 pub const fn value(&self) -> &Sourced<ProtectedString> {
2081 &self.value
2082 }
2083}
2084
2085#[derive(Clone, Debug, Eq, PartialEq)]
2087pub struct LoggingOption {
2088 name: Sourced<Identifier>,
2089 value: Sourced<ProtectedString>,
2090}
2091
2092impl LoggingOption {
2093 #[must_use]
2095 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2096 Self { name, value }
2097 }
2098
2099 #[must_use]
2101 pub const fn name(&self) -> &Sourced<Identifier> {
2102 &self.name
2103 }
2104
2105 #[must_use]
2107 pub const fn value(&self) -> &Sourced<ProtectedString> {
2108 &self.value
2109 }
2110}
2111
2112#[derive(Clone, Debug, Default, Eq, PartialEq)]
2117pub struct Logging {
2118 driver: Option<Sourced<ProtectedString>>,
2119 options: Option<Vec<Sourced<LoggingOption>>>,
2120 options_origins: Vec<Provenance>,
2121}
2122
2123impl Logging {
2124 #[must_use]
2126 pub const fn new() -> Self {
2127 Self {
2128 driver: None,
2129 options: None,
2130 options_origins: Vec::new(),
2131 }
2132 }
2133
2134 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
2136 self.driver = Some(driver);
2137 }
2138
2139 #[must_use]
2141 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
2142 self.driver.as_ref()
2143 }
2144
2145 pub fn set_options(&mut self, options: Vec<Sourced<LoggingOption>>) {
2147 self.options = Some(options);
2148 self.options_origins.clear();
2149 }
2150
2151 pub fn add_option(&mut self, option: Sourced<LoggingOption>) {
2153 self.options.get_or_insert_default().push(option);
2154 }
2155
2156 pub fn set_options_with_origins(&mut self, options: Vec<Sourced<LoggingOption>>, origins: Vec<Provenance>) {
2158 self.options = Some(options);
2159 self.options_origins = origins;
2160 }
2161
2162 #[must_use]
2164 pub fn options(&self) -> Option<&[Sourced<LoggingOption>]> {
2165 self.options.as_deref()
2166 }
2167
2168 #[must_use]
2170 pub fn options_origins(&self) -> &[Provenance] {
2171 &self.options_origins
2172 }
2173}
2174
2175#[derive(Clone, Debug, Eq, PartialEq)]
2180#[non_exhaustive]
2181pub enum ReloadAction {
2182 Command(Command),
2184 Signal(ProtectedString),
2186}
2187
2188impl EnvironmentVariable {
2189 #[must_use]
2191 pub const fn new(name: Identifier, value: EnvironmentValue) -> Self {
2192 Self { name, value }
2193 }
2194
2195 #[must_use]
2197 pub const fn name(&self) -> &Identifier {
2198 &self.name
2199 }
2200
2201 #[must_use]
2203 pub const fn value(&self) -> &EnvironmentValue {
2204 &self.value
2205 }
2206}
2207
2208#[derive(Clone, Debug, Eq, PartialEq)]
2210pub struct HostAddress {
2211 raw: String,
2212 kind: HostAddressKind,
2213}
2214
2215impl HostAddress {
2216 pub fn new(raw: impl Into<String>) -> Result<Self, ModelError> {
2222 let raw = raw.into();
2223 validate_text("host mapping address", &raw)?;
2224 let unbracketed = raw
2225 .strip_prefix('[')
2226 .and_then(|value| value.strip_suffix(']'))
2227 .unwrap_or(&raw);
2228 let kind = if raw == "host-gateway" {
2229 HostAddressKind::HostGateway
2230 } else {
2231 match unbracketed.parse::<IpAddr>() {
2232 Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
2233 Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
2234 bracketed: raw.starts_with('[') && raw.ends_with(']'),
2235 },
2236 Err(_) => HostAddressKind::Other,
2237 }
2238 };
2239 Ok(Self { raw, kind })
2240 }
2241
2242 #[must_use]
2244 pub fn raw(&self) -> &str {
2245 &self.raw
2246 }
2247
2248 #[must_use]
2250 pub const fn kind(&self) -> HostAddressKind {
2251 self.kind
2252 }
2253}
2254
2255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2257#[non_exhaustive]
2258pub enum HostAddressKind {
2259 Ipv4,
2261 Ipv6 {
2263 bracketed: bool,
2265 },
2266 HostGateway,
2268 Other,
2270}
2271
2272#[derive(Clone, Debug, Eq, PartialEq)]
2274pub struct HostMapping {
2275 hostname: Identifier,
2276 address: HostAddress,
2277}
2278
2279impl HostMapping {
2280 #[must_use]
2282 pub const fn new(hostname: Identifier, address: HostAddress) -> Self {
2283 Self { hostname, address }
2284 }
2285
2286 #[must_use]
2288 pub const fn hostname(&self) -> &Identifier {
2289 &self.hostname
2290 }
2291
2292 #[must_use]
2294 pub const fn address(&self) -> &HostAddress {
2295 &self.address
2296 }
2297}
2298
2299#[derive(Clone, Debug, Eq, PartialEq)]
2301#[non_exhaustive]
2302pub enum Protocol {
2303 Tcp,
2305 Udp,
2307 Sctp,
2309 Other(String),
2311}
2312
2313#[derive(Clone, Debug, Eq, PartialEq)]
2315pub struct Port {
2316 container: u16,
2317 published: Option<u16>,
2318 host_address: Option<String>,
2319 protocol: Protocol,
2320}
2321
2322impl Port {
2323 pub fn new(
2329 container: u16,
2330 published: Option<u16>,
2331 host_address: Option<String>,
2332 protocol: Protocol,
2333 ) -> Result<Self, ModelError> {
2334 if container == 0 {
2335 return Err(ModelError::ZeroContainerPort);
2336 }
2337 Ok(Self {
2338 container,
2339 published,
2340 host_address,
2341 protocol,
2342 })
2343 }
2344
2345 #[must_use]
2347 pub const fn container(&self) -> u16 {
2348 self.container
2349 }
2350
2351 #[must_use]
2353 pub const fn published(&self) -> Option<u16> {
2354 self.published
2355 }
2356
2357 #[must_use]
2359 pub fn host_address(&self) -> Option<&str> {
2360 self.host_address.as_deref()
2361 }
2362
2363 #[must_use]
2365 pub const fn protocol(&self) -> &Protocol {
2366 &self.protocol
2367 }
2368}
2369
2370#[derive(Clone, Debug, Eq, PartialEq)]
2372#[non_exhaustive]
2373pub enum MountSource {
2374 Volume(Identifier),
2376 HostPath(String),
2378 Anonymous,
2380}
2381
2382#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2384#[non_exhaustive]
2385pub enum SelinuxRelabel {
2386 Shared,
2388 Private,
2390}
2391
2392#[derive(Clone, Debug, Eq, PartialEq)]
2394pub struct Mount {
2395 source: MountSource,
2396 target: String,
2397 read_only: bool,
2398 selinux_relabel: Option<SelinuxRelabel>,
2399}
2400
2401impl Mount {
2402 pub fn new(source: MountSource, target: impl Into<String>, read_only: bool) -> Result<Self, ModelError> {
2408 let target = target.into();
2409 validate_text("mount target", &target)?;
2410 Ok(Self {
2411 source,
2412 target,
2413 read_only,
2414 selinux_relabel: None,
2415 })
2416 }
2417
2418 #[must_use]
2420 pub const fn source(&self) -> &MountSource {
2421 &self.source
2422 }
2423
2424 #[must_use]
2426 pub fn target(&self) -> &str {
2427 &self.target
2428 }
2429
2430 #[must_use]
2432 pub const fn read_only(&self) -> bool {
2433 self.read_only
2434 }
2435
2436 pub fn set_selinux_relabel(&mut self, relabel: SelinuxRelabel) {
2438 self.selinux_relabel = Some(relabel);
2439 }
2440
2441 #[must_use]
2443 pub const fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
2444 self.selinux_relabel
2445 }
2446}
2447
2448#[derive(Clone, Eq, PartialEq)]
2450pub struct NetworkAttachment {
2451 network: Identifier,
2452 aliases: Vec<String>,
2453 alias_sensitivities: Vec<bool>,
2454 alias_origins: Vec<Vec<Provenance>>,
2455 ipv4_address: Option<Sourced<ProtectedString>>,
2456 ipv6_address: Option<Sourced<ProtectedString>>,
2457}
2458
2459impl NetworkAttachment {
2460 #[must_use]
2462 pub fn new(network: Identifier, aliases: Vec<Sourced<ProtectedString>>) -> Self {
2463 Self {
2464 network,
2465 aliases: aliases.iter().map(|alias| alias.value().expose().to_owned()).collect(),
2466 alias_sensitivities: aliases.iter().map(|alias| alias.value().is_sensitive()).collect(),
2467 alias_origins: aliases.into_iter().map(|alias| alias.origins().to_vec()).collect(),
2468 ipv4_address: None,
2469 ipv6_address: None,
2470 }
2471 }
2472
2473 #[must_use]
2475 pub const fn network(&self) -> &Identifier {
2476 &self.network
2477 }
2478
2479 #[must_use]
2481 pub fn aliases(&self) -> &[String] {
2482 &self.aliases
2483 }
2484
2485 #[must_use]
2487 pub fn alias_origins(&self) -> &[Vec<Provenance>] {
2488 &self.alias_origins
2489 }
2490
2491 #[must_use]
2496 pub fn alias_sensitivities(&self) -> &[bool] {
2497 &self.alias_sensitivities
2498 }
2499
2500 pub fn set_aliases_with_provenance(&mut self, aliases: Vec<Sourced<ProtectedString>>) {
2502 self.aliases = aliases.iter().map(|alias| alias.value().expose().to_owned()).collect();
2503 self.alias_sensitivities = aliases.iter().map(|alias| alias.value().is_sensitive()).collect();
2504 self.alias_origins = aliases.into_iter().map(|alias| alias.origins().to_vec()).collect();
2505 }
2506
2507 pub fn add_alias(&mut self, alias: &Sourced<ProtectedString>) {
2509 self.aliases.push(alias.value().expose().to_owned());
2510 self.alias_sensitivities.push(alias.value().is_sensitive());
2511 self.alias_origins.push(alias.origins().to_vec());
2512 }
2513
2514 pub fn set_ipv4_address(&mut self, address: Sourced<ProtectedString>) {
2516 self.ipv4_address = Some(address);
2517 }
2518
2519 #[must_use]
2521 pub const fn ipv4_address(&self) -> Option<&Sourced<ProtectedString>> {
2522 self.ipv4_address.as_ref()
2523 }
2524
2525 pub fn set_ipv6_address(&mut self, address: Sourced<ProtectedString>) {
2527 self.ipv6_address = Some(address);
2528 }
2529
2530 #[must_use]
2532 pub const fn ipv6_address(&self) -> Option<&Sourced<ProtectedString>> {
2533 self.ipv6_address.as_ref()
2534 }
2535}
2536
2537impl fmt::Debug for NetworkAttachment {
2538 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2539 let aliases = self
2540 .aliases
2541 .iter()
2542 .enumerate()
2543 .map(|(index, alias)| {
2544 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
2545 "[REDACTED]"
2546 } else {
2547 alias.as_str()
2548 }
2549 })
2550 .collect::<Vec<_>>();
2551 formatter
2552 .debug_struct("NetworkAttachment")
2553 .field("network", &self.network)
2554 .field("aliases", &aliases)
2555 .field("alias_origins", &self.alias_origins)
2556 .field("ipv4_address", &self.ipv4_address)
2557 .field("ipv6_address", &self.ipv6_address)
2558 .finish()
2559 }
2560}
2561
2562#[derive(Clone, Debug, Eq, PartialEq)]
2564#[non_exhaustive]
2565pub enum ServiceDependencyCondition {
2566 Started,
2568 Healthy,
2570 CompletedSuccessfully,
2572 Other(ProtectedString),
2574}
2575
2576#[derive(Clone, Debug, Eq, PartialEq)]
2582pub struct ServiceDependency {
2583 service: Identifier,
2584 condition: Option<Sourced<ServiceDependencyCondition>>,
2585 restart: Option<Sourced<bool>>,
2586 required: Option<Sourced<bool>>,
2587}
2588
2589#[derive(Clone, Debug, Eq, PartialEq)]
2591pub struct KernelParameter {
2592 name: ProtectedString,
2593 value: ProtectedString,
2594}
2595
2596impl KernelParameter {
2597 #[must_use]
2599 pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
2600 Self { name, value }
2601 }
2602
2603 #[must_use]
2605 pub const fn name(&self) -> &ProtectedString {
2606 &self.name
2607 }
2608
2609 #[must_use]
2611 pub const fn value(&self) -> &ProtectedString {
2612 &self.value
2613 }
2614}
2615
2616#[derive(Clone, Debug, Eq, PartialEq)]
2618pub struct ResourceLimit {
2619 name: ProtectedString,
2620 soft: Option<Sourced<ProtectedString>>,
2621 hard: Option<Sourced<ProtectedString>>,
2622}
2623
2624impl ResourceLimit {
2625 #[must_use]
2627 pub const fn new(
2628 name: ProtectedString,
2629 soft: Option<Sourced<ProtectedString>>,
2630 hard: Option<Sourced<ProtectedString>>,
2631 ) -> Self {
2632 Self { name, soft, hard }
2633 }
2634
2635 #[must_use]
2637 pub const fn name(&self) -> &ProtectedString {
2638 &self.name
2639 }
2640
2641 #[must_use]
2643 pub const fn soft(&self) -> Option<&Sourced<ProtectedString>> {
2644 self.soft.as_ref()
2645 }
2646
2647 #[must_use]
2649 pub const fn hard(&self) -> Option<&Sourced<ProtectedString>> {
2650 self.hard.as_ref()
2651 }
2652}
2653
2654#[derive(Clone, Debug, Eq, PartialEq)]
2656#[non_exhaustive]
2657pub enum Device {
2658 Short(ProtectedString),
2660 Long {
2662 source: Option<Sourced<ProtectedString>>,
2664 target: Option<Sourced<ProtectedString>>,
2666 permissions: Option<Sourced<ProtectedString>>,
2668 },
2669}
2670
2671#[derive(Clone, Debug, Eq, PartialEq)]
2676#[non_exhaustive]
2677pub enum SecurityOption {
2678 AppArmor(ProtectedString),
2680 NoNewPrivileges(bool),
2682 SeccompProfile(ProtectedString),
2684 SecurityLabelDisable(bool),
2686 SecurityLabelFileType(ProtectedString),
2688 SecurityLabelLevel(ProtectedString),
2690 SecurityLabelNested(bool),
2692 SecurityLabelType(ProtectedString),
2694 Mask(ProtectedString),
2696 Unmask(ProtectedString),
2698}
2699
2700impl ServiceDependency {
2701 #[must_use]
2704 pub const fn new(service: Identifier) -> Self {
2705 Self {
2706 service,
2707 condition: None,
2708 restart: None,
2709 required: None,
2710 }
2711 }
2712
2713 #[must_use]
2715 pub const fn service(&self) -> &Identifier {
2716 &self.service
2717 }
2718
2719 pub fn set_condition(&mut self, condition: Sourced<ServiceDependencyCondition>) {
2721 self.condition = Some(condition);
2722 }
2723
2724 #[must_use]
2726 pub const fn condition(&self) -> Option<&Sourced<ServiceDependencyCondition>> {
2727 self.condition.as_ref()
2728 }
2729
2730 pub fn set_restart(&mut self, restart: Sourced<bool>) {
2732 self.restart = Some(restart);
2733 }
2734
2735 #[must_use]
2737 pub const fn restart(&self) -> Option<&Sourced<bool>> {
2738 self.restart.as_ref()
2739 }
2740
2741 pub fn set_required(&mut self, required: Sourced<bool>) {
2743 self.required = Some(required);
2744 }
2745
2746 #[must_use]
2748 pub const fn required(&self) -> Option<&Sourced<bool>> {
2749 self.required.as_ref()
2750 }
2751
2752 #[must_use]
2754 pub fn is_required(&self) -> bool {
2755 self.required.as_ref().is_none_or(|required| *required.value())
2756 }
2757}
2758
2759#[derive(Clone, Debug, Eq, PartialEq)]
2761pub struct Service {
2762 name: Identifier,
2763 runtime_name: Option<Sourced<ProtectedString>>,
2764 rootfs: Option<Sourced<ProtectedString>>,
2765 image: Option<Sourced<ImageReference>>,
2766 image_acquisition: Option<Sourced<Identifier>>,
2767 image_build: Option<Sourced<Identifier>>,
2768 command: Option<Sourced<Command>>,
2769 startup_notification: Option<Sourced<StartupNotification>>,
2770 entrypoint: Option<Sourced<Entrypoint>>,
2771 run_init: Option<Sourced<bool>>,
2772 stop_timeout: Option<Sourced<StopTimeout>>,
2773 pull_policy: Option<Sourced<PullPolicy>>,
2774 memory_limit: Option<Sourced<ProtectedString>>,
2775 exposed_ports: Option<Vec<Sourced<ExposedPort>>>,
2776 exposed_ports_origins: Vec<Provenance>,
2777 restart_policy: Option<Sourced<RestartPolicy>>,
2778 healthcheck: Option<Sourced<Healthcheck>>,
2779 labels: Vec<Sourced<MetadataLabel>>,
2780 annotations: Option<Vec<Sourced<Annotation>>>,
2781 annotations_origins: Vec<Provenance>,
2782 logging: Option<Sourced<Logging>>,
2783 reload_action: Option<Sourced<ReloadAction>>,
2784 user: Option<Sourced<ProtectedString>>,
2785 group: Option<Sourced<ProtectedString>>,
2786 user_namespace: Option<Sourced<ProtectedString>>,
2787 supplementary_groups: Vec<Sourced<ProtectedString>>,
2788 working_directory: Option<Sourced<ProtectedString>>,
2789 read_only_root_filesystem: Option<Sourced<bool>>,
2790 hostname: Option<Sourced<ProtectedString>>,
2791 dns_servers: Option<Vec<Sourced<ProtectedString>>>,
2792 dns_servers_origins: Vec<Provenance>,
2793 dns_options: Option<Vec<Sourced<ProtectedString>>>,
2794 dns_options_origins: Vec<Provenance>,
2795 dns_search_domains: Option<Vec<Sourced<ProtectedString>>>,
2796 dns_search_domains_origins: Vec<Provenance>,
2797 security_options: Option<Vec<Sourced<SecurityOption>>>,
2798 security_options_origins: Vec<Provenance>,
2799 pids_limit: Option<Sourced<ProtectedString>>,
2800 shm_size: Option<Sourced<ProtectedString>>,
2801 cap_add: Option<Vec<Sourced<ProtectedString>>>,
2802 cap_add_origins: Vec<Provenance>,
2803 cap_drop: Option<Vec<Sourced<ProtectedString>>>,
2804 cap_drop_origins: Vec<Provenance>,
2805 tmpfs: Option<Vec<Sourced<ProtectedString>>>,
2806 tmpfs_origins: Vec<Provenance>,
2807 sysctls: Option<Vec<Sourced<KernelParameter>>>,
2808 sysctls_origins: Vec<Provenance>,
2809 ulimits: Option<Vec<Sourced<ResourceLimit>>>,
2810 ulimits_origins: Vec<Provenance>,
2811 devices: Option<Vec<Sourced<Device>>>,
2812 devices_origins: Vec<Provenance>,
2813 stop_signal: Option<Sourced<ProtectedString>>,
2814 environment: Vec<Sourced<EnvironmentVariable>>,
2815 environment_files: Vec<Sourced<EnvironmentFile>>,
2816 host_mappings: Vec<Sourced<HostMapping>>,
2817 ports: Vec<Sourced<Port>>,
2818 mounts: Vec<Sourced<Mount>>,
2819 config_grants: Vec<Sourced<ResourceGrant>>,
2820 secret_grants: Vec<Sourced<ResourceGrant>>,
2821 networks: Vec<Sourced<NetworkAttachment>>,
2822 dependencies: Vec<Sourced<ServiceDependency>>,
2823}
2824
2825impl Service {
2826 #[must_use]
2828 pub const fn new(name: Identifier) -> Self {
2829 Self {
2830 name,
2831 runtime_name: None,
2832 rootfs: None,
2833 image: None,
2834 image_acquisition: None,
2835 image_build: None,
2836 command: None,
2837 startup_notification: None,
2838 entrypoint: None,
2839 run_init: None,
2840 stop_timeout: None,
2841 pull_policy: None,
2842 memory_limit: None,
2843 exposed_ports: None,
2844 exposed_ports_origins: Vec::new(),
2845 restart_policy: None,
2846 healthcheck: None,
2847 labels: Vec::new(),
2848 annotations: None,
2849 annotations_origins: Vec::new(),
2850 logging: None,
2851 reload_action: None,
2852 user: None,
2853 group: None,
2854 user_namespace: None,
2855 supplementary_groups: Vec::new(),
2856 working_directory: None,
2857 read_only_root_filesystem: None,
2858 hostname: None,
2859 dns_servers: None,
2860 dns_servers_origins: Vec::new(),
2861 dns_options: None,
2862 dns_options_origins: Vec::new(),
2863 dns_search_domains: None,
2864 dns_search_domains_origins: Vec::new(),
2865 security_options: None,
2866 security_options_origins: Vec::new(),
2867 pids_limit: None,
2868 shm_size: None,
2869 cap_add: None,
2870 cap_add_origins: Vec::new(),
2871 cap_drop: None,
2872 cap_drop_origins: Vec::new(),
2873 tmpfs: None,
2874 tmpfs_origins: Vec::new(),
2875 sysctls: None,
2876 sysctls_origins: Vec::new(),
2877 ulimits: None,
2878 ulimits_origins: Vec::new(),
2879 devices: None,
2880 devices_origins: Vec::new(),
2881 stop_signal: None,
2882 environment: Vec::new(),
2883 environment_files: Vec::new(),
2884 host_mappings: Vec::new(),
2885 ports: Vec::new(),
2886 mounts: Vec::new(),
2887 config_grants: Vec::new(),
2888 secret_grants: Vec::new(),
2889 networks: Vec::new(),
2890 dependencies: Vec::new(),
2891 }
2892 }
2893
2894 #[must_use]
2896 pub const fn name(&self) -> &Identifier {
2897 &self.name
2898 }
2899
2900 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
2902 self.runtime_name = Some(name);
2903 }
2904
2905 #[must_use]
2907 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
2908 self.runtime_name.as_ref()
2909 }
2910
2911 pub fn set_rootfs(&mut self, rootfs: Sourced<ProtectedString>) -> Result<(), ModelError> {
2918 self.ensure_rootfs_is_compatible()?;
2919 self.rootfs = Some(rootfs);
2920 Ok(())
2921 }
2922
2923 #[must_use]
2925 pub const fn rootfs(&self) -> Option<&Sourced<ProtectedString>> {
2926 self.rootfs.as_ref()
2927 }
2928
2929 pub fn set_image(&mut self, image: Sourced<ImageReference>) {
2931 self.image = Some(image);
2932 }
2933
2934 #[must_use]
2936 pub const fn image(&self) -> Option<&Sourced<ImageReference>> {
2937 self.image.as_ref()
2938 }
2939
2940 pub fn set_image_acquisition(&mut self, acquisition: Sourced<Identifier>) {
2944 self.image_acquisition = Some(acquisition);
2945 }
2946
2947 #[must_use]
2949 pub const fn image_acquisition(&self) -> Option<&Sourced<Identifier>> {
2950 self.image_acquisition.as_ref()
2951 }
2952
2953 pub fn set_image_build(&mut self, build: Sourced<Identifier>) {
2957 self.image_build = Some(build);
2958 }
2959
2960 #[must_use]
2962 pub const fn image_build(&self) -> Option<&Sourced<Identifier>> {
2963 self.image_build.as_ref()
2964 }
2965
2966 pub fn set_command(&mut self, command: Sourced<Command>) {
2968 self.command = Some(command);
2969 }
2970
2971 #[must_use]
2973 pub const fn command(&self) -> Option<&Sourced<Command>> {
2974 self.command.as_ref()
2975 }
2976
2977 pub fn set_startup_notification(&mut self, notification: Sourced<StartupNotification>) {
2979 self.startup_notification = Some(notification);
2980 }
2981
2982 #[must_use]
2984 pub const fn startup_notification(&self) -> Option<&Sourced<StartupNotification>> {
2985 self.startup_notification.as_ref()
2986 }
2987
2988 pub fn set_entrypoint(&mut self, entrypoint: Sourced<Entrypoint>) {
2990 self.entrypoint = Some(entrypoint);
2991 }
2992
2993 #[must_use]
2995 pub const fn entrypoint(&self) -> Option<&Sourced<Entrypoint>> {
2996 self.entrypoint.as_ref()
2997 }
2998
2999 pub fn set_run_init(&mut self, run_init: Sourced<bool>) {
3001 self.run_init = Some(run_init);
3002 }
3003
3004 #[must_use]
3006 pub const fn run_init(&self) -> Option<&Sourced<bool>> {
3007 self.run_init.as_ref()
3008 }
3009
3010 pub fn set_stop_timeout(&mut self, timeout: Sourced<StopTimeout>) {
3012 self.stop_timeout = Some(timeout);
3013 }
3014
3015 #[must_use]
3017 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
3018 self.stop_timeout.as_ref()
3019 }
3020
3021 pub fn set_pull_policy(&mut self, policy: Sourced<PullPolicy>) {
3023 self.pull_policy = Some(policy);
3024 }
3025
3026 #[must_use]
3028 pub const fn pull_policy(&self) -> Option<&Sourced<PullPolicy>> {
3029 self.pull_policy.as_ref()
3030 }
3031
3032 pub fn set_memory_limit(&mut self, limit: Sourced<ProtectedString>) {
3034 self.memory_limit = Some(limit);
3035 }
3036
3037 #[must_use]
3039 pub const fn memory_limit(&self) -> Option<&Sourced<ProtectedString>> {
3040 self.memory_limit.as_ref()
3041 }
3042
3043 pub fn set_exposed_ports(&mut self, ports: Vec<Sourced<ExposedPort>>) {
3045 self.exposed_ports = Some(ports);
3046 self.exposed_ports_origins.clear();
3047 }
3048
3049 pub fn set_exposed_ports_with_origins(&mut self, ports: Vec<Sourced<ExposedPort>>, origins: Vec<Provenance>) {
3051 self.exposed_ports = Some(ports);
3052 self.exposed_ports_origins = origins;
3053 }
3054
3055 pub fn add_exposed_port(&mut self, port: Sourced<ExposedPort>) {
3057 self.exposed_ports.get_or_insert_default().push(port);
3058 }
3059
3060 #[must_use]
3062 pub fn exposed_ports(&self) -> Option<&[Sourced<ExposedPort>]> {
3063 self.exposed_ports.as_deref()
3064 }
3065
3066 #[must_use]
3068 pub fn exposed_ports_origins(&self) -> &[Provenance] {
3069 &self.exposed_ports_origins
3070 }
3071
3072 pub fn set_restart_policy(&mut self, restart_policy: Sourced<RestartPolicy>) {
3074 self.restart_policy = Some(restart_policy);
3075 }
3076
3077 #[must_use]
3079 pub const fn restart_policy(&self) -> Option<&Sourced<RestartPolicy>> {
3080 self.restart_policy.as_ref()
3081 }
3082
3083 pub fn set_healthcheck(&mut self, healthcheck: Sourced<Healthcheck>) {
3085 self.healthcheck = Some(healthcheck);
3086 }
3087
3088 #[must_use]
3090 pub const fn healthcheck(&self) -> Option<&Sourced<Healthcheck>> {
3091 self.healthcheck.as_ref()
3092 }
3093
3094 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
3096 self.labels.push(label);
3097 }
3098
3099 #[must_use]
3101 pub fn labels(&self) -> &[Sourced<MetadataLabel>] {
3102 &self.labels
3103 }
3104
3105 pub fn set_annotations(&mut self, annotations: Vec<Sourced<Annotation>>) {
3107 self.annotations = Some(annotations);
3108 self.annotations_origins.clear();
3109 }
3110
3111 pub fn add_annotation(&mut self, annotation: Sourced<Annotation>) {
3113 self.annotations.get_or_insert_default().push(annotation);
3114 }
3115
3116 pub fn set_annotations_with_origins(&mut self, annotations: Vec<Sourced<Annotation>>, origins: Vec<Provenance>) {
3118 self.annotations = Some(annotations);
3119 self.annotations_origins = origins;
3120 }
3121
3122 #[must_use]
3124 pub fn annotations(&self) -> Option<&[Sourced<Annotation>]> {
3125 self.annotations.as_deref()
3126 }
3127
3128 #[must_use]
3130 pub fn annotations_origins(&self) -> &[Provenance] {
3131 &self.annotations_origins
3132 }
3133
3134 pub fn set_logging(&mut self, logging: Sourced<Logging>) {
3136 self.logging = Some(logging);
3137 }
3138
3139 #[must_use]
3141 pub const fn logging(&self) -> Option<&Sourced<Logging>> {
3142 self.logging.as_ref()
3143 }
3144
3145 pub fn set_reload_action(&mut self, reload_action: Sourced<ReloadAction>) {
3147 self.reload_action = Some(reload_action);
3148 }
3149
3150 #[must_use]
3152 pub const fn reload_action(&self) -> Option<&Sourced<ReloadAction>> {
3153 self.reload_action.as_ref()
3154 }
3155
3156 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
3158 self.user = Some(user);
3159 }
3160
3161 #[must_use]
3163 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
3164 self.user.as_ref()
3165 }
3166
3167 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
3169 self.group = Some(group);
3170 }
3171
3172 #[must_use]
3174 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
3175 self.group.as_ref()
3176 }
3177
3178 pub fn set_user_namespace(&mut self, user_namespace: Sourced<ProtectedString>) {
3180 self.user_namespace = Some(user_namespace);
3181 }
3182
3183 #[must_use]
3185 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
3186 self.user_namespace.as_ref()
3187 }
3188
3189 pub fn add_supplementary_group(&mut self, group: Sourced<ProtectedString>) {
3191 self.supplementary_groups.push(group);
3192 }
3193
3194 #[must_use]
3196 pub fn supplementary_groups(&self) -> &[Sourced<ProtectedString>] {
3197 &self.supplementary_groups
3198 }
3199
3200 pub fn set_working_directory(&mut self, working_directory: Sourced<ProtectedString>) {
3202 self.working_directory = Some(working_directory);
3203 }
3204
3205 #[must_use]
3207 pub const fn working_directory(&self) -> Option<&Sourced<ProtectedString>> {
3208 self.working_directory.as_ref()
3209 }
3210
3211 pub fn set_read_only_root_filesystem(&mut self, read_only: Sourced<bool>) {
3213 self.read_only_root_filesystem = Some(read_only);
3214 }
3215
3216 #[must_use]
3218 pub const fn read_only_root_filesystem(&self) -> Option<&Sourced<bool>> {
3219 self.read_only_root_filesystem.as_ref()
3220 }
3221
3222 pub fn set_hostname(&mut self, hostname: Sourced<ProtectedString>) {
3224 self.hostname = Some(hostname);
3225 }
3226
3227 #[must_use]
3229 pub const fn hostname(&self) -> Option<&Sourced<ProtectedString>> {
3230 self.hostname.as_ref()
3231 }
3232
3233 pub fn set_dns_servers_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3235 self.dns_servers = Some(values);
3236 self.dns_servers_origins = origins;
3237 }
3238
3239 pub fn set_dns_servers(&mut self, values: Vec<Sourced<ProtectedString>>) {
3241 self.set_dns_servers_with_origins(values, Vec::new());
3242 }
3243
3244 #[must_use]
3246 pub fn dns_servers(&self) -> Option<&[Sourced<ProtectedString>]> {
3247 self.dns_servers.as_deref()
3248 }
3249
3250 #[must_use]
3252 pub fn dns_servers_origins(&self) -> &[Provenance] {
3253 &self.dns_servers_origins
3254 }
3255
3256 pub fn set_dns_options_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3258 self.dns_options = Some(values);
3259 self.dns_options_origins = origins;
3260 }
3261
3262 pub fn set_dns_options(&mut self, values: Vec<Sourced<ProtectedString>>) {
3264 self.set_dns_options_with_origins(values, Vec::new());
3265 }
3266
3267 #[must_use]
3269 pub fn dns_options(&self) -> Option<&[Sourced<ProtectedString>]> {
3270 self.dns_options.as_deref()
3271 }
3272
3273 #[must_use]
3275 pub fn dns_options_origins(&self) -> &[Provenance] {
3276 &self.dns_options_origins
3277 }
3278
3279 pub fn set_dns_search_domains_with_origins(
3281 &mut self,
3282 values: Vec<Sourced<ProtectedString>>,
3283 origins: Vec<Provenance>,
3284 ) {
3285 self.dns_search_domains = Some(values);
3286 self.dns_search_domains_origins = origins;
3287 }
3288
3289 pub fn set_dns_search_domains(&mut self, values: Vec<Sourced<ProtectedString>>) {
3291 self.set_dns_search_domains_with_origins(values, Vec::new());
3292 }
3293
3294 #[must_use]
3296 pub fn dns_search_domains(&self) -> Option<&[Sourced<ProtectedString>]> {
3297 self.dns_search_domains.as_deref()
3298 }
3299
3300 #[must_use]
3302 pub fn dns_search_domains_origins(&self) -> &[Provenance] {
3303 &self.dns_search_domains_origins
3304 }
3305
3306 pub fn set_security_options_with_origins(
3308 &mut self,
3309 values: Vec<Sourced<SecurityOption>>,
3310 origins: Vec<Provenance>,
3311 ) {
3312 self.security_options = Some(values);
3313 self.security_options_origins = origins;
3314 }
3315
3316 pub fn set_security_options(&mut self, values: Vec<Sourced<SecurityOption>>) {
3318 self.set_security_options_with_origins(values, Vec::new());
3319 }
3320
3321 #[must_use]
3323 pub fn security_options(&self) -> Option<&[Sourced<SecurityOption>]> {
3324 self.security_options.as_deref()
3325 }
3326
3327 #[must_use]
3329 pub fn security_options_origins(&self) -> &[Provenance] {
3330 &self.security_options_origins
3331 }
3332
3333 pub fn set_pids_limit(&mut self, limit: Sourced<ProtectedString>) {
3335 self.pids_limit = Some(limit);
3336 }
3337
3338 #[must_use]
3340 pub const fn pids_limit(&self) -> Option<&Sourced<ProtectedString>> {
3341 self.pids_limit.as_ref()
3342 }
3343
3344 pub fn set_shm_size(&mut self, size: Sourced<ProtectedString>) {
3346 self.shm_size = Some(size);
3347 }
3348
3349 #[must_use]
3351 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
3352 self.shm_size.as_ref()
3353 }
3354
3355 pub fn set_cap_add(&mut self, values: Vec<Sourced<ProtectedString>>) {
3357 self.cap_add = Some(values);
3358 self.cap_add_origins.clear();
3359 }
3360
3361 pub fn set_cap_add_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3363 self.cap_add = Some(values);
3364 self.cap_add_origins = origins;
3365 }
3366
3367 #[must_use]
3369 pub fn cap_add(&self) -> Option<&[Sourced<ProtectedString>]> {
3370 self.cap_add.as_deref()
3371 }
3372
3373 #[must_use]
3375 pub fn cap_add_origins(&self) -> &[Provenance] {
3376 &self.cap_add_origins
3377 }
3378
3379 pub fn set_cap_drop(&mut self, values: Vec<Sourced<ProtectedString>>) {
3381 self.cap_drop = Some(values);
3382 self.cap_drop_origins.clear();
3383 }
3384
3385 pub fn set_cap_drop_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3387 self.cap_drop = Some(values);
3388 self.cap_drop_origins = origins;
3389 }
3390
3391 #[must_use]
3393 pub fn cap_drop(&self) -> Option<&[Sourced<ProtectedString>]> {
3394 self.cap_drop.as_deref()
3395 }
3396
3397 #[must_use]
3399 pub fn cap_drop_origins(&self) -> &[Provenance] {
3400 &self.cap_drop_origins
3401 }
3402
3403 pub fn set_tmpfs(&mut self, values: Vec<Sourced<ProtectedString>>) {
3405 self.tmpfs = Some(values);
3406 self.tmpfs_origins.clear();
3407 }
3408
3409 pub fn set_tmpfs_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3411 self.tmpfs = Some(values);
3412 self.tmpfs_origins = origins;
3413 }
3414
3415 #[must_use]
3417 pub fn tmpfs(&self) -> Option<&[Sourced<ProtectedString>]> {
3418 self.tmpfs.as_deref()
3419 }
3420
3421 #[must_use]
3423 pub fn tmpfs_origins(&self) -> &[Provenance] {
3424 &self.tmpfs_origins
3425 }
3426
3427 pub fn set_sysctls(&mut self, values: Vec<Sourced<KernelParameter>>) {
3429 self.sysctls = Some(values);
3430 self.sysctls_origins.clear();
3431 }
3432
3433 pub fn set_sysctls_with_origins(&mut self, values: Vec<Sourced<KernelParameter>>, origins: Vec<Provenance>) {
3435 self.sysctls = Some(values);
3436 self.sysctls_origins = origins;
3437 }
3438
3439 #[must_use]
3441 pub fn sysctls(&self) -> Option<&[Sourced<KernelParameter>]> {
3442 self.sysctls.as_deref()
3443 }
3444
3445 #[must_use]
3447 pub fn sysctls_origins(&self) -> &[Provenance] {
3448 &self.sysctls_origins
3449 }
3450
3451 pub fn set_ulimits(&mut self, values: Vec<Sourced<ResourceLimit>>) {
3453 self.ulimits = Some(values);
3454 self.ulimits_origins.clear();
3455 }
3456
3457 pub fn set_ulimits_with_origins(&mut self, values: Vec<Sourced<ResourceLimit>>, origins: Vec<Provenance>) {
3459 self.ulimits = Some(values);
3460 self.ulimits_origins = origins;
3461 }
3462
3463 #[must_use]
3465 pub fn ulimits(&self) -> Option<&[Sourced<ResourceLimit>]> {
3466 self.ulimits.as_deref()
3467 }
3468
3469 #[must_use]
3471 pub fn ulimits_origins(&self) -> &[Provenance] {
3472 &self.ulimits_origins
3473 }
3474
3475 pub fn set_devices(&mut self, values: Vec<Sourced<Device>>) {
3477 self.devices = Some(values);
3478 self.devices_origins.clear();
3479 }
3480
3481 pub fn set_devices_with_origins(&mut self, values: Vec<Sourced<Device>>, origins: Vec<Provenance>) {
3483 self.devices = Some(values);
3484 self.devices_origins = origins;
3485 }
3486
3487 #[must_use]
3489 pub fn devices(&self) -> Option<&[Sourced<Device>]> {
3490 self.devices.as_deref()
3491 }
3492
3493 #[must_use]
3495 pub fn devices_origins(&self) -> &[Provenance] {
3496 &self.devices_origins
3497 }
3498
3499 pub fn set_stop_signal(&mut self, signal: Sourced<ProtectedString>) {
3501 self.stop_signal = Some(signal);
3502 }
3503
3504 #[must_use]
3506 pub const fn stop_signal(&self) -> Option<&Sourced<ProtectedString>> {
3507 self.stop_signal.as_ref()
3508 }
3509
3510 pub fn add_environment(&mut self, value: Sourced<EnvironmentVariable>) {
3512 self.environment.push(value);
3513 }
3514
3515 #[must_use]
3517 pub fn environment(&self) -> &[Sourced<EnvironmentVariable>] {
3518 &self.environment
3519 }
3520
3521 pub fn add_environment_file(&mut self, value: Sourced<EnvironmentFile>) {
3523 self.environment_files.push(value);
3524 }
3525
3526 #[must_use]
3528 pub fn environment_files(&self) -> &[Sourced<EnvironmentFile>] {
3529 &self.environment_files
3530 }
3531
3532 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
3534 self.host_mappings.push(value);
3535 }
3536
3537 #[must_use]
3539 pub fn host_mappings(&self) -> &[Sourced<HostMapping>] {
3540 &self.host_mappings
3541 }
3542
3543 pub fn add_port(&mut self, value: Sourced<Port>) {
3545 self.ports.push(value);
3546 }
3547
3548 #[must_use]
3550 pub fn ports(&self) -> &[Sourced<Port>] {
3551 &self.ports
3552 }
3553
3554 pub fn add_mount(&mut self, value: Sourced<Mount>) {
3556 self.mounts.push(value);
3557 }
3558
3559 #[must_use]
3561 pub fn mounts(&self) -> &[Sourced<Mount>] {
3562 &self.mounts
3563 }
3564
3565 pub fn add_config_grant(&mut self, value: Sourced<ResourceGrant>) {
3567 self.config_grants.push(value);
3568 }
3569
3570 #[must_use]
3572 pub fn config_grants(&self) -> &[Sourced<ResourceGrant>] {
3573 &self.config_grants
3574 }
3575
3576 pub fn add_secret_grant(&mut self, value: Sourced<ResourceGrant>) {
3578 self.secret_grants.push(value);
3579 }
3580
3581 #[must_use]
3583 pub fn secret_grants(&self) -> &[Sourced<ResourceGrant>] {
3584 &self.secret_grants
3585 }
3586
3587 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
3589 self.networks.push(value);
3590 }
3591
3592 pub fn replace_network(
3602 &mut self,
3603 index: usize,
3604 value: Sourced<NetworkAttachment>,
3605 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
3606 let len = self.networks.len();
3607 let Some(slot) = self.networks.get_mut(index) else {
3608 return Err(ModelError::UnknownNetworkAttachmentIndex { index, len });
3609 };
3610 Ok(std::mem::replace(slot, value))
3611 }
3612
3613 #[must_use]
3615 pub fn networks(&self) -> &[Sourced<NetworkAttachment>] {
3616 &self.networks
3617 }
3618
3619 pub fn add_dependency(&mut self, value: Sourced<ServiceDependency>) {
3621 self.dependencies.push(value);
3622 }
3623
3624 #[must_use]
3626 pub fn dependencies(&self) -> &[Sourced<ServiceDependency>] {
3627 &self.dependencies
3628 }
3629
3630 pub fn validate_image_source_exclusivity(&self) -> Result<(), ModelError> {
3639 if self.rootfs.is_some() {
3640 self.ensure_rootfs_is_compatible()?;
3641 }
3642 Ok(())
3643 }
3644
3645 fn ensure_rootfs_is_compatible(&self) -> Result<(), ModelError> {
3646 let source = if self.image.is_some() {
3647 Some("image")
3648 } else if self.image_acquisition.is_some() {
3649 Some("image acquisition")
3650 } else if self.image_build.is_some() {
3651 Some("image build")
3652 } else {
3653 None
3654 };
3655 if let Some(source) = source {
3656 return Err(ModelError::RootfsImageSourceConflict {
3657 service: self.name.as_str().to_owned(),
3658 source,
3659 });
3660 }
3661 Ok(())
3662 }
3663}
3664
3665#[derive(Clone, Debug, Eq, PartialEq)]
3667pub struct Application {
3668 name: Identifier,
3669 retained_native_evidence: Vec<RetainedNativeEvidence>,
3670 image_acquisitions: Vec<Sourced<ImageAcquisition>>,
3671 image_builds: Vec<Sourced<ImageBuild>>,
3672 services: Vec<Sourced<Service>>,
3673 service_groups: Vec<Sourced<ServiceGroup>>,
3674 volumes: Vec<Sourced<Volume>>,
3675 networks: Vec<Sourced<Network>>,
3676 configs: Vec<Sourced<Config>>,
3677 secrets: Vec<Sourced<Secret>>,
3678}
3679
3680impl Application {
3681 #[must_use]
3683 pub const fn new(name: Identifier) -> Self {
3684 Self {
3685 name,
3686 retained_native_evidence: Vec::new(),
3687 image_acquisitions: Vec::new(),
3688 image_builds: Vec::new(),
3689 services: Vec::new(),
3690 service_groups: Vec::new(),
3691 volumes: Vec::new(),
3692 networks: Vec::new(),
3693 configs: Vec::new(),
3694 secrets: Vec::new(),
3695 }
3696 }
3697
3698 #[must_use]
3700 pub const fn name(&self) -> &Identifier {
3701 &self.name
3702 }
3703
3704 pub fn add_retained_native_evidence(&mut self, evidence: RetainedNativeEvidence) -> Result<(), ModelError> {
3711 let (kind, name) = evidence.subject.owner();
3712 let present = match kind {
3713 "service" => self.services.iter().any(|entry| entry.value().name() == name),
3714 "volume" => self.volumes.iter().any(|entry| entry.value().name() == name),
3715 _ => false,
3716 };
3717 if !present {
3718 return Err(ModelError::UnknownNativeEvidenceOwner {
3719 kind,
3720 name: name.as_str().to_owned(),
3721 });
3722 }
3723 self.retained_native_evidence.push(evidence);
3724 Ok(())
3725 }
3726
3727 #[must_use]
3729 pub fn retained_native_evidence(&self) -> &[RetainedNativeEvidence] {
3730 &self.retained_native_evidence
3731 }
3732
3733 pub fn add_image_acquisition(&mut self, acquisition: Sourced<ImageAcquisition>) -> Result<(), ModelError> {
3739 ensure_unique(
3740 "image acquisition",
3741 acquisition.value().name(),
3742 self.image_acquisitions.iter().map(|candidate| candidate.value().name()),
3743 )?;
3744 self.image_acquisitions.push(acquisition);
3745 Ok(())
3746 }
3747
3748 #[must_use]
3750 pub fn image_acquisitions(&self) -> &[Sourced<ImageAcquisition>] {
3751 &self.image_acquisitions
3752 }
3753
3754 pub fn add_image_build(&mut self, build: Sourced<ImageBuild>) -> Result<(), ModelError> {
3760 ensure_unique(
3761 "image build",
3762 build.value().name(),
3763 self.image_builds.iter().map(|candidate| candidate.value().name()),
3764 )?;
3765 self.image_builds.push(build);
3766 Ok(())
3767 }
3768
3769 #[must_use]
3771 pub fn image_builds(&self) -> &[Sourced<ImageBuild>] {
3772 &self.image_builds
3773 }
3774
3775 pub fn validate_image_artifact_references(&self) -> Result<(), ModelError> {
3785 for service in &self.services {
3786 if let Some(acquisition) = service.value().image_acquisition() {
3787 if !self.contains_image_acquisition(acquisition.value()) {
3788 return Err(ModelError::UnknownImageAcquisitionReference {
3789 service: service.value().name().as_str().to_owned(),
3790 acquisition: acquisition.value().as_str().to_owned(),
3791 });
3792 }
3793 }
3794 if let Some(build) = service.value().image_build() {
3795 if !self.contains_image_build(build.value()) {
3796 return Err(ModelError::UnknownImageBuildReference {
3797 service: service.value().name().as_str().to_owned(),
3798 build: build.value().as_str().to_owned(),
3799 });
3800 }
3801 }
3802 }
3803 for volume in &self.volumes {
3804 let Some(source) = volume.value().image_source() else {
3805 continue;
3806 };
3807 match source.value() {
3808 VolumeImageSource::Literal(_) => {}
3809 VolumeImageSource::ImageAcquisition(acquisition) => {
3810 if !self.contains_image_acquisition(acquisition) {
3811 return Err(ModelError::UnknownVolumeImageAcquisitionReference {
3812 volume: volume.value().name().as_str().to_owned(),
3813 acquisition: acquisition.as_str().to_owned(),
3814 });
3815 }
3816 }
3817 VolumeImageSource::ImageBuild(build) => {
3818 if !self.contains_image_build(build) {
3819 return Err(ModelError::UnknownVolumeImageBuildReference {
3820 volume: volume.value().name().as_str().to_owned(),
3821 build: build.as_str().to_owned(),
3822 });
3823 }
3824 }
3825 }
3826 }
3827 Ok(())
3828 }
3829
3830 pub fn validate_image_artifact_dependencies(
3841 &self,
3842 dependencies: &[Sourced<ArtifactDependency>],
3843 ) -> Result<(), ModelError> {
3844 self.validate_image_artifact_references()?;
3845
3846 let mut graph = BTreeMap::<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>::new();
3847 for dependency in dependencies {
3848 let source = dependency.value().source().value();
3849 let target = dependency.value().target().value();
3850 self.validate_artifact_dependency_node(source)?;
3851 self.validate_artifact_dependency_node(target)?;
3852 graph.entry(source.clone()).or_default().insert(target.clone());
3853 graph.entry(target.clone()).or_default();
3854 }
3855
3856 let mut state = BTreeMap::<ArtifactDependencyNode, VisitState>::new();
3857 let mut path = Vec::new();
3858 for node in graph.keys() {
3859 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
3860 continue;
3861 }
3862 if let Some(cycle) = detect_artifact_cycle(node, &graph, &mut state, &mut path) {
3863 return Err(ModelError::ImageArtifactDependencyCycle {
3864 nodes: cycle.into_iter().map(|node| node.display_name()).collect(),
3865 });
3866 }
3867 }
3868 Ok(())
3869 }
3870
3871 fn contains_image_acquisition(&self, name: &Identifier) -> bool {
3872 self.image_acquisitions
3873 .iter()
3874 .any(|candidate| candidate.value().name() == name)
3875 }
3876
3877 fn contains_image_build(&self, name: &Identifier) -> bool {
3878 self.image_builds
3879 .iter()
3880 .any(|candidate| candidate.value().name() == name)
3881 }
3882
3883 fn validate_artifact_dependency_node(&self, node: &ArtifactDependencyNode) -> Result<(), ModelError> {
3884 let (kind, name) = node.kind_and_name();
3885 let exists = match node {
3886 ArtifactDependencyNode::Volume(_) => self.volumes.iter().any(|volume| volume.value().name() == name),
3887 ArtifactDependencyNode::ImageAcquisition(_) => self.contains_image_acquisition(name),
3888 ArtifactDependencyNode::ImageBuild(_) => self.contains_image_build(name),
3889 };
3890 if exists {
3891 Ok(())
3892 } else {
3893 Err(ModelError::UnknownArtifactDependencyNode {
3894 kind,
3895 name: name.as_str().to_owned(),
3896 })
3897 }
3898 }
3899
3900 pub fn add_service(&mut self, service: Sourced<Service>) -> Result<(), ModelError> {
3908 ensure_unique(
3909 "service",
3910 service.value().name(),
3911 self.services.iter().map(|candidate| candidate.value().name()),
3912 )?;
3913 service.value().validate_image_source_exclusivity()?;
3914 if let Some(acquisition) = service.value().image_acquisition() {
3915 if !self
3916 .image_acquisitions
3917 .iter()
3918 .any(|candidate| candidate.value().name() == acquisition.value())
3919 {
3920 return Err(ModelError::UnknownImageAcquisitionReference {
3921 service: service.value().name().as_str().to_owned(),
3922 acquisition: acquisition.value().as_str().to_owned(),
3923 });
3924 }
3925 }
3926 if let Some(build) = service.value().image_build() {
3927 if !self
3928 .image_builds
3929 .iter()
3930 .any(|candidate| candidate.value().name() == build.value())
3931 {
3932 return Err(ModelError::UnknownImageBuildReference {
3933 service: service.value().name().as_str().to_owned(),
3934 build: build.value().as_str().to_owned(),
3935 });
3936 }
3937 }
3938 self.services.push(service);
3939 Ok(())
3940 }
3941
3942 #[must_use]
3944 pub fn services(&self) -> &[Sourced<Service>] {
3945 &self.services
3946 }
3947
3948 pub fn add_service_group(&mut self, group: Sourced<ServiceGroup>) -> Result<(), ModelError> {
3958 ensure_unique(
3959 "service group",
3960 group.value().name(),
3961 self.service_groups.iter().map(|candidate| candidate.value().name()),
3962 )?;
3963 for member in group.value().members() {
3964 if !self
3965 .services
3966 .iter()
3967 .any(|service| service.value().name() == member.value())
3968 {
3969 return Err(ModelError::UnknownServiceGroupMember {
3970 group: group.value().name().as_str().to_owned(),
3971 service: member.value().as_str().to_owned(),
3972 });
3973 }
3974 if let Some(existing) = self.service_groups.iter().find(|candidate| {
3975 candidate
3976 .value()
3977 .members()
3978 .iter()
3979 .any(|candidate_member| candidate_member.value() == member.value())
3980 }) {
3981 return Err(ModelError::ServiceInMultipleGroups {
3982 service: member.value().as_str().to_owned(),
3983 existing: existing.value().name().as_str().to_owned(),
3984 replacement: group.value().name().as_str().to_owned(),
3985 });
3986 }
3987 }
3988 self.service_groups.push(group);
3989 Ok(())
3990 }
3991
3992 #[must_use]
3994 pub fn service_groups(&self) -> &[Sourced<ServiceGroup>] {
3995 &self.service_groups
3996 }
3997
3998 pub fn add_volume(&mut self, volume: Sourced<Volume>) -> Result<(), ModelError> {
4004 ensure_unique(
4005 "volume",
4006 volume.value().name(),
4007 self.volumes.iter().map(|candidate| candidate.value().name()),
4008 )?;
4009 self.volumes.push(volume);
4010 Ok(())
4011 }
4012
4013 #[must_use]
4015 pub fn volumes(&self) -> &[Sourced<Volume>] {
4016 &self.volumes
4017 }
4018
4019 pub fn add_network(&mut self, network: Sourced<Network>) -> Result<(), ModelError> {
4025 ensure_unique(
4026 "network",
4027 network.value().name(),
4028 self.networks.iter().map(|candidate| candidate.value().name()),
4029 )?;
4030 self.networks.push(network);
4031 Ok(())
4032 }
4033
4034 #[must_use]
4036 pub fn networks(&self) -> &[Sourced<Network>] {
4037 &self.networks
4038 }
4039
4040 pub fn add_config(&mut self, config: Sourced<Config>) -> Result<(), ModelError> {
4046 ensure_unique(
4047 "config",
4048 config.value().name(),
4049 self.configs.iter().map(|candidate| candidate.value().name()),
4050 )?;
4051 self.configs.push(config);
4052 Ok(())
4053 }
4054
4055 #[must_use]
4057 pub fn configs(&self) -> &[Sourced<Config>] {
4058 &self.configs
4059 }
4060
4061 pub fn add_secret(&mut self, secret: Sourced<Secret>) -> Result<(), ModelError> {
4067 ensure_unique(
4068 "secret",
4069 secret.value().name(),
4070 self.secrets.iter().map(|candidate| candidate.value().name()),
4071 )?;
4072 self.secrets.push(secret);
4073 Ok(())
4074 }
4075
4076 #[must_use]
4078 pub fn secrets(&self) -> &[Sourced<Secret>] {
4079 &self.secrets
4080 }
4081}
4082
4083#[derive(Clone, Copy, Eq, PartialEq)]
4084enum VisitState {
4085 Visiting,
4086 Finished,
4087}
4088
4089fn detect_artifact_cycle(
4090 node: &ArtifactDependencyNode,
4091 graph: &BTreeMap<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>,
4092 state: &mut BTreeMap<ArtifactDependencyNode, VisitState>,
4093 path: &mut Vec<ArtifactDependencyNode>,
4094) -> Option<Vec<ArtifactDependencyNode>> {
4095 if state.get(node).is_some_and(|state| *state == VisitState::Visiting) {
4096 let index = path.iter().position(|candidate| candidate == node)?;
4097 let mut cycle = path[index..].to_vec();
4098 cycle.push(node.clone());
4099 return Some(cycle);
4100 }
4101 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
4102 return None;
4103 }
4104
4105 state.insert(node.clone(), VisitState::Visiting);
4106 path.push(node.clone());
4107 if let Some(targets) = graph.get(node) {
4108 for target in targets {
4109 if let Some(cycle) = detect_artifact_cycle(target, graph, state, path) {
4110 return Some(cycle);
4111 }
4112 }
4113 }
4114 path.pop();
4115 state.insert(node.clone(), VisitState::Finished);
4116 None
4117}
4118
4119fn ensure_unique<'a>(
4120 kind: &'static str,
4121 name: &Identifier,
4122 existing: impl Iterator<Item = &'a Identifier>,
4123) -> Result<(), ModelError> {
4124 if existing.into_iter().any(|candidate| candidate == name) {
4125 return Err(ModelError::DuplicateResource {
4126 kind,
4127 name: name.as_str().to_owned(),
4128 });
4129 }
4130 Ok(())
4131}
4132
4133fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
4134 if value.is_empty() {
4135 return Err(ModelError::EmptyValue(kind));
4136 }
4137 validate_no_nul(kind, value)
4138}
4139
4140fn validate_no_nul(kind: &'static str, value: &str) -> Result<(), ModelError> {
4141 if value.contains('\0') {
4142 return Err(ModelError::ContainsNul(kind));
4143 }
4144 Ok(())
4145}
4146
4147#[cfg(test)]
4148mod tests {
4149 use super::{
4150 Annotation, Application, ArtifactDependency, ArtifactDependencyNode, Command, Config, ConfigMaterial, Device,
4151 Entrypoint, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileSyntax, ExposedPort, GroupExitPolicy,
4152 HealthcheckDuration, HealthcheckRetries, HostAddress, HostAddressKind, HostMapping, Identifier,
4153 KernelParameter, Logging, LoggingOption, MetadataLabel, ModelError, Mount, MountSource, Network,
4154 NetworkAttachment, NetworkDriverOption, NetworkIpamConfig, Protocol, PullPolicy, ReloadAction, ResourceGrant,
4155 ResourceGrantSyntax, ResourceLimit, ResourceOwnership, RestartPolicy, Secret, SecretMaterial, SecurityOption,
4156 Service, ServiceDependency, ServiceDependencyCondition, ServiceGroup, ServiceGroupRuntime, StartupNotification,
4157 StopTimeout, Volume, VolumeImageSource,
4158 };
4159 use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Sourced};
4160
4161 #[test]
4162 fn preserves_service_order_and_rejects_duplicate_names() -> Result<(), String> {
4163 let mut application = Application::new(id("example")?);
4164 application
4165 .add_service(Sourced::generated(Service::new(id("web")?)))
4166 .map_err(|error| error.to_string())?;
4167 application
4168 .add_service(Sourced::generated(Service::new(id("database")?)))
4169 .map_err(|error| error.to_string())?;
4170
4171 let names: Vec<_> = application
4172 .services()
4173 .iter()
4174 .map(|service| service.value().name().as_str())
4175 .collect();
4176 assert_eq!(names, ["web", "database"]);
4177
4178 let duplicate = application.add_service(Sourced::generated(Service::new(id("web")?)));
4179 assert!(matches!(duplicate, Err(ModelError::DuplicateResource { .. })));
4180 Ok(())
4181 }
4182
4183 #[test]
4184 fn keeps_the_service_key_and_explicit_runtime_name_distinct() -> Result<(), String> {
4185 let mut service = Service::new(id("web")?);
4186 service.set_runtime_name(Sourced::generated(ProtectedString::plain("production-web")));
4187
4188 assert_eq!(service.name().as_str(), "web");
4189 assert_eq!(
4190 service.runtime_name().map(|name| name.value().expose()),
4191 Some("production-web")
4192 );
4193 Ok(())
4194 }
4195
4196 #[test]
4197 fn network_keeps_logical_and_runtime_names_and_literal_flags_distinct() -> Result<(), String> {
4198 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4199 let origin = crate::Provenance::source(source);
4200 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4201 network.set_runtime_name(Sourced::from_source(
4202 ProtectedString::plain("production-frontend"),
4203 origin.clone(),
4204 ));
4205 network.set_driver(Sourced::from_source(ProtectedString::plain("bridge"), origin.clone()));
4206 network.set_internal(Sourced::from_source(true, origin.clone()));
4207 network.set_ipv6(Sourced::from_source(false, origin.clone()));
4208 network.set_ipam_driver(Sourced::from_source(ProtectedString::plain("default"), origin));
4209
4210 assert_eq!(network.name().as_str(), "frontend");
4211 assert_eq!(
4212 network.runtime_name().map(|value| value.value().expose()),
4213 Some("production-frontend")
4214 );
4215 assert_eq!(network.driver().map(|value| value.value().expose()), Some("bridge"));
4216 assert_eq!(network.internal().map(Sourced::value), Some(&true));
4217 assert_eq!(network.ipv6().map(Sourced::value), Some(&false));
4218 assert_eq!(
4219 network.ipam_driver().map(|value| value.value().expose()),
4220 Some("default")
4221 );
4222 Ok(())
4223 }
4224
4225 #[test]
4226 fn network_collections_retain_resets_provenance_and_redact_protected_values() -> Result<(), String> {
4227 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4228 let origin = crate::Provenance::source(source);
4229 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4230 let option = NetworkDriverOption::new(
4231 Sourced::from_source(id("com.example.token")?, origin.clone()),
4232 Sourced::from_source(ProtectedString::sensitive("never-print-this"), origin.clone()),
4233 )
4234 .map_err(|error| error.to_string())?;
4235 let label = MetadataLabel::new(id("com.example.label")?, ProtectedString::sensitive("also-private"));
4236
4237 network
4238 .set_driver_options_with_origins(vec![Sourced::from_source(option, origin.clone())], vec![origin.clone()]);
4239 network.set_labels_with_origins(vec![Sourced::from_source(label, origin.clone())], vec![origin.clone()]);
4240 network.set_ipam_configs_with_origins(Vec::new(), vec![origin]);
4241
4242 assert_eq!(network.driver_options().map(<[_]>::len), Some(1));
4243 assert_eq!(network.labels().map(<[_]>::len), Some(1));
4244 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4245 assert_eq!(network.driver_options_origins().len(), 1);
4246 assert_eq!(network.labels_origins().len(), 1);
4247 assert_eq!(network.ipam_configs_origins().len(), 1);
4248 let debug = format!("{network:?}");
4249 assert!(!debug.contains("never-print-this"));
4250 assert!(!debug.contains("also-private"));
4251 assert!(debug.contains("[REDACTED]"));
4252
4253 network.set_driver_options(Vec::new());
4254 network.set_labels(Vec::new());
4255 network.set_ipam_configs(Vec::new());
4256 assert_eq!(network.driver_options().map(<[_]>::len), Some(0));
4257 assert_eq!(network.labels().map(<[_]>::len), Some(0));
4258 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4259 assert!(network.driver_options_origins().is_empty());
4260 assert!(network.labels_origins().is_empty());
4261 assert!(network.ipam_configs_origins().is_empty());
4262 Ok(())
4263 }
4264
4265 #[test]
4266 fn network_ipam_rows_preserve_association_order_and_reject_subnetless_values() -> Result<(), String> {
4267 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4268 let origin = crate::Provenance::source(source);
4269 let mut first = NetworkIpamConfig::new(Sourced::from_source(
4270 ProtectedString::plain("10.10.0.0/24"),
4271 origin.clone(),
4272 ))
4273 .map_err(|error| error.to_string())?;
4274 first
4275 .set_gateway(Sourced::from_source(
4276 ProtectedString::plain("10.10.0.1"),
4277 origin.clone(),
4278 ))
4279 .map_err(|error| error.to_string())?;
4280 let mut second = NetworkIpamConfig::new(Sourced::from_source(
4281 ProtectedString::plain("fd00:10::/64"),
4282 origin.clone(),
4283 ))
4284 .map_err(|error| error.to_string())?;
4285 second
4286 .set_ip_range(Sourced::from_source(
4287 ProtectedString::plain("fd00:10::100/120"),
4288 origin.clone(),
4289 ))
4290 .map_err(|error| error.to_string())?;
4291
4292 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4293 network.set_ipam_configs_with_origins(
4294 vec![
4295 Sourced::from_source(first, origin.clone()),
4296 Sourced::from_source(second, origin),
4297 ],
4298 Vec::new(),
4299 );
4300 let rows = network
4301 .ipam_configs()
4302 .ok_or_else(|| "IPAM configs were omitted".to_owned())?;
4303 assert_eq!(rows.len(), 2);
4304 assert_eq!(rows[0].value().subnet().value().expose(), "10.10.0.0/24");
4305 assert_eq!(
4306 rows[0].value().gateway().map(|value| value.value().expose()),
4307 Some("10.10.0.1")
4308 );
4309 assert_eq!(rows[0].value().ip_range(), None);
4310 assert_eq!(rows[1].value().subnet().value().expose(), "fd00:10::/64");
4311 assert_eq!(rows[1].value().gateway(), None);
4312 assert_eq!(
4313 rows[1].value().ip_range().map(|value| value.value().expose()),
4314 Some("fd00:10::100/120")
4315 );
4316
4317 assert!(matches!(
4318 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain(""))),
4319 Err(ModelError::EmptyValue("network IPAM subnet"))
4320 ));
4321 assert!(matches!(
4322 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain("10.0.0.0/24\0bad"))),
4323 Err(ModelError::ContainsNul("network IPAM subnet"))
4324 ));
4325 assert!(matches!(
4326 NetworkDriverOption::new(
4327 Sourced::generated(id("option")?),
4328 Sourced::generated(ProtectedString::plain("bad\0value")),
4329 ),
4330 Err(ModelError::ContainsNul("network driver option value"))
4331 ));
4332 Ok(())
4333 }
4334
4335 #[test]
4336 fn image_artifact_resources_are_ordered_unique_and_referenced_explicitly() -> Result<(), String> {
4337 let mut application = Application::new(id("example")?);
4338 application
4339 .add_image_acquisition(Sourced::generated(ImageAcquisition::new(id("base-image")?)))
4340 .map_err(|error| error.to_string())?;
4341 application
4342 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4343 .map_err(|error| error.to_string())?;
4344
4345 let mut web = Service::new(id("web")?);
4346 web.set_image_acquisition(Sourced::generated(id("base-image")?));
4347 web.set_image_build(Sourced::generated(id("web-build")?));
4348 application
4349 .add_service(Sourced::generated(web))
4350 .map_err(|error| error.to_string())?;
4351
4352 assert_eq!(
4353 application.image_acquisitions()[0].value().name().as_str(),
4354 "base-image"
4355 );
4356 assert_eq!(application.image_builds()[0].value().name().as_str(), "web-build");
4357 assert!(matches!(
4358 application.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?))),
4359 Err(ModelError::DuplicateResource {
4360 kind: "image build",
4361 ..
4362 })
4363 ));
4364
4365 let mut missing = Service::new(id("missing")?);
4366 missing.set_image_build(Sourced::generated(id("absent-build")?));
4367 assert!(matches!(
4368 application.add_service(Sourced::generated(missing)),
4369 Err(ModelError::UnknownImageBuildReference { .. })
4370 ));
4371 Ok(())
4372 }
4373
4374 #[test]
4375 fn volume_keeps_logical_runtime_and_service_names_and_local_fields_distinct() -> Result<(), String> {
4376 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4377 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4378 volume.set_runtime_name(Sourced::from_source(
4379 ProtectedString::plain("production-data"),
4380 origin.clone(),
4381 ));
4382 volume.set_service_name(Sourced::from_source(
4383 ProtectedString::plain("data-volume.service"),
4384 origin.clone(),
4385 ));
4386 volume.set_driver(Sourced::from_source(ProtectedString::plain("local"), origin.clone()));
4387 volume.set_device(Sourced::from_source(
4388 ProtectedString::plain("/srv/data"),
4389 origin.clone(),
4390 ));
4391 volume.set_volume_type(Sourced::from_source(ProtectedString::plain("none"), origin.clone()));
4392 volume.set_options(Sourced::from_source(ProtectedString::plain("bind"), origin.clone()));
4393
4394 assert_eq!(volume.name().as_str(), "data");
4395 assert_eq!(
4396 volume.runtime_name().map(|name| name.value().expose()),
4397 Some("production-data")
4398 );
4399 assert_eq!(
4400 volume.service_name().map(|name| name.value().expose()),
4401 Some("data-volume.service")
4402 );
4403 assert_eq!(volume.driver().map(|value| value.value().expose()), Some("local"));
4404 assert_eq!(volume.device().map(|value| value.value().expose()), Some("/srv/data"));
4405 assert_eq!(volume.volume_type().map(|value| value.value().expose()), Some("none"));
4406 assert_eq!(volume.options().map(|value| value.value().expose()), Some("bind"));
4407 assert_eq!(
4408 volume.options().map(Sourced::origins),
4409 Some(std::slice::from_ref(&origin))
4410 );
4411 Ok(())
4412 }
4413
4414 #[test]
4415 fn volume_preserves_resets_order_protected_values_and_identity_dimensions() -> Result<(), String> {
4416 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4417 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4418 assert!(volume.labels().is_none());
4419 volume.set_labels_with_origins(Vec::new(), vec![origin.clone()]);
4420 volume.set_user(Sourced::from_source(
4421 ProtectedString::plain("named-user"),
4422 origin.clone(),
4423 ));
4424 volume.set_group(Sourced::from_source(
4425 ProtectedString::plain("named-group"),
4426 origin.clone(),
4427 ));
4428 volume.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4429 volume.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin));
4430
4431 assert_eq!(volume.labels().map(<[_]>::len), Some(0));
4432 assert_eq!(volume.user().map(|value| value.value().expose()), Some("named-user"));
4433 assert_eq!(volume.group().map(|value| value.value().expose()), Some("named-group"));
4434 assert_eq!(volume.uid().map(|value| value.value().expose()), Some("1001"));
4435 assert_eq!(volume.gid().map(|value| value.value().expose()), Some("1002"));
4436 Ok(())
4437 }
4438
4439 #[test]
4440 fn volume_copy_and_image_sources_preserve_absence_and_typed_distinctions() -> Result<(), String> {
4441 let origin =
4442 crate::Provenance::source(crate::SourceId::new("cache.volume").map_err(|error| error.to_string())?);
4443 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4444 assert_eq!(volume.copy(), None);
4445 volume.set_copy(Sourced::from_source(false, origin.clone()));
4446 assert_eq!(volume.copy().map(Sourced::value), Some(&false));
4447 volume.set_copy(Sourced::from_source(true, origin.clone()));
4448 assert_eq!(volume.copy().map(Sourced::value), Some(&true));
4449
4450 volume
4451 .set_image_source(Sourced::from_source(
4452 VolumeImageSource::Literal(ProtectedString::sensitive("registry.example/private:1")),
4453 origin.clone(),
4454 ))
4455 .map_err(|error| error.to_string())?;
4456 assert!(matches!(
4457 volume.image_source().map(Sourced::value),
4458 Some(VolumeImageSource::Literal(_))
4459 ));
4460 assert!(!format!("{volume:?}").contains("registry.example/private:1"));
4461
4462 volume
4463 .set_image_source(Sourced::from_source(
4464 VolumeImageSource::ImageAcquisition(id("cache-image")?),
4465 origin.clone(),
4466 ))
4467 .map_err(|error| error.to_string())?;
4468 assert!(matches!(
4469 volume.image_source().map(Sourced::value),
4470 Some(VolumeImageSource::ImageAcquisition(name)) if name.as_str() == "cache-image"
4471 ));
4472 volume
4473 .set_image_source(Sourced::from_source(
4474 VolumeImageSource::ImageBuild(id("cache-build")?),
4475 origin,
4476 ))
4477 .map_err(|error| error.to_string())?;
4478 assert!(matches!(
4479 volume.image_source().map(Sourced::value),
4480 Some(VolumeImageSource::ImageBuild(name)) if name.as_str() == "cache-build"
4481 ));
4482 Ok(())
4483 }
4484
4485 #[test]
4486 fn volume_artifact_validation_is_deferred_and_explicit_edges_find_cycles() -> Result<(), String> {
4487 let mut application = Application::new(id("example")?);
4488 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4489 volume
4490 .set_image_source(Sourced::generated(VolumeImageSource::ImageBuild(id("cache-build")?)))
4491 .map_err(|error| error.to_string())?;
4492 application
4493 .add_volume(Sourced::generated(volume))
4494 .map_err(|error| error.to_string())?;
4495 assert!(matches!(
4496 application.validate_image_artifact_references(),
4497 Err(ModelError::UnknownVolumeImageBuildReference { .. })
4498 ));
4499
4500 application
4501 .add_image_build(Sourced::generated(ImageBuild::new(id("cache-build")?)))
4502 .map_err(|error| error.to_string())?;
4503 application
4504 .validate_image_artifact_references()
4505 .map_err(|error| error.to_string())?;
4506
4507 let volume_node = ArtifactDependencyNode::Volume(id("cache")?);
4508 let build_node = ArtifactDependencyNode::ImageBuild(id("cache-build")?);
4509 let dependencies = vec![
4510 Sourced::generated(ArtifactDependency::new(
4511 Sourced::generated(volume_node.clone()),
4512 Sourced::generated(build_node.clone()),
4513 )),
4514 Sourced::generated(ArtifactDependency::new(
4515 Sourced::generated(build_node),
4516 Sourced::generated(volume_node),
4517 )),
4518 ];
4519 assert!(matches!(
4520 application.validate_image_artifact_dependencies(&dependencies),
4521 Err(ModelError::ImageArtifactDependencyCycle { .. })
4522 ));
4523 let missing = vec![Sourced::generated(ArtifactDependency::new(
4524 Sourced::generated(ArtifactDependencyNode::ImageBuild(id("cache-build")?)),
4525 Sourced::generated(ArtifactDependencyNode::Volume(id("missing")?)),
4526 ))];
4527 assert!(matches!(
4528 application.validate_image_artifact_dependencies(&missing),
4529 Err(ModelError::UnknownArtifactDependencyNode { kind: "volume", .. })
4530 ));
4531 Ok(())
4532 }
4533
4534 #[test]
4535 fn volume_rejects_invalid_literal_image_values() -> Result<(), String> {
4536 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4537 assert!(matches!(
4538 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4539 ""
4540 )))),
4541 Err(ModelError::EmptyValue("volume image"))
4542 ));
4543 assert!(matches!(
4544 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4545 "bad\0image"
4546 )))),
4547 Err(ModelError::ContainsNul("volume image"))
4548 ));
4549 Ok(())
4550 }
4551
4552 #[test]
4553 fn collection_resets_retain_explicit_emptiness_and_clear_stale_origins() -> Result<(), String> {
4554 let origin =
4555 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
4556 let mut service = Service::new(id("web")?);
4557
4558 service.set_cap_add_with_origins(Vec::new(), vec![origin.clone()]);
4559 service.set_cap_drop_with_origins(Vec::new(), vec![origin.clone()]);
4560 service.set_tmpfs_with_origins(Vec::new(), vec![origin.clone()]);
4561 service.set_sysctls_with_origins(Vec::new(), vec![origin.clone()]);
4562 service.set_ulimits_with_origins(Vec::new(), vec![origin.clone()]);
4563 service.set_devices_with_origins(Vec::new(), vec![origin]);
4564
4565 assert_eq!(service.cap_add().map(<[_]>::len), Some(0));
4566 assert_eq!(service.cap_drop().map(<[_]>::len), Some(0));
4567 assert_eq!(service.tmpfs().map(<[_]>::len), Some(0));
4568 assert_eq!(service.sysctls().map(<[_]>::len), Some(0));
4569 assert_eq!(service.ulimits().map(<[_]>::len), Some(0));
4570 assert_eq!(service.devices().map(<[_]>::len), Some(0));
4571 assert_eq!(service.cap_add_origins().len(), 1);
4572 assert_eq!(service.cap_drop_origins().len(), 1);
4573 assert_eq!(service.tmpfs_origins().len(), 1);
4574 assert_eq!(service.sysctls_origins().len(), 1);
4575 assert_eq!(service.ulimits_origins().len(), 1);
4576 assert_eq!(service.devices_origins().len(), 1);
4577
4578 service.set_cap_add(Vec::new());
4579 service.set_cap_drop(Vec::new());
4580 service.set_tmpfs(Vec::new());
4581 service.set_sysctls(Vec::<Sourced<KernelParameter>>::new());
4582 service.set_ulimits(Vec::<Sourced<ResourceLimit>>::new());
4583 service.set_devices(Vec::<Sourced<Device>>::new());
4584
4585 assert!(service.cap_add_origins().is_empty());
4586 assert!(service.cap_drop_origins().is_empty());
4587 assert!(service.tmpfs_origins().is_empty());
4588 assert!(service.sysctls_origins().is_empty());
4589 assert!(service.ulimits_origins().is_empty());
4590 assert!(service.devices_origins().is_empty());
4591 Ok(())
4592 }
4593
4594 #[test]
4595 fn restart_policy_keeps_unlimited_and_finite_on_failure_distinct() {
4596 let finite = std::num::NonZeroU64::new(4);
4597 assert_eq!(RestartPolicy::on_failure(None).maximum_retries(), None);
4598 assert_eq!(RestartPolicy::on_failure(finite).maximum_retries(), finite);
4599 assert_eq!(RestartPolicy::Always.maximum_retries(), None);
4600 }
4601
4602 #[test]
4603 fn metadata_labels_preserve_empty_and_protected_values() -> Result<(), String> {
4604 let empty = MetadataLabel::new(id("com.example.empty")?, ProtectedString::plain(""));
4605 let protected = MetadataLabel::new(id("com.example.token")?, ProtectedString::sensitive("never-print-this"));
4606 let mut service = Service::new(id("web")?);
4607 service.add_label(Sourced::generated(empty));
4608 service.add_label(Sourced::generated(protected));
4609
4610 assert_eq!(service.labels()[0].value().value().expose(), "");
4611 let debug = format!("{:?}", service.labels()[1]);
4612 assert!(!debug.contains("never-print-this"));
4613 assert!(debug.contains("[REDACTED]"));
4614 Ok(())
4615 }
4616
4617 #[test]
4618 fn environment_files_preserve_order_options_provenance_and_redaction() -> Result<(), String> {
4619 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4620 let origin = crate::Provenance::source(source);
4621 let mut service = Service::new(id("web")?);
4622 service.add_environment_file(Sourced::from_source(
4623 EnvironmentFile::new(ProtectedString::plain("./base.env"), EnvironmentFileSyntax::Short)
4624 .map_err(|error| error.to_string())?,
4625 origin.clone(),
4626 ));
4627 let mut local = EnvironmentFile::new(ProtectedString::sensitive("./private.env"), EnvironmentFileSyntax::Long)
4628 .map_err(|error| error.to_string())?;
4629 local.set_required(Sourced::from_source(false, origin.clone()));
4630 local.set_format(Sourced::from_source(EnvironmentFileFormat::Raw, origin.clone()));
4631 service.add_environment_file(Sourced::from_source(local, origin));
4632
4633 assert_eq!(service.environment_files().len(), 2);
4634 assert_eq!(service.environment_files()[0].value().path().expose(), "./base.env");
4635 assert_eq!(
4636 service.environment_files()[0].value().syntax(),
4637 EnvironmentFileSyntax::Short
4638 );
4639 assert!(service.environment_files()[0].value().is_required());
4640 let local = service.environment_files()[1].value();
4641 assert_eq!(local.syntax(), EnvironmentFileSyntax::Long);
4642 assert!(!local.is_required());
4643 assert_eq!(local.required().map_or(0, |value| value.origins().len()), 1);
4644 assert!(matches!(
4645 local.format().map(Sourced::value),
4646 Some(EnvironmentFileFormat::Raw)
4647 ));
4648 let debug = format!("{service:?}");
4649 assert!(!debug.contains("private.env"));
4650 assert!(debug.contains("[REDACTED]"));
4651 assert!(matches!(
4652 EnvironmentFile::new(ProtectedString::plain(""), EnvironmentFileSyntax::Short),
4653 Err(ModelError::EmptyValue("environment-file path"))
4654 ));
4655 Ok(())
4656 }
4657
4658 #[test]
4659 fn service_groups_preserve_order_and_reject_ambiguous_membership() -> Result<(), String> {
4660 let mut application = Application::new(id("example")?);
4661 for name in ["web", "worker"] {
4662 application
4663 .add_service(Sourced::generated(Service::new(id(name)?)))
4664 .map_err(|error| error.to_string())?;
4665 }
4666
4667 let mut frontend = ServiceGroup::new(id("frontend")?, ResourceOwnership::Uncertain);
4668 frontend
4669 .add_member(Sourced::generated(id("web")?))
4670 .map_err(|error| error.to_string())?;
4671 assert!(matches!(
4672 frontend.add_member(Sourced::generated(id("web")?)),
4673 Err(ModelError::DuplicateServiceGroupMember { .. })
4674 ));
4675 application
4676 .add_service_group(Sourced::generated(frontend))
4677 .map_err(|error| error.to_string())?;
4678
4679 assert_eq!(application.service_groups()[0].value().name().as_str(), "frontend");
4680 assert_eq!(
4681 application.service_groups()[0].value().members()[0].value().as_str(),
4682 "web"
4683 );
4684
4685 let mut conflicting = ServiceGroup::new(id("backend")?, ResourceOwnership::Application);
4686 conflicting
4687 .add_member(Sourced::generated(id("web")?))
4688 .map_err(|error| error.to_string())?;
4689 assert!(matches!(
4690 application.add_service_group(Sourced::generated(conflicting)),
4691 Err(ModelError::ServiceInMultipleGroups { .. })
4692 ));
4693
4694 let mut missing = ServiceGroup::new(id("missing")?, ResourceOwnership::External);
4695 missing
4696 .add_member(Sourced::generated(id("database")?))
4697 .map_err(|error| error.to_string())?;
4698 assert!(matches!(
4699 application.add_service_group(Sourced::generated(missing)),
4700 Err(ModelError::UnknownServiceGroupMember { .. })
4701 ));
4702 Ok(())
4703 }
4704
4705 #[test]
4706 fn group_runtime_keeps_group_names_and_pod_settings_distinct() -> Result<(), String> {
4707 let source = crate::SourceId::new("pod.pod").map_err(|error| error.to_string())?;
4708 let origin = crate::Provenance::source(source);
4709 let mut group = ServiceGroup::new(id("frontend")?, ResourceOwnership::Application);
4710 let mut runtime = ServiceGroupRuntime::new();
4711 runtime.set_runtime_name(Sourced::from_source(
4712 ProtectedString::plain("production-frontend"),
4713 origin.clone(),
4714 ));
4715 runtime.set_service_name(Sourced::from_source(
4716 ProtectedString::plain("frontend-pod"),
4717 origin.clone(),
4718 ));
4719 runtime.set_host_mappings_with_origins(
4720 vec![Sourced::from_source(
4721 HostMapping::new(
4722 id("host.docker.internal")?,
4723 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
4724 ),
4725 origin.clone(),
4726 )],
4727 vec![origin.clone()],
4728 );
4729 runtime.set_ports_with_origins(Vec::new(), vec![origin.clone()]);
4730 runtime.set_networks_with_origins(
4731 vec![Sourced::from_source(
4732 NetworkAttachment::new(
4733 id("edge")?,
4734 vec![Sourced::from_source(
4735 ProtectedString::sensitive("private-alias"),
4736 origin.clone(),
4737 )],
4738 ),
4739 origin.clone(),
4740 )],
4741 vec![origin.clone()],
4742 );
4743 runtime.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4744 runtime.set_mounts_with_origins(
4745 vec![Sourced::from_source(
4746 Mount::new(MountSource::Anonymous, "/cache", false).map_err(|error| error.to_string())?,
4747 origin.clone(),
4748 )],
4749 vec![origin.clone()],
4750 );
4751 runtime.set_shm_size(Sourced::from_source(ProtectedString::sensitive("64m"), origin.clone()));
4752 runtime.set_exit_policy(Sourced::from_source(
4753 GroupExitPolicy::Raw(ProtectedString::sensitive("preserve-this")),
4754 origin.clone(),
4755 ));
4756 runtime.set_stop_timeout(Sourced::from_source(
4757 StopTimeout::new("30s").map_err(|error| error.to_string())?,
4758 origin.clone(),
4759 ));
4760 assert!(matches!(
4761 runtime.replace_network(1, Sourced::generated(NetworkAttachment::new(id("other")?, Vec::new()))),
4762 Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index: 1, len: 1 })
4763 ));
4764 group.set_runtime(Sourced::from_source(runtime, origin));
4765
4766 let runtime = group
4767 .runtime()
4768 .ok_or_else(|| "group runtime was omitted".to_owned())?
4769 .value();
4770 assert_eq!(group.name().as_str(), "frontend");
4771 assert_eq!(
4772 runtime.runtime_name().map(|name| name.value().expose()),
4773 Some("production-frontend")
4774 );
4775 assert_eq!(
4776 runtime.service_name().map(|name| name.value().expose()),
4777 Some("frontend-pod")
4778 );
4779 assert_eq!(runtime.host_mappings().map(<[_]>::len), Some(1));
4780 assert_eq!(runtime.ports().map(<[_]>::len), Some(0));
4781 assert_eq!(runtime.networks_origins().len(), 1);
4782 assert_eq!(runtime.mounts().map(<[_]>::len), Some(1));
4783 assert!(matches!(
4784 runtime.exit_policy().map(Sourced::value),
4785 Some(GroupExitPolicy::Raw(_))
4786 ));
4787 let debug = format!("{group:?}");
4788 for sensitive in ["private-alias", "64m", "preserve-this"] {
4789 assert!(!debug.contains(sensitive));
4790 }
4791 assert!(debug.contains("[REDACTED]"));
4792 Ok(())
4793 }
4794
4795 #[test]
4796 fn rootfs_startup_notification_and_podman_args_preserve_safe_contracts() -> Result<(), String> {
4797 let source = crate::SourceId::new("web.container").map_err(|error| error.to_string())?;
4798 let origin = crate::Provenance::source(source);
4799 let mut service = Service::new(id("web")?);
4800 service.set_startup_notification(Sourced::from_source(StartupNotification::Healthy, origin.clone()));
4801 assert!(matches!(
4802 service.startup_notification().map(Sourced::value),
4803 Some(StartupNotification::Healthy)
4804 ));
4805
4806 let mut with_image = Service::new(id("image-first")?);
4807 with_image.set_image(Sourced::generated(
4808 ImageReference::parse("example.invalid/web:1").map_err(|error| error.to_string())?,
4809 ));
4810 assert!(matches!(
4811 with_image.set_rootfs(Sourced::generated(ProtectedString::plain("/srv/rootfs"))),
4812 Err(ModelError::RootfsImageSourceConflict { source: "image", .. })
4813 ));
4814
4815 let mut with_rootfs = Service::new(id("rootfs-first")?);
4816 with_rootfs
4817 .set_rootfs(Sourced::generated(ProtectedString::sensitive("/private/rootfs")))
4818 .map_err(|error| error.to_string())?;
4819 with_rootfs.set_image_build(Sourced::generated(id("web-build")?));
4820 let mut application = Application::new(id("example")?);
4821 application
4822 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4823 .map_err(|error| error.to_string())?;
4824 assert!(matches!(
4825 application.add_service(Sourced::generated(with_rootfs)),
4826 Err(ModelError::RootfsImageSourceConflict {
4827 source: "image build",
4828 ..
4829 })
4830 ));
4831 Ok(())
4832 }
4833
4834 #[test]
4835 fn validates_raw_preserving_healthcheck_scalars() -> Result<(), String> {
4836 let duration = HealthcheckDuration::new("1m30s").map_err(|error| error.to_string())?;
4837 let retries = HealthcheckRetries::new("003").map_err(|error| error.to_string())?;
4838 assert_eq!(duration.as_str(), "1m30s");
4839 assert_eq!(retries.as_str(), "003");
4840 assert_eq!(
4841 HealthcheckRetries::new("three"),
4842 Err(ModelError::InvalidHealthcheckRetries)
4843 );
4844 assert!(matches!(
4845 HealthcheckDuration::new(""),
4846 Err(ModelError::EmptyValue("health-check duration"))
4847 ));
4848 Ok(())
4849 }
4850
4851 #[test]
4852 fn preserves_ordered_dependency_edges_and_field_provenance() -> Result<(), String> {
4853 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4854 let origin = crate::Provenance::source(source);
4855 let mut service = Service::new(id("web")?);
4856
4857 let mut database = ServiceDependency::new(id("database")?);
4858 database.set_condition(Sourced::from_source(
4859 ServiceDependencyCondition::Healthy,
4860 origin.clone(),
4861 ));
4862 database.set_required(Sourced::from_source(true, origin.clone()));
4863 service.add_dependency(Sourced::from_source(database, origin.clone()));
4864
4865 let cache = ServiceDependency::new(id("cache")?);
4866 assert!(cache.is_required());
4867 service.add_dependency(Sourced::from_source(cache, origin));
4868
4869 assert_eq!(
4870 service
4871 .dependencies()
4872 .iter()
4873 .map(|dependency| dependency.value().service().as_str())
4874 .collect::<Vec<_>>(),
4875 ["database", "cache"]
4876 );
4877 assert!(matches!(
4878 service.dependencies()[0].value().condition().map(Sourced::value),
4879 Some(ServiceDependencyCondition::Healthy)
4880 ));
4881 assert_eq!(service.dependencies()[0].origins().len(), 1);
4882 assert_eq!(
4883 service.dependencies()[0]
4884 .value()
4885 .condition()
4886 .map_or(0, |condition| condition.origins().len()),
4887 1
4888 );
4889 Ok(())
4890 }
4891
4892 #[test]
4893 fn retains_execution_identity_context_order_provenance_and_redaction() -> Result<(), String> {
4894 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4895 let origin = crate::Provenance::source(source);
4896 let mut service = Service::new(id("web")?);
4897
4898 service.set_user(Sourced::from_source(ProtectedString::sensitive("1001"), origin.clone()));
4899 service.set_group(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4900 service.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4901 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("audio"), origin.clone()));
4902 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("44"), origin.clone()));
4903 service.set_working_directory(Sourced::from_source(ProtectedString::plain("/srv/app"), origin.clone()));
4904 service.set_read_only_root_filesystem(Sourced::from_source(true, origin));
4905
4906 assert_eq!(service.user().map(|value| value.value().expose()), Some("1001"));
4907 assert_eq!(service.group().map(|value| value.value().expose()), Some("1002"));
4908 assert_eq!(
4909 service.user_namespace().map(|value| value.value().expose()),
4910 Some("keep-id")
4911 );
4912 assert_eq!(
4913 service
4914 .supplementary_groups()
4915 .iter()
4916 .map(|group| group.value().expose())
4917 .collect::<Vec<_>>(),
4918 ["audio", "44"]
4919 );
4920 assert_eq!(
4921 service.working_directory().map(|value| value.value().expose()),
4922 Some("/srv/app")
4923 );
4924 assert_eq!(service.read_only_root_filesystem().map(Sourced::value), Some(&true));
4925 assert_eq!(service.user().map_or(0, |value| value.origins().len()), 1);
4926 let debug = format!("{service:?}");
4927 assert!(!debug.contains("1001"));
4928 assert!(debug.contains("[REDACTED]"));
4929 Ok(())
4930 }
4931
4932 #[test]
4933 fn retains_config_secret_resources_grants_provenance_and_redaction() -> Result<(), String> {
4934 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4935 let origin = crate::Provenance::source(source);
4936 let mut application = Application::new(id("example")?);
4937
4938 let mut config = Config::new(id("settings")?, ResourceOwnership::Application);
4939 config.set_material(Sourced::from_source(
4940 ConfigMaterial::Content(ProtectedString::sensitive("private-config")),
4941 origin.clone(),
4942 ));
4943 application
4944 .add_config(Sourced::from_source(config, origin.clone()))
4945 .map_err(|error| error.to_string())?;
4946
4947 let mut secret = Secret::new(id("password")?, ResourceOwnership::External);
4948 secret.set_runtime_name(Sourced::from_source(
4949 ProtectedString::plain("production-password"),
4950 origin.clone(),
4951 ));
4952 secret.set_material(Sourced::from_source(
4953 SecretMaterial::Environment(ProtectedString::sensitive("private-environment-name")),
4954 origin.clone(),
4955 ));
4956 application
4957 .add_secret(Sourced::from_source(secret, origin.clone()))
4958 .map_err(|error| error.to_string())?;
4959
4960 let mut service = Service::new(id("web")?);
4961 service.add_config_grant(Sourced::from_source(
4962 ResourceGrant::new(ProtectedString::plain("settings"), ResourceGrantSyntax::Short)
4963 .map_err(|error| error.to_string())?,
4964 origin.clone(),
4965 ));
4966 let mut secret_grant = ResourceGrant::new(
4967 ProtectedString::sensitive("private-grant-source"),
4968 ResourceGrantSyntax::Long,
4969 )
4970 .map_err(|error| error.to_string())?;
4971 secret_grant.set_target(Sourced::from_source(
4972 ProtectedString::plain("database-password"),
4973 origin.clone(),
4974 ));
4975 secret_grant.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4976 secret_grant.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
4977 secret_grant.set_mode(Sourced::from_source(ProtectedString::plain("0440"), origin.clone()));
4978 service.add_secret_grant(Sourced::from_source(secret_grant, origin.clone()));
4979 application
4980 .add_service(Sourced::from_source(service, origin))
4981 .map_err(|error| error.to_string())?;
4982
4983 assert_eq!(application.configs().len(), 1);
4984 assert_eq!(application.secrets().len(), 1);
4985 assert_eq!(application.services()[0].value().config_grants().len(), 1);
4986 let grant = &application.services()[0].value().secret_grants()[0];
4987 assert_eq!(grant.value().syntax(), ResourceGrantSyntax::Long);
4988 assert_eq!(
4989 grant.value().target().map(|value| value.value().expose()),
4990 Some("database-password")
4991 );
4992 assert_eq!(grant.value().uid().map_or(0, |value| value.origins().len()), 1);
4993 assert_eq!(grant.origins().len(), 1);
4994 let debug = format!("{application:?}");
4995 for secret in ["private-config", "private-environment-name", "private-grant-source"] {
4996 assert!(!debug.contains(secret));
4997 }
4998 assert!(debug.contains("[REDACTED]"));
4999
5000 assert!(matches!(
5001 ResourceGrant::new(ProtectedString::plain(""), ResourceGrantSyntax::Short),
5002 Err(ModelError::EmptyValue("resource grant source"))
5003 ));
5004 assert!(matches!(
5005 application.add_config(Sourced::generated(Config::new(
5006 id("settings")?,
5007 ResourceOwnership::External,
5008 ))),
5009 Err(ModelError::DuplicateResource { kind: "config", .. })
5010 ));
5011 assert!(matches!(
5012 application.add_secret(Sourced::generated(Secret::new(
5013 id("password")?,
5014 ResourceOwnership::External,
5015 ))),
5016 Err(ModelError::DuplicateResource { kind: "secret", .. })
5017 ));
5018 Ok(())
5019 }
5020
5021 #[test]
5022 fn host_mappings_preserve_order_spelling_and_runtime_tokens() -> Result<(), String> {
5023 let mut service = Service::new(id("web")?);
5024 service.add_host_mapping(Sourced::generated(HostMapping::new(
5025 id("host.docker.internal")?,
5026 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
5027 )));
5028 service.add_host_mapping(Sourced::generated(HostMapping::new(
5029 id("ipv6")?,
5030 HostAddress::new("[::1]").map_err(|error| error.to_string())?,
5031 )));
5032
5033 assert_eq!(service.host_mappings().len(), 2);
5034 assert_eq!(
5035 service.host_mappings()[0].value().address().kind(),
5036 HostAddressKind::HostGateway
5037 );
5038 assert_eq!(service.host_mappings()[1].value().address().raw(), "[::1]");
5039 assert_eq!(
5040 service.host_mappings()[1].value().address().kind(),
5041 HostAddressKind::Ipv6 { bracketed: true }
5042 );
5043 assert!(matches!(HostAddress::new(""), Err(ModelError::EmptyValue(_))));
5044 Ok(())
5045 }
5046
5047 #[test]
5048 fn dns_collections_preserve_order_provenance_and_explicit_empty_state() -> Result<(), String> {
5049 let mut service = Service::new(id("web")?);
5050 assert!(service.dns_servers().is_none());
5051 service.set_dns_servers(Vec::new());
5052 assert!(matches!(service.dns_servers(), Some(values) if values.is_empty()));
5053 service.set_dns_options(vec![
5054 Sourced::generated(ProtectedString::plain("ndots:5")),
5055 Sourced::generated(ProtectedString::sensitive("rotate")),
5056 ]);
5057 service.set_dns_search_domains(vec![Sourced::generated(ProtectedString::plain("example.test"))]);
5058 assert_eq!(
5059 service
5060 .dns_options()
5061 .unwrap_or_default()
5062 .iter()
5063 .map(|value| value.value().expose())
5064 .collect::<Vec<_>>(),
5065 ["ndots:5", "rotate"]
5066 );
5067 assert!(!format!("{service:?}").contains("rotate"));
5068 Ok(())
5069 }
5070
5071 #[test]
5072 fn security_options_preserve_empty_order_duplicates_provenance_and_redaction() -> Result<(), String> {
5073 let origin =
5074 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5075 let mut service = Service::new(id("web")?);
5076
5077 assert!(service.security_options().is_none());
5078 service.set_security_options_with_origins(Vec::new(), vec![origin.clone()]);
5079 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5080 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5081
5082 service.set_security_options_with_origins(
5083 vec![
5084 Sourced::from_source(
5085 SecurityOption::AppArmor(ProtectedString::sensitive("apparmor-secret")),
5086 origin.clone(),
5087 ),
5088 Sourced::from_source(SecurityOption::NoNewPrivileges(true), origin.clone()),
5089 Sourced::from_source(
5090 SecurityOption::SeccompProfile(ProtectedString::sensitive("seccomp-secret")),
5091 origin.clone(),
5092 ),
5093 Sourced::from_source(SecurityOption::SecurityLabelDisable(false), origin.clone()),
5094 Sourced::from_source(
5095 SecurityOption::SecurityLabelFileType(ProtectedString::sensitive("file-type-secret")),
5096 origin.clone(),
5097 ),
5098 Sourced::from_source(
5099 SecurityOption::SecurityLabelLevel(ProtectedString::sensitive("level-secret")),
5100 origin.clone(),
5101 ),
5102 Sourced::from_source(SecurityOption::SecurityLabelNested(true), origin.clone()),
5103 Sourced::from_source(
5104 SecurityOption::SecurityLabelType(ProtectedString::sensitive("type-secret")),
5105 origin.clone(),
5106 ),
5107 Sourced::from_source(
5108 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5109 origin.clone(),
5110 ),
5111 Sourced::from_source(
5112 SecurityOption::Unmask(ProtectedString::sensitive("unmask-secret")),
5113 origin.clone(),
5114 ),
5115 Sourced::from_source(
5116 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5117 origin.clone(),
5118 ),
5119 ],
5120 vec![origin.clone()],
5121 );
5122
5123 let options = service.security_options().unwrap_or_default();
5124 assert_eq!(options.len(), 11);
5125 assert!(
5126 matches!(options[0].value(), SecurityOption::AppArmor(profile) if profile.expose() == "apparmor-secret")
5127 );
5128 assert!(matches!(options[1].value(), SecurityOption::NoNewPrivileges(true)));
5129 assert!(
5130 matches!(options[2].value(), SecurityOption::SeccompProfile(profile) if profile.expose() == "seccomp-secret")
5131 );
5132 assert!(matches!(
5133 options[3].value(),
5134 SecurityOption::SecurityLabelDisable(false)
5135 ));
5136 assert!(
5137 matches!(options[4].value(), SecurityOption::SecurityLabelFileType(profile) if profile.expose() == "file-type-secret")
5138 );
5139 assert!(
5140 matches!(options[5].value(), SecurityOption::SecurityLabelLevel(profile) if profile.expose() == "level-secret")
5141 );
5142 assert!(matches!(options[6].value(), SecurityOption::SecurityLabelNested(true)));
5143 assert!(
5144 matches!(options[7].value(), SecurityOption::SecurityLabelType(profile) if profile.expose() == "type-secret")
5145 );
5146 assert!(matches!(options[8].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5147 assert!(matches!(options[9].value(), SecurityOption::Unmask(path) if path.expose() == "unmask-secret"));
5148 assert!(matches!(options[10].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5149 assert_eq!(options[0].origins(), std::slice::from_ref(&origin));
5150 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5151
5152 let debug = format!("{service:?}");
5153 for secret in [
5154 "apparmor-secret",
5155 "seccomp-secret",
5156 "file-type-secret",
5157 "level-secret",
5158 "type-secret",
5159 "mask-secret",
5160 "unmask-secret",
5161 ] {
5162 assert!(!debug.contains(secret));
5163 }
5164 assert!(debug.contains("[REDACTED]"));
5165
5166 service.set_security_options(Vec::new());
5167 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5168 assert!(service.security_options_origins().is_empty());
5169 Ok(())
5170 }
5171
5172 #[test]
5173 fn retains_entrypoint_run_init_stop_pull_memory_and_exposed_port_intent() -> Result<(), String> {
5174 let origin =
5175 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5176 let mut service = Service::new(id("web")?);
5177 service.set_command(Sourced::from_source(
5178 Command::Exec(vec![ProtectedString::plain("serve")]),
5179 origin.clone(),
5180 ));
5181 service.set_entrypoint(Sourced::from_source(
5182 Entrypoint::Shell(ProtectedString::sensitive("/bin/sh -c private-entrypoint")),
5183 origin.clone(),
5184 ));
5185 service.set_run_init(Sourced::from_source(true, origin.clone()));
5186 service.set_stop_timeout(Sourced::from_source(
5187 StopTimeout::new("01m30s").map_err(|error| error.to_string())?,
5188 origin.clone(),
5189 ));
5190 service.set_pull_policy(Sourced::from_source(
5191 PullPolicy::Every(ProtectedString::sensitive("12h")),
5192 origin.clone(),
5193 ));
5194 service.set_memory_limit(Sourced::from_source(
5195 ProtectedString::sensitive("512MiB"),
5196 origin.clone(),
5197 ));
5198 assert!(service.exposed_ports().is_none());
5199 service.set_exposed_ports_with_origins(Vec::new(), vec![origin.clone()]);
5200 assert_eq!(service.exposed_ports().map(<[_]>::len), Some(0));
5201 assert_eq!(service.exposed_ports_origins(), std::slice::from_ref(&origin));
5202 service.add_exposed_port(Sourced::from_source(
5203 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5204 origin.clone(),
5205 ));
5206 service.add_exposed_port(Sourced::from_source(
5207 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5208 origin,
5209 ));
5210
5211 assert!(matches!(service.command().map(Sourced::value), Some(Command::Exec(_))));
5212 assert!(matches!(
5213 service.entrypoint().map(Sourced::value),
5214 Some(Entrypoint::Shell(_))
5215 ));
5216 assert_eq!(service.run_init().map(Sourced::value), Some(&true));
5217 assert_eq!(
5218 service.stop_timeout().map(|timeout| timeout.value().as_str()),
5219 Some("01m30s")
5220 );
5221 assert!(matches!(
5222 service.pull_policy().map(Sourced::value),
5223 Some(PullPolicy::Every(_))
5224 ));
5225 assert_eq!(
5226 service.memory_limit().map(|limit| limit.value().expose()),
5227 Some("512MiB")
5228 );
5229 let exposed_ports = service.exposed_ports().ok_or("missing exposed ports")?;
5230 assert_eq!(exposed_ports.len(), 2);
5231 assert_eq!(exposed_ports[0].value().container(), 8080);
5232 assert_eq!(exposed_ports[0].value().protocol(), &Protocol::Tcp);
5233 assert!(matches!(
5234 ExposedPort::new(0, Protocol::Udp),
5235 Err(ModelError::ZeroContainerPort)
5236 ));
5237 assert!(matches!(
5238 StopTimeout::new(""),
5239 Err(ModelError::EmptyValue("stop timeout"))
5240 ));
5241
5242 let debug = format!("{service:?}");
5243 for secret in ["private-entrypoint", "512MiB", "12h"] {
5244 assert!(!debug.contains(secret));
5245 }
5246 assert!(debug.contains("[REDACTED]"));
5247 Ok(())
5248 }
5249
5250 #[test]
5251 fn annotations_and_logging_preserve_empty_order_field_provenance_and_redaction() -> Result<(), String> {
5252 let origin =
5253 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5254 let mut service = Service::new(id("web")?);
5255
5256 assert!(service.annotations().is_none());
5257 service.set_annotations_with_origins(Vec::new(), vec![origin.clone()]);
5258 assert_eq!(service.annotations().map(<[_]>::len), Some(0));
5259 assert_eq!(service.annotations_origins(), std::slice::from_ref(&origin));
5260
5261 service.set_annotations_with_origins(
5262 vec![
5263 Sourced::from_source(
5264 Annotation::new(
5265 Sourced::from_source(id("io.example.first")?, origin.clone()),
5266 Sourced::from_source(ProtectedString::sensitive("annotation-secret"), origin.clone()),
5267 ),
5268 origin.clone(),
5269 ),
5270 Sourced::from_source(
5271 Annotation::new(
5272 Sourced::from_source(id("io.example.second")?, origin.clone()),
5273 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5274 ),
5275 origin.clone(),
5276 ),
5277 ],
5278 vec![origin.clone()],
5279 );
5280 let annotations = service.annotations().unwrap_or_default();
5281 assert_eq!(annotations.len(), 2);
5282 assert_eq!(annotations[0].value().name().value().as_str(), "io.example.first");
5283 assert_eq!(annotations[1].value().value().value().expose(), "");
5284 assert_eq!(annotations[0].value().name().origins(), std::slice::from_ref(&origin));
5285 assert_eq!(annotations[0].value().value().origins(), std::slice::from_ref(&origin));
5286
5287 let mut logging = Logging::new();
5288 assert!(logging.options().is_none());
5289 logging.set_driver(Sourced::from_source(ProtectedString::plain("journald"), origin.clone()));
5290 logging.set_options_with_origins(
5291 vec![
5292 Sourced::from_source(
5293 LoggingOption::new(
5294 Sourced::from_source(id("tag")?, origin.clone()),
5295 Sourced::from_source(ProtectedString::sensitive("logging-secret"), origin.clone()),
5296 ),
5297 origin.clone(),
5298 ),
5299 Sourced::from_source(
5300 LoggingOption::new(
5301 Sourced::from_source(id("labels")?, origin.clone()),
5302 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5303 ),
5304 origin.clone(),
5305 ),
5306 ],
5307 vec![origin.clone()],
5308 );
5309 service.set_logging(Sourced::from_source(logging, origin));
5310
5311 let logging = service.logging().map(Sourced::value).ok_or("missing logging")?;
5312 assert_eq!(logging.driver().map(|driver| driver.value().expose()), Some("journald"));
5313 assert_eq!(logging.options().map(<[_]>::len), Some(2));
5314 assert_eq!(
5315 logging.options().unwrap_or_default()[0].value().name().value().as_str(),
5316 "tag"
5317 );
5318 assert_eq!(logging.options_origins().len(), 1);
5319 let debug = format!("{service:?}");
5320 assert!(!debug.contains("annotation-secret"));
5321 assert!(!debug.contains("logging-secret"));
5322 assert!(debug.contains("[REDACTED]"));
5323 Ok(())
5324 }
5325
5326 #[test]
5327 fn network_attachments_retain_alias_provenance_and_redact_sensitive_values() -> Result<(), String> {
5328 let origin =
5329 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5330 let mut attachment = NetworkAttachment::new(
5331 id("frontend")?,
5332 vec![
5333 Sourced::from_source(ProtectedString::plain("web"), origin.clone()),
5334 Sourced::from_source(ProtectedString::sensitive("private-alias"), origin.clone()),
5335 ],
5336 );
5337 attachment.set_ipv4_address(Sourced::from_source(
5338 ProtectedString::plain("192.0.2.10"),
5339 origin.clone(),
5340 ));
5341 attachment.set_ipv6_address(Sourced::from_source(
5342 ProtectedString::plain("2001:db8::10"),
5343 origin.clone(),
5344 ));
5345 let metrics = Sourced::generated(ProtectedString::plain("metrics"));
5346 attachment.add_alias(&metrics);
5347
5348 assert_eq!(attachment.aliases(), ["web", "private-alias", "metrics"]);
5349 assert_eq!(attachment.alias_sensitivities(), [false, true, false]);
5350 assert_eq!(attachment.alias_origins().len(), 3);
5351 assert_eq!(attachment.alias_origins()[0].len(), 1);
5352 assert_eq!(attachment.alias_origins()[1], std::slice::from_ref(&origin));
5353 assert!(attachment.alias_origins()[2].is_empty());
5354 assert_eq!(
5355 attachment.ipv4_address().map(|address| address.value().expose()),
5356 Some("192.0.2.10")
5357 );
5358 assert_eq!(
5359 attachment.ipv6_address().map(|address| address.value().expose()),
5360 Some("2001:db8::10")
5361 );
5362 let debug = format!("{attachment:?}");
5363 assert!(!debug.contains("private-alias"));
5364 assert!(debug.contains("[REDACTED]"));
5365
5366 let mut service = Service::new(id("web")?);
5367 service.add_network(Sourced::generated(NetworkAttachment::new(
5368 id("previous")?,
5369 vec![Sourced::generated(ProtectedString::plain("previous-alias"))],
5370 )));
5371 let previous = service
5372 .replace_network(0, Sourced::generated(attachment))
5373 .map_err(|error| error.to_string())?;
5374 assert_eq!(previous.value().network().as_str(), "previous");
5375 assert_eq!(service.networks()[0].value().network().as_str(), "frontend");
5376 assert!(matches!(
5377 service.replace_network(1, Sourced::generated(NetworkAttachment::new(id("unused")?, Vec::new()))),
5378 Err(ModelError::UnknownNetworkAttachmentIndex { index: 1, len: 1 })
5379 ));
5380 Ok(())
5381 }
5382
5383 #[test]
5384 fn reload_action_is_one_explicit_command_or_signal() -> Result<(), String> {
5385 let origin =
5386 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5387 let mut service = Service::new(id("web")?);
5388 service.set_reload_action(Sourced::from_source(
5389 ReloadAction::Command(Command::Exec(vec![ProtectedString::plain("reload")])),
5390 origin.clone(),
5391 ));
5392 assert!(matches!(
5393 service.reload_action().map(Sourced::value),
5394 Some(ReloadAction::Command(Command::Exec(_)))
5395 ));
5396
5397 service.set_reload_action(Sourced::from_source(
5398 ReloadAction::Signal(ProtectedString::sensitive("SIGHUP")),
5399 origin,
5400 ));
5401 assert!(matches!(
5402 service.reload_action().map(Sourced::value),
5403 Some(ReloadAction::Signal(_))
5404 ));
5405 let debug = format!("{service:?}");
5406 assert!(!debug.contains("SIGHUP"));
5407 assert!(debug.contains("[REDACTED]"));
5408 Ok(())
5409 }
5410
5411 fn id(value: &str) -> Result<Identifier, String> {
5412 Identifier::new(value).map_err(|error| error.to_string())
5413 }
5414}