1use std::{collections::BTreeSet, error::Error, fmt};
4
5use crate::{
6 model::{
7 ComposeDocument, CpuRtRuntime, MemLimitUnit, ShmSizeUnit, StopGracePeriod, valid_generated_device_string,
8 valid_generated_expose_item, valid_generated_mem_amount, valid_generated_shm_amount,
9 valid_generated_tmpfs_item, valid_hostname, valid_positive_pids_decimal, valid_pull_policy_duration,
10 valid_ulimit_name,
11 },
12 source::SourceId,
13 syntax::SyntaxDocument,
14};
15use yaml_edit::{ScalarType, ScalarValue, YamlFile};
16
17use super::write_quoted;
18mod extensions;
19pub use extensions::{
20 GENERATED_EXTENSION_MAX_DEPTH, GENERATED_EXTENSION_MAX_NODES, GeneratedExtension, GeneratedExtensionEntry,
21 GeneratedExtensionValue,
22};
23use extensions::{add_extension, render_extensions};
24
25#[derive(Clone, Debug, Eq, PartialEq)]
27#[non_exhaustive]
28pub enum GenerationError {
29 EmptyValue(&'static str),
31 ContainsNul(&'static str),
33 ContainsLineBreak(&'static str),
35 InvalidEnvironmentName,
37 InvalidContainerName,
39 InvalidHostname,
41 InvalidPullPolicyDuration,
43 InvalidPidsLimit,
45 InvalidShmSize,
47 InvalidMemLimit,
49 InvalidDnsValue,
51 InvalidDnsOptionValue,
53 InvalidDnsSearchValue,
55 InvalidExposeValue,
57 InvalidSecurityOptionValue,
59 InvalidAnnotationName,
61 InvalidAnnotationValue,
63 InvalidFileResourceName,
65 InvalidFileResourcePath,
67 InvalidTmpfsItem,
69 InvalidDeviceValue(&'static str),
71 InvalidSysctlName,
73 InvalidSysctlValue,
75 InvalidLoggingOptionNumber,
77 InvalidNetworkDriverOptionNumber,
79 InvalidVolumeDriverOptionNumber,
81 InvalidExtensionName,
83 InvalidExtensionNumber,
85 InvalidFoldedExtensionString,
87 DuplicateExtensionKey,
89 ExtensionLimit,
91 InvalidUlimitName,
93 InvalidUlimitValue,
95 MissingUlimitRangeMember(&'static str),
97 InvalidStopGracePeriod,
99 InvalidShortComponent(&'static str),
101 InvalidSelinuxBind,
103 InvalidServiceRuntimeField(&'static str),
105 DuplicateField(&'static str),
107 DuplicateName {
109 kind: &'static str,
111 name: String,
113 },
114 DuplicateItem(&'static str),
116 InvalidPort,
118 UnrepresentableSctpHostIp,
120 MissingService,
122 InternalInvariant(&'static str),
124}
125
126impl fmt::Display for GenerationError {
127 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::EmptyValue(kind) => write!(formatter, "generated {kind} must not be empty"),
130 Self::ContainsNul(kind) => write!(formatter, "generated {kind} must not contain a NUL byte"),
131 Self::ContainsLineBreak(kind) => {
132 write!(formatter, "generated {kind} must not contain a carriage return or line feed")
133 }
134 Self::InvalidEnvironmentName => formatter.write_str("generated environment name must not contain `=`"),
135 Self::InvalidContainerName => {
136 formatter.write_str("generated container name must match `[a-zA-Z0-9][a-zA-Z0-9_.-]+`")
137 }
138 Self::InvalidHostname => formatter.write_str(
139 "generated hostname must be a resolved ASCII RFC-1123 name with labels of 1 to 63 characters and total length at most 253",
140 ),
141 Self::InvalidPullPolicyDuration => formatter.write_str(
142 "generated pull policy duration must match integer `w`, `d`, `h`, `m`, and `s` components",
143 ),
144 Self::InvalidPidsLimit => {
145 formatter.write_str("generated finite PID limit must be a positive integral decimal")
146 }
147 Self::InvalidShmSize => formatter.write_str(
148 "generated shared-memory size must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
149 ),
150 Self::InvalidMemLimit => formatter.write_str(
151 "generated memory limit must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
152 ),
153 Self::InvalidDnsValue => {
154 formatter.write_str("generated DNS server must be a non-empty resolved single-line string")
155 }
156 Self::InvalidDnsOptionValue => {
157 formatter.write_str("generated DNS option must be a non-empty resolved single-line string")
158 }
159 Self::InvalidDnsSearchValue => {
160 formatter.write_str("generated DNS search domain must be a non-empty resolved single-line string")
161 }
162 Self::InvalidExposeValue => formatter.write_str(
163 "generated expose item must be a resolved decimal port or range with an optional `tcp` or `udp` suffix",
164 ),
165 Self::InvalidSecurityOptionValue => {
166 formatter.write_str("generated security option must be a non-empty resolved single-line string")
167 }
168 Self::InvalidAnnotationName => formatter
169 .write_str("generated annotation name must be a non-empty resolved single-line string"),
170 Self::InvalidAnnotationValue => formatter
171 .write_str("generated annotation value must be a resolved single-line string"),
172 Self::InvalidFileResourceName => formatter.write_str(
173 "generated top-level config or secret name must be a non-empty resolved single-line string",
174 ),
175 Self::InvalidFileResourcePath => formatter.write_str(
176 "generated top-level config or secret file must be a non-empty resolved single-line string",
177 ),
178 Self::InvalidTmpfsItem => formatter.write_str(
179 "generated tmpfs item must be a non-empty path optionally followed by a colon and non-empty comma-separated raw options",
180 ),
181 Self::InvalidDeviceValue(member) => format_invalid_device_value(formatter, member),
182 Self::InvalidSysctlName => formatter
183 .write_str("generated sysctl name must be a non-empty resolved single-line string"),
184 Self::InvalidSysctlValue => formatter
185 .write_str("generated sysctl value must be a resolved single-line string"),
186 Self::InvalidLoggingOptionNumber => formatter
187 .write_str("generated logging option number must be one complete YAML number scalar"),
188 Self::InvalidNetworkDriverOptionNumber => formatter
189 .write_str("generated network driver option number must be one complete YAML number scalar"),
190 Self::InvalidVolumeDriverOptionNumber => formatter
191 .write_str("generated volume driver option number must be one complete YAML number scalar"),
192 Self::InvalidExtensionName => formatter.write_str("generated extension name requires the exact x- prefix and a non-empty single-line suffix"),
193 Self::InvalidExtensionNumber => formatter.write_str("generated extension number must be one complete YAML number scalar"),
194 Self::InvalidFoldedExtensionString => formatter.write_str("generated folded extension string cannot preserve its exact value"),
195 Self::DuplicateExtensionKey => formatter.write_str("generated extension contains a duplicate exact key"),
196 Self::ExtensionLimit => formatter.write_str("generated extension exceeds its depth or node limit"),
197 Self::InvalidUlimitName => formatter
198 .write_str("generated ulimit name must match lowercase ASCII `[a-z]+`"),
199 Self::InvalidUlimitValue => formatter
200 .write_str("generated ulimit value must be `-1` or a non-negative ASCII decimal"),
201 Self::MissingUlimitRangeMember(member) => {
202 write!(formatter, "generated ulimit range is missing required `{member}`")
203 }
204 Self::InvalidStopGracePeriod => formatter.write_str(
205 "generated stop grace period must match the ComposeLens duration policy using `us`, `ms`, `s`, `m`, or `h`, or contain an interpolation marker",
206 ),
207 Self::InvalidShortComponent(kind) => {
208 write!(formatter, "generated {kind} contains its reserved short-form separator")
209 }
210 Self::InvalidSelinuxBind => formatter
211 .write_str("generated SELinux bind source and target must not contain the short-syntax `:` separator"),
212 Self::InvalidServiceRuntimeField(field) => write!(formatter, "generated {field} must use a resolved, non-empty field-valid spelling"),
213 Self::DuplicateField(field) => write!(formatter, "generated field `{field}` was configured more than once"),
214 Self::DuplicateName { kind, name } => {
215 write!(formatter, "generated {kind} `{name}` was added more than once")
216 }
217 Self::DuplicateItem(kind) => write!(formatter, "generated {kind} contains an exact duplicate item"),
218 Self::InvalidPort => formatter.write_str("generated container target port must be greater than zero"),
219 Self::UnrepresentableSctpHostIp => formatter.write_str(
220 "generated SCTP port with a host address also requires a published port for Compose short syntax",
221 ),
222 Self::MissingService => formatter.write_str("generated Compose project requires at least one service"),
223 Self::InternalInvariant(stage) => write!(formatter, "generated Compose document failed {stage} validation"),
224 }
225 }
226}
227
228impl Error for GenerationError {}
229
230fn format_invalid_device_value(formatter: &mut fmt::Formatter<'_>, member: &str) -> fmt::Result {
231 write!(
232 formatter,
233 "generated device {member} must be a safe resolved single-line string{}",
234 if matches!(member, "short item" | "source") {
235 " and must not be empty"
236 } else {
237 ""
238 }
239 )
240}
241
242#[derive(Clone, Eq, PartialEq)]
244pub struct GeneratedString {
245 value: String,
246 sensitive: bool,
247}
248
249impl GeneratedString {
250 pub fn plain(value: impl Into<String>) -> Result<Self, GenerationError> {
256 Self::new(value.into(), false)
257 }
258
259 pub fn sensitive(value: impl Into<String>) -> Result<Self, GenerationError> {
265 Self::new(value.into(), true)
266 }
267
268 fn new(value: String, sensitive: bool) -> Result<Self, GenerationError> {
269 if value.contains('\0') {
270 return Err(GenerationError::ContainsNul("string"));
271 }
272 Ok(Self { value, sensitive })
273 }
274
275 #[must_use]
277 pub fn expose(&self) -> &str {
278 &self.value
279 }
280
281 #[must_use]
283 pub const fn is_sensitive(&self) -> bool {
284 self.sensitive
285 }
286}
287
288impl fmt::Debug for GeneratedString {
289 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
290 formatter
291 .debug_struct("GeneratedString")
292 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
293 .field("sensitive", &self.sensitive)
294 .finish()
295 }
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300#[non_exhaustive]
301pub enum GeneratedCommand {
302 Exec(Vec<GeneratedString>),
304 Shell(GeneratedString),
306 Empty,
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
312#[non_exhaustive]
313pub enum GeneratedEntrypoint {
314 List(Vec<GeneratedString>),
316 String(GeneratedString),
318 Empty,
320}
321
322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
324#[non_exhaustive]
325pub enum GeneratedRestartPolicy {
326 No,
328 Always,
330 OnFailure {
332 maximum_retries: Option<u64>,
334 },
335 UnlessStopped,
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
341#[non_exhaustive]
342pub enum GeneratedPullPolicy {
343 Always,
345 Never,
347 Missing,
349 IfNotPresentAlias,
351 Build,
353 Daily,
355 Weekly,
357 Every(GeneratedString),
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
363#[non_exhaustive]
364pub enum GeneratedPidsLimit {
365 Unlimited,
367 Finite(String),
369}
370
371#[derive(Clone, Debug, Eq, PartialEq)]
373#[non_exhaustive]
374pub enum GeneratedShmSize {
375 Explicit {
377 amount: GeneratedString,
379 unit: ShmSizeUnit,
381 },
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
386#[non_exhaustive]
387pub enum GeneratedMemLimit {
388 Explicit {
390 amount: GeneratedString,
392 unit: MemLimitUnit,
394 },
395}
396
397#[derive(Clone, Debug, Eq, PartialEq)]
399#[non_exhaustive]
400pub enum GeneratedTmpfs {
401 Scalar(GeneratedString),
403 List(Vec<GeneratedString>),
405}
406
407#[derive(Clone, Debug, Eq, PartialEq)]
409#[non_exhaustive]
410pub enum GeneratedDns {
411 Scalar(GeneratedString),
413 List(Vec<GeneratedString>),
415}
416
417#[derive(Clone, Debug, Eq, PartialEq)]
419#[non_exhaustive]
420pub enum GeneratedDnsSearch {
421 Scalar(GeneratedString),
423 List(Vec<GeneratedString>),
425}
426
427#[derive(Clone, Debug, Eq, PartialEq)]
429pub struct GeneratedLongDevice {
430 source: GeneratedString,
431 target: Option<GeneratedString>,
432 permissions: Option<GeneratedString>,
433}
434
435impl GeneratedLongDevice {
436 pub fn new(
444 source: GeneratedString,
445 target: Option<GeneratedString>,
446 permissions: Option<GeneratedString>,
447 ) -> Result<Self, GenerationError> {
448 validate_generated_device_member("source", &source, true)?;
449 if let Some(target) = &target {
450 validate_generated_device_member("target", target, false)?;
451 }
452 if let Some(permissions) = &permissions {
453 validate_generated_device_member("permissions", permissions, false)?;
454 }
455 Ok(Self {
456 source,
457 target,
458 permissions,
459 })
460 }
461
462 #[must_use]
464 pub const fn source(&self) -> &GeneratedString {
465 &self.source
466 }
467
468 #[must_use]
470 pub const fn target(&self) -> Option<&GeneratedString> {
471 self.target.as_ref()
472 }
473
474 #[must_use]
476 pub const fn permissions(&self) -> Option<&GeneratedString> {
477 self.permissions.as_ref()
478 }
479
480 fn is_sensitive(&self) -> bool {
481 self.source.is_sensitive()
482 || self.target.as_ref().is_some_and(GeneratedString::is_sensitive)
483 || self.permissions.as_ref().is_some_and(GeneratedString::is_sensitive)
484 }
485}
486
487#[derive(Clone, Debug, Eq, PartialEq)]
489#[non_exhaustive]
490pub enum GeneratedDevice {
491 Short(GeneratedString),
493 Long(GeneratedLongDevice),
495}
496
497#[derive(Clone, Debug, Eq, PartialEq)]
499#[non_exhaustive]
500pub enum GeneratedLoggingOptionValue {
501 String(GeneratedString),
503 Number(GeneratedString),
505 Null,
507}
508
509impl GeneratedLoggingOptionValue {
510 fn is_sensitive(&self) -> bool {
511 match self {
512 Self::String(value) | Self::Number(value) => value.is_sensitive(),
513 Self::Null => false,
514 }
515 }
516}
517
518#[derive(Clone, Debug, Eq, PartialEq)]
520pub struct GeneratedLoggingOption {
521 name: String,
522 value: GeneratedLoggingOptionValue,
523}
524
525impl GeneratedLoggingOption {
526 pub fn new(name: impl Into<String>, value: GeneratedLoggingOptionValue) -> Result<Self, GenerationError> {
533 let name = required("logging option key", name.into())?;
534 if let GeneratedLoggingOptionValue::Number(number) = &value
535 && !valid_yaml_number(number.expose())
536 {
537 return Err(GenerationError::InvalidLoggingOptionNumber);
538 }
539 Ok(Self { name, value })
540 }
541
542 #[must_use]
544 pub fn name(&self) -> &str {
545 &self.name
546 }
547
548 #[must_use]
550 pub const fn value(&self) -> &GeneratedLoggingOptionValue {
551 &self.value
552 }
553}
554
555#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct GeneratedLogging {
558 driver: GeneratedString,
559 options: Vec<GeneratedLoggingOption>,
560}
561
562impl GeneratedLogging {
563 pub fn new(driver: GeneratedString, options: Vec<GeneratedLoggingOption>) -> Result<Self, GenerationError> {
572 let mut seen = BTreeSet::new();
573 for option in &options {
574 if !seen.insert(option.name()) {
575 return Err(GenerationError::DuplicateName {
576 kind: "logging option",
577 name: option.name().to_owned(),
578 });
579 }
580 }
581 Ok(Self { driver, options })
582 }
583
584 #[must_use]
586 pub const fn driver(&self) -> &GeneratedString {
587 &self.driver
588 }
589
590 #[must_use]
592 pub fn options(&self) -> &[GeneratedLoggingOption] {
593 &self.options
594 }
595
596 fn is_sensitive(&self) -> bool {
597 self.driver.is_sensitive() || self.options.iter().any(|option| option.value.is_sensitive())
598 }
599}
600
601#[derive(Clone, Debug, Eq, PartialEq)]
603#[non_exhaustive]
604pub enum GeneratedNetworkDriverOptionValue {
605 String(GeneratedString),
607 Number(GeneratedString),
609}
610
611impl GeneratedNetworkDriverOptionValue {
612 fn is_sensitive(&self) -> bool {
613 match self {
614 Self::String(value) | Self::Number(value) => value.is_sensitive(),
615 }
616 }
617}
618
619#[derive(Clone, Debug, Eq, PartialEq)]
621pub struct GeneratedNetworkDriverOption {
622 name: String,
623 value: GeneratedNetworkDriverOptionValue,
624}
625
626impl GeneratedNetworkDriverOption {
627 pub fn new(name: impl Into<String>, value: GeneratedNetworkDriverOptionValue) -> Result<Self, GenerationError> {
634 let name = required("network driver option key", name.into())?;
635 if let GeneratedNetworkDriverOptionValue::Number(number) = &value
636 && !valid_yaml_number(number.expose())
637 {
638 return Err(GenerationError::InvalidNetworkDriverOptionNumber);
639 }
640 Ok(Self { name, value })
641 }
642
643 #[must_use]
645 pub fn name(&self) -> &str {
646 &self.name
647 }
648
649 #[must_use]
651 pub const fn value(&self) -> &GeneratedNetworkDriverOptionValue {
652 &self.value
653 }
654}
655
656#[derive(Clone, Debug, Eq, PartialEq)]
658#[non_exhaustive]
659pub enum GeneratedVolumeDriverOptionValue {
660 String(GeneratedString),
662 Number(GeneratedString),
664}
665
666impl GeneratedVolumeDriverOptionValue {
667 fn is_sensitive(&self) -> bool {
668 match self {
669 Self::String(value) | Self::Number(value) => value.is_sensitive(),
670 }
671 }
672}
673
674#[derive(Clone, Debug, Eq, PartialEq)]
676pub struct GeneratedVolumeDriverOption {
677 name: String,
678 value: GeneratedVolumeDriverOptionValue,
679}
680
681impl GeneratedVolumeDriverOption {
682 pub fn new(name: impl Into<String>, value: GeneratedVolumeDriverOptionValue) -> Result<Self, GenerationError> {
689 let name = required("volume driver option key", name.into())?;
690 if let GeneratedVolumeDriverOptionValue::Number(number) = &value
691 && !valid_yaml_number(number.expose())
692 {
693 return Err(GenerationError::InvalidVolumeDriverOptionNumber);
694 }
695 Ok(Self { name, value })
696 }
697
698 #[must_use]
700 pub fn name(&self) -> &str {
701 &self.name
702 }
703
704 #[must_use]
706 pub const fn value(&self) -> &GeneratedVolumeDriverOptionValue {
707 &self.value
708 }
709}
710
711#[derive(Clone, Debug, Eq, PartialEq)]
717pub struct GeneratedVolumeDefinition {
718 name: String,
719 custom_name: Option<String>,
720 driver: Option<GeneratedString>,
721 driver_opts: Option<Vec<GeneratedVolumeDriverOption>>,
722 labels: Option<Vec<GeneratedLabel>>,
723}
724
725impl GeneratedVolumeDefinition {
726 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
732 Ok(Self {
733 name: required("volume name", name.into())?,
734 custom_name: None,
735 driver: None,
736 driver_opts: None,
737 labels: None,
738 })
739 }
740
741 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
747 let name = required("custom volume name", name.into())?;
748 set_once(&mut self.custom_name, name, "volume name")
749 }
750
751 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
759 set_once(&mut self.driver, driver, "volume driver")
760 }
761
762 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedVolumeDriverOption>) -> Result<(), GenerationError> {
772 let mut seen = BTreeSet::new();
773 for option in &driver_opts {
774 if !seen.insert(option.name()) {
775 return Err(GenerationError::DuplicateName {
776 kind: "volume driver option",
777 name: option.name().to_owned(),
778 });
779 }
780 }
781 set_once(&mut self.driver_opts, driver_opts, "volume driver_opts")
782 }
783
784 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
794 let mut seen = BTreeSet::new();
795 for label in &labels {
796 if !seen.insert(label.name()) {
797 return Err(GenerationError::DuplicateName {
798 kind: "volume label",
799 name: label.name().to_owned(),
800 });
801 }
802 }
803 set_once(&mut self.labels, labels, "volume labels")
804 }
805
806 #[must_use]
808 pub fn name(&self) -> &str {
809 &self.name
810 }
811
812 #[must_use]
814 pub fn custom_name(&self) -> Option<&str> {
815 self.custom_name.as_deref()
816 }
817
818 #[must_use]
820 pub const fn driver(&self) -> Option<&GeneratedString> {
821 self.driver.as_ref()
822 }
823
824 #[must_use]
826 pub fn driver_opts(&self) -> Option<&[GeneratedVolumeDriverOption]> {
827 self.driver_opts.as_deref()
828 }
829
830 #[must_use]
832 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
833 self.labels.as_deref()
834 }
835
836 fn is_sensitive(&self) -> bool {
837 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
838 || self
839 .driver_opts
840 .as_ref()
841 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
842 || self
843 .labels
844 .as_ref()
845 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
846 }
847}
848
849#[derive(Clone, Debug, Eq, PartialEq)]
854pub struct GeneratedNetworkDefinition {
855 name: String,
856 custom_name: Option<String>,
857 driver: Option<GeneratedString>,
858 driver_opts: Option<Vec<GeneratedNetworkDriverOption>>,
859 enable_ipv6: Option<bool>,
860 internal: Option<bool>,
861 labels: Option<Vec<GeneratedLabel>>,
862}
863
864impl GeneratedNetworkDefinition {
865 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
871 Ok(Self {
872 name: required("network name", name.into())?,
873 custom_name: None,
874 driver: None,
875 driver_opts: None,
876 enable_ipv6: None,
877 internal: None,
878 labels: None,
879 })
880 }
881
882 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
888 let name = required("custom network name", name.into())?;
889 set_once(&mut self.custom_name, name, "network name")
890 }
891
892 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
900 set_once(&mut self.driver, driver, "network driver")
901 }
902
903 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedNetworkDriverOption>) -> Result<(), GenerationError> {
913 let mut seen = BTreeSet::new();
914 for option in &driver_opts {
915 if !seen.insert(option.name()) {
916 return Err(GenerationError::DuplicateName {
917 kind: "network driver option",
918 name: option.name().to_owned(),
919 });
920 }
921 }
922 set_once(&mut self.driver_opts, driver_opts, "network driver_opts")
923 }
924
925 pub fn set_enable_ipv6(&mut self, enable_ipv6: bool) -> Result<(), GenerationError> {
934 set_once(&mut self.enable_ipv6, enable_ipv6, "network enable_ipv6")
935 }
936
937 pub fn set_internal(&mut self, internal: bool) -> Result<(), GenerationError> {
946 set_once(&mut self.internal, internal, "network internal")
947 }
948
949 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
959 let mut seen = BTreeSet::new();
960 for label in &labels {
961 if !seen.insert(label.name()) {
962 return Err(GenerationError::DuplicateName {
963 kind: "network label",
964 name: label.name().to_owned(),
965 });
966 }
967 }
968 set_once(&mut self.labels, labels, "network labels")
969 }
970
971 #[must_use]
973 pub fn name(&self) -> &str {
974 &self.name
975 }
976
977 #[must_use]
979 pub fn custom_name(&self) -> Option<&str> {
980 self.custom_name.as_deref()
981 }
982
983 #[must_use]
985 pub const fn driver(&self) -> Option<&GeneratedString> {
986 self.driver.as_ref()
987 }
988
989 #[must_use]
991 pub fn driver_opts(&self) -> Option<&[GeneratedNetworkDriverOption]> {
992 self.driver_opts.as_deref()
993 }
994
995 #[must_use]
997 pub const fn enable_ipv6(&self) -> Option<bool> {
998 self.enable_ipv6
999 }
1000
1001 #[must_use]
1003 pub const fn internal(&self) -> Option<bool> {
1004 self.internal
1005 }
1006
1007 #[must_use]
1009 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
1010 self.labels.as_deref()
1011 }
1012
1013 fn is_sensitive(&self) -> bool {
1014 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
1015 || self
1016 .driver_opts
1017 .as_ref()
1018 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
1019 || self
1020 .labels
1021 .as_ref()
1022 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
1023 }
1024}
1025
1026impl GeneratedDevice {
1027 fn is_sensitive(&self) -> bool {
1028 match self {
1029 Self::Short(value) => value.is_sensitive(),
1030 Self::Long(value) => value.is_sensitive(),
1031 }
1032 }
1033}
1034
1035#[derive(Clone, Debug, Eq, PartialEq)]
1037pub struct GeneratedSysctl {
1038 name: String,
1039 value: GeneratedString,
1040}
1041
1042impl GeneratedSysctl {
1043 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1051 let name = name.into();
1052 if name.is_empty()
1053 || name.contains(['\0', '\r', '\n'])
1054 || name.contains('$')
1055 || value.expose().contains(['\r', '\n', '$'])
1056 {
1057 return Err(if name.is_empty() || name.contains(['\0', '\r', '\n', '$']) {
1058 GenerationError::InvalidSysctlName
1059 } else {
1060 GenerationError::InvalidSysctlValue
1061 });
1062 }
1063 Ok(Self { name, value })
1064 }
1065
1066 #[must_use]
1068 pub fn name(&self) -> &str {
1069 &self.name
1070 }
1071
1072 #[must_use]
1074 pub const fn value(&self) -> &GeneratedString {
1075 &self.value
1076 }
1077}
1078
1079#[derive(Clone, Debug, Eq, PartialEq)]
1081#[non_exhaustive]
1082pub enum GeneratedSysctls {
1083 Map(Vec<GeneratedSysctl>),
1085 List(Vec<GeneratedString>),
1087}
1088
1089#[derive(Clone, Debug, Eq, PartialEq)]
1091#[non_exhaustive]
1092pub enum GeneratedUlimitValue {
1093 Single(GeneratedString),
1095 Range {
1097 soft: Option<GeneratedString>,
1099 hard: Option<GeneratedString>,
1101 },
1102}
1103
1104#[derive(Clone, Debug, Eq, PartialEq)]
1106pub struct GeneratedUlimit {
1107 name: String,
1108 value: GeneratedUlimitValue,
1109}
1110
1111impl GeneratedUlimit {
1112 pub fn new(name: impl Into<String>, value: GeneratedUlimitValue) -> Result<Self, GenerationError> {
1119 let name = name.into();
1120 if !valid_ulimit_name(&name) {
1121 return Err(GenerationError::InvalidUlimitName);
1122 }
1123 match &value {
1124 GeneratedUlimitValue::Single(value) => validate_generated_ulimit_value(value)?,
1125 GeneratedUlimitValue::Range { soft, hard } => {
1126 let soft = soft.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("soft"))?;
1127 let hard = hard.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("hard"))?;
1128 validate_generated_ulimit_value(soft)?;
1129 validate_generated_ulimit_value(hard)?;
1130 }
1131 }
1132 Ok(Self { name, value })
1133 }
1134
1135 pub fn single(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1141 Self::new(name, GeneratedUlimitValue::Single(value))
1142 }
1143
1144 pub fn range(
1150 name: impl Into<String>,
1151 soft: GeneratedString,
1152 hard: GeneratedString,
1153 ) -> Result<Self, GenerationError> {
1154 Self::new(
1155 name,
1156 GeneratedUlimitValue::Range {
1157 soft: Some(soft),
1158 hard: Some(hard),
1159 },
1160 )
1161 }
1162
1163 #[must_use]
1165 pub fn name(&self) -> &str {
1166 &self.name
1167 }
1168
1169 #[must_use]
1171 pub const fn value(&self) -> &GeneratedUlimitValue {
1172 &self.value
1173 }
1174
1175 fn is_sensitive(&self) -> bool {
1176 match &self.value {
1177 GeneratedUlimitValue::Single(value) => value.is_sensitive(),
1178 GeneratedUlimitValue::Range { soft, hard } => {
1179 soft.iter().chain(hard.iter()).any(GeneratedString::is_sensitive)
1180 }
1181 }
1182 }
1183}
1184
1185#[derive(Clone, Debug, Eq, PartialEq)]
1187pub struct GeneratedUlimits {
1188 entries: Vec<GeneratedUlimit>,
1189}
1190
1191impl GeneratedUlimits {
1192 pub fn new(entries: Vec<GeneratedUlimit>) -> Result<Self, GenerationError> {
1198 let mut seen = BTreeSet::new();
1199 for entry in &entries {
1200 if !seen.insert(entry.name()) {
1201 return Err(GenerationError::DuplicateName {
1202 kind: "ulimit",
1203 name: entry.name().to_owned(),
1204 });
1205 }
1206 }
1207 Ok(Self { entries })
1208 }
1209
1210 #[must_use]
1212 pub fn entries(&self) -> &[GeneratedUlimit] {
1213 &self.entries
1214 }
1215
1216 #[must_use]
1218 pub fn is_empty(&self) -> bool {
1219 self.entries.is_empty()
1220 }
1221}
1222
1223#[derive(Clone, Debug, Eq, PartialEq)]
1225#[non_exhaustive]
1226pub enum GeneratedHostname {
1227 Resolved(GeneratedString),
1229}
1230
1231#[derive(Clone, Debug, Eq, PartialEq)]
1233pub struct GeneratedEnvironment {
1234 name: String,
1235 value: Option<GeneratedString>,
1236}
1237
1238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1240#[non_exhaustive]
1241pub enum GeneratedEnvironmentFileFormat {
1242 Raw,
1244}
1245
1246#[derive(Clone, Debug, Eq, PartialEq)]
1248#[non_exhaustive]
1249pub enum GeneratedEnvironmentFile {
1250 Short(GeneratedString),
1252 Long {
1254 path: GeneratedString,
1256 required: Option<bool>,
1258 format: Option<GeneratedEnvironmentFileFormat>,
1260 },
1261}
1262
1263#[derive(Clone, Debug, Eq, PartialEq)]
1265pub struct GeneratedLabel {
1266 name: String,
1267 value: GeneratedString,
1268}
1269
1270#[derive(Clone, Debug, Eq, PartialEq)]
1272pub struct GeneratedAnnotation {
1273 name: String,
1274 value: GeneratedString,
1275}
1276
1277impl GeneratedAnnotation {
1278 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1285 let name = name.into();
1286 if name.is_empty() || name.contains(['$', '\r', '\n', '\0']) {
1287 return Err(GenerationError::InvalidAnnotationName);
1288 }
1289 if value.expose().contains(['$', '\r', '\n', '\0']) {
1290 return Err(GenerationError::InvalidAnnotationValue);
1291 }
1292 Ok(Self { name, value })
1293 }
1294
1295 #[must_use]
1297 pub fn name(&self) -> &str {
1298 &self.name
1299 }
1300
1301 #[must_use]
1303 pub const fn value(&self) -> &GeneratedString {
1304 &self.value
1305 }
1306}
1307
1308impl GeneratedLabel {
1309 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1316 Ok(Self {
1317 name: required("label name", name.into())?,
1318 value,
1319 })
1320 }
1321
1322 #[must_use]
1324 pub fn name(&self) -> &str {
1325 &self.name
1326 }
1327
1328 #[must_use]
1330 pub const fn value(&self) -> &GeneratedString {
1331 &self.value
1332 }
1333}
1334
1335impl GeneratedEnvironment {
1336 pub fn literal(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1342 Ok(Self {
1343 name: environment_name(name.into())?,
1344 value: Some(value),
1345 })
1346 }
1347
1348 pub fn host(name: impl Into<String>) -> Result<Self, GenerationError> {
1354 Ok(Self {
1355 name: environment_name(name.into())?,
1356 value: None,
1357 })
1358 }
1359
1360 #[must_use]
1362 pub fn name(&self) -> &str {
1363 &self.name
1364 }
1365
1366 #[must_use]
1368 pub const fn value(&self) -> Option<&GeneratedString> {
1369 self.value.as_ref()
1370 }
1371}
1372
1373impl GeneratedEnvironmentFile {
1374 pub fn short(path: GeneratedString) -> Result<Self, GenerationError> {
1381 require_generated_string("environment-file path", &path)?;
1382 Ok(Self::Short(path))
1383 }
1384
1385 pub fn long(
1392 path: GeneratedString,
1393 required: Option<bool>,
1394 format: Option<GeneratedEnvironmentFileFormat>,
1395 ) -> Result<Self, GenerationError> {
1396 require_generated_string("environment-file path", &path)?;
1397 Ok(Self::Long { path, required, format })
1398 }
1399
1400 #[must_use]
1402 pub const fn path(&self) -> &GeneratedString {
1403 match self {
1404 Self::Short(path) | Self::Long { path, .. } => path,
1405 }
1406 }
1407
1408 #[must_use]
1410 pub const fn required(&self) -> Option<bool> {
1411 match self {
1412 Self::Short(_) => None,
1413 Self::Long { required, .. } => *required,
1414 }
1415 }
1416
1417 #[must_use]
1419 pub const fn format(&self) -> Option<GeneratedEnvironmentFileFormat> {
1420 match self {
1421 Self::Short(_) => None,
1422 Self::Long { format, .. } => *format,
1423 }
1424 }
1425
1426 #[must_use]
1428 pub const fn is_sensitive(&self) -> bool {
1429 self.path().is_sensitive()
1430 }
1431}
1432
1433#[derive(Clone, Debug, Eq, PartialEq)]
1435pub struct GeneratedExtraHost {
1436 hostname: String,
1437 address: String,
1438}
1439
1440impl GeneratedExtraHost {
1441 pub fn new(hostname: impl Into<String>, address: impl Into<String>) -> Result<Self, GenerationError> {
1447 let hostname = short_component("extra-host hostname", hostname.into(), '=')?;
1448 let address = short_component("extra-host address", address.into(), '=')?;
1449 Ok(Self { hostname, address })
1450 }
1451
1452 #[must_use]
1454 pub fn hostname(&self) -> &str {
1455 &self.hostname
1456 }
1457
1458 #[must_use]
1460 pub fn address(&self) -> &str {
1461 &self.address
1462 }
1463}
1464
1465#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1467#[non_exhaustive]
1468pub enum GeneratedProtocol {
1469 Tcp,
1471 Udp,
1473 Sctp,
1475}
1476
1477impl GeneratedProtocol {
1478 const fn as_str(self) -> &'static str {
1479 match self {
1480 Self::Tcp => "tcp",
1481 Self::Udp => "udp",
1482 Self::Sctp => "sctp",
1483 }
1484 }
1485}
1486
1487#[derive(Clone, Debug, Eq, PartialEq)]
1489pub struct GeneratedPort {
1490 target: u16,
1491 published: Option<u16>,
1492 host_ip: Option<String>,
1493 protocol: GeneratedProtocol,
1494}
1495
1496impl GeneratedPort {
1497 pub fn new(
1505 target: u16,
1506 published: Option<u16>,
1507 host_ip: Option<String>,
1508 protocol: GeneratedProtocol,
1509 ) -> Result<Self, GenerationError> {
1510 if target == 0 {
1511 return Err(GenerationError::InvalidPort);
1512 }
1513 if let Some(host_ip) = host_ip.as_deref() {
1514 required("port host address", host_ip.to_owned())?;
1515 if protocol == GeneratedProtocol::Sctp && published.is_none() {
1516 return Err(GenerationError::UnrepresentableSctpHostIp);
1517 }
1518 }
1519 Ok(Self {
1520 target,
1521 published,
1522 host_ip,
1523 protocol,
1524 })
1525 }
1526
1527 #[must_use]
1529 pub const fn target(&self) -> u16 {
1530 self.target
1531 }
1532
1533 #[must_use]
1535 pub const fn published(&self) -> Option<u16> {
1536 self.published
1537 }
1538
1539 #[must_use]
1541 pub fn host_ip(&self) -> Option<&str> {
1542 self.host_ip.as_deref()
1543 }
1544
1545 #[must_use]
1547 pub const fn protocol(&self) -> GeneratedProtocol {
1548 self.protocol
1549 }
1550}
1551
1552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1554#[non_exhaustive]
1555pub enum GeneratedSelinux {
1556 Private,
1558 Shared,
1560}
1561
1562impl GeneratedSelinux {
1563 const fn as_str(self) -> &'static str {
1564 match self {
1565 Self::Private => "Z",
1566 Self::Shared => "z",
1567 }
1568 }
1569}
1570
1571#[derive(Clone, Debug, Eq, PartialEq)]
1572enum GeneratedMountKind {
1573 Volume {
1574 source: String,
1575 },
1576 Bind {
1577 source: String,
1578 selinux: Option<GeneratedSelinux>,
1579 },
1580 Anonymous,
1581}
1582
1583#[derive(Clone, Debug, Eq, PartialEq)]
1585pub struct GeneratedMount {
1586 kind: GeneratedMountKind,
1587 target: String,
1588 read_only: bool,
1589}
1590
1591impl GeneratedMount {
1592 pub fn volume(
1598 source: impl Into<String>,
1599 target: impl Into<String>,
1600 read_only: bool,
1601 ) -> Result<Self, GenerationError> {
1602 Ok(Self {
1603 kind: GeneratedMountKind::Volume {
1604 source: required("volume source", source.into())?,
1605 },
1606 target: required("mount target", target.into())?,
1607 read_only,
1608 })
1609 }
1610
1611 pub fn bind(
1618 source: impl Into<String>,
1619 target: impl Into<String>,
1620 read_only: bool,
1621 selinux: Option<GeneratedSelinux>,
1622 ) -> Result<Self, GenerationError> {
1623 let source = required("bind source", source.into())?;
1624 let target = required("mount target", target.into())?;
1625 if selinux.is_some() && (source.contains(':') || target.contains(':')) {
1626 return Err(GenerationError::InvalidSelinuxBind);
1627 }
1628 Ok(Self {
1629 kind: GeneratedMountKind::Bind { source, selinux },
1630 target,
1631 read_only,
1632 })
1633 }
1634
1635 pub fn anonymous(target: impl Into<String>, read_only: bool) -> Result<Self, GenerationError> {
1641 Ok(Self {
1642 kind: GeneratedMountKind::Anonymous,
1643 target: required("mount target", target.into())?,
1644 read_only,
1645 })
1646 }
1647
1648 #[must_use]
1650 pub fn target(&self) -> &str {
1651 &self.target
1652 }
1653
1654 #[must_use]
1656 pub const fn read_only(&self) -> bool {
1657 self.read_only
1658 }
1659}
1660
1661#[derive(Clone, Eq, PartialEq)]
1663pub struct GeneratedNetworkAttachment {
1664 name: String,
1665 aliases: Vec<String>,
1666 alias_sensitivities: Vec<bool>,
1667 ipv4_address: Option<GeneratedString>,
1668 ipv6_address: Option<GeneratedString>,
1669}
1670
1671impl GeneratedNetworkAttachment {
1672 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1678 Ok(Self {
1679 name: required("network name", name.into())?,
1680 aliases: Vec::new(),
1681 alias_sensitivities: Vec::new(),
1682 ipv4_address: None,
1683 ipv6_address: None,
1684 })
1685 }
1686
1687 pub fn add_alias(&mut self, alias: impl Into<String>) -> Result<(), GenerationError> {
1693 self.add_alias_with_sensitivity(alias.into(), false)
1694 }
1695
1696 pub fn add_alias_value(&mut self, alias: &GeneratedString) -> Result<(), GenerationError> {
1703 let sensitive = alias.is_sensitive();
1704 self.add_alias_with_sensitivity(alias.expose().to_owned(), sensitive)
1705 }
1706
1707 fn add_alias_with_sensitivity(&mut self, alias: String, sensitive: bool) -> Result<(), GenerationError> {
1708 self.aliases.push(required("network alias", alias)?);
1709 self.alias_sensitivities.push(sensitive);
1710 Ok(())
1711 }
1712
1713 pub fn set_ipv4_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1721 set_once(&mut self.ipv4_address, address, "ipv4_address")
1722 }
1723
1724 pub fn set_ipv6_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1732 set_once(&mut self.ipv6_address, address, "ipv6_address")
1733 }
1734
1735 #[must_use]
1737 pub fn name(&self) -> &str {
1738 &self.name
1739 }
1740
1741 #[must_use]
1743 pub fn aliases(&self) -> &[String] {
1744 &self.aliases
1745 }
1746
1747 #[must_use]
1749 pub fn alias_sensitivities(&self) -> &[bool] {
1750 &self.alias_sensitivities
1751 }
1752
1753 #[must_use]
1755 pub const fn ipv4_address(&self) -> Option<&GeneratedString> {
1756 self.ipv4_address.as_ref()
1757 }
1758
1759 #[must_use]
1761 pub const fn ipv6_address(&self) -> Option<&GeneratedString> {
1762 self.ipv6_address.as_ref()
1763 }
1764
1765 fn is_sensitive(&self) -> bool {
1766 self.alias_sensitivities.iter().copied().any(std::convert::identity)
1767 || self.ipv4_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1768 || self.ipv6_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1769 }
1770}
1771
1772impl fmt::Debug for GeneratedNetworkAttachment {
1773 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1774 let aliases = self
1775 .aliases
1776 .iter()
1777 .enumerate()
1778 .map(|(index, alias)| {
1779 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
1780 "<redacted>"
1781 } else {
1782 alias.as_str()
1783 }
1784 })
1785 .collect::<Vec<_>>();
1786 formatter
1787 .debug_struct("GeneratedNetworkAttachment")
1788 .field("name", &self.name)
1789 .field("aliases", &aliases)
1790 .field("ipv4_address", &self.ipv4_address)
1791 .field("ipv6_address", &self.ipv6_address)
1792 .finish()
1793 }
1794}
1795
1796#[derive(Clone, Debug, Eq, PartialEq)]
1798pub struct GeneratedResource {
1799 name: String,
1800 external: bool,
1801 custom_name: Option<String>,
1802}
1803
1804impl GeneratedResource {
1805 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
1811 Ok(Self {
1812 name: required("resource name", name.into())?,
1813 external: false,
1814 custom_name: None,
1815 })
1816 }
1817
1818 pub fn external(name: impl Into<String>) -> Result<Self, GenerationError> {
1824 Ok(Self {
1825 name: required("resource name", name.into())?,
1826 external: true,
1827 custom_name: None,
1828 })
1829 }
1830
1831 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
1839 let name = required("custom resource name", name.into())?;
1840 set_once(&mut self.custom_name, name, "resource name")
1841 }
1842
1843 #[must_use]
1845 pub fn name(&self) -> &str {
1846 &self.name
1847 }
1848
1849 #[must_use]
1851 pub const fn is_external(&self) -> bool {
1852 self.external
1853 }
1854
1855 #[must_use]
1857 pub fn custom_name(&self) -> Option<&str> {
1858 self.custom_name.as_deref()
1859 }
1860}
1861
1862#[derive(Clone, Debug, Eq, PartialEq)]
1864#[non_exhaustive]
1865pub enum GeneratedCpuRtRuntime {
1866 Microseconds(GeneratedString),
1868 Duration(GeneratedString),
1870}
1871
1872impl GeneratedCpuRtRuntime {
1873 fn is_sensitive(&self) -> bool {
1874 match self {
1875 Self::Microseconds(value) | Self::Duration(value) => value.is_sensitive(),
1876 }
1877 }
1878}
1879
1880#[derive(Clone, Debug, Eq, PartialEq)]
1884#[non_exhaustive]
1885pub enum GeneratedServiceRuntimeField {
1886 Domainname(GeneratedString),
1888 Isolation(GeneratedString),
1890 MacAddress(GeneratedString),
1892 Uts(GeneratedString),
1894 UseApiSocket(bool),
1896 GpusAll(GeneratedString),
1898 CpuRtRuntime(GeneratedCpuRtRuntime),
1900 CpuShares(GeneratedString),
1902 Cpus(GeneratedString),
1904 Cpuset(GeneratedString),
1906 DeviceCgroupRules(Vec<GeneratedString>),
1908 Ipc(GeneratedString),
1910 MemReservation(GeneratedString),
1912 MemSwappiness(GeneratedString),
1914 MemswapLimit(GeneratedString),
1916 NetworkMode(GeneratedString),
1918 OomKillDisable(bool),
1920 OomScoreAdj(GeneratedString),
1922 Pid(GeneratedString),
1924 Scale(GeneratedString),
1926 VolumesFrom(Vec<GeneratedString>),
1928}
1929
1930impl GeneratedServiceRuntimeField {
1931 fn field_name(&self) -> &'static str {
1932 match self {
1933 Self::Domainname(_) => "domainname",
1934 Self::Isolation(_) => "isolation",
1935 Self::MacAddress(_) => "mac_address",
1936 Self::Uts(_) => "uts",
1937 Self::UseApiSocket(_) => "use_api_socket",
1938 Self::GpusAll(_) => "gpus",
1939 Self::CpuRtRuntime(_) => "cpu_rt_runtime",
1940 Self::CpuShares(_) => "cpu_shares",
1941 Self::Cpus(_) => "cpus",
1942 Self::Cpuset(_) => "cpuset",
1943 Self::DeviceCgroupRules(_) => "device_cgroup_rules",
1944 Self::Ipc(_) => "ipc",
1945 Self::MemReservation(_) => "mem_reservation",
1946 Self::MemSwappiness(_) => "mem_swappiness",
1947 Self::MemswapLimit(_) => "memswap_limit",
1948 Self::NetworkMode(_) => "network_mode",
1949 Self::OomKillDisable(_) => "oom_kill_disable",
1950 Self::OomScoreAdj(_) => "oom_score_adj",
1951 Self::Pid(_) => "pid",
1952 Self::Scale(_) => "scale",
1953 Self::VolumesFrom(_) => "volumes_from",
1954 }
1955 }
1956
1957 fn is_sensitive(&self) -> bool {
1958 match self {
1959 Self::Domainname(value)
1960 | Self::Isolation(value)
1961 | Self::MacAddress(value)
1962 | Self::Uts(value)
1963 | Self::GpusAll(value)
1964 | Self::CpuShares(value)
1965 | Self::Cpus(value)
1966 | Self::Cpuset(value)
1967 | Self::Ipc(value)
1968 | Self::MemReservation(value)
1969 | Self::MemSwappiness(value)
1970 | Self::MemswapLimit(value)
1971 | Self::NetworkMode(value)
1972 | Self::OomScoreAdj(value)
1973 | Self::Pid(value)
1974 | Self::Scale(value) => value.is_sensitive(),
1975 Self::DeviceCgroupRules(values) | Self::VolumesFrom(values) => {
1976 values.iter().any(GeneratedString::is_sensitive)
1977 }
1978 Self::UseApiSocket(_) | Self::OomKillDisable(_) => false,
1979 Self::CpuRtRuntime(value) => value.is_sensitive(),
1980 }
1981 }
1982}
1983
1984#[derive(Clone, Debug, Eq, PartialEq)]
1986pub struct GeneratedService {
1987 name: String,
1988 hostname: Option<GeneratedHostname>,
1989 container_name: Option<GeneratedString>,
1990 image: Option<GeneratedString>,
1991 entrypoint: Option<GeneratedEntrypoint>,
1992 command: Option<GeneratedCommand>,
1993 init: Option<bool>,
1994 stdin_open: Option<bool>,
1995 tty: Option<bool>,
1996 privileged: Option<bool>,
1997 environment_files: Vec<GeneratedEnvironmentFile>,
1998 environment: Vec<GeneratedEnvironment>,
1999 labels: Vec<GeneratedLabel>,
2000 annotations: Option<Vec<GeneratedAnnotation>>,
2001 user: Option<GeneratedString>,
2002 userns_mode: Option<GeneratedString>,
2003 group_add: Vec<GeneratedString>,
2004 cap_add: Option<Vec<GeneratedString>>,
2005 cap_drop: Option<Vec<GeneratedString>>,
2006 devices: Option<Vec<GeneratedDevice>>,
2007 dns: Option<GeneratedDns>,
2008 dns_options: Option<Vec<GeneratedString>>,
2009 dns_search: Option<GeneratedDnsSearch>,
2010 expose: Option<Vec<GeneratedString>>,
2011 security_options: Option<Vec<GeneratedString>>,
2012 working_dir: Option<GeneratedString>,
2013 read_only: Option<bool>,
2014 pids_limit: Option<GeneratedPidsLimit>,
2015 shm_size: Option<GeneratedShmSize>,
2016 mem_limit: Option<GeneratedMemLimit>,
2017 tmpfs: Option<GeneratedTmpfs>,
2018 sysctls: Option<GeneratedSysctls>,
2019 logging: Option<GeneratedLogging>,
2020 ulimits: Option<GeneratedUlimits>,
2021 pull_policy: Option<GeneratedPullPolicy>,
2022 restart: Option<GeneratedRestartPolicy>,
2023 stop_signal: Option<GeneratedString>,
2024 stop_grace_period: Option<GeneratedString>,
2025 extra_hosts: Vec<GeneratedExtraHost>,
2026 ports: Vec<GeneratedPort>,
2027 mounts: Vec<GeneratedMount>,
2028 networks: Vec<GeneratedNetworkAttachment>,
2029 runtime_fields: Vec<GeneratedServiceRuntimeField>,
2030 extensions: Vec<GeneratedExtension>,
2031}
2032
2033impl GeneratedService {
2034 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
2040 Ok(Self {
2041 name: required("service name", name.into())?,
2042 hostname: None,
2043 container_name: None,
2044 image: None,
2045 entrypoint: None,
2046 command: None,
2047 init: None,
2048 stdin_open: None,
2049 tty: None,
2050 privileged: None,
2051 environment_files: Vec::new(),
2052 environment: Vec::new(),
2053 labels: Vec::new(),
2054 annotations: None,
2055 user: None,
2056 userns_mode: None,
2057 group_add: Vec::new(),
2058 cap_add: None,
2059 cap_drop: None,
2060 devices: None,
2061 dns: None,
2062 dns_options: None,
2063 dns_search: None,
2064 expose: None,
2065 security_options: None,
2066 working_dir: None,
2067 read_only: None,
2068 pids_limit: None,
2069 shm_size: None,
2070 mem_limit: None,
2071 tmpfs: None,
2072 sysctls: None,
2073 logging: None,
2074 ulimits: None,
2075 pull_policy: None,
2076 restart: None,
2077 stop_signal: None,
2078 stop_grace_period: None,
2079 extra_hosts: Vec::new(),
2080 ports: Vec::new(),
2081 mounts: Vec::new(),
2082 networks: Vec::new(),
2083 runtime_fields: Vec::new(),
2084 extensions: Vec::new(),
2085 })
2086 }
2087
2088 pub fn add_extension(&mut self, extension: GeneratedExtension) -> Result<(), GenerationError> {
2093 add_extension(&mut self.extensions, extension)
2094 }
2095
2096 #[must_use]
2098 pub fn name(&self) -> &str {
2099 &self.name
2100 }
2101
2102 pub fn add_runtime_field(&mut self, field: GeneratedServiceRuntimeField) -> Result<(), GenerationError> {
2114 if self
2115 .runtime_fields
2116 .iter()
2117 .any(|existing| existing.field_name() == field.field_name())
2118 {
2119 return Err(GenerationError::DuplicateField(field.field_name()));
2120 }
2121 if !generated_runtime_field_safe(&field) {
2122 return Err(GenerationError::InvalidServiceRuntimeField(field.field_name()));
2123 }
2124 self.runtime_fields.push(field);
2125 Ok(())
2126 }
2127
2128 #[must_use]
2130 pub fn runtime_fields(&self) -> &[GeneratedServiceRuntimeField] {
2131 &self.runtime_fields
2132 }
2133
2134 pub fn set_hostname(&mut self, hostname: GeneratedHostname) -> Result<(), GenerationError> {
2142 let GeneratedHostname::Resolved(value) = &hostname;
2143 if !valid_hostname(value.expose()) {
2144 return Err(GenerationError::InvalidHostname);
2145 }
2146 set_once(&mut self.hostname, hostname, "hostname")
2147 }
2148
2149 pub fn set_container_name(&mut self, name: GeneratedString) -> Result<(), GenerationError> {
2157 if !valid_container_name(name.expose()) {
2158 return Err(GenerationError::InvalidContainerName);
2159 }
2160 set_once(&mut self.container_name, name, "container_name")
2161 }
2162
2163 pub fn set_image(&mut self, image: GeneratedString) -> Result<(), GenerationError> {
2170 require_generated_string("service image", &image)?;
2171 set_once(&mut self.image, image, "image")
2172 }
2173
2174 pub fn set_entrypoint(&mut self, entrypoint: GeneratedEntrypoint) -> Result<(), GenerationError> {
2180 set_once(&mut self.entrypoint, entrypoint, "entrypoint")
2181 }
2182
2183 pub fn set_command(&mut self, command: GeneratedCommand) -> Result<(), GenerationError> {
2189 set_once(&mut self.command, command, "command")
2190 }
2191
2192 pub fn set_init(&mut self, init: bool) -> Result<(), GenerationError> {
2198 set_once(&mut self.init, init, "init")
2199 }
2200
2201 pub fn set_stdin_open(&mut self, stdin_open: bool) -> Result<(), GenerationError> {
2207 set_once(&mut self.stdin_open, stdin_open, "stdin_open")
2208 }
2209
2210 pub fn set_tty(&mut self, tty: bool) -> Result<(), GenerationError> {
2216 set_once(&mut self.tty, tty, "tty")
2217 }
2218
2219 pub fn set_privileged(&mut self, privileged: bool) -> Result<(), GenerationError> {
2225 set_once(&mut self.privileged, privileged, "privileged")
2226 }
2227
2228 pub fn add_environment_file(&mut self, environment_file: GeneratedEnvironmentFile) {
2230 self.environment_files.push(environment_file);
2231 }
2232
2233 pub fn add_environment(&mut self, environment: GeneratedEnvironment) {
2235 self.environment.push(environment);
2236 }
2237
2238 pub fn add_label(&mut self, label: GeneratedLabel) -> Result<(), GenerationError> {
2244 if self.labels.iter().any(|candidate| candidate.name == label.name) {
2245 return Err(GenerationError::DuplicateName {
2246 kind: "service label",
2247 name: label.name,
2248 });
2249 }
2250 self.labels.push(label);
2251 Ok(())
2252 }
2253
2254 pub fn set_annotations(&mut self, annotations: Vec<GeneratedAnnotation>) -> Result<(), GenerationError> {
2265 let mut seen = BTreeSet::new();
2266 for annotation in &annotations {
2267 if annotation.name.is_empty() || annotation.name.contains(['$', '\r', '\n', '\0']) {
2268 return Err(GenerationError::InvalidAnnotationName);
2269 }
2270 if annotation.value.expose().contains(['$', '\r', '\n', '\0']) {
2271 return Err(GenerationError::InvalidAnnotationValue);
2272 }
2273 if !seen.insert(annotation.name.as_str()) {
2274 return Err(GenerationError::DuplicateName {
2275 kind: "service annotation",
2276 name: annotation.name.clone(),
2277 });
2278 }
2279 }
2280 set_once(&mut self.annotations, annotations, "annotations")
2281 }
2282
2283 #[must_use]
2285 pub fn annotations(&self) -> Option<&[GeneratedAnnotation]> {
2286 self.annotations.as_deref()
2287 }
2288
2289 pub fn set_user(&mut self, user: GeneratedString) -> Result<(), GenerationError> {
2295 set_once(&mut self.user, user, "user")
2296 }
2297
2298 pub fn set_userns_mode(&mut self, mode: GeneratedString) -> Result<(), GenerationError> {
2305 require_generated_string("user namespace mode", &mode)?;
2306 set_once(&mut self.userns_mode, mode, "userns_mode")
2307 }
2308
2309 pub fn add_supplementary_group(&mut self, group: GeneratedString) -> Result<(), GenerationError> {
2315 require_generated_string("supplementary group", &group)?;
2316 self.group_add.push(group);
2317 Ok(())
2318 }
2319
2320 pub fn set_cap_add(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2333 let mut seen = BTreeSet::new();
2334 for capability in &capabilities {
2335 require_generated_string("cap_add item", capability)?;
2336 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2337 return Err(GenerationError::ContainsLineBreak("cap_add item"));
2338 }
2339 if !seen.insert(capability.expose()) {
2340 return Err(GenerationError::DuplicateItem("cap_add"));
2341 }
2342 }
2343 set_once(&mut self.cap_add, capabilities, "cap_add")
2344 }
2345
2346 #[must_use]
2348 pub fn cap_add(&self) -> Option<&[GeneratedString]> {
2349 self.cap_add.as_deref()
2350 }
2351
2352 pub fn set_cap_drop(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2365 let mut seen = BTreeSet::new();
2366 for capability in &capabilities {
2367 require_generated_string("cap_drop item", capability)?;
2368 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2369 return Err(GenerationError::ContainsLineBreak("cap_drop item"));
2370 }
2371 if !seen.insert(capability.expose()) {
2372 return Err(GenerationError::DuplicateItem("cap_drop"));
2373 }
2374 }
2375 set_once(&mut self.cap_drop, capabilities, "cap_drop")
2376 }
2377
2378 #[must_use]
2380 pub fn cap_drop(&self) -> Option<&[GeneratedString]> {
2381 self.cap_drop.as_deref()
2382 }
2383
2384 pub fn set_devices(&mut self, devices: Vec<GeneratedDevice>) -> Result<(), GenerationError> {
2397 for device in &devices {
2398 match device {
2399 GeneratedDevice::Short(value) => {
2400 validate_generated_device_member("short item", value, true)?;
2401 }
2402 GeneratedDevice::Long(value) => {
2403 validate_generated_device_member("source", value.source(), true)?;
2404 if let Some(target) = value.target() {
2405 validate_generated_device_member("target", target, false)?;
2406 }
2407 if let Some(permissions) = value.permissions() {
2408 validate_generated_device_member("permissions", permissions, false)?;
2409 }
2410 }
2411 }
2412 }
2413 set_once(&mut self.devices, devices, "devices")
2414 }
2415
2416 pub fn set_dns(&mut self, dns: GeneratedDns) -> Result<(), GenerationError> {
2426 let values = match &dns {
2427 GeneratedDns::Scalar(value) => std::slice::from_ref(value),
2428 GeneratedDns::List(values) => values.as_slice(),
2429 };
2430 for value in values {
2431 if value.expose().is_empty()
2432 || value.expose().contains('$')
2433 || value.expose().contains('\r')
2434 || value.expose().contains('\n')
2435 {
2436 return Err(GenerationError::InvalidDnsValue);
2437 }
2438 }
2439 set_once(&mut self.dns, dns, "dns")
2440 }
2441
2442 #[must_use]
2444 pub const fn dns(&self) -> Option<&GeneratedDns> {
2445 self.dns.as_ref()
2446 }
2447
2448 pub fn set_dns_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2460 let mut seen = BTreeSet::new();
2461 for option in &options {
2462 if option.expose().is_empty()
2463 || option.expose().contains('$')
2464 || option.expose().contains('\r')
2465 || option.expose().contains('\n')
2466 || option.expose().contains('\0')
2467 {
2468 return Err(GenerationError::InvalidDnsOptionValue);
2469 }
2470 if !seen.insert(option.expose()) {
2471 return Err(GenerationError::DuplicateItem("dns_opt"));
2472 }
2473 }
2474 set_once(&mut self.dns_options, options, "dns_opt")
2475 }
2476
2477 #[must_use]
2479 pub fn dns_options(&self) -> Option<&[GeneratedString]> {
2480 self.dns_options.as_deref()
2481 }
2482
2483 pub fn set_dns_search(&mut self, search: GeneratedDnsSearch) -> Result<(), GenerationError> {
2493 let values = match &search {
2494 GeneratedDnsSearch::Scalar(value) => std::slice::from_ref(value),
2495 GeneratedDnsSearch::List(values) => values.as_slice(),
2496 };
2497 for value in values {
2498 if value.expose().is_empty()
2499 || value.expose().contains('$')
2500 || value.expose().contains('\r')
2501 || value.expose().contains('\n')
2502 || value.expose().contains('\0')
2503 {
2504 return Err(GenerationError::InvalidDnsSearchValue);
2505 }
2506 }
2507 set_once(&mut self.dns_search, search, "dns_search")
2508 }
2509
2510 #[must_use]
2512 pub const fn dns_search(&self) -> Option<&GeneratedDnsSearch> {
2513 self.dns_search.as_ref()
2514 }
2515
2516 pub fn set_expose(&mut self, expose: Vec<GeneratedString>) -> Result<(), GenerationError> {
2527 let mut seen = BTreeSet::new();
2528 for item in &expose {
2529 if !valid_generated_expose_item(item.expose()) {
2530 return Err(GenerationError::InvalidExposeValue);
2531 }
2532 if !seen.insert(item.expose()) {
2533 return Err(GenerationError::DuplicateItem("expose"));
2534 }
2535 }
2536 set_once(&mut self.expose, expose, "expose")
2537 }
2538
2539 #[must_use]
2541 pub fn expose(&self) -> Option<&[GeneratedString]> {
2542 self.expose.as_deref()
2543 }
2544
2545 pub fn set_security_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2555 for option in &options {
2556 if option.expose().is_empty()
2557 || option.expose().contains('$')
2558 || option.expose().contains('\r')
2559 || option.expose().contains('\n')
2560 || option.expose().contains('\0')
2561 {
2562 return Err(GenerationError::InvalidSecurityOptionValue);
2563 }
2564 }
2565 set_once(&mut self.security_options, options, "security_opt")
2566 }
2567
2568 #[must_use]
2570 pub fn security_options(&self) -> Option<&[GeneratedString]> {
2571 self.security_options.as_deref()
2572 }
2573
2574 #[must_use]
2576 pub fn devices(&self) -> Option<&[GeneratedDevice]> {
2577 self.devices.as_deref()
2578 }
2579
2580 pub fn set_working_dir(&mut self, directory: GeneratedString) -> Result<(), GenerationError> {
2587 require_generated_string("working directory", &directory)?;
2588 set_once(&mut self.working_dir, directory, "working_dir")
2589 }
2590
2591 pub fn set_read_only(&mut self, read_only: bool) -> Result<(), GenerationError> {
2597 set_once(&mut self.read_only, read_only, "read_only")
2598 }
2599
2600 pub fn set_pids_limit(&mut self, limit: GeneratedPidsLimit) -> Result<(), GenerationError> {
2608 if let GeneratedPidsLimit::Finite(decimal) = &limit
2609 && !valid_positive_pids_decimal(decimal)
2610 {
2611 return Err(GenerationError::InvalidPidsLimit);
2612 }
2613 set_once(&mut self.pids_limit, limit, "pids_limit")
2614 }
2615
2616 pub fn set_shm_size(&mut self, size: GeneratedShmSize) -> Result<(), GenerationError> {
2624 let GeneratedShmSize::Explicit { amount, .. } = &size;
2625 if !valid_generated_shm_amount(amount.expose()) {
2626 return Err(GenerationError::InvalidShmSize);
2627 }
2628 set_once(&mut self.shm_size, size, "shm_size")
2629 }
2630
2631 pub fn set_mem_limit(&mut self, limit: GeneratedMemLimit) -> Result<(), GenerationError> {
2639 let GeneratedMemLimit::Explicit { amount, .. } = &limit;
2640 if !valid_generated_mem_amount(amount.expose()) {
2641 return Err(GenerationError::InvalidMemLimit);
2642 }
2643 set_once(&mut self.mem_limit, limit, "mem_limit")
2644 }
2645
2646 pub fn set_tmpfs(&mut self, tmpfs: GeneratedTmpfs) -> Result<(), GenerationError> {
2657 let items = match &tmpfs {
2658 GeneratedTmpfs::Scalar(item) => std::slice::from_ref(item),
2659 GeneratedTmpfs::List(items) => items.as_slice(),
2660 };
2661 for item in items {
2662 require_generated_string("tmpfs item", item)?;
2663 if item.expose().contains('\r') || item.expose().contains('\n') {
2664 return Err(GenerationError::ContainsLineBreak("tmpfs item"));
2665 }
2666 if !valid_generated_tmpfs_item(item.expose()) {
2667 return Err(GenerationError::InvalidTmpfsItem);
2668 }
2669 }
2670 set_once(&mut self.tmpfs, tmpfs, "tmpfs")
2671 }
2672
2673 #[must_use]
2675 pub const fn tmpfs(&self) -> Option<&GeneratedTmpfs> {
2676 self.tmpfs.as_ref()
2677 }
2678
2679 pub fn set_sysctls(&mut self, sysctls: GeneratedSysctls) -> Result<(), GenerationError> {
2690 let mut seen = BTreeSet::new();
2691 match &sysctls {
2692 GeneratedSysctls::Map(entries) => {
2693 for entry in entries {
2694 if !seen.insert(entry.name()) {
2695 return Err(GenerationError::DuplicateName {
2696 kind: "sysctl",
2697 name: entry.name().to_owned(),
2698 });
2699 }
2700 }
2701 }
2702 GeneratedSysctls::List(items) => {
2703 for item in items {
2704 if item.expose().contains(['\r', '\n', '$']) {
2705 return Err(GenerationError::InvalidSysctlValue);
2706 }
2707 if !seen.insert(item.expose()) {
2708 return Err(GenerationError::DuplicateItem("sysctls"));
2709 }
2710 }
2711 }
2712 }
2713 set_once(&mut self.sysctls, sysctls, "sysctls")
2714 }
2715
2716 #[must_use]
2718 pub const fn sysctls(&self) -> Option<&GeneratedSysctls> {
2719 self.sysctls.as_ref()
2720 }
2721
2722 pub fn set_logging(&mut self, logging: GeneratedLogging) -> Result<(), GenerationError> {
2729 set_once(&mut self.logging, logging, "logging")
2730 }
2731
2732 #[must_use]
2734 pub const fn logging(&self) -> Option<&GeneratedLogging> {
2735 self.logging.as_ref()
2736 }
2737
2738 pub fn set_ulimits(&mut self, ulimits: GeneratedUlimits) -> Result<(), GenerationError> {
2747 set_once(&mut self.ulimits, ulimits, "ulimits")
2748 }
2749
2750 #[must_use]
2752 pub const fn ulimits(&self) -> Option<&GeneratedUlimits> {
2753 self.ulimits.as_ref()
2754 }
2755
2756 pub fn set_pull_policy(&mut self, policy: GeneratedPullPolicy) -> Result<(), GenerationError> {
2763 if let GeneratedPullPolicy::Every(duration) = &policy
2764 && !valid_pull_policy_duration(duration.expose())
2765 {
2766 return Err(GenerationError::InvalidPullPolicyDuration);
2767 }
2768 set_once(&mut self.pull_policy, policy, "pull_policy")
2769 }
2770
2771 pub fn set_restart(&mut self, restart: GeneratedRestartPolicy) -> Result<(), GenerationError> {
2777 set_once(&mut self.restart, restart, "restart")
2778 }
2779
2780 pub fn set_stop_signal(&mut self, signal: GeneratedString) -> Result<(), GenerationError> {
2787 set_once(&mut self.stop_signal, signal, "stop_signal")
2788 }
2789
2790 pub fn set_stop_grace_period(&mut self, period: GeneratedString) -> Result<(), GenerationError> {
2798 if !StopGracePeriod::parse(period.expose().to_owned()).is_valid() {
2799 return Err(GenerationError::InvalidStopGracePeriod);
2800 }
2801 set_once(&mut self.stop_grace_period, period, "stop_grace_period")
2802 }
2803
2804 pub fn add_extra_host(&mut self, host: GeneratedExtraHost) {
2806 self.extra_hosts.push(host);
2807 }
2808
2809 pub fn add_port(&mut self, port: GeneratedPort) {
2811 self.ports.push(port);
2812 }
2813
2814 pub fn add_mount(&mut self, mount: GeneratedMount) {
2816 self.mounts.push(mount);
2817 }
2818
2819 pub fn add_network(&mut self, network: GeneratedNetworkAttachment) -> Result<(), GenerationError> {
2825 if self.networks.iter().any(|candidate| candidate.name == network.name) {
2826 return Err(GenerationError::DuplicateName {
2827 kind: "service network",
2828 name: network.name,
2829 });
2830 }
2831 self.networks.push(network);
2832 Ok(())
2833 }
2834
2835 fn is_sensitive(&self) -> bool {
2836 self.extensions.iter().any(GeneratedExtension::is_sensitive)
2837 || matches!(
2838 self.hostname.as_ref(),
2839 Some(GeneratedHostname::Resolved(hostname)) if hostname.is_sensitive()
2840 )
2841 || self.image.as_ref().is_some_and(GeneratedString::is_sensitive)
2842 || self.entrypoint.as_ref().is_some_and(entrypoint_is_sensitive)
2843 || self.command.as_ref().is_some_and(command_is_sensitive)
2844 || self
2845 .environment_files
2846 .iter()
2847 .any(GeneratedEnvironmentFile::is_sensitive)
2848 || self
2849 .environment
2850 .iter()
2851 .filter_map(GeneratedEnvironment::value)
2852 .any(GeneratedString::is_sensitive)
2853 || self.labels.iter().any(|label| label.value.is_sensitive())
2854 || self
2855 .annotations
2856 .as_ref()
2857 .is_some_and(|items| items.iter().any(|annotation| annotation.value.is_sensitive()))
2858 || matches!(
2859 self.pull_policy.as_ref(),
2860 Some(GeneratedPullPolicy::Every(duration)) if duration.is_sensitive()
2861 )
2862 || matches!(
2863 self.shm_size.as_ref(),
2864 Some(GeneratedShmSize::Explicit { amount, .. }) if amount.is_sensitive()
2865 )
2866 || matches!(
2867 self.mem_limit.as_ref(),
2868 Some(GeneratedMemLimit::Explicit { amount, .. }) if amount.is_sensitive()
2869 )
2870 || match self.tmpfs.as_ref() {
2871 Some(GeneratedTmpfs::Scalar(item)) => item.is_sensitive(),
2872 Some(GeneratedTmpfs::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2873 None => false,
2874 }
2875 || match self.dns.as_ref() {
2876 Some(GeneratedDns::Scalar(value)) => value.is_sensitive(),
2877 Some(GeneratedDns::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2878 None => false,
2879 }
2880 || self
2881 .dns_options
2882 .as_ref()
2883 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2884 || self
2885 .runtime_fields
2886 .iter()
2887 .any(GeneratedServiceRuntimeField::is_sensitive)
2888 || match self.dns_search.as_ref() {
2889 Some(GeneratedDnsSearch::Scalar(value)) => value.is_sensitive(),
2890 Some(GeneratedDnsSearch::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2891 None => false,
2892 }
2893 || self
2894 .expose
2895 .as_ref()
2896 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2897 || self
2898 .security_options
2899 .as_ref()
2900 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2901 || match self.sysctls.as_ref() {
2902 Some(GeneratedSysctls::Map(entries)) => entries.iter().any(|entry| entry.value.is_sensitive()),
2903 Some(GeneratedSysctls::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2904 None => false,
2905 }
2906 || self.logging.as_ref().is_some_and(GeneratedLogging::is_sensitive)
2907 || self
2908 .ulimits
2909 .as_ref()
2910 .is_some_and(|limits| limits.entries.iter().any(GeneratedUlimit::is_sensitive))
2911 || [
2912 self.user.as_ref(),
2913 self.userns_mode.as_ref(),
2914 self.working_dir.as_ref(),
2915 self.stop_signal.as_ref(),
2916 self.stop_grace_period.as_ref(),
2917 ]
2918 .into_iter()
2919 .flatten()
2920 .any(GeneratedString::is_sensitive)
2921 || self.group_add.iter().any(GeneratedString::is_sensitive)
2922 || self
2923 .cap_add
2924 .as_ref()
2925 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2926 || self
2927 .cap_drop
2928 .as_ref()
2929 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2930 || self
2931 .devices
2932 .as_ref()
2933 .is_some_and(|items| items.iter().any(GeneratedDevice::is_sensitive))
2934 || self.networks.iter().any(GeneratedNetworkAttachment::is_sensitive)
2935 }
2936}
2937
2938#[derive(Clone, Debug, Eq, PartialEq)]
2939enum GeneratedNetwork {
2940 Basic(GeneratedResource),
2941 Definition(GeneratedNetworkDefinition),
2942}
2943
2944#[derive(Clone, Debug, Eq, PartialEq)]
2945enum GeneratedVolume {
2946 Basic(GeneratedResource),
2947 Definition(GeneratedVolumeDefinition),
2948}
2949
2950impl GeneratedVolume {
2951 fn name(&self) -> &str {
2952 match self {
2953 Self::Basic(volume) => volume.name(),
2954 Self::Definition(volume) => volume.name(),
2955 }
2956 }
2957
2958 fn is_sensitive(&self) -> bool {
2959 match self {
2960 Self::Basic(_) => false,
2961 Self::Definition(volume) => volume.is_sensitive(),
2962 }
2963 }
2964}
2965
2966impl GeneratedNetwork {
2967 fn name(&self) -> &str {
2968 match self {
2969 Self::Basic(network) => network.name(),
2970 Self::Definition(network) => network.name(),
2971 }
2972 }
2973
2974 fn is_sensitive(&self) -> bool {
2975 match self {
2976 Self::Basic(_) => false,
2977 Self::Definition(network) => network.is_sensitive(),
2978 }
2979 }
2980}
2981
2982#[derive(Clone, Eq, PartialEq)]
2987pub struct GeneratedConfigFileDefinition {
2988 name: String,
2989 file: GeneratedString,
2990}
2991
2992impl GeneratedConfigFileDefinition {
2993 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2999 Ok(Self {
3000 name: generated_file_resource_name(name.into())?,
3001 file: generated_file_resource_path(file)?,
3002 })
3003 }
3004
3005 #[must_use]
3007 pub fn name(&self) -> &str {
3008 &self.name
3009 }
3010
3011 #[must_use]
3013 pub const fn file(&self) -> &GeneratedString {
3014 &self.file
3015 }
3016
3017 fn is_sensitive(&self) -> bool {
3018 self.file.is_sensitive()
3019 }
3020}
3021
3022impl fmt::Debug for GeneratedConfigFileDefinition {
3023 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3024 formatter
3025 .debug_struct("GeneratedConfigFileDefinition")
3026 .field("name", &self.name)
3027 .field("file", &self.file)
3028 .finish()
3029 }
3030}
3031
3032#[derive(Clone, Eq, PartialEq)]
3037pub struct GeneratedSecretFileDefinition {
3038 name: String,
3039 file: GeneratedString,
3040}
3041
3042impl GeneratedSecretFileDefinition {
3043 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
3049 Ok(Self {
3050 name: generated_file_resource_name(name.into())?,
3051 file: generated_file_resource_path(file)?,
3052 })
3053 }
3054
3055 #[must_use]
3057 pub fn name(&self) -> &str {
3058 &self.name
3059 }
3060
3061 #[must_use]
3063 pub const fn file(&self) -> &GeneratedString {
3064 &self.file
3065 }
3066
3067 fn is_sensitive(&self) -> bool {
3068 self.file.is_sensitive()
3069 }
3070}
3071
3072impl fmt::Debug for GeneratedSecretFileDefinition {
3073 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3074 formatter
3075 .debug_struct("GeneratedSecretFileDefinition")
3076 .field("name", &self.name)
3077 .field("file", &self.file)
3078 .finish()
3079 }
3080}
3081
3082#[derive(Clone, Debug, Default, Eq, PartialEq)]
3084pub struct ComposeDocumentBuilder {
3085 name: Option<String>,
3086 services: Vec<GeneratedService>,
3087 networks: Vec<GeneratedNetwork>,
3088 volumes: Vec<GeneratedVolume>,
3089 configs: Vec<GeneratedConfigFileDefinition>,
3090 secrets: Vec<GeneratedSecretFileDefinition>,
3091 extensions: Vec<GeneratedExtension>,
3092}
3093
3094impl ComposeDocumentBuilder {
3095 #[must_use]
3097 pub const fn new() -> Self {
3098 Self {
3099 name: None,
3100 services: Vec::new(),
3101 networks: Vec::new(),
3102 volumes: Vec::new(),
3103 configs: Vec::new(),
3104 secrets: Vec::new(),
3105 extensions: Vec::new(),
3106 }
3107 }
3108
3109 pub fn add_extension(&mut self, extension: GeneratedExtension) -> Result<(), GenerationError> {
3114 add_extension(&mut self.extensions, extension)
3115 }
3116
3117 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
3123 let name = required("project name", name.into())?;
3124 set_once(&mut self.name, name, "name")
3125 }
3126
3127 pub fn add_service(&mut self, service: GeneratedService) -> Result<(), GenerationError> {
3133 insert_named(&mut self.services, service, "service", GeneratedService::name)
3134 }
3135
3136 pub fn add_network(&mut self, network: GeneratedResource) -> Result<(), GenerationError> {
3142 insert_named(
3143 &mut self.networks,
3144 GeneratedNetwork::Basic(network),
3145 "network",
3146 GeneratedNetwork::name,
3147 )
3148 }
3149
3150 pub fn add_network_definition(&mut self, network: GeneratedNetworkDefinition) -> Result<(), GenerationError> {
3160 insert_named(
3161 &mut self.networks,
3162 GeneratedNetwork::Definition(network),
3163 "network",
3164 GeneratedNetwork::name,
3165 )
3166 }
3167
3168 pub fn add_volume(&mut self, volume: GeneratedResource) -> Result<(), GenerationError> {
3174 insert_named(
3175 &mut self.volumes,
3176 GeneratedVolume::Basic(volume),
3177 "volume",
3178 GeneratedVolume::name,
3179 )
3180 }
3181
3182 pub fn add_volume_definition(&mut self, volume: GeneratedVolumeDefinition) -> Result<(), GenerationError> {
3193 insert_named(
3194 &mut self.volumes,
3195 GeneratedVolume::Definition(volume),
3196 "volume",
3197 GeneratedVolume::name,
3198 )
3199 }
3200
3201 pub fn add_config_file(&mut self, config: GeneratedConfigFileDefinition) -> Result<(), GenerationError> {
3207 insert_named(&mut self.configs, config, "config", GeneratedConfigFileDefinition::name)
3208 }
3209
3210 pub fn add_secret_file(&mut self, secret: GeneratedSecretFileDefinition) -> Result<(), GenerationError> {
3216 insert_named(&mut self.secrets, secret, "secret", GeneratedSecretFileDefinition::name)
3217 }
3218
3219 pub fn build(self, source_id: SourceId) -> Result<GeneratedComposeDocument, GenerationError> {
3226 if self.services.is_empty() {
3227 return Err(GenerationError::MissingService);
3228 }
3229 let sensitive = self.services.iter().any(GeneratedService::is_sensitive)
3230 || self.networks.iter().any(GeneratedNetwork::is_sensitive)
3231 || self.volumes.iter().any(GeneratedVolume::is_sensitive)
3232 || self.configs.iter().any(GeneratedConfigFileDefinition::is_sensitive)
3233 || self.secrets.iter().any(GeneratedSecretFileDefinition::is_sensitive)
3234 || self.extensions.iter().any(GeneratedExtension::is_sensitive);
3235 let text = render_document(&self);
3236 let syntax = SyntaxDocument::parse(source_id, text.clone())
3237 .map_err(|_| GenerationError::InternalInvariant("syntax-tree"))?;
3238 if !syntax.is_valid() {
3239 return Err(GenerationError::InternalInvariant("syntax"));
3240 }
3241 let model = ComposeDocument::parse(syntax.document());
3242 if !model.is_valid() {
3243 return Err(GenerationError::InternalInvariant("typed-model"));
3244 }
3245 let mut document = model
3246 .document()
3247 .cloned()
3248 .ok_or(GenerationError::InternalInvariant("document-root"))?;
3249 if sensitive {
3250 document.protect_extension_keys();
3251 }
3252 Ok(GeneratedComposeDocument {
3253 text,
3254 sensitive,
3255 document,
3256 })
3257 }
3258}
3259
3260#[derive(Clone, Eq, PartialEq)]
3262pub struct GeneratedComposeDocument {
3263 text: String,
3264 sensitive: bool,
3265 document: ComposeDocument,
3266}
3267
3268impl GeneratedComposeDocument {
3269 #[must_use]
3271 pub fn text(&self) -> &str {
3272 &self.text
3273 }
3274
3275 #[must_use]
3277 pub const fn document(&self) -> &ComposeDocument {
3278 &self.document
3279 }
3280
3281 #[must_use]
3283 pub const fn is_sensitive(&self) -> bool {
3284 self.sensitive
3285 }
3286}
3287
3288impl fmt::Debug for GeneratedComposeDocument {
3289 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3290 formatter
3291 .debug_struct("GeneratedComposeDocument")
3292 .field("text", &if self.sensitive { "<redacted>" } else { &self.text })
3293 .field("sensitive", &self.sensitive)
3294 .field("document", &if self.sensitive { "<redacted>" } else { "validated" })
3295 .finish()
3296 }
3297}
3298
3299fn render_document(project: &ComposeDocumentBuilder) -> String {
3300 let mut output = String::from("---\n");
3301 if let Some(name) = &project.name {
3302 output.push_str("name: ");
3303 write_quoted(&mut output, name);
3304 output.push('\n');
3305 }
3306 output.push_str("services:\n");
3307 for service in &project.services {
3308 write_indent(&mut output, 1);
3309 write_quoted(&mut output, &service.name);
3310 output.push_str(":\n");
3311 render_service(&mut output, service);
3312 }
3313 render_network_definitions(&mut output, &project.networks);
3314 render_volume_definitions(&mut output, &project.volumes);
3315 render_file_definitions(
3316 &mut output,
3317 "configs",
3318 &project.configs,
3319 GeneratedConfigFileDefinition::name,
3320 GeneratedConfigFileDefinition::file,
3321 );
3322 render_file_definitions(
3323 &mut output,
3324 "secrets",
3325 &project.secrets,
3326 GeneratedSecretFileDefinition::name,
3327 GeneratedSecretFileDefinition::file,
3328 );
3329 render_extensions(&mut output, &project.extensions, 0);
3330 output
3331}
3332
3333fn render_service(output: &mut String, service: &GeneratedService) {
3334 if let Some(GeneratedHostname::Resolved(hostname)) = &service.hostname {
3335 render_optional_string(output, "hostname", Some(hostname));
3336 }
3337 render_optional_string(output, "container_name", service.container_name.as_ref());
3338 render_optional_string(output, "image", service.image.as_ref());
3339 if let Some(entrypoint) = &service.entrypoint {
3340 render_entrypoint(output, entrypoint);
3341 }
3342 if let Some(command) = &service.command {
3343 render_command(output, command);
3344 }
3345 if let Some(init) = service.init {
3346 write_field(output, 2, "init");
3347 output.push_str(if init { "true\n" } else { "false\n" });
3348 }
3349 if let Some(stdin_open) = service.stdin_open {
3350 write_field(output, 2, "stdin_open");
3351 output.push_str(if stdin_open { "true\n" } else { "false\n" });
3352 }
3353 if let Some(tty) = service.tty {
3354 write_field(output, 2, "tty");
3355 output.push_str(if tty { "true\n" } else { "false\n" });
3356 }
3357 if let Some(privileged) = service.privileged {
3358 write_field(output, 2, "privileged");
3359 output.push_str(if privileged { "true\n" } else { "false\n" });
3360 }
3361 render_environment_files(output, &service.environment_files);
3362 render_environment(output, &service.environment);
3363 render_labels(output, &service.labels);
3364 if let Some(annotations) = &service.annotations {
3365 render_annotations(output, annotations);
3366 }
3367 render_optional_string(output, "user", service.user.as_ref());
3368 render_optional_string(output, "userns_mode", service.userns_mode.as_ref());
3369 render_string_sequence(output, "group_add", &service.group_add);
3370 if let Some(capabilities) = &service.cap_add {
3371 render_configured_string_sequence(output, "cap_add", capabilities);
3372 }
3373 if let Some(capabilities) = &service.cap_drop {
3374 render_configured_string_sequence(output, "cap_drop", capabilities);
3375 }
3376 render_optional_string(output, "working_dir", service.working_dir.as_ref());
3377 if let Some(read_only) = service.read_only {
3378 write_field(output, 2, "read_only");
3379 output.push_str(if read_only { "true\n" } else { "false\n" });
3380 }
3381 if let Some(pids_limit) = &service.pids_limit {
3382 render_pids_limit(output, pids_limit);
3383 }
3384 if let Some(shm_size) = &service.shm_size {
3385 render_shm_size(output, shm_size);
3386 }
3387 if let Some(mem_limit) = &service.mem_limit {
3388 render_mem_limit(output, mem_limit);
3389 }
3390 render_runtime_fields(output, &service.runtime_fields);
3391 if let Some(devices) = &service.devices {
3392 render_devices(output, devices);
3393 }
3394 if let Some(dns) = &service.dns {
3395 render_dns(output, dns);
3396 }
3397 if let Some(options) = &service.dns_options {
3398 render_configured_string_sequence(output, "dns_opt", options);
3399 }
3400 if let Some(search) = &service.dns_search {
3401 render_dns_search(output, search);
3402 }
3403 if let Some(expose) = &service.expose {
3404 render_configured_string_sequence(output, "expose", expose);
3405 }
3406 if let Some(options) = &service.security_options {
3407 render_configured_string_sequence(output, "security_opt", options);
3408 }
3409 if let Some(tmpfs) = &service.tmpfs {
3410 render_tmpfs(output, tmpfs);
3411 }
3412 if let Some(sysctls) = &service.sysctls {
3413 render_sysctls(output, sysctls);
3414 }
3415 if let Some(logging) = &service.logging {
3416 render_logging(output, logging);
3417 }
3418 if let Some(ulimits) = &service.ulimits {
3419 render_ulimits(output, ulimits);
3420 }
3421 if let Some(pull_policy) = &service.pull_policy {
3422 render_pull_policy(output, pull_policy);
3423 }
3424 if let Some(restart) = service.restart {
3425 render_restart(output, restart);
3426 }
3427 render_optional_string(output, "stop_signal", service.stop_signal.as_ref());
3428 render_optional_string(output, "stop_grace_period", service.stop_grace_period.as_ref());
3429 render_extra_hosts(output, &service.extra_hosts);
3430 render_ports(output, &service.ports);
3431 render_mounts(output, &service.mounts);
3432 render_networks(output, &service.networks);
3433 render_extensions(output, &service.extensions, 2);
3434}
3435
3436fn render_runtime_fields(output: &mut String, fields: &[GeneratedServiceRuntimeField]) {
3437 for field in fields {
3438 match field {
3439 GeneratedServiceRuntimeField::Domainname(value) => {
3440 render_optional_string(output, "domainname", Some(value));
3441 }
3442 GeneratedServiceRuntimeField::Isolation(value) => {
3443 render_optional_string(output, "isolation", Some(value));
3444 }
3445 GeneratedServiceRuntimeField::MacAddress(value) => {
3446 render_optional_string(output, "mac_address", Some(value));
3447 }
3448 GeneratedServiceRuntimeField::Uts(value) => {
3449 render_optional_string(output, "uts", Some(value));
3450 }
3451 GeneratedServiceRuntimeField::UseApiSocket(value) => {
3452 write_field(output, 2, "use_api_socket");
3453 output.push_str(if *value { "true\n" } else { "false\n" });
3454 }
3455 GeneratedServiceRuntimeField::GpusAll(value) => {
3456 render_optional_string(output, "gpus", Some(value));
3457 }
3458 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => {
3459 write_field(output, 2, "cpu_rt_runtime");
3460 output.push_str(value.expose());
3461 output.push('\n');
3462 }
3463 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3464 render_optional_string(output, "cpu_rt_runtime", Some(value));
3465 }
3466 GeneratedServiceRuntimeField::CpuShares(value) => {
3467 render_optional_string(output, "cpu_shares", Some(value));
3468 }
3469 GeneratedServiceRuntimeField::Cpus(value) => {
3470 render_optional_string(output, "cpus", Some(value));
3471 }
3472 GeneratedServiceRuntimeField::Cpuset(value) => {
3473 render_optional_string(output, "cpuset", Some(value));
3474 }
3475 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => {
3476 render_configured_string_sequence(output, "device_cgroup_rules", values);
3477 }
3478 GeneratedServiceRuntimeField::Ipc(value) => {
3479 render_optional_string(output, "ipc", Some(value));
3480 }
3481 GeneratedServiceRuntimeField::MemReservation(value) => {
3482 render_optional_string(output, "mem_reservation", Some(value));
3483 }
3484 GeneratedServiceRuntimeField::MemSwappiness(value) => {
3485 render_optional_string(output, "mem_swappiness", Some(value));
3486 }
3487 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3488 render_optional_string(output, "memswap_limit", Some(value));
3489 }
3490 GeneratedServiceRuntimeField::NetworkMode(value) => {
3491 render_optional_string(output, "network_mode", Some(value));
3492 }
3493 GeneratedServiceRuntimeField::OomKillDisable(value) => {
3494 write_field(output, 2, "oom_kill_disable");
3495 output.push_str(if *value { "true\n" } else { "false\n" });
3496 }
3497 GeneratedServiceRuntimeField::OomScoreAdj(value) => {
3498 render_optional_string(output, "oom_score_adj", Some(value));
3499 }
3500 GeneratedServiceRuntimeField::Pid(value) => {
3501 render_optional_string(output, "pid", Some(value));
3502 }
3503 GeneratedServiceRuntimeField::Scale(value) => {
3504 render_optional_string(output, "scale", Some(value));
3505 }
3506 GeneratedServiceRuntimeField::VolumesFrom(values) => {
3507 render_configured_string_sequence(output, "volumes_from", values);
3508 }
3509 }
3510 }
3511}
3512
3513fn generated_runtime_field_safe(field: &GeneratedServiceRuntimeField) -> bool {
3514 let safe = |value: &GeneratedString| !value.expose().is_empty() && !value.expose().contains(['\n', '\r', '$']);
3515 let unsigned = |value: &GeneratedString| safe(value) && value.expose().bytes().all(|byte| byte.is_ascii_digit());
3516 let bounded_unsigned = |value: &GeneratedString| unsigned(value) && value.expose().parse::<i128>().is_ok();
3517 let signed_range = |value: &GeneratedString, min: i32, max: i32| {
3518 safe(value)
3519 && value
3520 .expose()
3521 .parse::<i32>()
3522 .is_ok_and(|number| (min..=max).contains(&number))
3523 };
3524 let decimal = |value: &GeneratedString| safe(value) && normalize_generated_decimal(value.expose()).is_some();
3525 let reference = |value: &GeneratedString| {
3526 safe(value)
3527 && (!value.expose().contains(':')
3528 || value
3529 .expose()
3530 .split_once(':')
3531 .is_some_and(|(_, target)| !target.is_empty()))
3532 };
3533 match field {
3534 GeneratedServiceRuntimeField::Domainname(value)
3535 | GeneratedServiceRuntimeField::Isolation(value)
3536 | GeneratedServiceRuntimeField::MacAddress(value)
3537 | GeneratedServiceRuntimeField::Uts(value)
3538 | GeneratedServiceRuntimeField::Cpuset(value) => safe(value),
3539 GeneratedServiceRuntimeField::UseApiSocket(_) | GeneratedServiceRuntimeField::OomKillDisable(_) => true,
3540 GeneratedServiceRuntimeField::GpusAll(value) => safe(value) && value.expose() == "all",
3541 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => unsigned(value),
3542 GeneratedServiceRuntimeField::CpuShares(value) | GeneratedServiceRuntimeField::Scale(value) => {
3543 bounded_unsigned(value)
3544 }
3545 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3546 safe(value)
3547 && matches!(
3548 CpuRtRuntime::parse_string(value.expose().to_owned()),
3549 CpuRtRuntime::Duration(_)
3550 )
3551 }
3552 GeneratedServiceRuntimeField::Cpus(value) => decimal(value),
3553 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => values.iter().all(safe),
3554 GeneratedServiceRuntimeField::Ipc(value)
3555 | GeneratedServiceRuntimeField::NetworkMode(value)
3556 | GeneratedServiceRuntimeField::Pid(value) => reference(value),
3557 GeneratedServiceRuntimeField::MemReservation(value) => {
3558 safe(value) && valid_generated_runtime_memory(value.expose(), false)
3559 }
3560 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3561 safe(value) && valid_generated_runtime_memory(value.expose(), true)
3562 }
3563 GeneratedServiceRuntimeField::MemSwappiness(value) => signed_range(value, 0, 100),
3564 GeneratedServiceRuntimeField::OomScoreAdj(value) => signed_range(value, -1000, 1000),
3565 GeneratedServiceRuntimeField::VolumesFrom(values) => values.iter().all(reference),
3566 }
3567}
3568
3569fn normalize_generated_decimal(value: &str) -> Option<()> {
3570 let (whole, fraction) = value.split_once('.').unwrap_or((value, ""));
3571 let valid_shape = if value.contains('.') {
3572 !whole.is_empty() && !fraction.is_empty()
3573 } else {
3574 !whole.is_empty()
3575 };
3576 (valid_shape
3577 && whole.bytes().all(|byte| byte.is_ascii_digit())
3578 && fraction.bytes().all(|byte| byte.is_ascii_digit())
3579 && value.bytes().filter(|byte| *byte == b'.').count() <= 1)
3580 .then_some(())
3581}
3582
3583fn valid_generated_runtime_memory(value: &str, allow_unlimited: bool) -> bool {
3589 if allow_unlimited && value == "-1" {
3590 return true;
3591 }
3592 if !value.is_empty() && value.bytes().all(|byte| byte == b'0') {
3593 return true;
3594 }
3595 let Some(amount) = ["kb", "mb", "gb", "b", "k", "m", "g"]
3596 .into_iter()
3597 .find_map(|unit| value.strip_suffix(unit))
3598 else {
3599 return false;
3600 };
3601 !amount.is_empty() && amount.bytes().all(|byte| byte.is_ascii_digit())
3602}
3603
3604fn render_pids_limit(output: &mut String, limit: &GeneratedPidsLimit) {
3605 write_field(output, 2, "pids_limit");
3606 match limit {
3607 GeneratedPidsLimit::Unlimited => output.push_str("-1\n"),
3608 GeneratedPidsLimit::Finite(decimal) => {
3609 output.push_str(decimal);
3610 output.push('\n');
3611 }
3612 }
3613}
3614
3615fn render_shm_size(output: &mut String, size: &GeneratedShmSize) {
3616 let GeneratedShmSize::Explicit { amount, unit } = size;
3617 write_field(output, 2, "shm_size");
3618 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3619 output.push('\n');
3620}
3621
3622fn render_mem_limit(output: &mut String, limit: &GeneratedMemLimit) {
3623 let GeneratedMemLimit::Explicit { amount, unit } = limit;
3624 write_field(output, 2, "mem_limit");
3625 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3626 output.push('\n');
3627}
3628
3629fn render_devices(output: &mut String, devices: &[GeneratedDevice]) {
3630 if devices.is_empty() {
3631 output.push_str(" devices: []\n");
3632 return;
3633 }
3634 output.push_str(" devices:\n");
3635 for device in devices {
3636 match device {
3637 GeneratedDevice::Short(value) => {
3638 output.push_str(" - ");
3639 write_quoted(output, value.expose());
3640 output.push('\n');
3641 }
3642 GeneratedDevice::Long(value) => {
3643 output.push_str(" - source: ");
3644 write_quoted(output, value.source().expose());
3645 output.push('\n');
3646 if let Some(target) = value.target() {
3647 output.push_str(" target: ");
3648 write_quoted(output, target.expose());
3649 output.push('\n');
3650 }
3651 if let Some(permissions) = value.permissions() {
3652 output.push_str(" permissions: ");
3653 write_quoted(output, permissions.expose());
3654 output.push('\n');
3655 }
3656 }
3657 }
3658 }
3659}
3660
3661fn render_dns(output: &mut String, dns: &GeneratedDns) {
3662 match dns {
3663 GeneratedDns::Scalar(value) => render_optional_string(output, "dns", Some(value)),
3664 GeneratedDns::List(values) => render_configured_string_sequence(output, "dns", values),
3665 }
3666}
3667
3668fn render_dns_search(output: &mut String, search: &GeneratedDnsSearch) {
3669 match search {
3670 GeneratedDnsSearch::Scalar(value) => render_optional_string(output, "dns_search", Some(value)),
3671 GeneratedDnsSearch::List(values) => render_configured_string_sequence(output, "dns_search", values),
3672 }
3673}
3674
3675fn render_tmpfs(output: &mut String, tmpfs: &GeneratedTmpfs) {
3676 match tmpfs {
3677 GeneratedTmpfs::Scalar(item) => render_optional_string(output, "tmpfs", Some(item)),
3678 GeneratedTmpfs::List(items) => render_configured_string_sequence(output, "tmpfs", items),
3679 }
3680}
3681
3682fn render_sysctls(output: &mut String, sysctls: &GeneratedSysctls) {
3683 match sysctls {
3684 GeneratedSysctls::Map(entries) if entries.is_empty() => output.push_str(" sysctls: {}\n"),
3685 GeneratedSysctls::Map(entries) => {
3686 output.push_str(" sysctls:\n");
3687 for entry in entries {
3688 write_indent(output, 3);
3689 write_quoted(output, entry.name());
3690 output.push_str(": ");
3691 write_quoted(output, entry.value().expose());
3692 output.push('\n');
3693 }
3694 }
3695 GeneratedSysctls::List(items) => render_configured_string_sequence(output, "sysctls", items),
3696 }
3697}
3698
3699fn render_logging(output: &mut String, logging: &GeneratedLogging) {
3700 output.push_str(" logging:\n driver: ");
3701 write_quoted(output, logging.driver.expose());
3702 output.push('\n');
3703 if logging.options.is_empty() {
3704 output.push_str(" options: {}\n");
3705 return;
3706 }
3707 output.push_str(" options:\n");
3708 for option in &logging.options {
3709 write_indent(output, 4);
3710 write_quoted(output, option.name());
3711 output.push_str(": ");
3712 match option.value() {
3713 GeneratedLoggingOptionValue::String(value) => write_quoted(output, value.expose()),
3714 GeneratedLoggingOptionValue::Number(value) => output.push_str(value.expose()),
3715 GeneratedLoggingOptionValue::Null => output.push_str("null"),
3716 }
3717 output.push('\n');
3718 }
3719}
3720
3721fn render_ulimits(output: &mut String, ulimits: &GeneratedUlimits) {
3722 if ulimits.entries.is_empty() {
3723 output.push_str(" ulimits: {}\n");
3724 return;
3725 }
3726 output.push_str(" ulimits:\n");
3727 for limit in &ulimits.entries {
3728 write_indent(output, 3);
3729 write_quoted(output, limit.name());
3730 match limit.value() {
3731 GeneratedUlimitValue::Single(value) => {
3732 output.push_str(": ");
3733 write_quoted(output, value.expose());
3734 output.push('\n');
3735 }
3736 GeneratedUlimitValue::Range {
3737 soft: Some(soft),
3738 hard: Some(hard),
3739 } => {
3740 output.push_str(":\n");
3741 write_indent(output, 4);
3742 output.push_str("soft: ");
3743 write_quoted(output, soft.expose());
3744 output.push('\n');
3745 write_indent(output, 4);
3746 output.push_str("hard: ");
3747 write_quoted(output, hard.expose());
3748 output.push('\n');
3749 }
3750 GeneratedUlimitValue::Range { .. } => {
3751 unreachable!("generated ulimit ranges are validated during construction")
3752 }
3753 }
3754 }
3755}
3756
3757fn render_pull_policy(output: &mut String, policy: &GeneratedPullPolicy) {
3758 write_field(output, 2, "pull_policy");
3759 let value = match policy {
3760 GeneratedPullPolicy::Always => "always".to_owned(),
3761 GeneratedPullPolicy::Never => "never".to_owned(),
3762 GeneratedPullPolicy::Missing => "missing".to_owned(),
3763 GeneratedPullPolicy::IfNotPresentAlias => "if_not_present".to_owned(),
3764 GeneratedPullPolicy::Build => "build".to_owned(),
3765 GeneratedPullPolicy::Daily => "daily".to_owned(),
3766 GeneratedPullPolicy::Weekly => "weekly".to_owned(),
3767 GeneratedPullPolicy::Every(duration) => format!("every_{}", duration.expose()),
3768 };
3769 write_quoted(output, &value);
3770 output.push('\n');
3771}
3772
3773fn render_entrypoint(output: &mut String, entrypoint: &GeneratedEntrypoint) {
3774 match entrypoint {
3775 GeneratedEntrypoint::List(arguments) if arguments.is_empty() => output.push_str(" entrypoint: []\n"),
3776 GeneratedEntrypoint::List(arguments) => render_string_sequence(output, "entrypoint", arguments),
3777 GeneratedEntrypoint::String(entrypoint) => render_optional_string(output, "entrypoint", Some(entrypoint)),
3778 GeneratedEntrypoint::Empty => output.push_str(" entrypoint: []\n"),
3779 }
3780}
3781
3782fn render_restart(output: &mut String, restart: GeneratedRestartPolicy) {
3783 write_field(output, 2, "restart");
3784 let value = match restart {
3785 GeneratedRestartPolicy::No => "no".to_owned(),
3786 GeneratedRestartPolicy::Always => "always".to_owned(),
3787 GeneratedRestartPolicy::OnFailure { maximum_retries: None } => "on-failure".to_owned(),
3788 GeneratedRestartPolicy::OnFailure {
3789 maximum_retries: Some(maximum_retries),
3790 } => format!("on-failure:{maximum_retries}"),
3791 GeneratedRestartPolicy::UnlessStopped => "unless-stopped".to_owned(),
3792 };
3793 write_quoted(output, &value);
3794 output.push('\n');
3795}
3796
3797fn render_optional_string(output: &mut String, key: &str, value: Option<&GeneratedString>) {
3798 if let Some(value) = value {
3799 write_field(output, 2, key);
3800 write_quoted(output, value.expose());
3801 output.push('\n');
3802 }
3803}
3804
3805fn render_command(output: &mut String, command: &GeneratedCommand) {
3806 match command {
3807 GeneratedCommand::Exec(arguments) if arguments.is_empty() => output.push_str(" command: []\n"),
3808 GeneratedCommand::Exec(arguments) => render_string_sequence(output, "command", arguments),
3809 GeneratedCommand::Shell(command) => render_optional_string(output, "command", Some(command)),
3810 GeneratedCommand::Empty => output.push_str(" command: []\n"),
3811 }
3812}
3813
3814fn render_environment(output: &mut String, environment: &[GeneratedEnvironment]) {
3815 if environment.is_empty() {
3816 return;
3817 }
3818 output.push_str(" environment:\n");
3819 let mut variables: Vec<_> = environment.iter().collect();
3820 variables.sort_by(|left, right| left.name.cmp(&right.name));
3821 for variable in variables {
3822 output.push_str(" - ");
3823 let value = variable.value.as_ref().map_or_else(
3824 || variable.name.clone(),
3825 |value| format!("{}={}", variable.name, value.expose()),
3826 );
3827 write_quoted(output, &value);
3828 output.push('\n');
3829 }
3830}
3831
3832fn render_environment_files(output: &mut String, environment_files: &[GeneratedEnvironmentFile]) {
3833 if environment_files.is_empty() {
3834 return;
3835 }
3836 output.push_str(" env_file:\n");
3837 for environment_file in environment_files {
3838 match environment_file {
3839 GeneratedEnvironmentFile::Short(path) => {
3840 output.push_str(" - ");
3841 write_quoted(output, path.expose());
3842 output.push('\n');
3843 }
3844 GeneratedEnvironmentFile::Long { path, required, format } => {
3845 output.push_str(" - path: ");
3846 write_quoted(output, path.expose());
3847 output.push('\n');
3848 if let Some(required) = required {
3849 output.push_str(" required: ");
3850 output.push_str(if *required { "true\n" } else { "false\n" });
3851 }
3852 if let Some(format) = format {
3853 output.push_str(" format: ");
3854 write_quoted(
3855 output,
3856 match format {
3857 GeneratedEnvironmentFileFormat::Raw => "raw",
3858 },
3859 );
3860 output.push('\n');
3861 }
3862 }
3863 }
3864 }
3865}
3866
3867fn render_labels(output: &mut String, labels: &[GeneratedLabel]) {
3868 if labels.is_empty() {
3869 return;
3870 }
3871 output.push_str(" labels:\n");
3872 for label in labels {
3873 output.push_str(" ");
3874 write_quoted(output, &label.name);
3875 output.push_str(": ");
3876 write_quoted(output, label.value.expose());
3877 output.push('\n');
3878 }
3879}
3880
3881fn render_annotations(output: &mut String, annotations: &[GeneratedAnnotation]) {
3882 if annotations.is_empty() {
3883 output.push_str(" annotations: {}\n");
3884 return;
3885 }
3886 output.push_str(" annotations:\n");
3887 for annotation in annotations {
3888 output.push_str(" ");
3889 write_quoted(output, &annotation.name);
3890 output.push_str(": ");
3891 write_quoted(output, annotation.value.expose());
3892 output.push('\n');
3893 }
3894}
3895
3896fn render_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3897 if values.is_empty() {
3898 return;
3899 }
3900 write_indent(output, 2);
3901 output.push_str(key);
3902 output.push_str(":\n");
3903 for value in values {
3904 output.push_str(" - ");
3905 write_quoted(output, value.expose());
3906 output.push('\n');
3907 }
3908}
3909
3910fn render_configured_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3911 if values.is_empty() {
3912 write_indent(output, 2);
3913 output.push_str(key);
3914 output.push_str(": []\n");
3915 } else {
3916 render_string_sequence(output, key, values);
3917 }
3918}
3919
3920fn render_extra_hosts(output: &mut String, hosts: &[GeneratedExtraHost]) {
3921 if hosts.is_empty() {
3922 return;
3923 }
3924 output.push_str(" extra_hosts:\n");
3925 for host in hosts {
3926 output.push_str(" - ");
3927 write_quoted(output, &format!("{}={}", host.hostname, host.address));
3928 output.push('\n');
3929 }
3930}
3931
3932fn render_ports(output: &mut String, ports: &[GeneratedPort]) {
3933 if ports.is_empty() {
3934 return;
3935 }
3936 output.push_str(" ports:\n");
3937 for port in ports {
3938 if port.protocol == GeneratedProtocol::Sctp {
3939 render_short_sctp_port(output, port);
3940 continue;
3941 }
3942 output.push_str(" - target: ");
3943 output.push_str(&port.target.to_string());
3944 output.push('\n');
3945 if let Some(published) = port.published {
3946 output.push_str(" published: ");
3947 write_quoted(output, &published.to_string());
3948 output.push('\n');
3949 }
3950 if let Some(host_ip) = &port.host_ip {
3951 output.push_str(" host_ip: ");
3952 write_quoted(output, host_ip);
3953 output.push('\n');
3954 }
3955 output.push_str(" protocol: ");
3956 write_quoted(output, port.protocol.as_str());
3957 output.push('\n');
3958 }
3959}
3960
3961fn render_short_sctp_port(output: &mut String, port: &GeneratedPort) {
3962 let mut value = String::new();
3963 if let Some(host_ip) = &port.host_ip {
3964 if host_ip.contains(':') && !(host_ip.starts_with('[') && host_ip.ends_with(']')) {
3965 value.push('[');
3966 value.push_str(host_ip);
3967 value.push(']');
3968 } else {
3969 value.push_str(host_ip);
3970 }
3971 value.push(':');
3972 }
3973 if let Some(published) = port.published {
3974 value.push_str(&published.to_string());
3975 value.push(':');
3976 }
3977 value.push_str(&port.target.to_string());
3978 value.push_str("/sctp");
3979
3980 output.push_str(" - ");
3981 write_quoted(output, &value);
3982 output.push('\n');
3983}
3984
3985fn render_mounts(output: &mut String, mounts: &[GeneratedMount]) {
3986 if mounts.is_empty() {
3987 return;
3988 }
3989 output.push_str(" volumes:\n");
3990 for mount in mounts {
3991 match &mount.kind {
3992 GeneratedMountKind::Bind {
3993 source,
3994 selinux: Some(selinux),
3995 } => render_selinux_bind(output, source, mount, *selinux),
3996 kind => render_long_mount(output, kind, mount),
3997 }
3998 }
3999}
4000
4001fn render_selinux_bind(output: &mut String, source: &str, mount: &GeneratedMount, selinux: GeneratedSelinux) {
4002 let mut value = format!("{source}:{}:{}", mount.target, selinux.as_str());
4003 if mount.read_only {
4004 value.push_str(",ro");
4005 }
4006 output.push_str(" - ");
4007 write_quoted(output, &value);
4008 output.push('\n');
4009}
4010
4011fn render_long_mount(output: &mut String, kind: &GeneratedMountKind, mount: &GeneratedMount) {
4012 let (mount_type, source) = match kind {
4013 GeneratedMountKind::Volume { source } => ("volume", Some(source.as_str())),
4014 GeneratedMountKind::Bind { source, selinux: None } => ("bind", Some(source.as_str())),
4015 GeneratedMountKind::Anonymous => ("volume", None),
4016 GeneratedMountKind::Bind { selinux: Some(_), .. } => return,
4017 };
4018 output.push_str(" - type: ");
4019 write_quoted(output, mount_type);
4020 output.push('\n');
4021 if let Some(source) = source {
4022 output.push_str(" source: ");
4023 write_quoted(output, source);
4024 output.push('\n');
4025 }
4026 output.push_str(" target: ");
4027 write_quoted(output, &mount.target);
4028 output.push('\n');
4029 if mount.read_only {
4030 output.push_str(" read_only: true\n");
4031 }
4032}
4033
4034fn render_networks(output: &mut String, networks: &[GeneratedNetworkAttachment]) {
4035 if networks.is_empty() {
4036 return;
4037 }
4038 output.push_str(" networks:\n");
4039 for network in networks {
4040 output.push_str(" ");
4041 write_quoted(output, &network.name);
4042 if network.aliases.is_empty() && network.ipv4_address.is_none() && network.ipv6_address.is_none() {
4043 output.push_str(": {}\n");
4044 continue;
4045 }
4046 output.push_str(":\n");
4047 if !network.aliases.is_empty() {
4048 output.push_str(" aliases:\n");
4049 for alias in &network.aliases {
4050 output.push_str(" - ");
4051 write_quoted(output, alias);
4052 output.push('\n');
4053 }
4054 }
4055 for (field, address) in [
4056 ("ipv4_address", network.ipv4_address.as_ref()),
4057 ("ipv6_address", network.ipv6_address.as_ref()),
4058 ] {
4059 if let Some(address) = address {
4060 output.push_str(" ");
4061 output.push_str(field);
4062 output.push_str(": ");
4063 write_quoted(output, address.expose());
4064 output.push('\n');
4065 }
4066 }
4067 }
4068}
4069
4070fn render_network_definitions(output: &mut String, networks: &[GeneratedNetwork]) {
4071 if networks.is_empty() {
4072 return;
4073 }
4074 output.push_str("networks:\n");
4075 for network in networks {
4076 match network {
4077 GeneratedNetwork::Basic(network) => render_basic_resource(output, network),
4078 GeneratedNetwork::Definition(network) => render_network_definition(output, network),
4079 }
4080 }
4081}
4082
4083fn render_network_definition(output: &mut String, network: &GeneratedNetworkDefinition) {
4084 output.push_str(" ");
4085 write_quoted(output, &network.name);
4086 if network.custom_name.is_none()
4087 && network.driver.is_none()
4088 && network.driver_opts.is_none()
4089 && network.enable_ipv6.is_none()
4090 && network.internal.is_none()
4091 && network.labels.is_none()
4092 {
4093 output.push_str(": {}\n");
4094 return;
4095 }
4096 output.push_str(":\n");
4097 if let Some(custom_name) = &network.custom_name {
4098 output.push_str(" name: ");
4099 write_quoted(output, custom_name);
4100 output.push('\n');
4101 }
4102 if let Some(driver) = &network.driver {
4103 output.push_str(" driver: ");
4104 write_quoted(output, driver.expose());
4105 output.push('\n');
4106 }
4107 if let Some(driver_opts) = &network.driver_opts {
4108 if driver_opts.is_empty() {
4109 output.push_str(" driver_opts: {}\n");
4110 } else {
4111 output.push_str(" driver_opts:\n");
4112 for option in driver_opts {
4113 output.push_str(" ");
4114 write_quoted(output, option.name());
4115 output.push_str(": ");
4116 match option.value() {
4117 GeneratedNetworkDriverOptionValue::String(value) => {
4118 write_quoted(output, value.expose());
4119 }
4120 GeneratedNetworkDriverOptionValue::Number(value) => {
4121 output.push_str(value.expose());
4122 }
4123 }
4124 output.push('\n');
4125 }
4126 }
4127 }
4128 if let Some(enable_ipv6) = network.enable_ipv6 {
4129 output.push_str(" enable_ipv6: ");
4130 output.push_str(if enable_ipv6 { "true\n" } else { "false\n" });
4131 }
4132 if let Some(internal) = network.internal {
4133 output.push_str(" internal: ");
4134 output.push_str(if internal { "true\n" } else { "false\n" });
4135 }
4136 if let Some(labels) = &network.labels {
4137 if labels.is_empty() {
4138 output.push_str(" labels: {}\n");
4139 } else {
4140 output.push_str(" labels:\n");
4141 for label in labels {
4142 output.push_str(" ");
4143 write_quoted(output, label.name());
4144 output.push_str(": ");
4145 write_quoted(output, label.value().expose());
4146 output.push('\n');
4147 }
4148 }
4149 }
4150}
4151
4152fn render_volume_definitions(output: &mut String, volumes: &[GeneratedVolume]) {
4153 if volumes.is_empty() {
4154 return;
4155 }
4156 output.push_str("volumes:\n");
4157 for volume in volumes {
4158 match volume {
4159 GeneratedVolume::Basic(volume) => render_basic_resource(output, volume),
4160 GeneratedVolume::Definition(volume) => render_volume_definition(output, volume),
4161 }
4162 }
4163}
4164
4165fn render_file_definitions<T>(
4166 output: &mut String,
4167 field: &str,
4168 definitions: &[T],
4169 name: impl Fn(&T) -> &str,
4170 file: impl Fn(&T) -> &GeneratedString,
4171) {
4172 if definitions.is_empty() {
4173 return;
4174 }
4175 output.push_str(field);
4176 output.push_str(":\n");
4177 for definition in definitions {
4178 output.push_str(" ");
4179 write_quoted(output, name(definition));
4180 output.push_str(":\n file: ");
4181 write_quoted(output, file(definition).expose());
4182 output.push('\n');
4183 }
4184}
4185
4186fn render_volume_definition(output: &mut String, volume: &GeneratedVolumeDefinition) {
4187 output.push_str(" ");
4188 write_quoted(output, &volume.name);
4189 if volume.custom_name.is_none()
4190 && volume.driver.is_none()
4191 && volume.driver_opts.is_none()
4192 && volume.labels.is_none()
4193 {
4194 output.push_str(": {}\n");
4195 return;
4196 }
4197 output.push_str(":\n");
4198 if let Some(custom_name) = &volume.custom_name {
4199 output.push_str(" name: ");
4200 write_quoted(output, custom_name);
4201 output.push('\n');
4202 }
4203 if let Some(driver) = &volume.driver {
4204 output.push_str(" driver: ");
4205 write_quoted(output, driver.expose());
4206 output.push('\n');
4207 }
4208 if let Some(driver_opts) = &volume.driver_opts {
4209 if driver_opts.is_empty() {
4210 output.push_str(" driver_opts: {}\n");
4211 } else {
4212 output.push_str(" driver_opts:\n");
4213 for option in driver_opts {
4214 output.push_str(" ");
4215 write_quoted(output, option.name());
4216 output.push_str(": ");
4217 match option.value() {
4218 GeneratedVolumeDriverOptionValue::String(value) => write_quoted(output, value.expose()),
4219 GeneratedVolumeDriverOptionValue::Number(value) => output.push_str(value.expose()),
4220 }
4221 output.push('\n');
4222 }
4223 }
4224 }
4225 if let Some(labels) = &volume.labels {
4226 if labels.is_empty() {
4227 output.push_str(" labels: {}\n");
4228 } else {
4229 output.push_str(" labels:\n");
4230 for label in labels {
4231 output.push_str(" ");
4232 write_quoted(output, label.name());
4233 output.push_str(": ");
4234 write_quoted(output, label.value().expose());
4235 output.push('\n');
4236 }
4237 }
4238 }
4239}
4240
4241fn render_basic_resource(output: &mut String, resource: &GeneratedResource) {
4242 output.push_str(" ");
4243 write_quoted(output, &resource.name);
4244 if !resource.external && resource.custom_name.is_none() {
4245 output.push_str(": {}\n");
4246 return;
4247 }
4248 output.push_str(":\n");
4249 if let Some(custom_name) = &resource.custom_name {
4250 output.push_str(" name: ");
4251 write_quoted(output, custom_name);
4252 output.push('\n');
4253 }
4254 if resource.external {
4255 output.push_str(" external: true\n");
4256 }
4257}
4258
4259fn write_field(output: &mut String, depth: usize, key: &str) {
4260 write_indent(output, depth);
4261 output.push_str(key);
4262 output.push_str(": ");
4263}
4264
4265fn write_indent(output: &mut String, depth: usize) {
4266 for _ in 0..depth {
4267 output.push_str(" ");
4268 }
4269}
4270
4271fn required(kind: &'static str, value: String) -> Result<String, GenerationError> {
4272 if value.is_empty() {
4273 return Err(GenerationError::EmptyValue(kind));
4274 }
4275 if value.contains('\0') {
4276 return Err(GenerationError::ContainsNul(kind));
4277 }
4278 Ok(value)
4279}
4280
4281fn require_generated_string(kind: &'static str, value: &GeneratedString) -> Result<(), GenerationError> {
4282 if value.expose().is_empty() {
4283 return Err(GenerationError::EmptyValue(kind));
4284 }
4285 Ok(())
4286}
4287
4288fn generated_file_resource_name(value: String) -> Result<String, GenerationError> {
4289 if value.is_empty() || value.contains(['\0', '\r', '\n', '$']) {
4290 Err(GenerationError::InvalidFileResourceName)
4291 } else {
4292 Ok(value)
4293 }
4294}
4295
4296fn generated_file_resource_path(value: GeneratedString) -> Result<GeneratedString, GenerationError> {
4297 if value.expose().is_empty() || value.expose().contains(['\0', '\r', '\n', '$']) {
4298 Err(GenerationError::InvalidFileResourcePath)
4299 } else {
4300 Ok(value)
4301 }
4302}
4303
4304fn validate_generated_device_member(
4305 member: &'static str,
4306 value: &GeneratedString,
4307 require_non_empty: bool,
4308) -> Result<(), GenerationError> {
4309 if valid_generated_device_string(value.expose(), require_non_empty) {
4310 Ok(())
4311 } else {
4312 Err(GenerationError::InvalidDeviceValue(member))
4313 }
4314}
4315
4316fn validate_generated_ulimit_value(value: &GeneratedString) -> Result<(), GenerationError> {
4317 let value = value.expose();
4318 if value.contains(['\r', '\n', '$'])
4319 || (value != "-1" && (value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit())))
4320 {
4321 return Err(GenerationError::InvalidUlimitValue);
4322 }
4323 Ok(())
4324}
4325
4326fn valid_yaml_number(value: &str) -> bool {
4327 let ordinary = !value.is_empty()
4328 && value.bytes().any(|byte| byte.is_ascii_digit())
4329 && value.bytes().all(|byte| {
4330 byte.is_ascii_digit()
4331 || matches!(
4332 byte,
4333 b'+' | b'-'
4334 | b'.'
4335 | b'_'
4336 | b'e'
4337 | b'E'
4338 | b'x'
4339 | b'X'
4340 | b'o'
4341 | b'O'
4342 | b'a'..=b'f'
4343 | b'A'..=b'F'
4344 )
4345 });
4346 let special = matches!(
4347 value,
4348 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF" | ".nan" | ".NaN" | ".NAN"
4349 );
4350 if !ordinary && !special {
4351 return false;
4352 }
4353 let parse = YamlFile::parse(value);
4354 if !parse.ok() {
4355 return false;
4356 }
4357 let file = parse.tree();
4358 let Some(document) = file.document() else {
4359 return false;
4360 };
4361 let Some(scalar) = document.as_scalar() else {
4362 return false;
4363 };
4364 let position = scalar.byte_range();
4365 position.start == 0
4366 && position.end as usize == value.len()
4367 && matches!(
4368 ScalarValue::from_scalar(&scalar).scalar_type(),
4369 ScalarType::Integer | ScalarType::Float
4370 )
4371}
4372
4373fn environment_name(value: String) -> Result<String, GenerationError> {
4374 let value = required("environment name", value)?;
4375 if value.contains('=') {
4376 return Err(GenerationError::InvalidEnvironmentName);
4377 }
4378 Ok(value)
4379}
4380
4381fn valid_container_name(value: &str) -> bool {
4382 let mut bytes = value.bytes();
4383 bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
4384 && bytes
4385 .next()
4386 .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4387 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4388}
4389
4390fn short_component(kind: &'static str, value: String, separator: char) -> Result<String, GenerationError> {
4391 let value = required(kind, value)?;
4392 if value.contains(separator) {
4393 return Err(GenerationError::InvalidShortComponent(kind));
4394 }
4395 Ok(value)
4396}
4397
4398fn set_once<T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), GenerationError> {
4399 if slot.is_some() {
4400 return Err(GenerationError::DuplicateField(field));
4401 }
4402 *slot = Some(value);
4403 Ok(())
4404}
4405
4406fn insert_named<T>(
4407 values: &mut Vec<T>,
4408 value: T,
4409 kind: &'static str,
4410 name: impl Fn(&T) -> &str,
4411) -> Result<(), GenerationError> {
4412 let value_name = name(&value);
4413 if values.iter().any(|candidate| name(candidate) == value_name) {
4414 return Err(GenerationError::DuplicateName {
4415 kind,
4416 name: value_name.to_owned(),
4417 });
4418 }
4419 values.push(value);
4420 Ok(())
4421}
4422
4423fn command_is_sensitive(command: &GeneratedCommand) -> bool {
4424 match command {
4425 GeneratedCommand::Exec(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4426 GeneratedCommand::Shell(command) => command.is_sensitive(),
4427 GeneratedCommand::Empty => false,
4428 }
4429}
4430
4431fn entrypoint_is_sensitive(entrypoint: &GeneratedEntrypoint) -> bool {
4432 match entrypoint {
4433 GeneratedEntrypoint::List(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4434 GeneratedEntrypoint::String(entrypoint) => entrypoint.is_sensitive(),
4435 GeneratedEntrypoint::Empty => false,
4436 }
4437}