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 InvalidDeferred(&'static str),
142}
143
144impl fmt::Display for ModelError {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::EmptyValue(kind) => write!(formatter, "{kind} must not be empty"),
148 Self::ContainsNul(kind) => write!(formatter, "{kind} must not contain a NUL byte"),
149 Self::ReversedSpan { start, end } => {
150 write!(formatter, "source span end {end} is before start {start}")
151 }
152 Self::DuplicateResource { kind, name } => {
153 write!(formatter, "duplicate {kind} `{name}`")
154 }
155 Self::UnknownNativeEvidenceOwner { kind, name } => {
156 write!(formatter, "retained native evidence references unknown {kind} `{name}`")
157 }
158 Self::MissingNativeEvidenceProvenance { component } => {
159 write!(
160 formatter,
161 "retained native evidence {component} must carry source provenance"
162 )
163 }
164 Self::EmptyNativeEvidenceEvent => {
165 formatter.write_str("retained native evidence must contain at least one physical source segment")
166 }
167 Self::UnprotectedNativeEvidenceSegment => {
168 formatter.write_str("retained native evidence physical segments must be sensitive")
169 }
170 Self::DuplicateServiceGroupMember { group, service } => {
171 write!(
172 formatter,
173 "service group `{group}` contains duplicate member `{service}`"
174 )
175 }
176 Self::UnknownServiceGroupMember { group, service } => {
177 write!(
178 formatter,
179 "service group `{group}` references unknown service `{service}`"
180 )
181 }
182 Self::ServiceInMultipleGroups {
183 service,
184 existing,
185 replacement,
186 } => write!(
187 formatter,
188 "service `{service}` belongs to both service groups `{existing}` and `{replacement}`"
189 ),
190 Self::UnknownImageAcquisitionReference { service, acquisition } => write!(
191 formatter,
192 "service `{service}` references unknown image acquisition `{acquisition}`"
193 ),
194 Self::UnknownImageBuildReference { service, build } => {
195 write!(
196 formatter,
197 "service `{service}` references unknown image build `{build}`"
198 )
199 }
200 Self::UnknownVolumeImageAcquisitionReference { volume, acquisition } => write!(
201 formatter,
202 "volume `{volume}` references unknown image acquisition `{acquisition}`"
203 ),
204 Self::UnknownVolumeImageBuildReference { volume, build } => {
205 write!(formatter, "volume `{volume}` references unknown image build `{build}`")
206 }
207 Self::UnknownArtifactDependencyNode { kind, name } => {
208 write!(formatter, "artifact dependency references unknown {kind} `{name}`")
209 }
210 Self::ImageArtifactDependencyCycle { nodes } => {
211 write!(formatter, "image-artifact dependency cycle: {}", nodes.join(" -> "))
212 }
213 Self::InvalidImageReference(reason) => write!(formatter, "invalid image reference: {reason}"),
214 Self::ZeroContainerPort => formatter.write_str("container port must not be zero"),
215 Self::UnknownNetworkAttachmentIndex { index, len } => {
216 write!(
217 formatter,
218 "network attachment index {index} is outside collection length {len}"
219 )
220 }
221 Self::UnknownServiceGroupRuntimeNetworkIndex { index, len } => {
222 write!(
223 formatter,
224 "group-runtime network attachment index {index} is outside collection length {len}"
225 )
226 }
227 Self::RootfsImageSourceConflict { service, source } => write!(
228 formatter,
229 "service `{service}` combines rootfs with image source `{source}`"
230 ),
231 Self::InvalidDeferred(reason) => formatter.write_str(reason),
232 Self::InvalidHealthcheckRetries => {
233 formatter.write_str("health-check retries must be a non-negative decimal integer")
234 }
235 }
236 }
237}
238
239impl Error for ModelError {}
240
241#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
243pub struct Identifier(String);
244
245impl Identifier {
246 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
252 let value = value.into();
253 validate_text("identifier", &value)?;
254 Ok(Self(value))
255 }
256
257 #[must_use]
259 pub fn as_str(&self) -> &str {
260 &self.0
261 }
262}
263
264#[derive(Clone, Copy, Debug, Eq, PartialEq)]
266#[non_exhaustive]
267pub enum ResourceOwnership {
268 Application,
270 External,
272 Implicit,
274 Uncertain,
276}
277
278#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
283#[non_exhaustive]
284pub enum RetainedNativeEvidenceSubject {
285 QuadletServicePodmanArgs(Identifier),
287 QuadletVolumeContainersConfModules(Identifier),
289 QuadletVolumeGlobalArgs(Identifier),
291 QuadletVolumePodmanArgs(Identifier),
293}
294
295impl RetainedNativeEvidenceSubject {
296 #[must_use]
298 pub fn conversion_subject(&self) -> String {
299 match self {
300 Self::QuadletServicePodmanArgs(name) => {
301 format!("services.{}.podman_args", name.as_str())
302 }
303 Self::QuadletVolumeContainersConfModules(name) => {
304 format!("volumes.{}.containers_conf_modules", name.as_str())
305 }
306 Self::QuadletVolumeGlobalArgs(name) => {
307 format!("volumes.{}.global_args", name.as_str())
308 }
309 Self::QuadletVolumePodmanArgs(name) => {
310 format!("volumes.{}.podman_args", name.as_str())
311 }
312 }
313 }
314
315 const fn owner(&self) -> (&'static str, &Identifier) {
316 match self {
317 Self::QuadletServicePodmanArgs(name) => ("service", name),
318 Self::QuadletVolumeContainersConfModules(name)
319 | Self::QuadletVolumeGlobalArgs(name)
320 | Self::QuadletVolumePodmanArgs(name) => ("volume", name),
321 }
322 }
323}
324
325#[derive(Clone, Debug, Eq, PartialEq)]
327#[non_exhaustive]
328pub enum RetainedNativeEvidenceEvent {
329 Value(Vec<Sourced<ProtectedString>>),
331 Reset(Vec<Sourced<ProtectedString>>),
333}
334
335impl RetainedNativeEvidenceEvent {
336 #[must_use]
338 pub fn physical_segments(&self) -> &[Sourced<ProtectedString>] {
339 match self {
340 Self::Value(segments) | Self::Reset(segments) => segments,
341 }
342 }
343}
344
345#[derive(Clone, Debug, Eq, PartialEq)]
351pub struct RetainedNativeEvidence {
352 subject: RetainedNativeEvidenceSubject,
353 event: Sourced<RetainedNativeEvidenceEvent>,
354}
355
356impl RetainedNativeEvidence {
357 pub fn new(
367 subject: RetainedNativeEvidenceSubject,
368 event: Sourced<RetainedNativeEvidenceEvent>,
369 ) -> Result<Self, ModelError> {
370 if event.origins().is_empty() {
371 return Err(ModelError::MissingNativeEvidenceProvenance { component: "event" });
372 }
373 let segments = event.value().physical_segments();
374 if segments.is_empty() {
375 return Err(ModelError::EmptyNativeEvidenceEvent);
376 }
377 if segments.iter().any(|segment| segment.origins().is_empty()) {
378 return Err(ModelError::MissingNativeEvidenceProvenance {
379 component: "physical segment",
380 });
381 }
382 if segments.iter().any(|segment| !segment.value().is_sensitive()) {
383 return Err(ModelError::UnprotectedNativeEvidenceSegment);
384 }
385 Ok(Self { subject, event })
386 }
387
388 #[must_use]
390 pub const fn subject(&self) -> &RetainedNativeEvidenceSubject {
391 &self.subject
392 }
393
394 #[must_use]
396 pub const fn event(&self) -> &Sourced<RetainedNativeEvidenceEvent> {
397 &self.event
398 }
399}
400
401#[derive(Clone, Debug, Eq, PartialEq)]
403pub struct Volume {
404 name: Identifier,
405 ownership: ResourceOwnership,
406 runtime_name: Option<Sourced<ProtectedString>>,
407 service_name: Option<Sourced<ProtectedString>>,
408 driver: Option<Sourced<ProtectedString>>,
409 device: Option<Sourced<ProtectedString>>,
410 type_spelling: Option<Sourced<ProtectedString>>,
411 options: Option<Sourced<ProtectedString>>,
412 labels: Option<Vec<Sourced<MetadataLabel>>>,
413 labels_origins: Vec<Provenance>,
414 copy: Option<Sourced<bool>>,
415 user: Option<Sourced<ProtectedString>>,
416 group: Option<Sourced<ProtectedString>>,
417 uid: Option<Sourced<ProtectedString>>,
418 gid: Option<Sourced<ProtectedString>>,
419 image_source: Option<Sourced<VolumeImageSource>>,
420}
421
422impl Volume {
423 #[must_use]
425 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
426 Self {
427 name,
428 ownership,
429 runtime_name: None,
430 service_name: None,
431 driver: None,
432 device: None,
433 type_spelling: None,
434 options: None,
435 labels: None,
436 labels_origins: Vec::new(),
437 copy: None,
438 user: None,
439 group: None,
440 uid: None,
441 gid: None,
442 image_source: None,
443 }
444 }
445
446 #[must_use]
448 pub const fn name(&self) -> &Identifier {
449 &self.name
450 }
451
452 #[must_use]
454 pub const fn ownership(&self) -> ResourceOwnership {
455 self.ownership
456 }
457
458 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
460 self.runtime_name = Some(name);
461 }
462
463 #[must_use]
465 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
466 self.runtime_name.as_ref()
467 }
468
469 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
471 self.service_name = Some(name);
472 }
473
474 #[must_use]
476 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
477 self.service_name.as_ref()
478 }
479
480 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
482 self.driver = Some(driver);
483 }
484
485 #[must_use]
487 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
488 self.driver.as_ref()
489 }
490
491 pub fn set_device(&mut self, device: Sourced<ProtectedString>) {
493 self.device = Some(device);
494 }
495
496 #[must_use]
498 pub const fn device(&self) -> Option<&Sourced<ProtectedString>> {
499 self.device.as_ref()
500 }
501
502 pub fn set_volume_type(&mut self, volume_type: Sourced<ProtectedString>) {
504 self.type_spelling = Some(volume_type);
505 }
506
507 #[must_use]
509 pub const fn volume_type(&self) -> Option<&Sourced<ProtectedString>> {
510 self.type_spelling.as_ref()
511 }
512
513 pub fn set_options(&mut self, options: Sourced<ProtectedString>) {
518 self.options = Some(options);
519 }
520
521 #[must_use]
523 pub const fn options(&self) -> Option<&Sourced<ProtectedString>> {
524 self.options.as_ref()
525 }
526
527 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
529 self.set_labels_with_origins(labels, Vec::new());
530 }
531
532 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
534 self.labels = Some(labels);
535 self.labels_origins = origins;
536 }
537
538 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
540 self.labels.get_or_insert_default().push(label);
541 }
542
543 #[must_use]
545 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
546 self.labels.as_deref()
547 }
548
549 #[must_use]
551 pub fn labels_origins(&self) -> &[Provenance] {
552 &self.labels_origins
553 }
554
555 pub fn set_copy(&mut self, copy: Sourced<bool>) {
557 self.copy = Some(copy);
558 }
559
560 #[must_use]
562 pub const fn copy(&self) -> Option<&Sourced<bool>> {
563 self.copy.as_ref()
564 }
565
566 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
568 self.user = Some(user);
569 }
570
571 #[must_use]
573 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
574 self.user.as_ref()
575 }
576
577 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
579 self.group = Some(group);
580 }
581
582 #[must_use]
584 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
585 self.group.as_ref()
586 }
587
588 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
590 self.uid = Some(uid);
591 }
592
593 #[must_use]
595 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
596 self.uid.as_ref()
597 }
598
599 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
601 self.gid = Some(gid);
602 }
603
604 #[must_use]
606 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
607 self.gid.as_ref()
608 }
609
610 pub fn set_image_source(&mut self, image_source: Sourced<VolumeImageSource>) -> Result<(), ModelError> {
620 image_source.value().validate()?;
621 self.image_source = Some(image_source);
622 Ok(())
623 }
624
625 #[must_use]
627 pub const fn image_source(&self) -> Option<&Sourced<VolumeImageSource>> {
628 self.image_source.as_ref()
629 }
630}
631
632#[derive(Clone, Debug, Eq, PartialEq)]
637#[non_exhaustive]
638pub enum VolumeImageSource {
639 Literal(ProtectedString),
641 ImageAcquisition(Identifier),
643 ImageBuild(Identifier),
645}
646
647impl VolumeImageSource {
648 fn validate(&self) -> Result<(), ModelError> {
649 if let Self::Literal(image) = self {
650 validate_text("volume image", image.expose())?;
651 }
652 Ok(())
653 }
654}
655
656#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
661#[non_exhaustive]
662pub enum ArtifactDependencyNode {
663 Volume(Identifier),
665 ImageAcquisition(Identifier),
667 ImageBuild(Identifier),
669}
670
671impl ArtifactDependencyNode {
672 fn kind_and_name(&self) -> (&'static str, &Identifier) {
673 match self {
674 Self::Volume(name) => ("volume", name),
675 Self::ImageAcquisition(name) => ("image acquisition", name),
676 Self::ImageBuild(name) => ("image build", name),
677 }
678 }
679
680 fn display_name(&self) -> String {
681 let (kind, name) = self.kind_and_name();
682 format!("{kind}:{}", name.as_str())
683 }
684}
685
686#[derive(Clone, Debug, Eq, PartialEq)]
691pub struct ArtifactDependency {
692 source: Sourced<ArtifactDependencyNode>,
693 target: Sourced<ArtifactDependencyNode>,
694}
695
696impl ArtifactDependency {
697 #[must_use]
699 pub const fn new(source: Sourced<ArtifactDependencyNode>, target: Sourced<ArtifactDependencyNode>) -> Self {
700 Self { source, target }
701 }
702
703 #[must_use]
705 pub const fn source(&self) -> &Sourced<ArtifactDependencyNode> {
706 &self.source
707 }
708
709 #[must_use]
711 pub const fn target(&self) -> &Sourced<ArtifactDependencyNode> {
712 &self.target
713 }
714}
715
716#[derive(Clone, Debug, Eq, PartialEq)]
718pub struct Network {
719 name: Identifier,
720 ownership: ResourceOwnership,
721 runtime_name: Option<Sourced<ProtectedString>>,
722 driver: Option<Sourced<ProtectedString>>,
723 driver_options: Option<Vec<Sourced<NetworkDriverOption>>>,
724 driver_options_origins: Vec<Provenance>,
725 labels: Option<Vec<Sourced<MetadataLabel>>>,
726 labels_origins: Vec<Provenance>,
727 internal: Option<Sourced<bool>>,
728 ipv6: Option<Sourced<bool>>,
729 ipam_driver: Option<Sourced<ProtectedString>>,
730 ipam_configs: Option<Vec<Sourced<NetworkIpamConfig>>>,
731 ipam_configs_origins: Vec<Provenance>,
732}
733
734#[derive(Clone, Debug, Eq, PartialEq)]
740pub struct NetworkDriverOption {
741 name: Sourced<Identifier>,
742 value: Sourced<ProtectedString>,
743}
744
745impl NetworkDriverOption {
746 pub fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Result<Self, ModelError> {
753 validate_no_nul("network driver option value", value.value().expose())?;
754 Ok(Self { name, value })
755 }
756
757 #[must_use]
759 pub const fn name(&self) -> &Sourced<Identifier> {
760 &self.name
761 }
762
763 #[must_use]
765 pub const fn value(&self) -> &Sourced<ProtectedString> {
766 &self.value
767 }
768}
769
770#[derive(Clone, Debug, Eq, PartialEq)]
776pub struct NetworkIpamConfig {
777 subnet: Sourced<ProtectedString>,
778 gateway: Option<Sourced<ProtectedString>>,
779 ip_range: Option<Sourced<ProtectedString>>,
780}
781
782impl NetworkIpamConfig {
783 pub fn new(subnet: Sourced<ProtectedString>) -> Result<Self, ModelError> {
789 validate_text("network IPAM subnet", subnet.value().expose())?;
790 Ok(Self {
791 subnet,
792 gateway: None,
793 ip_range: None,
794 })
795 }
796
797 #[must_use]
799 pub const fn subnet(&self) -> &Sourced<ProtectedString> {
800 &self.subnet
801 }
802
803 pub fn set_gateway(&mut self, gateway: Sourced<ProtectedString>) -> Result<(), ModelError> {
809 validate_text("network IPAM gateway", gateway.value().expose())?;
810 self.gateway = Some(gateway);
811 Ok(())
812 }
813
814 #[must_use]
816 pub const fn gateway(&self) -> Option<&Sourced<ProtectedString>> {
817 self.gateway.as_ref()
818 }
819
820 pub fn set_ip_range(&mut self, ip_range: Sourced<ProtectedString>) -> Result<(), ModelError> {
826 validate_text("network IPAM IP range", ip_range.value().expose())?;
827 self.ip_range = Some(ip_range);
828 Ok(())
829 }
830
831 #[must_use]
833 pub const fn ip_range(&self) -> Option<&Sourced<ProtectedString>> {
834 self.ip_range.as_ref()
835 }
836}
837
838#[derive(Clone, Debug, Eq, PartialEq)]
840#[non_exhaustive]
841pub enum ConfigMaterial {
842 File(ProtectedString),
844 Environment(ProtectedString),
846 Content(ProtectedString),
848}
849
850#[derive(Clone, Debug, Eq, PartialEq)]
852pub struct Config {
853 name: Identifier,
854 ownership: ResourceOwnership,
855 runtime_name: Option<Sourced<ProtectedString>>,
856 material: Option<Sourced<ConfigMaterial>>,
857}
858
859impl Config {
860 #[must_use]
862 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
863 Self {
864 name,
865 ownership,
866 runtime_name: None,
867 material: None,
868 }
869 }
870
871 #[must_use]
873 pub const fn name(&self) -> &Identifier {
874 &self.name
875 }
876
877 #[must_use]
879 pub const fn ownership(&self) -> ResourceOwnership {
880 self.ownership
881 }
882
883 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
885 self.runtime_name = Some(name);
886 }
887
888 #[must_use]
890 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
891 self.runtime_name.as_ref()
892 }
893
894 pub fn set_material(&mut self, material: Sourced<ConfigMaterial>) {
896 self.material = Some(material);
897 }
898
899 #[must_use]
901 pub const fn material(&self) -> Option<&Sourced<ConfigMaterial>> {
902 self.material.as_ref()
903 }
904}
905
906#[derive(Clone, Debug, Eq, PartialEq)]
908#[non_exhaustive]
909pub enum SecretMaterial {
910 File(ProtectedString),
912 Environment(ProtectedString),
914}
915
916#[derive(Clone, Debug, Eq, PartialEq)]
918pub struct Secret {
919 name: Identifier,
920 ownership: ResourceOwnership,
921 runtime_name: Option<Sourced<ProtectedString>>,
922 material: Option<Sourced<SecretMaterial>>,
923}
924
925impl Secret {
926 #[must_use]
928 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
929 Self {
930 name,
931 ownership,
932 runtime_name: None,
933 material: None,
934 }
935 }
936
937 #[must_use]
939 pub const fn name(&self) -> &Identifier {
940 &self.name
941 }
942
943 #[must_use]
945 pub const fn ownership(&self) -> ResourceOwnership {
946 self.ownership
947 }
948
949 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
951 self.runtime_name = Some(name);
952 }
953
954 #[must_use]
956 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
957 self.runtime_name.as_ref()
958 }
959
960 pub fn set_material(&mut self, material: Sourced<SecretMaterial>) {
962 self.material = Some(material);
963 }
964
965 #[must_use]
967 pub const fn material(&self) -> Option<&Sourced<SecretMaterial>> {
968 self.material.as_ref()
969 }
970}
971
972#[derive(Clone, Copy, Debug, Eq, PartialEq)]
974#[non_exhaustive]
975pub enum ResourceGrantSyntax {
976 Short,
978 Long,
980}
981
982#[derive(Clone, Debug, Eq, PartialEq)]
988pub struct ResourceGrant {
989 source: ProtectedString,
990 syntax: ResourceGrantSyntax,
991 target: Option<Sourced<ProtectedString>>,
992 uid: Option<Sourced<ProtectedString>>,
993 gid: Option<Sourced<ProtectedString>>,
994 mode: Option<Sourced<ProtectedString>>,
995}
996
997impl ResourceGrant {
998 pub fn new(source: ProtectedString, syntax: ResourceGrantSyntax) -> Result<Self, ModelError> {
1004 validate_text("resource grant source", source.expose())?;
1005 Ok(Self {
1006 source,
1007 syntax,
1008 target: None,
1009 uid: None,
1010 gid: None,
1011 mode: None,
1012 })
1013 }
1014
1015 #[must_use]
1017 pub const fn source(&self) -> &ProtectedString {
1018 &self.source
1019 }
1020
1021 #[must_use]
1023 pub const fn syntax(&self) -> ResourceGrantSyntax {
1024 self.syntax
1025 }
1026
1027 pub fn set_target(&mut self, target: Sourced<ProtectedString>) {
1029 self.target = Some(target);
1030 }
1031
1032 #[must_use]
1034 pub const fn target(&self) -> Option<&Sourced<ProtectedString>> {
1035 self.target.as_ref()
1036 }
1037
1038 pub fn set_uid(&mut self, uid: Sourced<ProtectedString>) {
1040 self.uid = Some(uid);
1041 }
1042
1043 #[must_use]
1045 pub const fn uid(&self) -> Option<&Sourced<ProtectedString>> {
1046 self.uid.as_ref()
1047 }
1048
1049 pub fn set_gid(&mut self, gid: Sourced<ProtectedString>) {
1051 self.gid = Some(gid);
1052 }
1053
1054 #[must_use]
1056 pub const fn gid(&self) -> Option<&Sourced<ProtectedString>> {
1057 self.gid.as_ref()
1058 }
1059
1060 pub fn set_mode(&mut self, mode: Sourced<ProtectedString>) {
1062 self.mode = Some(mode);
1063 }
1064
1065 #[must_use]
1067 pub const fn mode(&self) -> Option<&Sourced<ProtectedString>> {
1068 self.mode.as_ref()
1069 }
1070}
1071
1072impl Network {
1073 #[must_use]
1075 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1076 Self {
1077 name,
1078 ownership,
1079 runtime_name: None,
1080 driver: None,
1081 driver_options: None,
1082 driver_options_origins: Vec::new(),
1083 labels: None,
1084 labels_origins: Vec::new(),
1085 internal: None,
1086 ipv6: None,
1087 ipam_driver: None,
1088 ipam_configs: None,
1089 ipam_configs_origins: Vec::new(),
1090 }
1091 }
1092
1093 #[must_use]
1095 pub const fn name(&self) -> &Identifier {
1096 &self.name
1097 }
1098
1099 #[must_use]
1101 pub const fn ownership(&self) -> ResourceOwnership {
1102 self.ownership
1103 }
1104
1105 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1107 self.runtime_name = Some(name);
1108 }
1109
1110 #[must_use]
1112 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1113 self.runtime_name.as_ref()
1114 }
1115
1116 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
1118 self.driver = Some(driver);
1119 }
1120
1121 #[must_use]
1123 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
1124 self.driver.as_ref()
1125 }
1126
1127 pub fn set_driver_options(&mut self, options: Vec<Sourced<NetworkDriverOption>>) {
1129 self.driver_options = Some(options);
1130 self.driver_options_origins.clear();
1131 }
1132
1133 pub fn set_driver_options_with_origins(
1135 &mut self,
1136 options: Vec<Sourced<NetworkDriverOption>>,
1137 origins: Vec<Provenance>,
1138 ) {
1139 self.driver_options = Some(options);
1140 self.driver_options_origins = origins;
1141 }
1142
1143 pub fn add_driver_option(&mut self, option: Sourced<NetworkDriverOption>) {
1145 self.driver_options.get_or_insert_default().push(option);
1146 }
1147
1148 #[must_use]
1150 pub fn driver_options(&self) -> Option<&[Sourced<NetworkDriverOption>]> {
1151 self.driver_options.as_deref()
1152 }
1153
1154 #[must_use]
1156 pub fn driver_options_origins(&self) -> &[Provenance] {
1157 &self.driver_options_origins
1158 }
1159
1160 pub fn set_labels(&mut self, labels: Vec<Sourced<MetadataLabel>>) {
1162 self.labels = Some(labels);
1163 self.labels_origins.clear();
1164 }
1165
1166 pub fn set_labels_with_origins(&mut self, labels: Vec<Sourced<MetadataLabel>>, origins: Vec<Provenance>) {
1168 self.labels = Some(labels);
1169 self.labels_origins = origins;
1170 }
1171
1172 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
1174 self.labels.get_or_insert_default().push(label);
1175 }
1176
1177 #[must_use]
1179 pub fn labels(&self) -> Option<&[Sourced<MetadataLabel>]> {
1180 self.labels.as_deref()
1181 }
1182
1183 #[must_use]
1185 pub fn labels_origins(&self) -> &[Provenance] {
1186 &self.labels_origins
1187 }
1188
1189 pub fn set_internal(&mut self, internal: Sourced<bool>) {
1191 self.internal = Some(internal);
1192 }
1193
1194 #[must_use]
1196 pub const fn internal(&self) -> Option<&Sourced<bool>> {
1197 self.internal.as_ref()
1198 }
1199
1200 pub fn set_ipv6(&mut self, ipv6: Sourced<bool>) {
1202 self.ipv6 = Some(ipv6);
1203 }
1204
1205 #[must_use]
1207 pub const fn ipv6(&self) -> Option<&Sourced<bool>> {
1208 self.ipv6.as_ref()
1209 }
1210
1211 pub fn set_ipam_driver(&mut self, driver: Sourced<ProtectedString>) {
1213 self.ipam_driver = Some(driver);
1214 }
1215
1216 #[must_use]
1218 pub const fn ipam_driver(&self) -> Option<&Sourced<ProtectedString>> {
1219 self.ipam_driver.as_ref()
1220 }
1221
1222 pub fn set_ipam_configs(&mut self, configs: Vec<Sourced<NetworkIpamConfig>>) {
1224 self.ipam_configs = Some(configs);
1225 self.ipam_configs_origins.clear();
1226 }
1227
1228 pub fn set_ipam_configs_with_origins(
1230 &mut self,
1231 configs: Vec<Sourced<NetworkIpamConfig>>,
1232 origins: Vec<Provenance>,
1233 ) {
1234 self.ipam_configs = Some(configs);
1235 self.ipam_configs_origins = origins;
1236 }
1237
1238 pub fn add_ipam_config(&mut self, config: Sourced<NetworkIpamConfig>) {
1240 self.ipam_configs.get_or_insert_default().push(config);
1241 }
1242
1243 #[must_use]
1245 pub fn ipam_configs(&self) -> Option<&[Sourced<NetworkIpamConfig>]> {
1246 self.ipam_configs.as_deref()
1247 }
1248
1249 #[must_use]
1251 pub fn ipam_configs_origins(&self) -> &[Provenance] {
1252 &self.ipam_configs_origins
1253 }
1254}
1255
1256#[derive(Clone, Debug, Eq, PartialEq)]
1261pub struct ServiceGroup {
1262 name: Identifier,
1263 ownership: ResourceOwnership,
1264 members: Vec<Sourced<Identifier>>,
1265 runtime: Option<Sourced<ServiceGroupRuntime>>,
1266}
1267
1268impl ServiceGroup {
1269 #[must_use]
1271 pub const fn new(name: Identifier, ownership: ResourceOwnership) -> Self {
1272 Self {
1273 name,
1274 ownership,
1275 members: Vec::new(),
1276 runtime: None,
1277 }
1278 }
1279
1280 #[must_use]
1282 pub const fn name(&self) -> &Identifier {
1283 &self.name
1284 }
1285
1286 #[must_use]
1288 pub const fn ownership(&self) -> ResourceOwnership {
1289 self.ownership
1290 }
1291
1292 pub fn add_member(&mut self, member: Sourced<Identifier>) -> Result<(), ModelError> {
1298 if self.members.iter().any(|candidate| candidate.value() == member.value()) {
1299 return Err(ModelError::DuplicateServiceGroupMember {
1300 group: self.name.as_str().to_owned(),
1301 service: member.value().as_str().to_owned(),
1302 });
1303 }
1304 self.members.push(member);
1305 Ok(())
1306 }
1307
1308 #[must_use]
1310 pub fn members(&self) -> &[Sourced<Identifier>] {
1311 &self.members
1312 }
1313
1314 pub fn set_runtime(&mut self, runtime: Sourced<ServiceGroupRuntime>) {
1319 self.runtime = Some(runtime);
1320 }
1321
1322 #[must_use]
1324 pub const fn runtime(&self) -> Option<&Sourced<ServiceGroupRuntime>> {
1325 self.runtime.as_ref()
1326 }
1327}
1328
1329#[derive(Clone, Debug, Eq, PartialEq)]
1331#[non_exhaustive]
1332pub enum GroupExitPolicy {
1333 Stop,
1335 Continue,
1337 Raw(ProtectedString),
1339}
1340
1341#[derive(Clone, Debug, Default, Eq, PartialEq)]
1347pub struct ServiceGroupRuntime {
1348 runtime_name: Option<Sourced<ProtectedString>>,
1349 service_name: Option<Sourced<ProtectedString>>,
1350 host_mappings: Option<Vec<Sourced<HostMapping>>>,
1351 host_mappings_origins: Vec<Provenance>,
1352 ports: Option<Vec<Sourced<Port>>>,
1353 ports_origins: Vec<Provenance>,
1354 networks: Option<Vec<Sourced<NetworkAttachment>>>,
1355 networks_origins: Vec<Provenance>,
1356 user_namespace: Option<Sourced<ProtectedString>>,
1357 mounts: Option<Vec<Sourced<Mount>>>,
1358 mounts_origins: Vec<Provenance>,
1359 shm_size: Option<Sourced<ProtectedString>>,
1360 exit_policy: Option<Sourced<GroupExitPolicy>>,
1361 stop_timeout: Option<Sourced<StopTimeout>>,
1362}
1363
1364impl ServiceGroupRuntime {
1365 #[must_use]
1367 pub const fn new() -> Self {
1368 Self {
1369 runtime_name: None,
1370 service_name: None,
1371 host_mappings: None,
1372 host_mappings_origins: Vec::new(),
1373 ports: None,
1374 ports_origins: Vec::new(),
1375 networks: None,
1376 networks_origins: Vec::new(),
1377 user_namespace: None,
1378 mounts: None,
1379 mounts_origins: Vec::new(),
1380 shm_size: None,
1381 exit_policy: None,
1382 stop_timeout: None,
1383 }
1384 }
1385
1386 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
1388 self.runtime_name = Some(name);
1389 }
1390
1391 #[must_use]
1393 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
1394 self.runtime_name.as_ref()
1395 }
1396
1397 pub fn set_service_name(&mut self, name: Sourced<ProtectedString>) {
1399 self.service_name = Some(name);
1400 }
1401
1402 #[must_use]
1404 pub const fn service_name(&self) -> Option<&Sourced<ProtectedString>> {
1405 self.service_name.as_ref()
1406 }
1407
1408 pub fn set_host_mappings(&mut self, values: Vec<Sourced<HostMapping>>) {
1410 self.set_host_mappings_with_origins(values, Vec::new());
1411 }
1412
1413 pub fn set_host_mappings_with_origins(&mut self, values: Vec<Sourced<HostMapping>>, origins: Vec<Provenance>) {
1415 self.host_mappings = Some(values);
1416 self.host_mappings_origins = origins;
1417 }
1418
1419 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
1421 self.host_mappings.get_or_insert_default().push(value);
1422 }
1423
1424 #[must_use]
1426 pub fn host_mappings(&self) -> Option<&[Sourced<HostMapping>]> {
1427 self.host_mappings.as_deref()
1428 }
1429
1430 #[must_use]
1432 pub fn host_mappings_origins(&self) -> &[Provenance] {
1433 &self.host_mappings_origins
1434 }
1435
1436 pub fn set_ports(&mut self, values: Vec<Sourced<Port>>) {
1438 self.set_ports_with_origins(values, Vec::new());
1439 }
1440
1441 pub fn set_ports_with_origins(&mut self, values: Vec<Sourced<Port>>, origins: Vec<Provenance>) {
1443 self.ports = Some(values);
1444 self.ports_origins = origins;
1445 }
1446
1447 pub fn add_port(&mut self, value: Sourced<Port>) {
1449 self.ports.get_or_insert_default().push(value);
1450 }
1451
1452 #[must_use]
1454 pub fn ports(&self) -> Option<&[Sourced<Port>]> {
1455 self.ports.as_deref()
1456 }
1457
1458 #[must_use]
1460 pub fn ports_origins(&self) -> &[Provenance] {
1461 &self.ports_origins
1462 }
1463
1464 pub fn set_networks(&mut self, values: Vec<Sourced<NetworkAttachment>>) {
1466 self.set_networks_with_origins(values, Vec::new());
1467 }
1468
1469 pub fn set_networks_with_origins(&mut self, values: Vec<Sourced<NetworkAttachment>>, origins: Vec<Provenance>) {
1471 self.networks = Some(values);
1472 self.networks_origins = origins;
1473 }
1474
1475 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
1477 self.networks.get_or_insert_default().push(value);
1478 }
1479
1480 pub fn replace_network(
1487 &mut self,
1488 index: usize,
1489 value: Sourced<NetworkAttachment>,
1490 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
1491 let len = self.networks.as_ref().map_or(0, Vec::len);
1492 let Some(networks) = self.networks.as_mut() else {
1493 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1494 };
1495 let Some(slot) = networks.get_mut(index) else {
1496 return Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index, len });
1497 };
1498 Ok(std::mem::replace(slot, value))
1499 }
1500
1501 #[must_use]
1503 pub fn networks(&self) -> Option<&[Sourced<NetworkAttachment>]> {
1504 self.networks.as_deref()
1505 }
1506
1507 #[must_use]
1509 pub fn networks_origins(&self) -> &[Provenance] {
1510 &self.networks_origins
1511 }
1512
1513 pub fn set_user_namespace(&mut self, value: Sourced<ProtectedString>) {
1515 self.user_namespace = Some(value);
1516 }
1517
1518 #[must_use]
1520 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
1521 self.user_namespace.as_ref()
1522 }
1523
1524 pub fn set_mounts(&mut self, values: Vec<Sourced<Mount>>) {
1526 self.set_mounts_with_origins(values, Vec::new());
1527 }
1528
1529 pub fn set_mounts_with_origins(&mut self, values: Vec<Sourced<Mount>>, origins: Vec<Provenance>) {
1531 self.mounts = Some(values);
1532 self.mounts_origins = origins;
1533 }
1534
1535 pub fn add_mount(&mut self, value: Sourced<Mount>) {
1537 self.mounts.get_or_insert_default().push(value);
1538 }
1539
1540 #[must_use]
1542 pub fn mounts(&self) -> Option<&[Sourced<Mount>]> {
1543 self.mounts.as_deref()
1544 }
1545
1546 #[must_use]
1548 pub fn mounts_origins(&self) -> &[Provenance] {
1549 &self.mounts_origins
1550 }
1551
1552 pub fn set_shm_size(&mut self, value: Sourced<ProtectedString>) {
1554 self.shm_size = Some(value);
1555 }
1556
1557 #[must_use]
1559 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
1560 self.shm_size.as_ref()
1561 }
1562
1563 pub fn set_exit_policy(&mut self, value: Sourced<GroupExitPolicy>) {
1565 self.exit_policy = Some(value);
1566 }
1567
1568 #[must_use]
1570 pub const fn exit_policy(&self) -> Option<&Sourced<GroupExitPolicy>> {
1571 self.exit_policy.as_ref()
1572 }
1573
1574 pub fn set_stop_timeout(&mut self, value: Sourced<StopTimeout>) {
1576 self.stop_timeout = Some(value);
1577 }
1578
1579 #[must_use]
1581 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
1582 self.stop_timeout.as_ref()
1583 }
1584}
1585
1586#[derive(Clone, Debug, Eq, PartialEq)]
1588#[non_exhaustive]
1589pub enum Command {
1590 Exec(Vec<ProtectedString>),
1592 Shell(ProtectedString),
1594 Empty,
1596}
1597
1598#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1603#[non_exhaustive]
1604pub enum StartupNotification {
1605 Runtime,
1607 Application,
1609 Healthy,
1611}
1612
1613#[derive(Clone, Debug, Eq, PartialEq)]
1618#[non_exhaustive]
1619pub enum Entrypoint {
1620 Exec(Vec<ProtectedString>),
1622 Shell(ProtectedString),
1624 Empty,
1626}
1627
1628#[derive(Clone, Debug, Eq, PartialEq)]
1633#[non_exhaustive]
1634pub enum PullPolicy {
1635 Always,
1637 Missing,
1639 Never,
1641 IfNotPresent,
1643 Build,
1645 Daily,
1647 Weekly,
1649 Every(ProtectedString),
1651 Raw(ProtectedString),
1653}
1654
1655#[derive(Clone, Debug, Eq, PartialEq)]
1660pub struct StopTimeout(String);
1661
1662impl StopTimeout {
1663 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1669 let value = value.into();
1670 validate_text("stop timeout", &value)?;
1671 Ok(Self(value))
1672 }
1673
1674 #[must_use]
1676 pub fn as_str(&self) -> &str {
1677 &self.0
1678 }
1679}
1680
1681#[derive(Clone, Debug, Eq, PartialEq)]
1683pub struct ExposedPort {
1684 container: u16,
1685 protocol: Protocol,
1686}
1687
1688impl ExposedPort {
1689 pub fn new(container: u16, protocol: Protocol) -> Result<Self, ModelError> {
1695 if container == 0 {
1696 return Err(ModelError::ZeroContainerPort);
1697 }
1698 Ok(Self { container, protocol })
1699 }
1700
1701 #[must_use]
1703 pub const fn container(&self) -> u16 {
1704 self.container
1705 }
1706
1707 #[must_use]
1709 pub const fn protocol(&self) -> &Protocol {
1710 &self.protocol
1711 }
1712}
1713
1714#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1720#[non_exhaustive]
1721pub enum RestartPolicy {
1722 Never,
1724 Always,
1726 OnFailure {
1728 maximum_retries: Option<std::num::NonZeroU64>,
1730 },
1731 UnlessStopped,
1733}
1734
1735impl RestartPolicy {
1736 #[must_use]
1738 pub const fn on_failure(maximum_retries: Option<std::num::NonZeroU64>) -> Self {
1739 Self::OnFailure { maximum_retries }
1740 }
1741
1742 #[must_use]
1744 pub const fn maximum_retries(self) -> Option<std::num::NonZeroU64> {
1745 match self {
1746 Self::OnFailure { maximum_retries } => maximum_retries,
1747 Self::Never | Self::Always | Self::UnlessStopped => None,
1748 }
1749 }
1750}
1751
1752#[derive(Clone, Debug, Eq, PartialEq)]
1754#[non_exhaustive]
1755pub enum HealthcheckCommand {
1756 Exec(Vec<ProtectedString>),
1758 Shell(ProtectedString),
1760}
1761
1762#[derive(Clone, Debug, Eq, PartialEq)]
1764pub struct HealthcheckDuration(String);
1765
1766impl HealthcheckDuration {
1767 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1776 let value = value.into();
1777 validate_text("health-check duration", &value)?;
1778 Ok(Self(value))
1779 }
1780
1781 #[must_use]
1783 pub fn as_str(&self) -> &str {
1784 &self.0
1785 }
1786}
1787
1788#[derive(Clone, Debug, Eq, PartialEq)]
1790pub struct HealthcheckRetries(String);
1791
1792impl HealthcheckRetries {
1793 pub fn new(value: impl Into<String>) -> Result<Self, ModelError> {
1800 let value = value.into();
1801 validate_text("health-check retries", &value)?;
1802 if !value.bytes().all(|byte| byte.is_ascii_digit()) {
1803 return Err(ModelError::InvalidHealthcheckRetries);
1804 }
1805 Ok(Self(value))
1806 }
1807
1808 #[must_use]
1810 pub fn as_str(&self) -> &str {
1811 &self.0
1812 }
1813}
1814
1815#[derive(Clone, Debug, Default, Eq, PartialEq)]
1817pub struct Healthcheck {
1818 command: Option<Sourced<HealthcheckCommand>>,
1819 disabled: Option<Sourced<bool>>,
1820 interval: Option<Sourced<HealthcheckDuration>>,
1821 timeout: Option<Sourced<HealthcheckDuration>>,
1822 retries: Option<Sourced<HealthcheckRetries>>,
1823 start_period: Option<Sourced<HealthcheckDuration>>,
1824 start_interval: Option<Sourced<HealthcheckDuration>>,
1825}
1826
1827impl Healthcheck {
1828 #[must_use]
1830 pub const fn new() -> Self {
1831 Self {
1832 command: None,
1833 disabled: None,
1834 interval: None,
1835 timeout: None,
1836 retries: None,
1837 start_period: None,
1838 start_interval: None,
1839 }
1840 }
1841
1842 pub fn set_command(&mut self, command: Sourced<HealthcheckCommand>) {
1844 self.command = Some(command);
1845 }
1846
1847 #[must_use]
1849 pub const fn command(&self) -> Option<&Sourced<HealthcheckCommand>> {
1850 self.command.as_ref()
1851 }
1852
1853 pub fn set_disabled(&mut self, disabled: Sourced<bool>) {
1855 self.disabled = Some(disabled);
1856 }
1857
1858 #[must_use]
1860 pub const fn disabled(&self) -> Option<&Sourced<bool>> {
1861 self.disabled.as_ref()
1862 }
1863
1864 pub fn set_interval(&mut self, interval: Sourced<HealthcheckDuration>) {
1866 self.interval = Some(interval);
1867 }
1868
1869 #[must_use]
1871 pub const fn interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1872 self.interval.as_ref()
1873 }
1874
1875 pub fn set_timeout(&mut self, timeout: Sourced<HealthcheckDuration>) {
1877 self.timeout = Some(timeout);
1878 }
1879
1880 #[must_use]
1882 pub const fn timeout(&self) -> Option<&Sourced<HealthcheckDuration>> {
1883 self.timeout.as_ref()
1884 }
1885
1886 pub fn set_retries(&mut self, retries: Sourced<HealthcheckRetries>) {
1888 self.retries = Some(retries);
1889 }
1890
1891 #[must_use]
1893 pub const fn retries(&self) -> Option<&Sourced<HealthcheckRetries>> {
1894 self.retries.as_ref()
1895 }
1896
1897 pub fn set_start_period(&mut self, start_period: Sourced<HealthcheckDuration>) {
1899 self.start_period = Some(start_period);
1900 }
1901
1902 #[must_use]
1904 pub const fn start_period(&self) -> Option<&Sourced<HealthcheckDuration>> {
1905 self.start_period.as_ref()
1906 }
1907
1908 pub fn set_start_interval(&mut self, start_interval: Sourced<HealthcheckDuration>) {
1910 self.start_interval = Some(start_interval);
1911 }
1912
1913 #[must_use]
1915 pub const fn start_interval(&self) -> Option<&Sourced<HealthcheckDuration>> {
1916 self.start_interval.as_ref()
1917 }
1918}
1919
1920#[derive(Clone, Debug, Eq, PartialEq)]
1922#[non_exhaustive]
1923pub enum EnvironmentValue {
1924 Literal(ProtectedString),
1926 Deferred(crate::DeferredExpression),
1928 UnresolvedSource {
1930 source_format: Identifier,
1932 expression: ProtectedString,
1934 },
1935 Required,
1937 Host,
1939 Unset,
1941}
1942
1943#[derive(Clone, Debug, Eq, PartialEq)]
1945pub struct EnvironmentVariable {
1946 name: Identifier,
1947 value: EnvironmentValue,
1948}
1949
1950#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1952#[non_exhaustive]
1953pub enum EnvironmentFileSyntax {
1954 Short,
1956 Long,
1958}
1959
1960#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1962#[non_exhaustive]
1963pub enum EnvironmentFileFormat {
1964 Raw,
1966}
1967
1968#[derive(Clone, Debug, Eq, PartialEq)]
1974pub struct EnvironmentFile {
1975 path: ProtectedString,
1976 syntax: EnvironmentFileSyntax,
1977 required: Option<Sourced<bool>>,
1978 format: Option<Sourced<EnvironmentFileFormat>>,
1979}
1980
1981impl EnvironmentFile {
1982 pub fn new(path: ProtectedString, syntax: EnvironmentFileSyntax) -> Result<Self, ModelError> {
1988 validate_text("environment-file path", path.expose())?;
1989 Ok(Self {
1990 path,
1991 syntax,
1992 required: None,
1993 format: None,
1994 })
1995 }
1996
1997 #[must_use]
1999 pub const fn path(&self) -> &ProtectedString {
2000 &self.path
2001 }
2002
2003 #[must_use]
2005 pub const fn syntax(&self) -> EnvironmentFileSyntax {
2006 self.syntax
2007 }
2008
2009 pub fn set_required(&mut self, required: Sourced<bool>) {
2011 self.required = Some(required);
2012 }
2013
2014 #[must_use]
2016 pub const fn required(&self) -> Option<&Sourced<bool>> {
2017 self.required.as_ref()
2018 }
2019
2020 #[must_use]
2022 pub fn is_required(&self) -> bool {
2023 self.required.as_ref().is_none_or(|required| *required.value())
2024 }
2025
2026 pub fn set_format(&mut self, format: Sourced<EnvironmentFileFormat>) {
2028 self.format = Some(format);
2029 }
2030
2031 #[must_use]
2033 pub const fn format(&self) -> Option<&Sourced<EnvironmentFileFormat>> {
2034 self.format.as_ref()
2035 }
2036}
2037
2038#[derive(Clone, Debug, Eq, PartialEq)]
2044pub struct MetadataLabel {
2045 name: Identifier,
2046 value: ProtectedString,
2047}
2048
2049impl MetadataLabel {
2050 #[must_use]
2052 pub const fn new(name: Identifier, value: ProtectedString) -> Self {
2053 Self { name, value }
2054 }
2055
2056 #[must_use]
2058 pub const fn name(&self) -> &Identifier {
2059 &self.name
2060 }
2061
2062 #[must_use]
2064 pub const fn value(&self) -> &ProtectedString {
2065 &self.value
2066 }
2067}
2068
2069#[derive(Clone, Debug, Eq, PartialEq)]
2074pub struct Annotation {
2075 name: Sourced<Identifier>,
2076 value: Sourced<ProtectedString>,
2077}
2078
2079impl Annotation {
2080 #[must_use]
2082 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2083 Self { name, value }
2084 }
2085
2086 #[must_use]
2088 pub const fn name(&self) -> &Sourced<Identifier> {
2089 &self.name
2090 }
2091
2092 #[must_use]
2094 pub const fn value(&self) -> &Sourced<ProtectedString> {
2095 &self.value
2096 }
2097}
2098
2099#[derive(Clone, Debug, Eq, PartialEq)]
2101pub struct LoggingOption {
2102 name: Sourced<Identifier>,
2103 value: Sourced<ProtectedString>,
2104}
2105
2106impl LoggingOption {
2107 #[must_use]
2109 pub const fn new(name: Sourced<Identifier>, value: Sourced<ProtectedString>) -> Self {
2110 Self { name, value }
2111 }
2112
2113 #[must_use]
2115 pub const fn name(&self) -> &Sourced<Identifier> {
2116 &self.name
2117 }
2118
2119 #[must_use]
2121 pub const fn value(&self) -> &Sourced<ProtectedString> {
2122 &self.value
2123 }
2124}
2125
2126#[derive(Clone, Debug, Default, Eq, PartialEq)]
2131pub struct Logging {
2132 driver: Option<Sourced<ProtectedString>>,
2133 options: Option<Vec<Sourced<LoggingOption>>>,
2134 options_origins: Vec<Provenance>,
2135}
2136
2137impl Logging {
2138 #[must_use]
2140 pub const fn new() -> Self {
2141 Self {
2142 driver: None,
2143 options: None,
2144 options_origins: Vec::new(),
2145 }
2146 }
2147
2148 pub fn set_driver(&mut self, driver: Sourced<ProtectedString>) {
2150 self.driver = Some(driver);
2151 }
2152
2153 #[must_use]
2155 pub const fn driver(&self) -> Option<&Sourced<ProtectedString>> {
2156 self.driver.as_ref()
2157 }
2158
2159 pub fn set_options(&mut self, options: Vec<Sourced<LoggingOption>>) {
2161 self.options = Some(options);
2162 self.options_origins.clear();
2163 }
2164
2165 pub fn add_option(&mut self, option: Sourced<LoggingOption>) {
2167 self.options.get_or_insert_default().push(option);
2168 }
2169
2170 pub fn set_options_with_origins(&mut self, options: Vec<Sourced<LoggingOption>>, origins: Vec<Provenance>) {
2172 self.options = Some(options);
2173 self.options_origins = origins;
2174 }
2175
2176 #[must_use]
2178 pub fn options(&self) -> Option<&[Sourced<LoggingOption>]> {
2179 self.options.as_deref()
2180 }
2181
2182 #[must_use]
2184 pub fn options_origins(&self) -> &[Provenance] {
2185 &self.options_origins
2186 }
2187}
2188
2189#[derive(Clone, Debug, Eq, PartialEq)]
2194#[non_exhaustive]
2195pub enum ReloadAction {
2196 Command(Command),
2198 Signal(ProtectedString),
2200}
2201
2202impl EnvironmentVariable {
2203 #[must_use]
2205 pub const fn new(name: Identifier, value: EnvironmentValue) -> Self {
2206 Self { name, value }
2207 }
2208
2209 #[must_use]
2211 pub const fn name(&self) -> &Identifier {
2212 &self.name
2213 }
2214
2215 #[must_use]
2217 pub const fn value(&self) -> &EnvironmentValue {
2218 &self.value
2219 }
2220}
2221
2222#[derive(Clone, Debug, Eq, PartialEq)]
2224pub struct HostAddress {
2225 raw: String,
2226 kind: HostAddressKind,
2227}
2228
2229impl HostAddress {
2230 pub fn new(raw: impl Into<String>) -> Result<Self, ModelError> {
2236 let raw = raw.into();
2237 validate_text("host mapping address", &raw)?;
2238 let unbracketed = raw
2239 .strip_prefix('[')
2240 .and_then(|value| value.strip_suffix(']'))
2241 .unwrap_or(&raw);
2242 let kind = if raw == "host-gateway" {
2243 HostAddressKind::HostGateway
2244 } else {
2245 match unbracketed.parse::<IpAddr>() {
2246 Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
2247 Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
2248 bracketed: raw.starts_with('[') && raw.ends_with(']'),
2249 },
2250 Err(_) => HostAddressKind::Other,
2251 }
2252 };
2253 Ok(Self { raw, kind })
2254 }
2255
2256 #[must_use]
2258 pub fn raw(&self) -> &str {
2259 &self.raw
2260 }
2261
2262 #[must_use]
2264 pub const fn kind(&self) -> HostAddressKind {
2265 self.kind
2266 }
2267}
2268
2269#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2271#[non_exhaustive]
2272pub enum HostAddressKind {
2273 Ipv4,
2275 Ipv6 {
2277 bracketed: bool,
2279 },
2280 HostGateway,
2282 Other,
2284}
2285
2286#[derive(Clone, Debug, Eq, PartialEq)]
2288pub struct HostMapping {
2289 hostname: Identifier,
2290 address: HostAddress,
2291}
2292
2293impl HostMapping {
2294 #[must_use]
2296 pub const fn new(hostname: Identifier, address: HostAddress) -> Self {
2297 Self { hostname, address }
2298 }
2299
2300 #[must_use]
2302 pub const fn hostname(&self) -> &Identifier {
2303 &self.hostname
2304 }
2305
2306 #[must_use]
2308 pub const fn address(&self) -> &HostAddress {
2309 &self.address
2310 }
2311}
2312
2313#[derive(Clone, Debug, Eq, PartialEq)]
2315#[non_exhaustive]
2316pub enum Protocol {
2317 Tcp,
2319 Udp,
2321 Sctp,
2323 Other(String),
2325}
2326
2327#[derive(Clone, Debug, Eq, PartialEq)]
2332pub struct Port {
2333 container: u16,
2334 published: Option<u16>,
2335 host_address: Option<String>,
2336 protocol: Protocol,
2337}
2338
2339impl Port {
2340 pub fn new(
2346 container: u16,
2347 published: Option<u16>,
2348 host_address: Option<String>,
2349 protocol: Protocol,
2350 ) -> Result<Self, ModelError> {
2351 if container == 0 {
2352 return Err(ModelError::ZeroContainerPort);
2353 }
2354 Ok(Self {
2355 container,
2356 published,
2357 host_address,
2358 protocol,
2359 })
2360 }
2361
2362 #[must_use]
2364 pub const fn container(&self) -> u16 {
2365 self.container
2366 }
2367
2368 #[must_use]
2370 pub const fn published(&self) -> Option<u16> {
2371 self.published
2372 }
2373
2374 #[must_use]
2376 pub fn host_address(&self) -> Option<&str> {
2377 self.host_address.as_deref()
2378 }
2379
2380 #[must_use]
2382 pub const fn protocol(&self) -> &Protocol {
2383 &self.protocol
2384 }
2385}
2386
2387#[derive(Clone, Debug, Eq, PartialEq)]
2389#[non_exhaustive]
2390pub enum MountSource {
2391 Volume(Identifier),
2393 HostPath(String),
2395 DeferredHostPath(crate::DeferredExpression),
2397 Anonymous,
2399}
2400
2401#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2403#[non_exhaustive]
2404pub enum SelinuxRelabel {
2405 Shared,
2407 Private,
2409}
2410
2411#[derive(Clone, Debug, Eq, PartialEq)]
2413pub struct Mount {
2414 source: MountSource,
2415 target: String,
2416 read_only: bool,
2417 selinux_relabel: Option<SelinuxRelabel>,
2418 literal_paths: bool,
2419}
2420
2421impl Mount {
2422 #[must_use]
2425 pub const fn with_literal_paths(mut self) -> Self {
2426 self.literal_paths = true;
2427 self
2428 }
2429
2430 #[must_use]
2432 pub const fn paths_are_literal(&self) -> bool {
2433 self.literal_paths
2434 }
2435
2436 pub fn new(source: MountSource, target: impl Into<String>, read_only: bool) -> Result<Self, ModelError> {
2442 let target = target.into();
2443 validate_text("mount target", &target)?;
2444 let literal_paths = matches!(&source, MountSource::DeferredHostPath(_));
2445 Ok(Self {
2446 source,
2447 target,
2448 read_only,
2449 selinux_relabel: None,
2450 literal_paths,
2451 })
2452 }
2453
2454 #[must_use]
2456 pub const fn source(&self) -> &MountSource {
2457 &self.source
2458 }
2459
2460 #[must_use]
2462 pub fn target(&self) -> &str {
2463 &self.target
2464 }
2465
2466 #[must_use]
2468 pub const fn read_only(&self) -> bool {
2469 self.read_only
2470 }
2471
2472 pub fn set_selinux_relabel(&mut self, relabel: SelinuxRelabel) {
2474 self.selinux_relabel = Some(relabel);
2475 }
2476
2477 #[must_use]
2479 pub const fn selinux_relabel(&self) -> Option<SelinuxRelabel> {
2480 self.selinux_relabel
2481 }
2482}
2483
2484#[derive(Clone, Eq, PartialEq)]
2486pub struct NetworkAttachment {
2487 network: Identifier,
2488 aliases: Vec<String>,
2489 alias_sensitivities: Vec<bool>,
2490 alias_origins: Vec<Vec<Provenance>>,
2491 ipv4_address: Option<Sourced<ProtectedString>>,
2492 ipv6_address: Option<Sourced<ProtectedString>>,
2493}
2494
2495impl NetworkAttachment {
2496 #[must_use]
2498 pub fn new(network: Identifier, aliases: Vec<Sourced<ProtectedString>>) -> Self {
2499 Self {
2500 network,
2501 aliases: aliases.iter().map(|alias| alias.value().expose().to_owned()).collect(),
2502 alias_sensitivities: aliases.iter().map(|alias| alias.value().is_sensitive()).collect(),
2503 alias_origins: aliases.into_iter().map(|alias| alias.origins().to_vec()).collect(),
2504 ipv4_address: None,
2505 ipv6_address: None,
2506 }
2507 }
2508
2509 #[must_use]
2511 pub const fn network(&self) -> &Identifier {
2512 &self.network
2513 }
2514
2515 #[must_use]
2517 pub fn aliases(&self) -> &[String] {
2518 &self.aliases
2519 }
2520
2521 #[must_use]
2523 pub fn alias_origins(&self) -> &[Vec<Provenance>] {
2524 &self.alias_origins
2525 }
2526
2527 #[must_use]
2532 pub fn alias_sensitivities(&self) -> &[bool] {
2533 &self.alias_sensitivities
2534 }
2535
2536 pub fn set_aliases_with_provenance(&mut self, aliases: Vec<Sourced<ProtectedString>>) {
2538 self.aliases = aliases.iter().map(|alias| alias.value().expose().to_owned()).collect();
2539 self.alias_sensitivities = aliases.iter().map(|alias| alias.value().is_sensitive()).collect();
2540 self.alias_origins = aliases.into_iter().map(|alias| alias.origins().to_vec()).collect();
2541 }
2542
2543 pub fn add_alias(&mut self, alias: &Sourced<ProtectedString>) {
2545 self.aliases.push(alias.value().expose().to_owned());
2546 self.alias_sensitivities.push(alias.value().is_sensitive());
2547 self.alias_origins.push(alias.origins().to_vec());
2548 }
2549
2550 pub fn set_ipv4_address(&mut self, address: Sourced<ProtectedString>) {
2552 self.ipv4_address = Some(address);
2553 }
2554
2555 #[must_use]
2557 pub const fn ipv4_address(&self) -> Option<&Sourced<ProtectedString>> {
2558 self.ipv4_address.as_ref()
2559 }
2560
2561 pub fn set_ipv6_address(&mut self, address: Sourced<ProtectedString>) {
2563 self.ipv6_address = Some(address);
2564 }
2565
2566 #[must_use]
2568 pub const fn ipv6_address(&self) -> Option<&Sourced<ProtectedString>> {
2569 self.ipv6_address.as_ref()
2570 }
2571}
2572
2573impl fmt::Debug for NetworkAttachment {
2574 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2575 let aliases = self
2576 .aliases
2577 .iter()
2578 .enumerate()
2579 .map(|(index, alias)| {
2580 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
2581 "[REDACTED]"
2582 } else {
2583 alias.as_str()
2584 }
2585 })
2586 .collect::<Vec<_>>();
2587 formatter
2588 .debug_struct("NetworkAttachment")
2589 .field("network", &self.network)
2590 .field("aliases", &aliases)
2591 .field("alias_origins", &self.alias_origins)
2592 .field("ipv4_address", &self.ipv4_address)
2593 .field("ipv6_address", &self.ipv6_address)
2594 .finish()
2595 }
2596}
2597
2598#[derive(Clone, Debug, Eq, PartialEq)]
2600#[non_exhaustive]
2601pub enum ServiceDependencyCondition {
2602 Started,
2604 Healthy,
2606 CompletedSuccessfully,
2608 Other(ProtectedString),
2610}
2611
2612#[derive(Clone, Debug, Eq, PartialEq)]
2618pub struct ServiceDependency {
2619 service: Identifier,
2620 condition: Option<Sourced<ServiceDependencyCondition>>,
2621 restart: Option<Sourced<bool>>,
2622 required: Option<Sourced<bool>>,
2623}
2624
2625#[derive(Clone, Debug, Eq, PartialEq)]
2627pub struct KernelParameter {
2628 name: ProtectedString,
2629 value: ProtectedString,
2630}
2631
2632impl KernelParameter {
2633 #[must_use]
2635 pub const fn new(name: ProtectedString, value: ProtectedString) -> Self {
2636 Self { name, value }
2637 }
2638
2639 #[must_use]
2641 pub const fn name(&self) -> &ProtectedString {
2642 &self.name
2643 }
2644
2645 #[must_use]
2647 pub const fn value(&self) -> &ProtectedString {
2648 &self.value
2649 }
2650}
2651
2652#[derive(Clone, Debug, Eq, PartialEq)]
2654pub struct ResourceLimit {
2655 name: ProtectedString,
2656 soft: Option<Sourced<ProtectedString>>,
2657 hard: Option<Sourced<ProtectedString>>,
2658}
2659
2660impl ResourceLimit {
2661 #[must_use]
2663 pub const fn new(
2664 name: ProtectedString,
2665 soft: Option<Sourced<ProtectedString>>,
2666 hard: Option<Sourced<ProtectedString>>,
2667 ) -> Self {
2668 Self { name, soft, hard }
2669 }
2670
2671 #[must_use]
2673 pub const fn name(&self) -> &ProtectedString {
2674 &self.name
2675 }
2676
2677 #[must_use]
2679 pub const fn soft(&self) -> Option<&Sourced<ProtectedString>> {
2680 self.soft.as_ref()
2681 }
2682
2683 #[must_use]
2685 pub const fn hard(&self) -> Option<&Sourced<ProtectedString>> {
2686 self.hard.as_ref()
2687 }
2688}
2689
2690#[derive(Clone, Debug, Eq, PartialEq)]
2692#[non_exhaustive]
2693pub enum Device {
2694 Short(ProtectedString),
2696 Long {
2698 source: Option<Sourced<ProtectedString>>,
2700 target: Option<Sourced<ProtectedString>>,
2702 permissions: Option<Sourced<ProtectedString>>,
2704 },
2705}
2706
2707#[derive(Clone, Debug, Eq, PartialEq)]
2712#[non_exhaustive]
2713pub enum SecurityOption {
2714 AppArmor(ProtectedString),
2716 NoNewPrivileges(bool),
2718 SeccompProfile(ProtectedString),
2720 SecurityLabelDisable(bool),
2722 SecurityLabelFileType(ProtectedString),
2724 SecurityLabelLevel(ProtectedString),
2726 SecurityLabelNested(bool),
2728 SecurityLabelType(ProtectedString),
2730 Mask(ProtectedString),
2732 Unmask(ProtectedString),
2734}
2735
2736impl ServiceDependency {
2737 #[must_use]
2740 pub const fn new(service: Identifier) -> Self {
2741 Self {
2742 service,
2743 condition: None,
2744 restart: None,
2745 required: None,
2746 }
2747 }
2748
2749 #[must_use]
2751 pub const fn service(&self) -> &Identifier {
2752 &self.service
2753 }
2754
2755 pub fn set_condition(&mut self, condition: Sourced<ServiceDependencyCondition>) {
2757 self.condition = Some(condition);
2758 }
2759
2760 #[must_use]
2762 pub const fn condition(&self) -> Option<&Sourced<ServiceDependencyCondition>> {
2763 self.condition.as_ref()
2764 }
2765
2766 pub fn set_restart(&mut self, restart: Sourced<bool>) {
2768 self.restart = Some(restart);
2769 }
2770
2771 #[must_use]
2773 pub const fn restart(&self) -> Option<&Sourced<bool>> {
2774 self.restart.as_ref()
2775 }
2776
2777 pub fn set_required(&mut self, required: Sourced<bool>) {
2779 self.required = Some(required);
2780 }
2781
2782 #[must_use]
2784 pub const fn required(&self) -> Option<&Sourced<bool>> {
2785 self.required.as_ref()
2786 }
2787
2788 #[must_use]
2790 pub fn is_required(&self) -> bool {
2791 self.required.as_ref().is_none_or(|required| *required.value())
2792 }
2793}
2794
2795#[derive(Clone, Debug, Eq, PartialEq)]
2797pub struct Service {
2798 name: Identifier,
2799 runtime_name: Option<Sourced<ProtectedString>>,
2800 rootfs: Option<Sourced<ProtectedString>>,
2801 image: Option<Sourced<ImageReference>>,
2802 image_acquisition: Option<Sourced<Identifier>>,
2803 image_build: Option<Sourced<Identifier>>,
2804 command: Option<Sourced<Command>>,
2805 startup_notification: Option<Sourced<StartupNotification>>,
2806 entrypoint: Option<Sourced<Entrypoint>>,
2807 run_init: Option<Sourced<bool>>,
2808 stop_timeout: Option<Sourced<StopTimeout>>,
2809 pull_policy: Option<Sourced<PullPolicy>>,
2810 memory_limit: Option<Sourced<ProtectedString>>,
2811 exposed_ports: Option<Vec<Sourced<ExposedPort>>>,
2812 exposed_ports_origins: Vec<Provenance>,
2813 restart_policy: Option<Sourced<RestartPolicy>>,
2814 healthcheck: Option<Sourced<Healthcheck>>,
2815 labels: Vec<Sourced<MetadataLabel>>,
2816 annotations: Option<Vec<Sourced<Annotation>>>,
2817 annotations_origins: Vec<Provenance>,
2818 logging: Option<Sourced<Logging>>,
2819 reload_action: Option<Sourced<ReloadAction>>,
2820 user: Option<Sourced<ProtectedString>>,
2821 group: Option<Sourced<ProtectedString>>,
2822 user_namespace: Option<Sourced<ProtectedString>>,
2823 supplementary_groups: Vec<Sourced<ProtectedString>>,
2824 working_directory: Option<Sourced<ProtectedString>>,
2825 read_only_root_filesystem: Option<Sourced<bool>>,
2826 hostname: Option<Sourced<ProtectedString>>,
2827 dns_servers: Option<Vec<Sourced<ProtectedString>>>,
2828 dns_servers_origins: Vec<Provenance>,
2829 dns_options: Option<Vec<Sourced<ProtectedString>>>,
2830 dns_options_origins: Vec<Provenance>,
2831 dns_search_domains: Option<Vec<Sourced<ProtectedString>>>,
2832 dns_search_domains_origins: Vec<Provenance>,
2833 security_options: Option<Vec<Sourced<SecurityOption>>>,
2834 security_options_origins: Vec<Provenance>,
2835 pids_limit: Option<Sourced<ProtectedString>>,
2836 shm_size: Option<Sourced<ProtectedString>>,
2837 cap_add: Option<Vec<Sourced<ProtectedString>>>,
2838 cap_add_origins: Vec<Provenance>,
2839 cap_drop: Option<Vec<Sourced<ProtectedString>>>,
2840 cap_drop_origins: Vec<Provenance>,
2841 tmpfs: Option<Vec<Sourced<ProtectedString>>>,
2842 tmpfs_origins: Vec<Provenance>,
2843 sysctls: Option<Vec<Sourced<KernelParameter>>>,
2844 sysctls_origins: Vec<Provenance>,
2845 ulimits: Option<Vec<Sourced<ResourceLimit>>>,
2846 ulimits_origins: Vec<Provenance>,
2847 devices: Option<Vec<Sourced<Device>>>,
2848 devices_origins: Vec<Provenance>,
2849 stop_signal: Option<Sourced<ProtectedString>>,
2850 environment: Vec<Sourced<EnvironmentVariable>>,
2851 environment_files: Vec<Sourced<EnvironmentFile>>,
2852 execution_environment: Option<Sourced<crate::ServiceExecutionEnvironment>>,
2853 host_mappings: Vec<Sourced<HostMapping>>,
2854 ports: Vec<Sourced<Port>>,
2855 mounts: Vec<Sourced<Mount>>,
2856 config_grants: Vec<Sourced<ResourceGrant>>,
2857 secret_grants: Vec<Sourced<ResourceGrant>>,
2858 networks: Vec<Sourced<NetworkAttachment>>,
2859 dependencies: Vec<Sourced<ServiceDependency>>,
2860}
2861
2862impl Service {
2863 pub fn set_execution_environment(&mut self, value: Sourced<crate::ServiceExecutionEnvironment>) {
2865 self.execution_environment = Some(value);
2866 }
2867 #[must_use]
2869 pub const fn execution_environment(&self) -> Option<&Sourced<crate::ServiceExecutionEnvironment>> {
2870 self.execution_environment.as_ref()
2871 }
2872
2873 #[must_use]
2875 pub const fn new(name: Identifier) -> Self {
2876 Self {
2877 name,
2878 runtime_name: None,
2879 rootfs: None,
2880 image: None,
2881 image_acquisition: None,
2882 image_build: None,
2883 command: None,
2884 startup_notification: None,
2885 entrypoint: None,
2886 run_init: None,
2887 stop_timeout: None,
2888 pull_policy: None,
2889 memory_limit: None,
2890 exposed_ports: None,
2891 exposed_ports_origins: Vec::new(),
2892 restart_policy: None,
2893 healthcheck: None,
2894 labels: Vec::new(),
2895 annotations: None,
2896 annotations_origins: Vec::new(),
2897 logging: None,
2898 reload_action: None,
2899 user: None,
2900 group: None,
2901 user_namespace: None,
2902 supplementary_groups: Vec::new(),
2903 working_directory: None,
2904 read_only_root_filesystem: None,
2905 hostname: None,
2906 dns_servers: None,
2907 dns_servers_origins: Vec::new(),
2908 dns_options: None,
2909 dns_options_origins: Vec::new(),
2910 dns_search_domains: None,
2911 dns_search_domains_origins: Vec::new(),
2912 security_options: None,
2913 security_options_origins: Vec::new(),
2914 pids_limit: None,
2915 shm_size: None,
2916 cap_add: None,
2917 cap_add_origins: Vec::new(),
2918 cap_drop: None,
2919 cap_drop_origins: Vec::new(),
2920 tmpfs: None,
2921 tmpfs_origins: Vec::new(),
2922 sysctls: None,
2923 sysctls_origins: Vec::new(),
2924 ulimits: None,
2925 ulimits_origins: Vec::new(),
2926 devices: None,
2927 devices_origins: Vec::new(),
2928 stop_signal: None,
2929 environment: Vec::new(),
2930 environment_files: Vec::new(),
2931 execution_environment: None,
2932 host_mappings: Vec::new(),
2933 ports: Vec::new(),
2934 mounts: Vec::new(),
2935 config_grants: Vec::new(),
2936 secret_grants: Vec::new(),
2937 networks: Vec::new(),
2938 dependencies: Vec::new(),
2939 }
2940 }
2941
2942 #[must_use]
2944 pub const fn name(&self) -> &Identifier {
2945 &self.name
2946 }
2947
2948 pub fn set_runtime_name(&mut self, name: Sourced<ProtectedString>) {
2950 self.runtime_name = Some(name);
2951 }
2952
2953 #[must_use]
2955 pub const fn runtime_name(&self) -> Option<&Sourced<ProtectedString>> {
2956 self.runtime_name.as_ref()
2957 }
2958
2959 pub fn set_rootfs(&mut self, rootfs: Sourced<ProtectedString>) -> Result<(), ModelError> {
2966 self.ensure_rootfs_is_compatible()?;
2967 self.rootfs = Some(rootfs);
2968 Ok(())
2969 }
2970
2971 #[must_use]
2973 pub const fn rootfs(&self) -> Option<&Sourced<ProtectedString>> {
2974 self.rootfs.as_ref()
2975 }
2976
2977 pub fn set_image(&mut self, image: Sourced<ImageReference>) {
2979 self.image = Some(image);
2980 }
2981
2982 #[must_use]
2984 pub const fn image(&self) -> Option<&Sourced<ImageReference>> {
2985 self.image.as_ref()
2986 }
2987
2988 pub fn set_image_acquisition(&mut self, acquisition: Sourced<Identifier>) {
2992 self.image_acquisition = Some(acquisition);
2993 }
2994
2995 #[must_use]
2997 pub const fn image_acquisition(&self) -> Option<&Sourced<Identifier>> {
2998 self.image_acquisition.as_ref()
2999 }
3000
3001 pub fn set_image_build(&mut self, build: Sourced<Identifier>) {
3005 self.image_build = Some(build);
3006 }
3007
3008 #[must_use]
3010 pub const fn image_build(&self) -> Option<&Sourced<Identifier>> {
3011 self.image_build.as_ref()
3012 }
3013
3014 pub fn set_command(&mut self, command: Sourced<Command>) {
3016 self.command = Some(command);
3017 }
3018
3019 #[must_use]
3021 pub const fn command(&self) -> Option<&Sourced<Command>> {
3022 self.command.as_ref()
3023 }
3024
3025 pub fn set_startup_notification(&mut self, notification: Sourced<StartupNotification>) {
3027 self.startup_notification = Some(notification);
3028 }
3029
3030 #[must_use]
3032 pub const fn startup_notification(&self) -> Option<&Sourced<StartupNotification>> {
3033 self.startup_notification.as_ref()
3034 }
3035
3036 pub fn set_entrypoint(&mut self, entrypoint: Sourced<Entrypoint>) {
3038 self.entrypoint = Some(entrypoint);
3039 }
3040
3041 #[must_use]
3043 pub const fn entrypoint(&self) -> Option<&Sourced<Entrypoint>> {
3044 self.entrypoint.as_ref()
3045 }
3046
3047 pub fn set_run_init(&mut self, run_init: Sourced<bool>) {
3049 self.run_init = Some(run_init);
3050 }
3051
3052 #[must_use]
3054 pub const fn run_init(&self) -> Option<&Sourced<bool>> {
3055 self.run_init.as_ref()
3056 }
3057
3058 pub fn set_stop_timeout(&mut self, timeout: Sourced<StopTimeout>) {
3060 self.stop_timeout = Some(timeout);
3061 }
3062
3063 #[must_use]
3065 pub const fn stop_timeout(&self) -> Option<&Sourced<StopTimeout>> {
3066 self.stop_timeout.as_ref()
3067 }
3068
3069 pub fn set_pull_policy(&mut self, policy: Sourced<PullPolicy>) {
3071 self.pull_policy = Some(policy);
3072 }
3073
3074 #[must_use]
3076 pub const fn pull_policy(&self) -> Option<&Sourced<PullPolicy>> {
3077 self.pull_policy.as_ref()
3078 }
3079
3080 pub fn set_memory_limit(&mut self, limit: Sourced<ProtectedString>) {
3082 self.memory_limit = Some(limit);
3083 }
3084
3085 #[must_use]
3087 pub const fn memory_limit(&self) -> Option<&Sourced<ProtectedString>> {
3088 self.memory_limit.as_ref()
3089 }
3090
3091 pub fn set_exposed_ports(&mut self, ports: Vec<Sourced<ExposedPort>>) {
3093 self.exposed_ports = Some(ports);
3094 self.exposed_ports_origins.clear();
3095 }
3096
3097 pub fn set_exposed_ports_with_origins(&mut self, ports: Vec<Sourced<ExposedPort>>, origins: Vec<Provenance>) {
3099 self.exposed_ports = Some(ports);
3100 self.exposed_ports_origins = origins;
3101 }
3102
3103 pub fn add_exposed_port(&mut self, port: Sourced<ExposedPort>) {
3105 self.exposed_ports.get_or_insert_default().push(port);
3106 }
3107
3108 #[must_use]
3110 pub fn exposed_ports(&self) -> Option<&[Sourced<ExposedPort>]> {
3111 self.exposed_ports.as_deref()
3112 }
3113
3114 #[must_use]
3116 pub fn exposed_ports_origins(&self) -> &[Provenance] {
3117 &self.exposed_ports_origins
3118 }
3119
3120 pub fn set_restart_policy(&mut self, restart_policy: Sourced<RestartPolicy>) {
3122 self.restart_policy = Some(restart_policy);
3123 }
3124
3125 #[must_use]
3127 pub const fn restart_policy(&self) -> Option<&Sourced<RestartPolicy>> {
3128 self.restart_policy.as_ref()
3129 }
3130
3131 pub fn set_healthcheck(&mut self, healthcheck: Sourced<Healthcheck>) {
3133 self.healthcheck = Some(healthcheck);
3134 }
3135
3136 #[must_use]
3138 pub const fn healthcheck(&self) -> Option<&Sourced<Healthcheck>> {
3139 self.healthcheck.as_ref()
3140 }
3141
3142 pub fn add_label(&mut self, label: Sourced<MetadataLabel>) {
3144 self.labels.push(label);
3145 }
3146
3147 #[must_use]
3149 pub fn labels(&self) -> &[Sourced<MetadataLabel>] {
3150 &self.labels
3151 }
3152
3153 pub fn set_annotations(&mut self, annotations: Vec<Sourced<Annotation>>) {
3155 self.annotations = Some(annotations);
3156 self.annotations_origins.clear();
3157 }
3158
3159 pub fn add_annotation(&mut self, annotation: Sourced<Annotation>) {
3161 self.annotations.get_or_insert_default().push(annotation);
3162 }
3163
3164 pub fn set_annotations_with_origins(&mut self, annotations: Vec<Sourced<Annotation>>, origins: Vec<Provenance>) {
3166 self.annotations = Some(annotations);
3167 self.annotations_origins = origins;
3168 }
3169
3170 #[must_use]
3172 pub fn annotations(&self) -> Option<&[Sourced<Annotation>]> {
3173 self.annotations.as_deref()
3174 }
3175
3176 #[must_use]
3178 pub fn annotations_origins(&self) -> &[Provenance] {
3179 &self.annotations_origins
3180 }
3181
3182 pub fn set_logging(&mut self, logging: Sourced<Logging>) {
3184 self.logging = Some(logging);
3185 }
3186
3187 #[must_use]
3189 pub const fn logging(&self) -> Option<&Sourced<Logging>> {
3190 self.logging.as_ref()
3191 }
3192
3193 pub fn set_reload_action(&mut self, reload_action: Sourced<ReloadAction>) {
3195 self.reload_action = Some(reload_action);
3196 }
3197
3198 #[must_use]
3200 pub const fn reload_action(&self) -> Option<&Sourced<ReloadAction>> {
3201 self.reload_action.as_ref()
3202 }
3203
3204 pub fn set_user(&mut self, user: Sourced<ProtectedString>) {
3206 self.user = Some(user);
3207 }
3208
3209 #[must_use]
3211 pub const fn user(&self) -> Option<&Sourced<ProtectedString>> {
3212 self.user.as_ref()
3213 }
3214
3215 pub fn set_group(&mut self, group: Sourced<ProtectedString>) {
3217 self.group = Some(group);
3218 }
3219
3220 #[must_use]
3222 pub const fn group(&self) -> Option<&Sourced<ProtectedString>> {
3223 self.group.as_ref()
3224 }
3225
3226 pub fn set_user_namespace(&mut self, user_namespace: Sourced<ProtectedString>) {
3228 self.user_namespace = Some(user_namespace);
3229 }
3230
3231 #[must_use]
3233 pub const fn user_namespace(&self) -> Option<&Sourced<ProtectedString>> {
3234 self.user_namespace.as_ref()
3235 }
3236
3237 pub fn add_supplementary_group(&mut self, group: Sourced<ProtectedString>) {
3239 self.supplementary_groups.push(group);
3240 }
3241
3242 #[must_use]
3244 pub fn supplementary_groups(&self) -> &[Sourced<ProtectedString>] {
3245 &self.supplementary_groups
3246 }
3247
3248 pub fn set_working_directory(&mut self, working_directory: Sourced<ProtectedString>) {
3250 self.working_directory = Some(working_directory);
3251 }
3252
3253 #[must_use]
3255 pub const fn working_directory(&self) -> Option<&Sourced<ProtectedString>> {
3256 self.working_directory.as_ref()
3257 }
3258
3259 pub fn set_read_only_root_filesystem(&mut self, read_only: Sourced<bool>) {
3261 self.read_only_root_filesystem = Some(read_only);
3262 }
3263
3264 #[must_use]
3266 pub const fn read_only_root_filesystem(&self) -> Option<&Sourced<bool>> {
3267 self.read_only_root_filesystem.as_ref()
3268 }
3269
3270 pub fn set_hostname(&mut self, hostname: Sourced<ProtectedString>) {
3272 self.hostname = Some(hostname);
3273 }
3274
3275 #[must_use]
3277 pub const fn hostname(&self) -> Option<&Sourced<ProtectedString>> {
3278 self.hostname.as_ref()
3279 }
3280
3281 pub fn set_dns_servers_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3283 self.dns_servers = Some(values);
3284 self.dns_servers_origins = origins;
3285 }
3286
3287 pub fn set_dns_servers(&mut self, values: Vec<Sourced<ProtectedString>>) {
3289 self.set_dns_servers_with_origins(values, Vec::new());
3290 }
3291
3292 #[must_use]
3294 pub fn dns_servers(&self) -> Option<&[Sourced<ProtectedString>]> {
3295 self.dns_servers.as_deref()
3296 }
3297
3298 #[must_use]
3300 pub fn dns_servers_origins(&self) -> &[Provenance] {
3301 &self.dns_servers_origins
3302 }
3303
3304 pub fn set_dns_options_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3306 self.dns_options = Some(values);
3307 self.dns_options_origins = origins;
3308 }
3309
3310 pub fn set_dns_options(&mut self, values: Vec<Sourced<ProtectedString>>) {
3312 self.set_dns_options_with_origins(values, Vec::new());
3313 }
3314
3315 #[must_use]
3317 pub fn dns_options(&self) -> Option<&[Sourced<ProtectedString>]> {
3318 self.dns_options.as_deref()
3319 }
3320
3321 #[must_use]
3323 pub fn dns_options_origins(&self) -> &[Provenance] {
3324 &self.dns_options_origins
3325 }
3326
3327 pub fn set_dns_search_domains_with_origins(
3329 &mut self,
3330 values: Vec<Sourced<ProtectedString>>,
3331 origins: Vec<Provenance>,
3332 ) {
3333 self.dns_search_domains = Some(values);
3334 self.dns_search_domains_origins = origins;
3335 }
3336
3337 pub fn set_dns_search_domains(&mut self, values: Vec<Sourced<ProtectedString>>) {
3339 self.set_dns_search_domains_with_origins(values, Vec::new());
3340 }
3341
3342 #[must_use]
3344 pub fn dns_search_domains(&self) -> Option<&[Sourced<ProtectedString>]> {
3345 self.dns_search_domains.as_deref()
3346 }
3347
3348 #[must_use]
3350 pub fn dns_search_domains_origins(&self) -> &[Provenance] {
3351 &self.dns_search_domains_origins
3352 }
3353
3354 pub fn set_security_options_with_origins(
3356 &mut self,
3357 values: Vec<Sourced<SecurityOption>>,
3358 origins: Vec<Provenance>,
3359 ) {
3360 self.security_options = Some(values);
3361 self.security_options_origins = origins;
3362 }
3363
3364 pub fn set_security_options(&mut self, values: Vec<Sourced<SecurityOption>>) {
3366 self.set_security_options_with_origins(values, Vec::new());
3367 }
3368
3369 #[must_use]
3371 pub fn security_options(&self) -> Option<&[Sourced<SecurityOption>]> {
3372 self.security_options.as_deref()
3373 }
3374
3375 #[must_use]
3377 pub fn security_options_origins(&self) -> &[Provenance] {
3378 &self.security_options_origins
3379 }
3380
3381 pub fn set_pids_limit(&mut self, limit: Sourced<ProtectedString>) {
3383 self.pids_limit = Some(limit);
3384 }
3385
3386 #[must_use]
3388 pub const fn pids_limit(&self) -> Option<&Sourced<ProtectedString>> {
3389 self.pids_limit.as_ref()
3390 }
3391
3392 pub fn set_shm_size(&mut self, size: Sourced<ProtectedString>) {
3394 self.shm_size = Some(size);
3395 }
3396
3397 #[must_use]
3399 pub const fn shm_size(&self) -> Option<&Sourced<ProtectedString>> {
3400 self.shm_size.as_ref()
3401 }
3402
3403 pub fn set_cap_add(&mut self, values: Vec<Sourced<ProtectedString>>) {
3405 self.cap_add = Some(values);
3406 self.cap_add_origins.clear();
3407 }
3408
3409 pub fn set_cap_add_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3411 self.cap_add = Some(values);
3412 self.cap_add_origins = origins;
3413 }
3414
3415 #[must_use]
3417 pub fn cap_add(&self) -> Option<&[Sourced<ProtectedString>]> {
3418 self.cap_add.as_deref()
3419 }
3420
3421 #[must_use]
3423 pub fn cap_add_origins(&self) -> &[Provenance] {
3424 &self.cap_add_origins
3425 }
3426
3427 pub fn set_cap_drop(&mut self, values: Vec<Sourced<ProtectedString>>) {
3429 self.cap_drop = Some(values);
3430 self.cap_drop_origins.clear();
3431 }
3432
3433 pub fn set_cap_drop_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3435 self.cap_drop = Some(values);
3436 self.cap_drop_origins = origins;
3437 }
3438
3439 #[must_use]
3441 pub fn cap_drop(&self) -> Option<&[Sourced<ProtectedString>]> {
3442 self.cap_drop.as_deref()
3443 }
3444
3445 #[must_use]
3447 pub fn cap_drop_origins(&self) -> &[Provenance] {
3448 &self.cap_drop_origins
3449 }
3450
3451 pub fn set_tmpfs(&mut self, values: Vec<Sourced<ProtectedString>>) {
3453 self.tmpfs = Some(values);
3454 self.tmpfs_origins.clear();
3455 }
3456
3457 pub fn set_tmpfs_with_origins(&mut self, values: Vec<Sourced<ProtectedString>>, origins: Vec<Provenance>) {
3459 self.tmpfs = Some(values);
3460 self.tmpfs_origins = origins;
3461 }
3462
3463 #[must_use]
3465 pub fn tmpfs(&self) -> Option<&[Sourced<ProtectedString>]> {
3466 self.tmpfs.as_deref()
3467 }
3468
3469 #[must_use]
3471 pub fn tmpfs_origins(&self) -> &[Provenance] {
3472 &self.tmpfs_origins
3473 }
3474
3475 pub fn set_sysctls(&mut self, values: Vec<Sourced<KernelParameter>>) {
3477 self.sysctls = Some(values);
3478 self.sysctls_origins.clear();
3479 }
3480
3481 pub fn set_sysctls_with_origins(&mut self, values: Vec<Sourced<KernelParameter>>, origins: Vec<Provenance>) {
3483 self.sysctls = Some(values);
3484 self.sysctls_origins = origins;
3485 }
3486
3487 #[must_use]
3489 pub fn sysctls(&self) -> Option<&[Sourced<KernelParameter>]> {
3490 self.sysctls.as_deref()
3491 }
3492
3493 #[must_use]
3495 pub fn sysctls_origins(&self) -> &[Provenance] {
3496 &self.sysctls_origins
3497 }
3498
3499 pub fn set_ulimits(&mut self, values: Vec<Sourced<ResourceLimit>>) {
3501 self.ulimits = Some(values);
3502 self.ulimits_origins.clear();
3503 }
3504
3505 pub fn set_ulimits_with_origins(&mut self, values: Vec<Sourced<ResourceLimit>>, origins: Vec<Provenance>) {
3507 self.ulimits = Some(values);
3508 self.ulimits_origins = origins;
3509 }
3510
3511 #[must_use]
3513 pub fn ulimits(&self) -> Option<&[Sourced<ResourceLimit>]> {
3514 self.ulimits.as_deref()
3515 }
3516
3517 #[must_use]
3519 pub fn ulimits_origins(&self) -> &[Provenance] {
3520 &self.ulimits_origins
3521 }
3522
3523 pub fn set_devices(&mut self, values: Vec<Sourced<Device>>) {
3525 self.devices = Some(values);
3526 self.devices_origins.clear();
3527 }
3528
3529 pub fn set_devices_with_origins(&mut self, values: Vec<Sourced<Device>>, origins: Vec<Provenance>) {
3531 self.devices = Some(values);
3532 self.devices_origins = origins;
3533 }
3534
3535 #[must_use]
3537 pub fn devices(&self) -> Option<&[Sourced<Device>]> {
3538 self.devices.as_deref()
3539 }
3540
3541 #[must_use]
3543 pub fn devices_origins(&self) -> &[Provenance] {
3544 &self.devices_origins
3545 }
3546
3547 pub fn set_stop_signal(&mut self, signal: Sourced<ProtectedString>) {
3549 self.stop_signal = Some(signal);
3550 }
3551
3552 #[must_use]
3554 pub const fn stop_signal(&self) -> Option<&Sourced<ProtectedString>> {
3555 self.stop_signal.as_ref()
3556 }
3557
3558 pub fn add_environment(&mut self, value: Sourced<EnvironmentVariable>) {
3560 self.environment.push(value);
3561 }
3562
3563 #[must_use]
3565 pub fn environment(&self) -> &[Sourced<EnvironmentVariable>] {
3566 &self.environment
3567 }
3568
3569 pub fn add_environment_file(&mut self, value: Sourced<EnvironmentFile>) {
3571 self.environment_files.push(value);
3572 }
3573
3574 #[must_use]
3576 pub fn environment_files(&self) -> &[Sourced<EnvironmentFile>] {
3577 &self.environment_files
3578 }
3579
3580 pub fn add_host_mapping(&mut self, value: Sourced<HostMapping>) {
3582 self.host_mappings.push(value);
3583 }
3584
3585 #[must_use]
3587 pub fn host_mappings(&self) -> &[Sourced<HostMapping>] {
3588 &self.host_mappings
3589 }
3590
3591 pub fn add_port(&mut self, value: Sourced<Port>) {
3593 self.ports.push(value);
3594 }
3595
3596 #[must_use]
3598 pub fn ports(&self) -> &[Sourced<Port>] {
3599 &self.ports
3600 }
3601
3602 pub fn add_mount(&mut self, value: Sourced<Mount>) {
3604 self.mounts.push(value);
3605 }
3606
3607 #[must_use]
3609 pub fn mounts(&self) -> &[Sourced<Mount>] {
3610 &self.mounts
3611 }
3612
3613 pub fn add_config_grant(&mut self, value: Sourced<ResourceGrant>) {
3615 self.config_grants.push(value);
3616 }
3617
3618 #[must_use]
3620 pub fn config_grants(&self) -> &[Sourced<ResourceGrant>] {
3621 &self.config_grants
3622 }
3623
3624 pub fn add_secret_grant(&mut self, value: Sourced<ResourceGrant>) {
3626 self.secret_grants.push(value);
3627 }
3628
3629 #[must_use]
3631 pub fn secret_grants(&self) -> &[Sourced<ResourceGrant>] {
3632 &self.secret_grants
3633 }
3634
3635 pub fn add_network(&mut self, value: Sourced<NetworkAttachment>) {
3637 self.networks.push(value);
3638 }
3639
3640 pub fn replace_network(
3650 &mut self,
3651 index: usize,
3652 value: Sourced<NetworkAttachment>,
3653 ) -> Result<Sourced<NetworkAttachment>, ModelError> {
3654 let len = self.networks.len();
3655 let Some(slot) = self.networks.get_mut(index) else {
3656 return Err(ModelError::UnknownNetworkAttachmentIndex { index, len });
3657 };
3658 Ok(std::mem::replace(slot, value))
3659 }
3660
3661 #[must_use]
3663 pub fn networks(&self) -> &[Sourced<NetworkAttachment>] {
3664 &self.networks
3665 }
3666
3667 pub fn add_dependency(&mut self, value: Sourced<ServiceDependency>) {
3669 self.dependencies.push(value);
3670 }
3671
3672 #[must_use]
3674 pub fn dependencies(&self) -> &[Sourced<ServiceDependency>] {
3675 &self.dependencies
3676 }
3677
3678 pub fn validate_image_source_exclusivity(&self) -> Result<(), ModelError> {
3687 if self.rootfs.is_some() {
3688 self.ensure_rootfs_is_compatible()?;
3689 }
3690 Ok(())
3691 }
3692
3693 fn ensure_rootfs_is_compatible(&self) -> Result<(), ModelError> {
3694 let source = if self.image.is_some() {
3695 Some("image")
3696 } else if self.image_acquisition.is_some() {
3697 Some("image acquisition")
3698 } else if self.image_build.is_some() {
3699 Some("image build")
3700 } else {
3701 None
3702 };
3703 if let Some(source) = source {
3704 return Err(ModelError::RootfsImageSourceConflict {
3705 service: self.name.as_str().to_owned(),
3706 source,
3707 });
3708 }
3709 Ok(())
3710 }
3711}
3712
3713#[derive(Clone, Debug, Eq, PartialEq)]
3715pub struct Application {
3716 name: Identifier,
3717 retained_native_evidence: Vec<RetainedNativeEvidence>,
3718 vendor_metadata: Vec<crate::VendorMetadata>,
3719 image_acquisitions: Vec<Sourced<ImageAcquisition>>,
3720 image_builds: Vec<Sourced<ImageBuild>>,
3721 services: Vec<Sourced<Service>>,
3722 service_groups: Vec<Sourced<ServiceGroup>>,
3723 volumes: Vec<Sourced<Volume>>,
3724 networks: Vec<Sourced<Network>>,
3725 configs: Vec<Sourced<Config>>,
3726 secrets: Vec<Sourced<Secret>>,
3727}
3728
3729impl Application {
3730 #[must_use]
3732 pub const fn new(name: Identifier) -> Self {
3733 Self {
3734 name,
3735 retained_native_evidence: Vec::new(),
3736 vendor_metadata: Vec::new(),
3737 image_acquisitions: Vec::new(),
3738 image_builds: Vec::new(),
3739 services: Vec::new(),
3740 service_groups: Vec::new(),
3741 volumes: Vec::new(),
3742 networks: Vec::new(),
3743 configs: Vec::new(),
3744 secrets: Vec::new(),
3745 }
3746 }
3747
3748 #[must_use]
3750 pub const fn name(&self) -> &Identifier {
3751 &self.name
3752 }
3753
3754 #[must_use]
3756 pub fn vendor_metadata(&self) -> &[crate::VendorMetadata] {
3757 &self.vendor_metadata
3758 }
3759
3760 pub fn add_vendor_metadata(&mut self, metadata: crate::VendorMetadata) -> Result<(), crate::MetadataError> {
3765 if let crate::MetadataScope::Service(owner) = metadata.scope()
3766 && !self.services.iter().any(|service| service.value().name() == owner)
3767 {
3768 return Err(crate::MetadataError::UnknownOwner);
3769 }
3770 if self.vendor_metadata.iter().any(|existing| {
3771 existing.scope() == metadata.scope()
3772 && existing.namespace().value().expose() == metadata.namespace().value().expose()
3773 }) {
3774 return Err(crate::MetadataError::Duplicate);
3775 }
3776 let bytes = self.vendor_metadata.iter().try_fold(metadata.measure()?, |sum, item| {
3777 sum.checked_add(item.measure()?).ok_or(crate::MetadataError::Limit)
3778 })?;
3779 if bytes > crate::METADATA_MAX_BYTES {
3780 return Err(crate::MetadataError::Limit);
3781 }
3782 self.vendor_metadata.push(metadata);
3783 Ok(())
3784 }
3785
3786 pub fn add_retained_native_evidence(&mut self, evidence: RetainedNativeEvidence) -> Result<(), ModelError> {
3793 let (kind, name) = evidence.subject.owner();
3794 let present = match kind {
3795 "service" => self.services.iter().any(|entry| entry.value().name() == name),
3796 "volume" => self.volumes.iter().any(|entry| entry.value().name() == name),
3797 _ => false,
3798 };
3799 if !present {
3800 return Err(ModelError::UnknownNativeEvidenceOwner {
3801 kind,
3802 name: name.as_str().to_owned(),
3803 });
3804 }
3805 self.retained_native_evidence.push(evidence);
3806 Ok(())
3807 }
3808
3809 #[must_use]
3811 pub fn retained_native_evidence(&self) -> &[RetainedNativeEvidence] {
3812 &self.retained_native_evidence
3813 }
3814
3815 pub fn add_image_acquisition(&mut self, acquisition: Sourced<ImageAcquisition>) -> Result<(), ModelError> {
3821 ensure_unique(
3822 "image acquisition",
3823 acquisition.value().name(),
3824 self.image_acquisitions.iter().map(|candidate| candidate.value().name()),
3825 )?;
3826 self.image_acquisitions.push(acquisition);
3827 Ok(())
3828 }
3829
3830 #[must_use]
3832 pub fn image_acquisitions(&self) -> &[Sourced<ImageAcquisition>] {
3833 &self.image_acquisitions
3834 }
3835
3836 pub fn add_image_build(&mut self, build: Sourced<ImageBuild>) -> Result<(), ModelError> {
3842 ensure_unique(
3843 "image build",
3844 build.value().name(),
3845 self.image_builds.iter().map(|candidate| candidate.value().name()),
3846 )?;
3847 self.image_builds.push(build);
3848 Ok(())
3849 }
3850
3851 #[must_use]
3853 pub fn image_builds(&self) -> &[Sourced<ImageBuild>] {
3854 &self.image_builds
3855 }
3856
3857 pub fn validate_image_artifact_references(&self) -> Result<(), ModelError> {
3867 for service in &self.services {
3868 if let Some(acquisition) = service.value().image_acquisition()
3869 && !self.contains_image_acquisition(acquisition.value())
3870 {
3871 return Err(ModelError::UnknownImageAcquisitionReference {
3872 service: service.value().name().as_str().to_owned(),
3873 acquisition: acquisition.value().as_str().to_owned(),
3874 });
3875 }
3876 if let Some(build) = service.value().image_build()
3877 && !self.contains_image_build(build.value())
3878 {
3879 return Err(ModelError::UnknownImageBuildReference {
3880 service: service.value().name().as_str().to_owned(),
3881 build: build.value().as_str().to_owned(),
3882 });
3883 }
3884 }
3885 for volume in &self.volumes {
3886 let Some(source) = volume.value().image_source() else {
3887 continue;
3888 };
3889 match source.value() {
3890 VolumeImageSource::Literal(_) => {}
3891 VolumeImageSource::ImageAcquisition(acquisition) => {
3892 if !self.contains_image_acquisition(acquisition) {
3893 return Err(ModelError::UnknownVolumeImageAcquisitionReference {
3894 volume: volume.value().name().as_str().to_owned(),
3895 acquisition: acquisition.as_str().to_owned(),
3896 });
3897 }
3898 }
3899 VolumeImageSource::ImageBuild(build) => {
3900 if !self.contains_image_build(build) {
3901 return Err(ModelError::UnknownVolumeImageBuildReference {
3902 volume: volume.value().name().as_str().to_owned(),
3903 build: build.as_str().to_owned(),
3904 });
3905 }
3906 }
3907 }
3908 }
3909 Ok(())
3910 }
3911
3912 pub fn validate_image_artifact_dependencies(
3923 &self,
3924 dependencies: &[Sourced<ArtifactDependency>],
3925 ) -> Result<(), ModelError> {
3926 self.validate_image_artifact_references()?;
3927
3928 let mut graph = BTreeMap::<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>::new();
3929 for dependency in dependencies {
3930 let source = dependency.value().source().value();
3931 let target = dependency.value().target().value();
3932 self.validate_artifact_dependency_node(source)?;
3933 self.validate_artifact_dependency_node(target)?;
3934 graph.entry(source.clone()).or_default().insert(target.clone());
3935 graph.entry(target.clone()).or_default();
3936 }
3937
3938 let mut state = BTreeMap::<ArtifactDependencyNode, VisitState>::new();
3939 let mut path = Vec::new();
3940 for node in graph.keys() {
3941 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
3942 continue;
3943 }
3944 if let Some(cycle) = detect_artifact_cycle(node, &graph, &mut state, &mut path) {
3945 return Err(ModelError::ImageArtifactDependencyCycle {
3946 nodes: cycle.into_iter().map(|node| node.display_name()).collect(),
3947 });
3948 }
3949 }
3950 Ok(())
3951 }
3952
3953 fn contains_image_acquisition(&self, name: &Identifier) -> bool {
3954 self.image_acquisitions
3955 .iter()
3956 .any(|candidate| candidate.value().name() == name)
3957 }
3958
3959 fn contains_image_build(&self, name: &Identifier) -> bool {
3960 self.image_builds
3961 .iter()
3962 .any(|candidate| candidate.value().name() == name)
3963 }
3964
3965 fn validate_artifact_dependency_node(&self, node: &ArtifactDependencyNode) -> Result<(), ModelError> {
3966 let (kind, name) = node.kind_and_name();
3967 let exists = match node {
3968 ArtifactDependencyNode::Volume(_) => self.volumes.iter().any(|volume| volume.value().name() == name),
3969 ArtifactDependencyNode::ImageAcquisition(_) => self.contains_image_acquisition(name),
3970 ArtifactDependencyNode::ImageBuild(_) => self.contains_image_build(name),
3971 };
3972 if exists {
3973 Ok(())
3974 } else {
3975 Err(ModelError::UnknownArtifactDependencyNode {
3976 kind,
3977 name: name.as_str().to_owned(),
3978 })
3979 }
3980 }
3981
3982 pub fn add_service(&mut self, service: Sourced<Service>) -> Result<(), ModelError> {
3990 ensure_unique(
3991 "service",
3992 service.value().name(),
3993 self.services.iter().map(|candidate| candidate.value().name()),
3994 )?;
3995 service.value().validate_image_source_exclusivity()?;
3996 crate::deferred::validate_service(service.value())?;
3997 if let Some(acquisition) = service.value().image_acquisition()
3998 && !self
3999 .image_acquisitions
4000 .iter()
4001 .any(|candidate| candidate.value().name() == acquisition.value())
4002 {
4003 return Err(ModelError::UnknownImageAcquisitionReference {
4004 service: service.value().name().as_str().to_owned(),
4005 acquisition: acquisition.value().as_str().to_owned(),
4006 });
4007 }
4008 if let Some(build) = service.value().image_build()
4009 && !self
4010 .image_builds
4011 .iter()
4012 .any(|candidate| candidate.value().name() == build.value())
4013 {
4014 return Err(ModelError::UnknownImageBuildReference {
4015 service: service.value().name().as_str().to_owned(),
4016 build: build.value().as_str().to_owned(),
4017 });
4018 }
4019 self.services.push(service);
4020 Ok(())
4021 }
4022
4023 #[must_use]
4025 pub fn services(&self) -> &[Sourced<Service>] {
4026 &self.services
4027 }
4028
4029 pub fn withhold_literal_environment_values(&mut self) {
4034 for service in &mut self.services {
4035 for environment in &mut service.value_mut().environment {
4036 if matches!(
4037 environment.value().value(),
4038 EnvironmentValue::Literal(_) | EnvironmentValue::UnresolvedSource { .. }
4039 ) {
4040 environment.value_mut().value = EnvironmentValue::Required;
4041 }
4042 }
4043 }
4044 }
4045
4046 pub fn add_service_group(&mut self, group: Sourced<ServiceGroup>) -> Result<(), ModelError> {
4056 ensure_unique(
4057 "service group",
4058 group.value().name(),
4059 self.service_groups.iter().map(|candidate| candidate.value().name()),
4060 )?;
4061 for member in group.value().members() {
4062 if !self
4063 .services
4064 .iter()
4065 .any(|service| service.value().name() == member.value())
4066 {
4067 return Err(ModelError::UnknownServiceGroupMember {
4068 group: group.value().name().as_str().to_owned(),
4069 service: member.value().as_str().to_owned(),
4070 });
4071 }
4072 if let Some(existing) = self.service_groups.iter().find(|candidate| {
4073 candidate
4074 .value()
4075 .members()
4076 .iter()
4077 .any(|candidate_member| candidate_member.value() == member.value())
4078 }) {
4079 return Err(ModelError::ServiceInMultipleGroups {
4080 service: member.value().as_str().to_owned(),
4081 existing: existing.value().name().as_str().to_owned(),
4082 replacement: group.value().name().as_str().to_owned(),
4083 });
4084 }
4085 }
4086 self.service_groups.push(group);
4087 Ok(())
4088 }
4089
4090 #[must_use]
4092 pub fn service_groups(&self) -> &[Sourced<ServiceGroup>] {
4093 &self.service_groups
4094 }
4095
4096 pub fn add_volume(&mut self, volume: Sourced<Volume>) -> Result<(), ModelError> {
4102 ensure_unique(
4103 "volume",
4104 volume.value().name(),
4105 self.volumes.iter().map(|candidate| candidate.value().name()),
4106 )?;
4107 self.volumes.push(volume);
4108 Ok(())
4109 }
4110
4111 #[must_use]
4113 pub fn volumes(&self) -> &[Sourced<Volume>] {
4114 &self.volumes
4115 }
4116
4117 pub fn add_network(&mut self, network: Sourced<Network>) -> Result<(), ModelError> {
4123 ensure_unique(
4124 "network",
4125 network.value().name(),
4126 self.networks.iter().map(|candidate| candidate.value().name()),
4127 )?;
4128 self.networks.push(network);
4129 Ok(())
4130 }
4131
4132 #[must_use]
4134 pub fn networks(&self) -> &[Sourced<Network>] {
4135 &self.networks
4136 }
4137
4138 pub fn add_config(&mut self, config: Sourced<Config>) -> Result<(), ModelError> {
4144 ensure_unique(
4145 "config",
4146 config.value().name(),
4147 self.configs.iter().map(|candidate| candidate.value().name()),
4148 )?;
4149 self.configs.push(config);
4150 Ok(())
4151 }
4152
4153 #[must_use]
4155 pub fn configs(&self) -> &[Sourced<Config>] {
4156 &self.configs
4157 }
4158
4159 pub fn add_secret(&mut self, secret: Sourced<Secret>) -> Result<(), ModelError> {
4165 ensure_unique(
4166 "secret",
4167 secret.value().name(),
4168 self.secrets.iter().map(|candidate| candidate.value().name()),
4169 )?;
4170 self.secrets.push(secret);
4171 Ok(())
4172 }
4173
4174 #[must_use]
4176 pub fn secrets(&self) -> &[Sourced<Secret>] {
4177 &self.secrets
4178 }
4179}
4180
4181#[derive(Clone, Copy, Eq, PartialEq)]
4182enum VisitState {
4183 Visiting,
4184 Finished,
4185}
4186
4187fn detect_artifact_cycle(
4188 node: &ArtifactDependencyNode,
4189 graph: &BTreeMap<ArtifactDependencyNode, BTreeSet<ArtifactDependencyNode>>,
4190 state: &mut BTreeMap<ArtifactDependencyNode, VisitState>,
4191 path: &mut Vec<ArtifactDependencyNode>,
4192) -> Option<Vec<ArtifactDependencyNode>> {
4193 if state.get(node).is_some_and(|state| *state == VisitState::Visiting) {
4194 let index = path.iter().position(|candidate| candidate == node)?;
4195 let mut cycle = path[index..].to_vec();
4196 cycle.push(node.clone());
4197 return Some(cycle);
4198 }
4199 if state.get(node).is_some_and(|state| *state == VisitState::Finished) {
4200 return None;
4201 }
4202
4203 state.insert(node.clone(), VisitState::Visiting);
4204 path.push(node.clone());
4205 if let Some(targets) = graph.get(node) {
4206 for target in targets {
4207 if let Some(cycle) = detect_artifact_cycle(target, graph, state, path) {
4208 return Some(cycle);
4209 }
4210 }
4211 }
4212 path.pop();
4213 state.insert(node.clone(), VisitState::Finished);
4214 None
4215}
4216
4217fn ensure_unique<'a>(
4218 kind: &'static str,
4219 name: &Identifier,
4220 existing: impl Iterator<Item = &'a Identifier>,
4221) -> Result<(), ModelError> {
4222 if existing.into_iter().any(|candidate| candidate == name) {
4223 return Err(ModelError::DuplicateResource {
4224 kind,
4225 name: name.as_str().to_owned(),
4226 });
4227 }
4228 Ok(())
4229}
4230
4231fn validate_text(kind: &'static str, value: &str) -> Result<(), ModelError> {
4232 if value.is_empty() {
4233 return Err(ModelError::EmptyValue(kind));
4234 }
4235 validate_no_nul(kind, value)
4236}
4237
4238fn validate_no_nul(kind: &'static str, value: &str) -> Result<(), ModelError> {
4239 if value.contains('\0') {
4240 return Err(ModelError::ContainsNul(kind));
4241 }
4242 Ok(())
4243}
4244
4245#[cfg(test)]
4246mod tests {
4247 use super::{
4248 Annotation, Application, ArtifactDependency, ArtifactDependencyNode, Command, Config, ConfigMaterial, Device,
4249 Entrypoint, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileSyntax, EnvironmentValue,
4250 EnvironmentVariable, ExposedPort, GroupExitPolicy, HealthcheckDuration, HealthcheckRetries, HostAddress,
4251 HostAddressKind, HostMapping, Identifier, KernelParameter, Logging, LoggingOption, MetadataLabel, ModelError,
4252 Mount, MountSource, Network, NetworkAttachment, NetworkDriverOption, NetworkIpamConfig, Protocol, PullPolicy,
4253 ReloadAction, ResourceGrant, ResourceGrantSyntax, ResourceLimit, ResourceOwnership, RestartPolicy, Secret,
4254 SecretMaterial, SecurityOption, Service, ServiceDependency, ServiceDependencyCondition, ServiceGroup,
4255 ServiceGroupRuntime, StartupNotification, StopTimeout, Volume, VolumeImageSource,
4256 };
4257 use crate::{ImageAcquisition, ImageBuild, ImageReference, ProtectedString, Sourced};
4258
4259 #[test]
4260 fn withholding_environment_values_keeps_names_and_provenance_without_value_bytes() -> Result<(), String> {
4261 let mut application = Application::new(id("application")?);
4262 let mut service = Service::new(id("web")?);
4263 let origin =
4264 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
4265 service.add_environment(Sourced::from_source(
4266 EnvironmentVariable::new(
4267 id("MODE")?,
4268 EnvironmentValue::Literal(ProtectedString::sensitive("benign-name-private-canary")),
4269 ),
4270 origin.clone(),
4271 ));
4272 service.add_environment(Sourced::generated(EnvironmentVariable::new(
4273 id("FROM_HOST")?,
4274 EnvironmentValue::Host,
4275 )));
4276 application
4277 .add_service(Sourced::generated(service))
4278 .map_err(|error| error.to_string())?;
4279
4280 application.withhold_literal_environment_values();
4281 let environment = application.services()[0].value().environment();
4282 assert_eq!(environment[0].value().name().as_str(), "MODE");
4283 assert_eq!(environment[0].origins(), &[origin]);
4284 assert!(matches!(environment[0].value().value(), EnvironmentValue::Required));
4285 assert!(matches!(environment[1].value().value(), EnvironmentValue::Host));
4286 assert!(!format!("{application:?}").contains("benign-name-private-canary"));
4287 Ok(())
4288 }
4289
4290 #[test]
4291 fn preserves_service_order_and_rejects_duplicate_names() -> Result<(), String> {
4292 let mut application = Application::new(id("example")?);
4293 application
4294 .add_service(Sourced::generated(Service::new(id("web")?)))
4295 .map_err(|error| error.to_string())?;
4296 application
4297 .add_service(Sourced::generated(Service::new(id("database")?)))
4298 .map_err(|error| error.to_string())?;
4299
4300 let names: Vec<_> = application
4301 .services()
4302 .iter()
4303 .map(|service| service.value().name().as_str())
4304 .collect();
4305 assert_eq!(names, ["web", "database"]);
4306
4307 let duplicate = application.add_service(Sourced::generated(Service::new(id("web")?)));
4308 assert!(matches!(duplicate, Err(ModelError::DuplicateResource { .. })));
4309 Ok(())
4310 }
4311
4312 #[test]
4313 fn keeps_the_service_key_and_explicit_runtime_name_distinct() -> Result<(), String> {
4314 let mut service = Service::new(id("web")?);
4315 service.set_runtime_name(Sourced::generated(ProtectedString::plain("production-web")));
4316
4317 assert_eq!(service.name().as_str(), "web");
4318 assert_eq!(
4319 service.runtime_name().map(|name| name.value().expose()),
4320 Some("production-web")
4321 );
4322 Ok(())
4323 }
4324
4325 #[test]
4326 fn network_keeps_logical_and_runtime_names_and_literal_flags_distinct() -> Result<(), String> {
4327 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4328 let origin = crate::Provenance::source(source);
4329 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4330 network.set_runtime_name(Sourced::from_source(
4331 ProtectedString::plain("production-frontend"),
4332 origin.clone(),
4333 ));
4334 network.set_driver(Sourced::from_source(ProtectedString::plain("bridge"), origin.clone()));
4335 network.set_internal(Sourced::from_source(true, origin.clone()));
4336 network.set_ipv6(Sourced::from_source(false, origin.clone()));
4337 network.set_ipam_driver(Sourced::from_source(ProtectedString::plain("default"), origin));
4338
4339 assert_eq!(network.name().as_str(), "frontend");
4340 assert_eq!(
4341 network.runtime_name().map(|value| value.value().expose()),
4342 Some("production-frontend")
4343 );
4344 assert_eq!(network.driver().map(|value| value.value().expose()), Some("bridge"));
4345 assert_eq!(network.internal().map(Sourced::value), Some(&true));
4346 assert_eq!(network.ipv6().map(Sourced::value), Some(&false));
4347 assert_eq!(
4348 network.ipam_driver().map(|value| value.value().expose()),
4349 Some("default")
4350 );
4351 Ok(())
4352 }
4353
4354 #[test]
4355 fn network_collections_retain_resets_provenance_and_redact_protected_values() -> Result<(), String> {
4356 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4357 let origin = crate::Provenance::source(source);
4358 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4359 let option = NetworkDriverOption::new(
4360 Sourced::from_source(id("com.example.token")?, origin.clone()),
4361 Sourced::from_source(ProtectedString::sensitive("never-print-this"), origin.clone()),
4362 )
4363 .map_err(|error| error.to_string())?;
4364 let label = MetadataLabel::new(id("com.example.label")?, ProtectedString::sensitive("also-private"));
4365
4366 network
4367 .set_driver_options_with_origins(vec![Sourced::from_source(option, origin.clone())], vec![origin.clone()]);
4368 network.set_labels_with_origins(vec![Sourced::from_source(label, origin.clone())], vec![origin.clone()]);
4369 network.set_ipam_configs_with_origins(Vec::new(), vec![origin]);
4370
4371 assert_eq!(network.driver_options().map(<[_]>::len), Some(1));
4372 assert_eq!(network.labels().map(<[_]>::len), Some(1));
4373 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4374 assert_eq!(network.driver_options_origins().len(), 1);
4375 assert_eq!(network.labels_origins().len(), 1);
4376 assert_eq!(network.ipam_configs_origins().len(), 1);
4377 let debug = format!("{network:?}");
4378 assert!(!debug.contains("never-print-this"));
4379 assert!(!debug.contains("also-private"));
4380 assert!(debug.contains("[REDACTED]"));
4381
4382 network.set_driver_options(Vec::new());
4383 network.set_labels(Vec::new());
4384 network.set_ipam_configs(Vec::new());
4385 assert_eq!(network.driver_options().map(<[_]>::len), Some(0));
4386 assert_eq!(network.labels().map(<[_]>::len), Some(0));
4387 assert_eq!(network.ipam_configs().map(<[_]>::len), Some(0));
4388 assert!(network.driver_options_origins().is_empty());
4389 assert!(network.labels_origins().is_empty());
4390 assert!(network.ipam_configs_origins().is_empty());
4391 Ok(())
4392 }
4393
4394 #[test]
4395 fn network_ipam_rows_preserve_association_order_and_reject_subnetless_values() -> Result<(), String> {
4396 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4397 let origin = crate::Provenance::source(source);
4398 let mut first = NetworkIpamConfig::new(Sourced::from_source(
4399 ProtectedString::plain("10.10.0.0/24"),
4400 origin.clone(),
4401 ))
4402 .map_err(|error| error.to_string())?;
4403 first
4404 .set_gateway(Sourced::from_source(
4405 ProtectedString::plain("10.10.0.1"),
4406 origin.clone(),
4407 ))
4408 .map_err(|error| error.to_string())?;
4409 let mut second = NetworkIpamConfig::new(Sourced::from_source(
4410 ProtectedString::plain("fd00:10::/64"),
4411 origin.clone(),
4412 ))
4413 .map_err(|error| error.to_string())?;
4414 second
4415 .set_ip_range(Sourced::from_source(
4416 ProtectedString::plain("fd00:10::100/120"),
4417 origin.clone(),
4418 ))
4419 .map_err(|error| error.to_string())?;
4420
4421 let mut network = Network::new(id("frontend")?, ResourceOwnership::Application);
4422 network.set_ipam_configs_with_origins(
4423 vec![
4424 Sourced::from_source(first, origin.clone()),
4425 Sourced::from_source(second, origin),
4426 ],
4427 Vec::new(),
4428 );
4429 let rows = network
4430 .ipam_configs()
4431 .ok_or_else(|| "IPAM configs were omitted".to_owned())?;
4432 assert_eq!(rows.len(), 2);
4433 assert_eq!(rows[0].value().subnet().value().expose(), "10.10.0.0/24");
4434 assert_eq!(
4435 rows[0].value().gateway().map(|value| value.value().expose()),
4436 Some("10.10.0.1")
4437 );
4438 assert_eq!(rows[0].value().ip_range(), None);
4439 assert_eq!(rows[1].value().subnet().value().expose(), "fd00:10::/64");
4440 assert_eq!(rows[1].value().gateway(), None);
4441 assert_eq!(
4442 rows[1].value().ip_range().map(|value| value.value().expose()),
4443 Some("fd00:10::100/120")
4444 );
4445
4446 assert!(matches!(
4447 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain(""))),
4448 Err(ModelError::EmptyValue("network IPAM subnet"))
4449 ));
4450 assert!(matches!(
4451 NetworkIpamConfig::new(Sourced::generated(ProtectedString::plain("10.0.0.0/24\0bad"))),
4452 Err(ModelError::ContainsNul("network IPAM subnet"))
4453 ));
4454 assert!(matches!(
4455 NetworkDriverOption::new(
4456 Sourced::generated(id("option")?),
4457 Sourced::generated(ProtectedString::plain("bad\0value")),
4458 ),
4459 Err(ModelError::ContainsNul("network driver option value"))
4460 ));
4461 Ok(())
4462 }
4463
4464 #[test]
4465 fn image_artifact_resources_are_ordered_unique_and_referenced_explicitly() -> Result<(), String> {
4466 let mut application = Application::new(id("example")?);
4467 application
4468 .add_image_acquisition(Sourced::generated(ImageAcquisition::new(id("base-image")?)))
4469 .map_err(|error| error.to_string())?;
4470 application
4471 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4472 .map_err(|error| error.to_string())?;
4473
4474 let mut web = Service::new(id("web")?);
4475 web.set_image_acquisition(Sourced::generated(id("base-image")?));
4476 web.set_image_build(Sourced::generated(id("web-build")?));
4477 application
4478 .add_service(Sourced::generated(web))
4479 .map_err(|error| error.to_string())?;
4480
4481 assert_eq!(
4482 application.image_acquisitions()[0].value().name().as_str(),
4483 "base-image"
4484 );
4485 assert_eq!(application.image_builds()[0].value().name().as_str(), "web-build");
4486 assert!(matches!(
4487 application.add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?))),
4488 Err(ModelError::DuplicateResource {
4489 kind: "image build",
4490 ..
4491 })
4492 ));
4493
4494 let mut missing = Service::new(id("missing")?);
4495 missing.set_image_build(Sourced::generated(id("absent-build")?));
4496 assert!(matches!(
4497 application.add_service(Sourced::generated(missing)),
4498 Err(ModelError::UnknownImageBuildReference { .. })
4499 ));
4500 Ok(())
4501 }
4502
4503 #[test]
4504 fn volume_keeps_logical_runtime_and_service_names_and_local_fields_distinct() -> Result<(), String> {
4505 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4506 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4507 volume.set_runtime_name(Sourced::from_source(
4508 ProtectedString::plain("production-data"),
4509 origin.clone(),
4510 ));
4511 volume.set_service_name(Sourced::from_source(
4512 ProtectedString::plain("data-volume.service"),
4513 origin.clone(),
4514 ));
4515 volume.set_driver(Sourced::from_source(ProtectedString::plain("local"), origin.clone()));
4516 volume.set_device(Sourced::from_source(
4517 ProtectedString::plain("/srv/data"),
4518 origin.clone(),
4519 ));
4520 volume.set_volume_type(Sourced::from_source(ProtectedString::plain("none"), origin.clone()));
4521 volume.set_options(Sourced::from_source(ProtectedString::plain("bind"), origin.clone()));
4522
4523 assert_eq!(volume.name().as_str(), "data");
4524 assert_eq!(
4525 volume.runtime_name().map(|name| name.value().expose()),
4526 Some("production-data")
4527 );
4528 assert_eq!(
4529 volume.service_name().map(|name| name.value().expose()),
4530 Some("data-volume.service")
4531 );
4532 assert_eq!(volume.driver().map(|value| value.value().expose()), Some("local"));
4533 assert_eq!(volume.device().map(|value| value.value().expose()), Some("/srv/data"));
4534 assert_eq!(volume.volume_type().map(|value| value.value().expose()), Some("none"));
4535 assert_eq!(volume.options().map(|value| value.value().expose()), Some("bind"));
4536 assert_eq!(
4537 volume.options().map(Sourced::origins),
4538 Some(std::slice::from_ref(&origin))
4539 );
4540 Ok(())
4541 }
4542
4543 #[test]
4544 fn volume_preserves_resets_order_protected_values_and_identity_dimensions() -> Result<(), String> {
4545 let origin = crate::Provenance::source(crate::SourceId::new("data.volume").map_err(|error| error.to_string())?);
4546 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4547 assert!(volume.labels().is_none());
4548 volume.set_labels_with_origins(Vec::new(), vec![origin.clone()]);
4549 volume.set_user(Sourced::from_source(
4550 ProtectedString::plain("named-user"),
4551 origin.clone(),
4552 ));
4553 volume.set_group(Sourced::from_source(
4554 ProtectedString::plain("named-group"),
4555 origin.clone(),
4556 ));
4557 volume.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
4558 volume.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin));
4559
4560 assert_eq!(volume.labels().map(<[_]>::len), Some(0));
4561 assert_eq!(volume.user().map(|value| value.value().expose()), Some("named-user"));
4562 assert_eq!(volume.group().map(|value| value.value().expose()), Some("named-group"));
4563 assert_eq!(volume.uid().map(|value| value.value().expose()), Some("1001"));
4564 assert_eq!(volume.gid().map(|value| value.value().expose()), Some("1002"));
4565 Ok(())
4566 }
4567
4568 #[test]
4569 fn volume_copy_and_image_sources_preserve_absence_and_typed_distinctions() -> Result<(), String> {
4570 let origin =
4571 crate::Provenance::source(crate::SourceId::new("cache.volume").map_err(|error| error.to_string())?);
4572 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4573 assert_eq!(volume.copy(), None);
4574 volume.set_copy(Sourced::from_source(false, origin.clone()));
4575 assert_eq!(volume.copy().map(Sourced::value), Some(&false));
4576 volume.set_copy(Sourced::from_source(true, origin.clone()));
4577 assert_eq!(volume.copy().map(Sourced::value), Some(&true));
4578
4579 volume
4580 .set_image_source(Sourced::from_source(
4581 VolumeImageSource::Literal(ProtectedString::sensitive("registry.example/private:1")),
4582 origin.clone(),
4583 ))
4584 .map_err(|error| error.to_string())?;
4585 assert!(matches!(
4586 volume.image_source().map(Sourced::value),
4587 Some(VolumeImageSource::Literal(_))
4588 ));
4589 assert!(!format!("{volume:?}").contains("registry.example/private:1"));
4590
4591 volume
4592 .set_image_source(Sourced::from_source(
4593 VolumeImageSource::ImageAcquisition(id("cache-image")?),
4594 origin.clone(),
4595 ))
4596 .map_err(|error| error.to_string())?;
4597 assert!(matches!(
4598 volume.image_source().map(Sourced::value),
4599 Some(VolumeImageSource::ImageAcquisition(name)) if name.as_str() == "cache-image"
4600 ));
4601 volume
4602 .set_image_source(Sourced::from_source(
4603 VolumeImageSource::ImageBuild(id("cache-build")?),
4604 origin,
4605 ))
4606 .map_err(|error| error.to_string())?;
4607 assert!(matches!(
4608 volume.image_source().map(Sourced::value),
4609 Some(VolumeImageSource::ImageBuild(name)) if name.as_str() == "cache-build"
4610 ));
4611 Ok(())
4612 }
4613
4614 #[test]
4615 fn volume_artifact_validation_is_deferred_and_explicit_edges_find_cycles() -> Result<(), String> {
4616 let mut application = Application::new(id("example")?);
4617 let mut volume = Volume::new(id("cache")?, ResourceOwnership::Application);
4618 volume
4619 .set_image_source(Sourced::generated(VolumeImageSource::ImageBuild(id("cache-build")?)))
4620 .map_err(|error| error.to_string())?;
4621 application
4622 .add_volume(Sourced::generated(volume))
4623 .map_err(|error| error.to_string())?;
4624 assert!(matches!(
4625 application.validate_image_artifact_references(),
4626 Err(ModelError::UnknownVolumeImageBuildReference { .. })
4627 ));
4628
4629 application
4630 .add_image_build(Sourced::generated(ImageBuild::new(id("cache-build")?)))
4631 .map_err(|error| error.to_string())?;
4632 application
4633 .validate_image_artifact_references()
4634 .map_err(|error| error.to_string())?;
4635
4636 let volume_node = ArtifactDependencyNode::Volume(id("cache")?);
4637 let build_node = ArtifactDependencyNode::ImageBuild(id("cache-build")?);
4638 let dependencies = vec![
4639 Sourced::generated(ArtifactDependency::new(
4640 Sourced::generated(volume_node.clone()),
4641 Sourced::generated(build_node.clone()),
4642 )),
4643 Sourced::generated(ArtifactDependency::new(
4644 Sourced::generated(build_node),
4645 Sourced::generated(volume_node),
4646 )),
4647 ];
4648 assert!(matches!(
4649 application.validate_image_artifact_dependencies(&dependencies),
4650 Err(ModelError::ImageArtifactDependencyCycle { .. })
4651 ));
4652 let missing = vec![Sourced::generated(ArtifactDependency::new(
4653 Sourced::generated(ArtifactDependencyNode::ImageBuild(id("cache-build")?)),
4654 Sourced::generated(ArtifactDependencyNode::Volume(id("missing")?)),
4655 ))];
4656 assert!(matches!(
4657 application.validate_image_artifact_dependencies(&missing),
4658 Err(ModelError::UnknownArtifactDependencyNode { kind: "volume", .. })
4659 ));
4660 Ok(())
4661 }
4662
4663 #[test]
4664 fn volume_rejects_invalid_literal_image_values() -> Result<(), String> {
4665 let mut volume = Volume::new(id("data")?, ResourceOwnership::Application);
4666 assert!(matches!(
4667 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4668 ""
4669 )))),
4670 Err(ModelError::EmptyValue("volume image"))
4671 ));
4672 assert!(matches!(
4673 volume.set_image_source(Sourced::generated(VolumeImageSource::Literal(ProtectedString::plain(
4674 "bad\0image"
4675 )))),
4676 Err(ModelError::ContainsNul("volume image"))
4677 ));
4678 Ok(())
4679 }
4680
4681 #[test]
4682 fn collection_resets_retain_explicit_emptiness_and_clear_stale_origins() -> Result<(), String> {
4683 let origin =
4684 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
4685 let mut service = Service::new(id("web")?);
4686
4687 service.set_cap_add_with_origins(Vec::new(), vec![origin.clone()]);
4688 service.set_cap_drop_with_origins(Vec::new(), vec![origin.clone()]);
4689 service.set_tmpfs_with_origins(Vec::new(), vec![origin.clone()]);
4690 service.set_sysctls_with_origins(Vec::new(), vec![origin.clone()]);
4691 service.set_ulimits_with_origins(Vec::new(), vec![origin.clone()]);
4692 service.set_devices_with_origins(Vec::new(), vec![origin]);
4693
4694 assert_eq!(service.cap_add().map(<[_]>::len), Some(0));
4695 assert_eq!(service.cap_drop().map(<[_]>::len), Some(0));
4696 assert_eq!(service.tmpfs().map(<[_]>::len), Some(0));
4697 assert_eq!(service.sysctls().map(<[_]>::len), Some(0));
4698 assert_eq!(service.ulimits().map(<[_]>::len), Some(0));
4699 assert_eq!(service.devices().map(<[_]>::len), Some(0));
4700 assert_eq!(service.cap_add_origins().len(), 1);
4701 assert_eq!(service.cap_drop_origins().len(), 1);
4702 assert_eq!(service.tmpfs_origins().len(), 1);
4703 assert_eq!(service.sysctls_origins().len(), 1);
4704 assert_eq!(service.ulimits_origins().len(), 1);
4705 assert_eq!(service.devices_origins().len(), 1);
4706
4707 service.set_cap_add(Vec::new());
4708 service.set_cap_drop(Vec::new());
4709 service.set_tmpfs(Vec::new());
4710 service.set_sysctls(Vec::<Sourced<KernelParameter>>::new());
4711 service.set_ulimits(Vec::<Sourced<ResourceLimit>>::new());
4712 service.set_devices(Vec::<Sourced<Device>>::new());
4713
4714 assert!(service.cap_add_origins().is_empty());
4715 assert!(service.cap_drop_origins().is_empty());
4716 assert!(service.tmpfs_origins().is_empty());
4717 assert!(service.sysctls_origins().is_empty());
4718 assert!(service.ulimits_origins().is_empty());
4719 assert!(service.devices_origins().is_empty());
4720 Ok(())
4721 }
4722
4723 #[test]
4724 fn restart_policy_keeps_unlimited_and_finite_on_failure_distinct() {
4725 let finite = std::num::NonZeroU64::new(4);
4726 assert_eq!(RestartPolicy::on_failure(None).maximum_retries(), None);
4727 assert_eq!(RestartPolicy::on_failure(finite).maximum_retries(), finite);
4728 assert_eq!(RestartPolicy::Always.maximum_retries(), None);
4729 }
4730
4731 #[test]
4732 fn metadata_labels_preserve_empty_and_protected_values() -> Result<(), String> {
4733 let empty = MetadataLabel::new(id("com.example.empty")?, ProtectedString::plain(""));
4734 let protected = MetadataLabel::new(id("com.example.token")?, ProtectedString::sensitive("never-print-this"));
4735 let mut service = Service::new(id("web")?);
4736 service.add_label(Sourced::generated(empty));
4737 service.add_label(Sourced::generated(protected));
4738
4739 assert_eq!(service.labels()[0].value().value().expose(), "");
4740 let debug = format!("{:?}", service.labels()[1]);
4741 assert!(!debug.contains("never-print-this"));
4742 assert!(debug.contains("[REDACTED]"));
4743 Ok(())
4744 }
4745
4746 #[test]
4747 fn environment_files_preserve_order_options_provenance_and_redaction() -> Result<(), String> {
4748 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4749 let origin = crate::Provenance::source(source);
4750 let mut service = Service::new(id("web")?);
4751 service.add_environment_file(Sourced::from_source(
4752 EnvironmentFile::new(ProtectedString::plain("./base.env"), EnvironmentFileSyntax::Short)
4753 .map_err(|error| error.to_string())?,
4754 origin.clone(),
4755 ));
4756 let mut local = EnvironmentFile::new(ProtectedString::sensitive("./private.env"), EnvironmentFileSyntax::Long)
4757 .map_err(|error| error.to_string())?;
4758 local.set_required(Sourced::from_source(false, origin.clone()));
4759 local.set_format(Sourced::from_source(EnvironmentFileFormat::Raw, origin.clone()));
4760 service.add_environment_file(Sourced::from_source(local, origin));
4761
4762 assert_eq!(service.environment_files().len(), 2);
4763 assert_eq!(service.environment_files()[0].value().path().expose(), "./base.env");
4764 assert_eq!(
4765 service.environment_files()[0].value().syntax(),
4766 EnvironmentFileSyntax::Short
4767 );
4768 assert!(service.environment_files()[0].value().is_required());
4769 let local = service.environment_files()[1].value();
4770 assert_eq!(local.syntax(), EnvironmentFileSyntax::Long);
4771 assert!(!local.is_required());
4772 assert_eq!(local.required().map_or(0, |value| value.origins().len()), 1);
4773 assert!(matches!(
4774 local.format().map(Sourced::value),
4775 Some(EnvironmentFileFormat::Raw)
4776 ));
4777 let debug = format!("{service:?}");
4778 assert!(!debug.contains("private.env"));
4779 assert!(debug.contains("[REDACTED]"));
4780 assert!(matches!(
4781 EnvironmentFile::new(ProtectedString::plain(""), EnvironmentFileSyntax::Short),
4782 Err(ModelError::EmptyValue("environment-file path"))
4783 ));
4784 Ok(())
4785 }
4786
4787 #[test]
4788 fn service_groups_preserve_order_and_reject_ambiguous_membership() -> Result<(), String> {
4789 let mut application = Application::new(id("example")?);
4790 for name in ["web", "worker"] {
4791 application
4792 .add_service(Sourced::generated(Service::new(id(name)?)))
4793 .map_err(|error| error.to_string())?;
4794 }
4795
4796 let mut frontend = ServiceGroup::new(id("frontend")?, ResourceOwnership::Uncertain);
4797 frontend
4798 .add_member(Sourced::generated(id("web")?))
4799 .map_err(|error| error.to_string())?;
4800 assert!(matches!(
4801 frontend.add_member(Sourced::generated(id("web")?)),
4802 Err(ModelError::DuplicateServiceGroupMember { .. })
4803 ));
4804 application
4805 .add_service_group(Sourced::generated(frontend))
4806 .map_err(|error| error.to_string())?;
4807
4808 assert_eq!(application.service_groups()[0].value().name().as_str(), "frontend");
4809 assert_eq!(
4810 application.service_groups()[0].value().members()[0].value().as_str(),
4811 "web"
4812 );
4813
4814 let mut conflicting = ServiceGroup::new(id("backend")?, ResourceOwnership::Application);
4815 conflicting
4816 .add_member(Sourced::generated(id("web")?))
4817 .map_err(|error| error.to_string())?;
4818 assert!(matches!(
4819 application.add_service_group(Sourced::generated(conflicting)),
4820 Err(ModelError::ServiceInMultipleGroups { .. })
4821 ));
4822
4823 let mut missing = ServiceGroup::new(id("missing")?, ResourceOwnership::External);
4824 missing
4825 .add_member(Sourced::generated(id("database")?))
4826 .map_err(|error| error.to_string())?;
4827 assert!(matches!(
4828 application.add_service_group(Sourced::generated(missing)),
4829 Err(ModelError::UnknownServiceGroupMember { .. })
4830 ));
4831 Ok(())
4832 }
4833
4834 #[test]
4835 fn group_runtime_keeps_group_names_and_pod_settings_distinct() -> Result<(), String> {
4836 let source = crate::SourceId::new("pod.pod").map_err(|error| error.to_string())?;
4837 let origin = crate::Provenance::source(source);
4838 let mut group = ServiceGroup::new(id("frontend")?, ResourceOwnership::Application);
4839 let mut runtime = ServiceGroupRuntime::new();
4840 runtime.set_runtime_name(Sourced::from_source(
4841 ProtectedString::plain("production-frontend"),
4842 origin.clone(),
4843 ));
4844 runtime.set_service_name(Sourced::from_source(
4845 ProtectedString::plain("frontend-pod"),
4846 origin.clone(),
4847 ));
4848 runtime.set_host_mappings_with_origins(
4849 vec![Sourced::from_source(
4850 HostMapping::new(
4851 id("host.docker.internal")?,
4852 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
4853 ),
4854 origin.clone(),
4855 )],
4856 vec![origin.clone()],
4857 );
4858 runtime.set_ports_with_origins(Vec::new(), vec![origin.clone()]);
4859 runtime.set_networks_with_origins(
4860 vec![Sourced::from_source(
4861 NetworkAttachment::new(
4862 id("edge")?,
4863 vec![Sourced::from_source(
4864 ProtectedString::sensitive("private-alias"),
4865 origin.clone(),
4866 )],
4867 ),
4868 origin.clone(),
4869 )],
4870 vec![origin.clone()],
4871 );
4872 runtime.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
4873 runtime.set_mounts_with_origins(
4874 vec![Sourced::from_source(
4875 Mount::new(MountSource::Anonymous, "/cache", false).map_err(|error| error.to_string())?,
4876 origin.clone(),
4877 )],
4878 vec![origin.clone()],
4879 );
4880 runtime.set_shm_size(Sourced::from_source(ProtectedString::sensitive("64m"), origin.clone()));
4881 runtime.set_exit_policy(Sourced::from_source(
4882 GroupExitPolicy::Raw(ProtectedString::sensitive("preserve-this")),
4883 origin.clone(),
4884 ));
4885 runtime.set_stop_timeout(Sourced::from_source(
4886 StopTimeout::new("30s").map_err(|error| error.to_string())?,
4887 origin.clone(),
4888 ));
4889 assert!(matches!(
4890 runtime.replace_network(1, Sourced::generated(NetworkAttachment::new(id("other")?, Vec::new()))),
4891 Err(ModelError::UnknownServiceGroupRuntimeNetworkIndex { index: 1, len: 1 })
4892 ));
4893 group.set_runtime(Sourced::from_source(runtime, origin));
4894
4895 let runtime = group
4896 .runtime()
4897 .ok_or_else(|| "group runtime was omitted".to_owned())?
4898 .value();
4899 assert_eq!(group.name().as_str(), "frontend");
4900 assert_eq!(
4901 runtime.runtime_name().map(|name| name.value().expose()),
4902 Some("production-frontend")
4903 );
4904 assert_eq!(
4905 runtime.service_name().map(|name| name.value().expose()),
4906 Some("frontend-pod")
4907 );
4908 assert_eq!(runtime.host_mappings().map(<[_]>::len), Some(1));
4909 assert_eq!(runtime.ports().map(<[_]>::len), Some(0));
4910 assert_eq!(runtime.networks_origins().len(), 1);
4911 assert_eq!(runtime.mounts().map(<[_]>::len), Some(1));
4912 assert!(matches!(
4913 runtime.exit_policy().map(Sourced::value),
4914 Some(GroupExitPolicy::Raw(_))
4915 ));
4916 let debug = format!("{group:?}");
4917 for sensitive in ["private-alias", "64m", "preserve-this"] {
4918 assert!(!debug.contains(sensitive));
4919 }
4920 assert!(debug.contains("[REDACTED]"));
4921 Ok(())
4922 }
4923
4924 #[test]
4925 fn rootfs_startup_notification_and_podman_args_preserve_safe_contracts() -> Result<(), String> {
4926 let source = crate::SourceId::new("web.container").map_err(|error| error.to_string())?;
4927 let origin = crate::Provenance::source(source);
4928 let mut service = Service::new(id("web")?);
4929 service.set_startup_notification(Sourced::from_source(StartupNotification::Healthy, origin.clone()));
4930 assert!(matches!(
4931 service.startup_notification().map(Sourced::value),
4932 Some(StartupNotification::Healthy)
4933 ));
4934
4935 let mut with_image = Service::new(id("image-first")?);
4936 with_image.set_image(Sourced::generated(
4937 ImageReference::parse("example.invalid/web:1").map_err(|error| error.to_string())?,
4938 ));
4939 assert!(matches!(
4940 with_image.set_rootfs(Sourced::generated(ProtectedString::plain("/srv/rootfs"))),
4941 Err(ModelError::RootfsImageSourceConflict { source: "image", .. })
4942 ));
4943
4944 let mut with_rootfs = Service::new(id("rootfs-first")?);
4945 with_rootfs
4946 .set_rootfs(Sourced::generated(ProtectedString::sensitive("/private/rootfs")))
4947 .map_err(|error| error.to_string())?;
4948 with_rootfs.set_image_build(Sourced::generated(id("web-build")?));
4949 let mut application = Application::new(id("example")?);
4950 application
4951 .add_image_build(Sourced::generated(ImageBuild::new(id("web-build")?)))
4952 .map_err(|error| error.to_string())?;
4953 assert!(matches!(
4954 application.add_service(Sourced::generated(with_rootfs)),
4955 Err(ModelError::RootfsImageSourceConflict {
4956 source: "image build",
4957 ..
4958 })
4959 ));
4960 Ok(())
4961 }
4962
4963 #[test]
4964 fn validates_raw_preserving_healthcheck_scalars() -> Result<(), String> {
4965 let duration = HealthcheckDuration::new("1m30s").map_err(|error| error.to_string())?;
4966 let retries = HealthcheckRetries::new("003").map_err(|error| error.to_string())?;
4967 assert_eq!(duration.as_str(), "1m30s");
4968 assert_eq!(retries.as_str(), "003");
4969 assert_eq!(
4970 HealthcheckRetries::new("three"),
4971 Err(ModelError::InvalidHealthcheckRetries)
4972 );
4973 assert!(matches!(
4974 HealthcheckDuration::new(""),
4975 Err(ModelError::EmptyValue("health-check duration"))
4976 ));
4977 Ok(())
4978 }
4979
4980 #[test]
4981 fn preserves_ordered_dependency_edges_and_field_provenance() -> Result<(), String> {
4982 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
4983 let origin = crate::Provenance::source(source);
4984 let mut service = Service::new(id("web")?);
4985
4986 let mut database = ServiceDependency::new(id("database")?);
4987 database.set_condition(Sourced::from_source(
4988 ServiceDependencyCondition::Healthy,
4989 origin.clone(),
4990 ));
4991 database.set_required(Sourced::from_source(true, origin.clone()));
4992 service.add_dependency(Sourced::from_source(database, origin.clone()));
4993
4994 let cache = ServiceDependency::new(id("cache")?);
4995 assert!(cache.is_required());
4996 service.add_dependency(Sourced::from_source(cache, origin));
4997
4998 assert_eq!(
4999 service
5000 .dependencies()
5001 .iter()
5002 .map(|dependency| dependency.value().service().as_str())
5003 .collect::<Vec<_>>(),
5004 ["database", "cache"]
5005 );
5006 assert!(matches!(
5007 service.dependencies()[0].value().condition().map(Sourced::value),
5008 Some(ServiceDependencyCondition::Healthy)
5009 ));
5010 assert_eq!(service.dependencies()[0].origins().len(), 1);
5011 assert_eq!(
5012 service.dependencies()[0]
5013 .value()
5014 .condition()
5015 .map_or(0, |condition| condition.origins().len()),
5016 1
5017 );
5018 Ok(())
5019 }
5020
5021 #[test]
5022 fn retains_execution_identity_context_order_provenance_and_redaction() -> Result<(), String> {
5023 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
5024 let origin = crate::Provenance::source(source);
5025 let mut service = Service::new(id("web")?);
5026
5027 service.set_user(Sourced::from_source(ProtectedString::sensitive("1001"), origin.clone()));
5028 service.set_group(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
5029 service.set_user_namespace(Sourced::from_source(ProtectedString::plain("keep-id"), origin.clone()));
5030 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("audio"), origin.clone()));
5031 service.add_supplementary_group(Sourced::from_source(ProtectedString::plain("44"), origin.clone()));
5032 service.set_working_directory(Sourced::from_source(ProtectedString::plain("/srv/app"), origin.clone()));
5033 service.set_read_only_root_filesystem(Sourced::from_source(true, origin));
5034
5035 assert_eq!(service.user().map(|value| value.value().expose()), Some("1001"));
5036 assert_eq!(service.group().map(|value| value.value().expose()), Some("1002"));
5037 assert_eq!(
5038 service.user_namespace().map(|value| value.value().expose()),
5039 Some("keep-id")
5040 );
5041 assert_eq!(
5042 service
5043 .supplementary_groups()
5044 .iter()
5045 .map(|group| group.value().expose())
5046 .collect::<Vec<_>>(),
5047 ["audio", "44"]
5048 );
5049 assert_eq!(
5050 service.working_directory().map(|value| value.value().expose()),
5051 Some("/srv/app")
5052 );
5053 assert_eq!(service.read_only_root_filesystem().map(Sourced::value), Some(&true));
5054 assert_eq!(service.user().map_or(0, |value| value.origins().len()), 1);
5055 let debug = format!("{service:?}");
5056 assert!(!debug.contains("1001"));
5057 assert!(debug.contains("[REDACTED]"));
5058 Ok(())
5059 }
5060
5061 #[test]
5062 fn retains_config_secret_resources_grants_provenance_and_redaction() -> Result<(), String> {
5063 let source = crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?;
5064 let origin = crate::Provenance::source(source);
5065 let mut application = Application::new(id("example")?);
5066
5067 let mut config = Config::new(id("settings")?, ResourceOwnership::Application);
5068 config.set_material(Sourced::from_source(
5069 ConfigMaterial::Content(ProtectedString::sensitive("private-config")),
5070 origin.clone(),
5071 ));
5072 application
5073 .add_config(Sourced::from_source(config, origin.clone()))
5074 .map_err(|error| error.to_string())?;
5075
5076 let mut secret = Secret::new(id("password")?, ResourceOwnership::External);
5077 secret.set_runtime_name(Sourced::from_source(
5078 ProtectedString::plain("production-password"),
5079 origin.clone(),
5080 ));
5081 secret.set_material(Sourced::from_source(
5082 SecretMaterial::Environment(ProtectedString::sensitive("private-environment-name")),
5083 origin.clone(),
5084 ));
5085 application
5086 .add_secret(Sourced::from_source(secret, origin.clone()))
5087 .map_err(|error| error.to_string())?;
5088
5089 let mut service = Service::new(id("web")?);
5090 service.add_config_grant(Sourced::from_source(
5091 ResourceGrant::new(ProtectedString::plain("settings"), ResourceGrantSyntax::Short)
5092 .map_err(|error| error.to_string())?,
5093 origin.clone(),
5094 ));
5095 let mut secret_grant = ResourceGrant::new(
5096 ProtectedString::sensitive("private-grant-source"),
5097 ResourceGrantSyntax::Long,
5098 )
5099 .map_err(|error| error.to_string())?;
5100 secret_grant.set_target(Sourced::from_source(
5101 ProtectedString::plain("database-password"),
5102 origin.clone(),
5103 ));
5104 secret_grant.set_uid(Sourced::from_source(ProtectedString::plain("1001"), origin.clone()));
5105 secret_grant.set_gid(Sourced::from_source(ProtectedString::plain("1002"), origin.clone()));
5106 secret_grant.set_mode(Sourced::from_source(ProtectedString::plain("0440"), origin.clone()));
5107 service.add_secret_grant(Sourced::from_source(secret_grant, origin.clone()));
5108 application
5109 .add_service(Sourced::from_source(service, origin))
5110 .map_err(|error| error.to_string())?;
5111
5112 assert_eq!(application.configs().len(), 1);
5113 assert_eq!(application.secrets().len(), 1);
5114 assert_eq!(application.services()[0].value().config_grants().len(), 1);
5115 let grant = &application.services()[0].value().secret_grants()[0];
5116 assert_eq!(grant.value().syntax(), ResourceGrantSyntax::Long);
5117 assert_eq!(
5118 grant.value().target().map(|value| value.value().expose()),
5119 Some("database-password")
5120 );
5121 assert_eq!(grant.value().uid().map_or(0, |value| value.origins().len()), 1);
5122 assert_eq!(grant.origins().len(), 1);
5123 let debug = format!("{application:?}");
5124 for secret in ["private-config", "private-environment-name", "private-grant-source"] {
5125 assert!(!debug.contains(secret));
5126 }
5127 assert!(debug.contains("[REDACTED]"));
5128
5129 assert!(matches!(
5130 ResourceGrant::new(ProtectedString::plain(""), ResourceGrantSyntax::Short),
5131 Err(ModelError::EmptyValue("resource grant source"))
5132 ));
5133 assert!(matches!(
5134 application.add_config(Sourced::generated(Config::new(
5135 id("settings")?,
5136 ResourceOwnership::External,
5137 ))),
5138 Err(ModelError::DuplicateResource { kind: "config", .. })
5139 ));
5140 assert!(matches!(
5141 application.add_secret(Sourced::generated(Secret::new(
5142 id("password")?,
5143 ResourceOwnership::External,
5144 ))),
5145 Err(ModelError::DuplicateResource { kind: "secret", .. })
5146 ));
5147 Ok(())
5148 }
5149
5150 #[test]
5151 fn host_mappings_preserve_order_spelling_and_runtime_tokens() -> Result<(), String> {
5152 let mut service = Service::new(id("web")?);
5153 service.add_host_mapping(Sourced::generated(HostMapping::new(
5154 id("host.docker.internal")?,
5155 HostAddress::new("host-gateway").map_err(|error| error.to_string())?,
5156 )));
5157 service.add_host_mapping(Sourced::generated(HostMapping::new(
5158 id("ipv6")?,
5159 HostAddress::new("[::1]").map_err(|error| error.to_string())?,
5160 )));
5161
5162 assert_eq!(service.host_mappings().len(), 2);
5163 assert_eq!(
5164 service.host_mappings()[0].value().address().kind(),
5165 HostAddressKind::HostGateway
5166 );
5167 assert_eq!(service.host_mappings()[1].value().address().raw(), "[::1]");
5168 assert_eq!(
5169 service.host_mappings()[1].value().address().kind(),
5170 HostAddressKind::Ipv6 { bracketed: true }
5171 );
5172 assert!(matches!(HostAddress::new(""), Err(ModelError::EmptyValue(_))));
5173 Ok(())
5174 }
5175
5176 #[test]
5177 fn dns_collections_preserve_order_provenance_and_explicit_empty_state() -> Result<(), String> {
5178 let mut service = Service::new(id("web")?);
5179 assert!(service.dns_servers().is_none());
5180 service.set_dns_servers(Vec::new());
5181 assert!(matches!(service.dns_servers(), Some(values) if values.is_empty()));
5182 service.set_dns_options(vec![
5183 Sourced::generated(ProtectedString::plain("ndots:5")),
5184 Sourced::generated(ProtectedString::sensitive("rotate")),
5185 ]);
5186 service.set_dns_search_domains(vec![Sourced::generated(ProtectedString::plain("example.test"))]);
5187 assert_eq!(
5188 service
5189 .dns_options()
5190 .unwrap_or_default()
5191 .iter()
5192 .map(|value| value.value().expose())
5193 .collect::<Vec<_>>(),
5194 ["ndots:5", "rotate"]
5195 );
5196 assert!(!format!("{service:?}").contains("rotate"));
5197 Ok(())
5198 }
5199
5200 #[test]
5201 fn security_options_preserve_empty_order_duplicates_provenance_and_redaction() -> Result<(), String> {
5202 let origin =
5203 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5204 let mut service = Service::new(id("web")?);
5205
5206 assert!(service.security_options().is_none());
5207 service.set_security_options_with_origins(Vec::new(), vec![origin.clone()]);
5208 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5209 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5210
5211 service.set_security_options_with_origins(
5212 vec![
5213 Sourced::from_source(
5214 SecurityOption::AppArmor(ProtectedString::sensitive("apparmor-secret")),
5215 origin.clone(),
5216 ),
5217 Sourced::from_source(SecurityOption::NoNewPrivileges(true), origin.clone()),
5218 Sourced::from_source(
5219 SecurityOption::SeccompProfile(ProtectedString::sensitive("seccomp-secret")),
5220 origin.clone(),
5221 ),
5222 Sourced::from_source(SecurityOption::SecurityLabelDisable(false), origin.clone()),
5223 Sourced::from_source(
5224 SecurityOption::SecurityLabelFileType(ProtectedString::sensitive("file-type-secret")),
5225 origin.clone(),
5226 ),
5227 Sourced::from_source(
5228 SecurityOption::SecurityLabelLevel(ProtectedString::sensitive("level-secret")),
5229 origin.clone(),
5230 ),
5231 Sourced::from_source(SecurityOption::SecurityLabelNested(true), origin.clone()),
5232 Sourced::from_source(
5233 SecurityOption::SecurityLabelType(ProtectedString::sensitive("type-secret")),
5234 origin.clone(),
5235 ),
5236 Sourced::from_source(
5237 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5238 origin.clone(),
5239 ),
5240 Sourced::from_source(
5241 SecurityOption::Unmask(ProtectedString::sensitive("unmask-secret")),
5242 origin.clone(),
5243 ),
5244 Sourced::from_source(
5245 SecurityOption::Mask(ProtectedString::sensitive("mask-secret")),
5246 origin.clone(),
5247 ),
5248 ],
5249 vec![origin.clone()],
5250 );
5251
5252 let options = service.security_options().unwrap_or_default();
5253 assert_eq!(options.len(), 11);
5254 assert!(
5255 matches!(options[0].value(), SecurityOption::AppArmor(profile) if profile.expose() == "apparmor-secret")
5256 );
5257 assert!(matches!(options[1].value(), SecurityOption::NoNewPrivileges(true)));
5258 assert!(
5259 matches!(options[2].value(), SecurityOption::SeccompProfile(profile) if profile.expose() == "seccomp-secret")
5260 );
5261 assert!(matches!(
5262 options[3].value(),
5263 SecurityOption::SecurityLabelDisable(false)
5264 ));
5265 assert!(
5266 matches!(options[4].value(), SecurityOption::SecurityLabelFileType(profile) if profile.expose() == "file-type-secret")
5267 );
5268 assert!(
5269 matches!(options[5].value(), SecurityOption::SecurityLabelLevel(profile) if profile.expose() == "level-secret")
5270 );
5271 assert!(matches!(options[6].value(), SecurityOption::SecurityLabelNested(true)));
5272 assert!(
5273 matches!(options[7].value(), SecurityOption::SecurityLabelType(profile) if profile.expose() == "type-secret")
5274 );
5275 assert!(matches!(options[8].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5276 assert!(matches!(options[9].value(), SecurityOption::Unmask(path) if path.expose() == "unmask-secret"));
5277 assert!(matches!(options[10].value(), SecurityOption::Mask(path) if path.expose() == "mask-secret"));
5278 assert_eq!(options[0].origins(), std::slice::from_ref(&origin));
5279 assert_eq!(service.security_options_origins(), std::slice::from_ref(&origin));
5280
5281 let debug = format!("{service:?}");
5282 for secret in [
5283 "apparmor-secret",
5284 "seccomp-secret",
5285 "file-type-secret",
5286 "level-secret",
5287 "type-secret",
5288 "mask-secret",
5289 "unmask-secret",
5290 ] {
5291 assert!(!debug.contains(secret));
5292 }
5293 assert!(debug.contains("[REDACTED]"));
5294
5295 service.set_security_options(Vec::new());
5296 assert_eq!(service.security_options().map(<[_]>::len), Some(0));
5297 assert!(service.security_options_origins().is_empty());
5298 Ok(())
5299 }
5300
5301 #[test]
5302 fn retains_entrypoint_run_init_stop_pull_memory_and_exposed_port_intent() -> Result<(), String> {
5303 let origin =
5304 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5305 let mut service = Service::new(id("web")?);
5306 service.set_command(Sourced::from_source(
5307 Command::Exec(vec![ProtectedString::plain("serve")]),
5308 origin.clone(),
5309 ));
5310 service.set_entrypoint(Sourced::from_source(
5311 Entrypoint::Shell(ProtectedString::sensitive("/bin/sh -c private-entrypoint")),
5312 origin.clone(),
5313 ));
5314 service.set_run_init(Sourced::from_source(true, origin.clone()));
5315 service.set_stop_timeout(Sourced::from_source(
5316 StopTimeout::new("01m30s").map_err(|error| error.to_string())?,
5317 origin.clone(),
5318 ));
5319 service.set_pull_policy(Sourced::from_source(
5320 PullPolicy::Every(ProtectedString::sensitive("12h")),
5321 origin.clone(),
5322 ));
5323 service.set_memory_limit(Sourced::from_source(
5324 ProtectedString::sensitive("512MiB"),
5325 origin.clone(),
5326 ));
5327 assert!(service.exposed_ports().is_none());
5328 service.set_exposed_ports_with_origins(Vec::new(), vec![origin.clone()]);
5329 assert_eq!(service.exposed_ports().map(<[_]>::len), Some(0));
5330 assert_eq!(service.exposed_ports_origins(), std::slice::from_ref(&origin));
5331 service.add_exposed_port(Sourced::from_source(
5332 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5333 origin.clone(),
5334 ));
5335 service.add_exposed_port(Sourced::from_source(
5336 ExposedPort::new(8080, Protocol::Tcp).map_err(|error| error.to_string())?,
5337 origin,
5338 ));
5339
5340 assert!(matches!(service.command().map(Sourced::value), Some(Command::Exec(_))));
5341 assert!(matches!(
5342 service.entrypoint().map(Sourced::value),
5343 Some(Entrypoint::Shell(_))
5344 ));
5345 assert_eq!(service.run_init().map(Sourced::value), Some(&true));
5346 assert_eq!(
5347 service.stop_timeout().map(|timeout| timeout.value().as_str()),
5348 Some("01m30s")
5349 );
5350 assert!(matches!(
5351 service.pull_policy().map(Sourced::value),
5352 Some(PullPolicy::Every(_))
5353 ));
5354 assert_eq!(
5355 service.memory_limit().map(|limit| limit.value().expose()),
5356 Some("512MiB")
5357 );
5358 let exposed_ports = service.exposed_ports().ok_or("missing exposed ports")?;
5359 assert_eq!(exposed_ports.len(), 2);
5360 assert_eq!(exposed_ports[0].value().container(), 8080);
5361 assert_eq!(exposed_ports[0].value().protocol(), &Protocol::Tcp);
5362 assert!(matches!(
5363 ExposedPort::new(0, Protocol::Udp),
5364 Err(ModelError::ZeroContainerPort)
5365 ));
5366 assert!(matches!(
5367 StopTimeout::new(""),
5368 Err(ModelError::EmptyValue("stop timeout"))
5369 ));
5370
5371 let debug = format!("{service:?}");
5372 for secret in ["private-entrypoint", "512MiB", "12h"] {
5373 assert!(!debug.contains(secret));
5374 }
5375 assert!(debug.contains("[REDACTED]"));
5376 Ok(())
5377 }
5378
5379 #[test]
5380 fn annotations_and_logging_preserve_empty_order_field_provenance_and_redaction() -> Result<(), String> {
5381 let origin =
5382 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5383 let mut service = Service::new(id("web")?);
5384
5385 assert!(service.annotations().is_none());
5386 service.set_annotations_with_origins(Vec::new(), vec![origin.clone()]);
5387 assert_eq!(service.annotations().map(<[_]>::len), Some(0));
5388 assert_eq!(service.annotations_origins(), std::slice::from_ref(&origin));
5389
5390 service.set_annotations_with_origins(
5391 vec![
5392 Sourced::from_source(
5393 Annotation::new(
5394 Sourced::from_source(id("io.example.first")?, origin.clone()),
5395 Sourced::from_source(ProtectedString::sensitive("annotation-secret"), origin.clone()),
5396 ),
5397 origin.clone(),
5398 ),
5399 Sourced::from_source(
5400 Annotation::new(
5401 Sourced::from_source(id("io.example.second")?, origin.clone()),
5402 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5403 ),
5404 origin.clone(),
5405 ),
5406 ],
5407 vec![origin.clone()],
5408 );
5409 let annotations = service.annotations().unwrap_or_default();
5410 assert_eq!(annotations.len(), 2);
5411 assert_eq!(annotations[0].value().name().value().as_str(), "io.example.first");
5412 assert_eq!(annotations[1].value().value().value().expose(), "");
5413 assert_eq!(annotations[0].value().name().origins(), std::slice::from_ref(&origin));
5414 assert_eq!(annotations[0].value().value().origins(), std::slice::from_ref(&origin));
5415
5416 let mut logging = Logging::new();
5417 assert!(logging.options().is_none());
5418 logging.set_driver(Sourced::from_source(ProtectedString::plain("journald"), origin.clone()));
5419 logging.set_options_with_origins(
5420 vec![
5421 Sourced::from_source(
5422 LoggingOption::new(
5423 Sourced::from_source(id("tag")?, origin.clone()),
5424 Sourced::from_source(ProtectedString::sensitive("logging-secret"), origin.clone()),
5425 ),
5426 origin.clone(),
5427 ),
5428 Sourced::from_source(
5429 LoggingOption::new(
5430 Sourced::from_source(id("labels")?, origin.clone()),
5431 Sourced::from_source(ProtectedString::plain(""), origin.clone()),
5432 ),
5433 origin.clone(),
5434 ),
5435 ],
5436 vec![origin.clone()],
5437 );
5438 service.set_logging(Sourced::from_source(logging, origin));
5439
5440 let logging = service.logging().map(Sourced::value).ok_or("missing logging")?;
5441 assert_eq!(logging.driver().map(|driver| driver.value().expose()), Some("journald"));
5442 assert_eq!(logging.options().map(<[_]>::len), Some(2));
5443 assert_eq!(
5444 logging.options().unwrap_or_default()[0].value().name().value().as_str(),
5445 "tag"
5446 );
5447 assert_eq!(logging.options_origins().len(), 1);
5448 let debug = format!("{service:?}");
5449 assert!(!debug.contains("annotation-secret"));
5450 assert!(!debug.contains("logging-secret"));
5451 assert!(debug.contains("[REDACTED]"));
5452 Ok(())
5453 }
5454
5455 #[test]
5456 fn network_attachments_retain_alias_provenance_and_redact_sensitive_values() -> Result<(), String> {
5457 let origin =
5458 crate::Provenance::source(crate::SourceId::new("compose.yaml").map_err(|error| error.to_string())?);
5459 let mut attachment = NetworkAttachment::new(
5460 id("frontend")?,
5461 vec![
5462 Sourced::from_source(ProtectedString::plain("web"), origin.clone()),
5463 Sourced::from_source(ProtectedString::sensitive("private-alias"), origin.clone()),
5464 ],
5465 );
5466 attachment.set_ipv4_address(Sourced::from_source(
5467 ProtectedString::plain("192.0.2.10"),
5468 origin.clone(),
5469 ));
5470 attachment.set_ipv6_address(Sourced::from_source(
5471 ProtectedString::plain("2001:db8::10"),
5472 origin.clone(),
5473 ));
5474 let metrics = Sourced::generated(ProtectedString::plain("metrics"));
5475 attachment.add_alias(&metrics);
5476
5477 assert_eq!(attachment.aliases(), ["web", "private-alias", "metrics"]);
5478 assert_eq!(attachment.alias_sensitivities(), [false, true, false]);
5479 assert_eq!(attachment.alias_origins().len(), 3);
5480 assert_eq!(attachment.alias_origins()[0].len(), 1);
5481 assert_eq!(attachment.alias_origins()[1], std::slice::from_ref(&origin));
5482 assert!(attachment.alias_origins()[2].is_empty());
5483 assert_eq!(
5484 attachment.ipv4_address().map(|address| address.value().expose()),
5485 Some("192.0.2.10")
5486 );
5487 assert_eq!(
5488 attachment.ipv6_address().map(|address| address.value().expose()),
5489 Some("2001:db8::10")
5490 );
5491 let debug = format!("{attachment:?}");
5492 assert!(!debug.contains("private-alias"));
5493 assert!(debug.contains("[REDACTED]"));
5494
5495 let mut service = Service::new(id("web")?);
5496 service.add_network(Sourced::generated(NetworkAttachment::new(
5497 id("previous")?,
5498 vec![Sourced::generated(ProtectedString::plain("previous-alias"))],
5499 )));
5500 let previous = service
5501 .replace_network(0, Sourced::generated(attachment))
5502 .map_err(|error| error.to_string())?;
5503 assert_eq!(previous.value().network().as_str(), "previous");
5504 assert_eq!(service.networks()[0].value().network().as_str(), "frontend");
5505 assert!(matches!(
5506 service.replace_network(1, Sourced::generated(NetworkAttachment::new(id("unused")?, Vec::new()))),
5507 Err(ModelError::UnknownNetworkAttachmentIndex { index: 1, len: 1 })
5508 ));
5509 Ok(())
5510 }
5511
5512 #[test]
5513 fn reload_action_is_one_explicit_command_or_signal() -> Result<(), String> {
5514 let origin =
5515 crate::Provenance::source(crate::SourceId::new("quadlet.container").map_err(|error| error.to_string())?);
5516 let mut service = Service::new(id("web")?);
5517 service.set_reload_action(Sourced::from_source(
5518 ReloadAction::Command(Command::Exec(vec![ProtectedString::plain("reload")])),
5519 origin.clone(),
5520 ));
5521 assert!(matches!(
5522 service.reload_action().map(Sourced::value),
5523 Some(ReloadAction::Command(Command::Exec(_)))
5524 ));
5525
5526 service.set_reload_action(Sourced::from_source(
5527 ReloadAction::Signal(ProtectedString::sensitive("SIGHUP")),
5528 origin,
5529 ));
5530 assert!(matches!(
5531 service.reload_action().map(Sourced::value),
5532 Some(ReloadAction::Signal(_))
5533 ));
5534 let debug = format!("{service:?}");
5535 assert!(!debug.contains("SIGHUP"));
5536 assert!(debug.contains("[REDACTED]"));
5537 Ok(())
5538 }
5539
5540 fn id(value: &str) -> Result<Identifier, String> {
5541 Identifier::new(value).map_err(|error| error.to_string())
5542 }
5543}