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;
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21#[non_exhaustive]
22pub enum GenerationError {
23 EmptyValue(&'static str),
25 ContainsNul(&'static str),
27 ContainsLineBreak(&'static str),
29 InvalidEnvironmentName,
31 InvalidContainerName,
33 InvalidHostname,
35 InvalidPullPolicyDuration,
37 InvalidPidsLimit,
39 InvalidShmSize,
41 InvalidMemLimit,
43 InvalidDnsValue,
45 InvalidDnsOptionValue,
47 InvalidDnsSearchValue,
49 InvalidExposeValue,
51 InvalidSecurityOptionValue,
53 InvalidAnnotationName,
55 InvalidAnnotationValue,
57 InvalidFileResourceName,
59 InvalidFileResourcePath,
61 InvalidTmpfsItem,
63 InvalidDeviceValue(&'static str),
65 InvalidSysctlName,
67 InvalidSysctlValue,
69 InvalidLoggingOptionNumber,
71 InvalidNetworkDriverOptionNumber,
73 InvalidVolumeDriverOptionNumber,
75 InvalidUlimitName,
77 InvalidUlimitValue,
79 MissingUlimitRangeMember(&'static str),
81 InvalidStopGracePeriod,
83 InvalidShortComponent(&'static str),
85 InvalidSelinuxBind,
87 InvalidServiceRuntimeField(&'static str),
89 DuplicateField(&'static str),
91 DuplicateName {
93 kind: &'static str,
95 name: String,
97 },
98 DuplicateItem(&'static str),
100 InvalidPort,
102 UnrepresentableSctpHostIp,
104 MissingService,
106 InternalInvariant(&'static str),
108}
109
110impl fmt::Display for GenerationError {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 Self::EmptyValue(kind) => write!(formatter, "generated {kind} must not be empty"),
114 Self::ContainsNul(kind) => write!(formatter, "generated {kind} must not contain a NUL byte"),
115 Self::ContainsLineBreak(kind) => {
116 write!(formatter, "generated {kind} must not contain a carriage return or line feed")
117 }
118 Self::InvalidEnvironmentName => formatter.write_str("generated environment name must not contain `=`"),
119 Self::InvalidContainerName => {
120 formatter.write_str("generated container name must match `[a-zA-Z0-9][a-zA-Z0-9_.-]+`")
121 }
122 Self::InvalidHostname => formatter.write_str(
123 "generated hostname must be a resolved ASCII RFC-1123 name with labels of 1 to 63 characters and total length at most 253",
124 ),
125 Self::InvalidPullPolicyDuration => formatter.write_str(
126 "generated pull policy duration must match integer `w`, `d`, `h`, `m`, and `s` components",
127 ),
128 Self::InvalidPidsLimit => {
129 formatter.write_str("generated finite PID limit must be a positive integral decimal")
130 }
131 Self::InvalidShmSize => formatter.write_str(
132 "generated shared-memory size must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
133 ),
134 Self::InvalidMemLimit => formatter.write_str(
135 "generated memory limit must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
136 ),
137 Self::InvalidDnsValue => {
138 formatter.write_str("generated DNS server must be a non-empty resolved single-line string")
139 }
140 Self::InvalidDnsOptionValue => {
141 formatter.write_str("generated DNS option must be a non-empty resolved single-line string")
142 }
143 Self::InvalidDnsSearchValue => {
144 formatter.write_str("generated DNS search domain must be a non-empty resolved single-line string")
145 }
146 Self::InvalidExposeValue => formatter.write_str(
147 "generated expose item must be a resolved decimal port or range with an optional `tcp` or `udp` suffix",
148 ),
149 Self::InvalidSecurityOptionValue => {
150 formatter.write_str("generated security option must be a non-empty resolved single-line string")
151 }
152 Self::InvalidAnnotationName => formatter
153 .write_str("generated annotation name must be a non-empty resolved single-line string"),
154 Self::InvalidAnnotationValue => formatter
155 .write_str("generated annotation value must be a resolved single-line string"),
156 Self::InvalidFileResourceName => formatter.write_str(
157 "generated top-level config or secret name must be a non-empty resolved single-line string",
158 ),
159 Self::InvalidFileResourcePath => formatter.write_str(
160 "generated top-level config or secret file must be a non-empty resolved single-line string",
161 ),
162 Self::InvalidTmpfsItem => formatter.write_str(
163 "generated tmpfs item must be a non-empty path optionally followed by a colon and non-empty comma-separated raw options",
164 ),
165 Self::InvalidDeviceValue(member) => write!(
166 formatter,
167 "generated device {member} must be a safe resolved single-line string{}",
168 if matches!(*member, "short item" | "source") {
169 " and must not be empty"
170 } else {
171 ""
172 }
173 ),
174 Self::InvalidSysctlName => formatter
175 .write_str("generated sysctl name must be a non-empty resolved single-line string"),
176 Self::InvalidSysctlValue => formatter
177 .write_str("generated sysctl value must be a resolved single-line string"),
178 Self::InvalidLoggingOptionNumber => formatter
179 .write_str("generated logging option number must be one complete YAML number scalar"),
180 Self::InvalidNetworkDriverOptionNumber => formatter
181 .write_str("generated network driver option number must be one complete YAML number scalar"),
182 Self::InvalidVolumeDriverOptionNumber => formatter
183 .write_str("generated volume driver option number must be one complete YAML number scalar"),
184 Self::InvalidUlimitName => formatter
185 .write_str("generated ulimit name must match lowercase ASCII `[a-z]+`"),
186 Self::InvalidUlimitValue => formatter
187 .write_str("generated ulimit value must be `-1` or a non-negative ASCII decimal"),
188 Self::MissingUlimitRangeMember(member) => {
189 write!(formatter, "generated ulimit range is missing required `{member}`")
190 }
191 Self::InvalidStopGracePeriod => formatter.write_str(
192 "generated stop grace period must match the ComposeLens duration policy using `us`, `ms`, `s`, `m`, or `h`, or contain an interpolation marker",
193 ),
194 Self::InvalidShortComponent(kind) => {
195 write!(formatter, "generated {kind} contains its reserved short-form separator")
196 }
197 Self::InvalidSelinuxBind => formatter
198 .write_str("generated SELinux bind source and target must not contain the short-syntax `:` separator"),
199 Self::InvalidServiceRuntimeField(field) => write!(formatter, "generated {field} must use a resolved, non-empty field-valid spelling"),
200 Self::DuplicateField(field) => write!(formatter, "generated field `{field}` was configured more than once"),
201 Self::DuplicateName { kind, name } => {
202 write!(formatter, "generated {kind} `{name}` was added more than once")
203 }
204 Self::DuplicateItem(kind) => write!(formatter, "generated {kind} contains an exact duplicate item"),
205 Self::InvalidPort => formatter.write_str("generated container target port must be greater than zero"),
206 Self::UnrepresentableSctpHostIp => formatter.write_str(
207 "generated SCTP port with a host address also requires a published port for Compose short syntax",
208 ),
209 Self::MissingService => formatter.write_str("generated Compose project requires at least one service"),
210 Self::InternalInvariant(stage) => write!(formatter, "generated Compose document failed {stage} validation"),
211 }
212 }
213}
214
215impl Error for GenerationError {}
216
217#[derive(Clone, Eq, PartialEq)]
219pub struct GeneratedString {
220 value: String,
221 sensitive: bool,
222}
223
224impl GeneratedString {
225 pub fn plain(value: impl Into<String>) -> Result<Self, GenerationError> {
231 Self::new(value.into(), false)
232 }
233
234 pub fn sensitive(value: impl Into<String>) -> Result<Self, GenerationError> {
240 Self::new(value.into(), true)
241 }
242
243 fn new(value: String, sensitive: bool) -> Result<Self, GenerationError> {
244 if value.contains('\0') {
245 return Err(GenerationError::ContainsNul("string"));
246 }
247 Ok(Self { value, sensitive })
248 }
249
250 #[must_use]
252 pub fn expose(&self) -> &str {
253 &self.value
254 }
255
256 #[must_use]
258 pub const fn is_sensitive(&self) -> bool {
259 self.sensitive
260 }
261}
262
263impl fmt::Debug for GeneratedString {
264 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
265 formatter
266 .debug_struct("GeneratedString")
267 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
268 .field("sensitive", &self.sensitive)
269 .finish()
270 }
271}
272
273#[derive(Clone, Debug, Eq, PartialEq)]
275#[non_exhaustive]
276pub enum GeneratedCommand {
277 Exec(Vec<GeneratedString>),
279 Shell(GeneratedString),
281 Empty,
283}
284
285#[derive(Clone, Debug, Eq, PartialEq)]
287#[non_exhaustive]
288pub enum GeneratedEntrypoint {
289 List(Vec<GeneratedString>),
291 String(GeneratedString),
293 Empty,
295}
296
297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
299#[non_exhaustive]
300pub enum GeneratedRestartPolicy {
301 No,
303 Always,
305 OnFailure {
307 maximum_retries: Option<u64>,
309 },
310 UnlessStopped,
312}
313
314#[derive(Clone, Debug, Eq, PartialEq)]
316#[non_exhaustive]
317pub enum GeneratedPullPolicy {
318 Always,
320 Never,
322 Missing,
324 IfNotPresentAlias,
326 Build,
328 Daily,
330 Weekly,
332 Every(GeneratedString),
334}
335
336#[derive(Clone, Debug, Eq, PartialEq)]
338#[non_exhaustive]
339pub enum GeneratedPidsLimit {
340 Unlimited,
342 Finite(String),
344}
345
346#[derive(Clone, Debug, Eq, PartialEq)]
348#[non_exhaustive]
349pub enum GeneratedShmSize {
350 Explicit {
352 amount: GeneratedString,
354 unit: ShmSizeUnit,
356 },
357}
358
359#[derive(Clone, Debug, Eq, PartialEq)]
361#[non_exhaustive]
362pub enum GeneratedMemLimit {
363 Explicit {
365 amount: GeneratedString,
367 unit: MemLimitUnit,
369 },
370}
371
372#[derive(Clone, Debug, Eq, PartialEq)]
374#[non_exhaustive]
375pub enum GeneratedTmpfs {
376 Scalar(GeneratedString),
378 List(Vec<GeneratedString>),
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
384#[non_exhaustive]
385pub enum GeneratedDns {
386 Scalar(GeneratedString),
388 List(Vec<GeneratedString>),
390}
391
392#[derive(Clone, Debug, Eq, PartialEq)]
394#[non_exhaustive]
395pub enum GeneratedDnsSearch {
396 Scalar(GeneratedString),
398 List(Vec<GeneratedString>),
400}
401
402#[derive(Clone, Debug, Eq, PartialEq)]
404pub struct GeneratedLongDevice {
405 source: GeneratedString,
406 target: Option<GeneratedString>,
407 permissions: Option<GeneratedString>,
408}
409
410impl GeneratedLongDevice {
411 pub fn new(
419 source: GeneratedString,
420 target: Option<GeneratedString>,
421 permissions: Option<GeneratedString>,
422 ) -> Result<Self, GenerationError> {
423 validate_generated_device_member("source", &source, true)?;
424 if let Some(target) = &target {
425 validate_generated_device_member("target", target, false)?;
426 }
427 if let Some(permissions) = &permissions {
428 validate_generated_device_member("permissions", permissions, false)?;
429 }
430 Ok(Self {
431 source,
432 target,
433 permissions,
434 })
435 }
436
437 #[must_use]
439 pub const fn source(&self) -> &GeneratedString {
440 &self.source
441 }
442
443 #[must_use]
445 pub const fn target(&self) -> Option<&GeneratedString> {
446 self.target.as_ref()
447 }
448
449 #[must_use]
451 pub const fn permissions(&self) -> Option<&GeneratedString> {
452 self.permissions.as_ref()
453 }
454
455 fn is_sensitive(&self) -> bool {
456 self.source.is_sensitive()
457 || self.target.as_ref().is_some_and(GeneratedString::is_sensitive)
458 || self.permissions.as_ref().is_some_and(GeneratedString::is_sensitive)
459 }
460}
461
462#[derive(Clone, Debug, Eq, PartialEq)]
464#[non_exhaustive]
465pub enum GeneratedDevice {
466 Short(GeneratedString),
468 Long(GeneratedLongDevice),
470}
471
472#[derive(Clone, Debug, Eq, PartialEq)]
474#[non_exhaustive]
475pub enum GeneratedLoggingOptionValue {
476 String(GeneratedString),
478 Number(GeneratedString),
480 Null,
482}
483
484impl GeneratedLoggingOptionValue {
485 fn is_sensitive(&self) -> bool {
486 match self {
487 Self::String(value) | Self::Number(value) => value.is_sensitive(),
488 Self::Null => false,
489 }
490 }
491}
492
493#[derive(Clone, Debug, Eq, PartialEq)]
495pub struct GeneratedLoggingOption {
496 name: String,
497 value: GeneratedLoggingOptionValue,
498}
499
500impl GeneratedLoggingOption {
501 pub fn new(name: impl Into<String>, value: GeneratedLoggingOptionValue) -> Result<Self, GenerationError> {
508 let name = required("logging option key", name.into())?;
509 if let GeneratedLoggingOptionValue::Number(number) = &value {
510 if !valid_yaml_number(number.expose()) {
511 return Err(GenerationError::InvalidLoggingOptionNumber);
512 }
513 }
514 Ok(Self { name, value })
515 }
516
517 #[must_use]
519 pub fn name(&self) -> &str {
520 &self.name
521 }
522
523 #[must_use]
525 pub const fn value(&self) -> &GeneratedLoggingOptionValue {
526 &self.value
527 }
528}
529
530#[derive(Clone, Debug, Eq, PartialEq)]
532pub struct GeneratedLogging {
533 driver: GeneratedString,
534 options: Vec<GeneratedLoggingOption>,
535}
536
537impl GeneratedLogging {
538 pub fn new(driver: GeneratedString, options: Vec<GeneratedLoggingOption>) -> Result<Self, GenerationError> {
547 let mut seen = BTreeSet::new();
548 for option in &options {
549 if !seen.insert(option.name()) {
550 return Err(GenerationError::DuplicateName {
551 kind: "logging option",
552 name: option.name().to_owned(),
553 });
554 }
555 }
556 Ok(Self { driver, options })
557 }
558
559 #[must_use]
561 pub const fn driver(&self) -> &GeneratedString {
562 &self.driver
563 }
564
565 #[must_use]
567 pub fn options(&self) -> &[GeneratedLoggingOption] {
568 &self.options
569 }
570
571 fn is_sensitive(&self) -> bool {
572 self.driver.is_sensitive() || self.options.iter().any(|option| option.value.is_sensitive())
573 }
574}
575
576#[derive(Clone, Debug, Eq, PartialEq)]
578#[non_exhaustive]
579pub enum GeneratedNetworkDriverOptionValue {
580 String(GeneratedString),
582 Number(GeneratedString),
584}
585
586impl GeneratedNetworkDriverOptionValue {
587 fn is_sensitive(&self) -> bool {
588 match self {
589 Self::String(value) | Self::Number(value) => value.is_sensitive(),
590 }
591 }
592}
593
594#[derive(Clone, Debug, Eq, PartialEq)]
596pub struct GeneratedNetworkDriverOption {
597 name: String,
598 value: GeneratedNetworkDriverOptionValue,
599}
600
601impl GeneratedNetworkDriverOption {
602 pub fn new(name: impl Into<String>, value: GeneratedNetworkDriverOptionValue) -> Result<Self, GenerationError> {
609 let name = required("network driver option key", name.into())?;
610 if let GeneratedNetworkDriverOptionValue::Number(number) = &value {
611 if !valid_yaml_number(number.expose()) {
612 return Err(GenerationError::InvalidNetworkDriverOptionNumber);
613 }
614 }
615 Ok(Self { name, value })
616 }
617
618 #[must_use]
620 pub fn name(&self) -> &str {
621 &self.name
622 }
623
624 #[must_use]
626 pub const fn value(&self) -> &GeneratedNetworkDriverOptionValue {
627 &self.value
628 }
629}
630
631#[derive(Clone, Debug, Eq, PartialEq)]
633#[non_exhaustive]
634pub enum GeneratedVolumeDriverOptionValue {
635 String(GeneratedString),
637 Number(GeneratedString),
639}
640
641impl GeneratedVolumeDriverOptionValue {
642 fn is_sensitive(&self) -> bool {
643 match self {
644 Self::String(value) | Self::Number(value) => value.is_sensitive(),
645 }
646 }
647}
648
649#[derive(Clone, Debug, Eq, PartialEq)]
651pub struct GeneratedVolumeDriverOption {
652 name: String,
653 value: GeneratedVolumeDriverOptionValue,
654}
655
656impl GeneratedVolumeDriverOption {
657 pub fn new(name: impl Into<String>, value: GeneratedVolumeDriverOptionValue) -> Result<Self, GenerationError> {
664 let name = required("volume driver option key", name.into())?;
665 if let GeneratedVolumeDriverOptionValue::Number(number) = &value {
666 if !valid_yaml_number(number.expose()) {
667 return Err(GenerationError::InvalidVolumeDriverOptionNumber);
668 }
669 }
670 Ok(Self { name, value })
671 }
672
673 #[must_use]
675 pub fn name(&self) -> &str {
676 &self.name
677 }
678
679 #[must_use]
681 pub const fn value(&self) -> &GeneratedVolumeDriverOptionValue {
682 &self.value
683 }
684}
685
686#[derive(Clone, Debug, Eq, PartialEq)]
692pub struct GeneratedVolumeDefinition {
693 name: String,
694 custom_name: Option<String>,
695 driver: Option<GeneratedString>,
696 driver_opts: Option<Vec<GeneratedVolumeDriverOption>>,
697 labels: Option<Vec<GeneratedLabel>>,
698}
699
700impl GeneratedVolumeDefinition {
701 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
707 Ok(Self {
708 name: required("volume name", name.into())?,
709 custom_name: None,
710 driver: None,
711 driver_opts: None,
712 labels: None,
713 })
714 }
715
716 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
722 let name = required("custom volume name", name.into())?;
723 set_once(&mut self.custom_name, name, "volume name")
724 }
725
726 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
734 set_once(&mut self.driver, driver, "volume driver")
735 }
736
737 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedVolumeDriverOption>) -> Result<(), GenerationError> {
747 let mut seen = BTreeSet::new();
748 for option in &driver_opts {
749 if !seen.insert(option.name()) {
750 return Err(GenerationError::DuplicateName {
751 kind: "volume driver option",
752 name: option.name().to_owned(),
753 });
754 }
755 }
756 set_once(&mut self.driver_opts, driver_opts, "volume driver_opts")
757 }
758
759 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
769 let mut seen = BTreeSet::new();
770 for label in &labels {
771 if !seen.insert(label.name()) {
772 return Err(GenerationError::DuplicateName {
773 kind: "volume label",
774 name: label.name().to_owned(),
775 });
776 }
777 }
778 set_once(&mut self.labels, labels, "volume labels")
779 }
780
781 #[must_use]
783 pub fn name(&self) -> &str {
784 &self.name
785 }
786
787 #[must_use]
789 pub fn custom_name(&self) -> Option<&str> {
790 self.custom_name.as_deref()
791 }
792
793 #[must_use]
795 pub const fn driver(&self) -> Option<&GeneratedString> {
796 self.driver.as_ref()
797 }
798
799 #[must_use]
801 pub fn driver_opts(&self) -> Option<&[GeneratedVolumeDriverOption]> {
802 self.driver_opts.as_deref()
803 }
804
805 #[must_use]
807 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
808 self.labels.as_deref()
809 }
810
811 fn is_sensitive(&self) -> bool {
812 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
813 || self
814 .driver_opts
815 .as_ref()
816 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
817 || self
818 .labels
819 .as_ref()
820 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
821 }
822}
823
824#[derive(Clone, Debug, Eq, PartialEq)]
829pub struct GeneratedNetworkDefinition {
830 name: String,
831 custom_name: Option<String>,
832 driver: Option<GeneratedString>,
833 driver_opts: Option<Vec<GeneratedNetworkDriverOption>>,
834 enable_ipv6: Option<bool>,
835 internal: Option<bool>,
836 labels: Option<Vec<GeneratedLabel>>,
837}
838
839impl GeneratedNetworkDefinition {
840 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
846 Ok(Self {
847 name: required("network name", name.into())?,
848 custom_name: None,
849 driver: None,
850 driver_opts: None,
851 enable_ipv6: None,
852 internal: None,
853 labels: None,
854 })
855 }
856
857 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
863 let name = required("custom network name", name.into())?;
864 set_once(&mut self.custom_name, name, "network name")
865 }
866
867 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
875 set_once(&mut self.driver, driver, "network driver")
876 }
877
878 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedNetworkDriverOption>) -> Result<(), GenerationError> {
888 let mut seen = BTreeSet::new();
889 for option in &driver_opts {
890 if !seen.insert(option.name()) {
891 return Err(GenerationError::DuplicateName {
892 kind: "network driver option",
893 name: option.name().to_owned(),
894 });
895 }
896 }
897 set_once(&mut self.driver_opts, driver_opts, "network driver_opts")
898 }
899
900 pub fn set_enable_ipv6(&mut self, enable_ipv6: bool) -> Result<(), GenerationError> {
909 set_once(&mut self.enable_ipv6, enable_ipv6, "network enable_ipv6")
910 }
911
912 pub fn set_internal(&mut self, internal: bool) -> Result<(), GenerationError> {
921 set_once(&mut self.internal, internal, "network internal")
922 }
923
924 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
934 let mut seen = BTreeSet::new();
935 for label in &labels {
936 if !seen.insert(label.name()) {
937 return Err(GenerationError::DuplicateName {
938 kind: "network label",
939 name: label.name().to_owned(),
940 });
941 }
942 }
943 set_once(&mut self.labels, labels, "network labels")
944 }
945
946 #[must_use]
948 pub fn name(&self) -> &str {
949 &self.name
950 }
951
952 #[must_use]
954 pub fn custom_name(&self) -> Option<&str> {
955 self.custom_name.as_deref()
956 }
957
958 #[must_use]
960 pub const fn driver(&self) -> Option<&GeneratedString> {
961 self.driver.as_ref()
962 }
963
964 #[must_use]
966 pub fn driver_opts(&self) -> Option<&[GeneratedNetworkDriverOption]> {
967 self.driver_opts.as_deref()
968 }
969
970 #[must_use]
972 pub const fn enable_ipv6(&self) -> Option<bool> {
973 self.enable_ipv6
974 }
975
976 #[must_use]
978 pub const fn internal(&self) -> Option<bool> {
979 self.internal
980 }
981
982 #[must_use]
984 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
985 self.labels.as_deref()
986 }
987
988 fn is_sensitive(&self) -> bool {
989 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
990 || self
991 .driver_opts
992 .as_ref()
993 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
994 || self
995 .labels
996 .as_ref()
997 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
998 }
999}
1000
1001impl GeneratedDevice {
1002 fn is_sensitive(&self) -> bool {
1003 match self {
1004 Self::Short(value) => value.is_sensitive(),
1005 Self::Long(value) => value.is_sensitive(),
1006 }
1007 }
1008}
1009
1010#[derive(Clone, Debug, Eq, PartialEq)]
1012pub struct GeneratedSysctl {
1013 name: String,
1014 value: GeneratedString,
1015}
1016
1017impl GeneratedSysctl {
1018 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1026 let name = name.into();
1027 if name.is_empty()
1028 || name.contains(['\0', '\r', '\n'])
1029 || name.contains('$')
1030 || value.expose().contains(['\r', '\n', '$'])
1031 {
1032 return Err(if name.is_empty() || name.contains(['\0', '\r', '\n', '$']) {
1033 GenerationError::InvalidSysctlName
1034 } else {
1035 GenerationError::InvalidSysctlValue
1036 });
1037 }
1038 Ok(Self { name, value })
1039 }
1040
1041 #[must_use]
1043 pub fn name(&self) -> &str {
1044 &self.name
1045 }
1046
1047 #[must_use]
1049 pub const fn value(&self) -> &GeneratedString {
1050 &self.value
1051 }
1052}
1053
1054#[derive(Clone, Debug, Eq, PartialEq)]
1056#[non_exhaustive]
1057pub enum GeneratedSysctls {
1058 Map(Vec<GeneratedSysctl>),
1060 List(Vec<GeneratedString>),
1062}
1063
1064#[derive(Clone, Debug, Eq, PartialEq)]
1066#[non_exhaustive]
1067pub enum GeneratedUlimitValue {
1068 Single(GeneratedString),
1070 Range {
1072 soft: Option<GeneratedString>,
1074 hard: Option<GeneratedString>,
1076 },
1077}
1078
1079#[derive(Clone, Debug, Eq, PartialEq)]
1081pub struct GeneratedUlimit {
1082 name: String,
1083 value: GeneratedUlimitValue,
1084}
1085
1086impl GeneratedUlimit {
1087 pub fn new(name: impl Into<String>, value: GeneratedUlimitValue) -> Result<Self, GenerationError> {
1094 let name = name.into();
1095 if !valid_ulimit_name(&name) {
1096 return Err(GenerationError::InvalidUlimitName);
1097 }
1098 match &value {
1099 GeneratedUlimitValue::Single(value) => validate_generated_ulimit_value(value)?,
1100 GeneratedUlimitValue::Range { soft, hard } => {
1101 let soft = soft.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("soft"))?;
1102 let hard = hard.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("hard"))?;
1103 validate_generated_ulimit_value(soft)?;
1104 validate_generated_ulimit_value(hard)?;
1105 }
1106 }
1107 Ok(Self { name, value })
1108 }
1109
1110 pub fn single(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1116 Self::new(name, GeneratedUlimitValue::Single(value))
1117 }
1118
1119 pub fn range(
1125 name: impl Into<String>,
1126 soft: GeneratedString,
1127 hard: GeneratedString,
1128 ) -> Result<Self, GenerationError> {
1129 Self::new(
1130 name,
1131 GeneratedUlimitValue::Range {
1132 soft: Some(soft),
1133 hard: Some(hard),
1134 },
1135 )
1136 }
1137
1138 #[must_use]
1140 pub fn name(&self) -> &str {
1141 &self.name
1142 }
1143
1144 #[must_use]
1146 pub const fn value(&self) -> &GeneratedUlimitValue {
1147 &self.value
1148 }
1149
1150 fn is_sensitive(&self) -> bool {
1151 match &self.value {
1152 GeneratedUlimitValue::Single(value) => value.is_sensitive(),
1153 GeneratedUlimitValue::Range { soft, hard } => {
1154 soft.iter().chain(hard.iter()).any(GeneratedString::is_sensitive)
1155 }
1156 }
1157 }
1158}
1159
1160#[derive(Clone, Debug, Eq, PartialEq)]
1162pub struct GeneratedUlimits {
1163 entries: Vec<GeneratedUlimit>,
1164}
1165
1166impl GeneratedUlimits {
1167 pub fn new(entries: Vec<GeneratedUlimit>) -> Result<Self, GenerationError> {
1173 let mut seen = BTreeSet::new();
1174 for entry in &entries {
1175 if !seen.insert(entry.name()) {
1176 return Err(GenerationError::DuplicateName {
1177 kind: "ulimit",
1178 name: entry.name().to_owned(),
1179 });
1180 }
1181 }
1182 Ok(Self { entries })
1183 }
1184
1185 #[must_use]
1187 pub fn entries(&self) -> &[GeneratedUlimit] {
1188 &self.entries
1189 }
1190
1191 #[must_use]
1193 pub fn is_empty(&self) -> bool {
1194 self.entries.is_empty()
1195 }
1196}
1197
1198#[derive(Clone, Debug, Eq, PartialEq)]
1200#[non_exhaustive]
1201pub enum GeneratedHostname {
1202 Resolved(GeneratedString),
1204}
1205
1206#[derive(Clone, Debug, Eq, PartialEq)]
1208pub struct GeneratedEnvironment {
1209 name: String,
1210 value: Option<GeneratedString>,
1211}
1212
1213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1215#[non_exhaustive]
1216pub enum GeneratedEnvironmentFileFormat {
1217 Raw,
1219}
1220
1221#[derive(Clone, Debug, Eq, PartialEq)]
1223#[non_exhaustive]
1224pub enum GeneratedEnvironmentFile {
1225 Short(GeneratedString),
1227 Long {
1229 path: GeneratedString,
1231 required: Option<bool>,
1233 format: Option<GeneratedEnvironmentFileFormat>,
1235 },
1236}
1237
1238#[derive(Clone, Debug, Eq, PartialEq)]
1240pub struct GeneratedLabel {
1241 name: String,
1242 value: GeneratedString,
1243}
1244
1245#[derive(Clone, Debug, Eq, PartialEq)]
1247pub struct GeneratedAnnotation {
1248 name: String,
1249 value: GeneratedString,
1250}
1251
1252impl GeneratedAnnotation {
1253 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1260 let name = name.into();
1261 if name.is_empty() || name.contains(['$', '\r', '\n', '\0']) {
1262 return Err(GenerationError::InvalidAnnotationName);
1263 }
1264 if value.expose().contains(['$', '\r', '\n', '\0']) {
1265 return Err(GenerationError::InvalidAnnotationValue);
1266 }
1267 Ok(Self { name, value })
1268 }
1269
1270 #[must_use]
1272 pub fn name(&self) -> &str {
1273 &self.name
1274 }
1275
1276 #[must_use]
1278 pub const fn value(&self) -> &GeneratedString {
1279 &self.value
1280 }
1281}
1282
1283impl GeneratedLabel {
1284 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1291 Ok(Self {
1292 name: required("label name", name.into())?,
1293 value,
1294 })
1295 }
1296
1297 #[must_use]
1299 pub fn name(&self) -> &str {
1300 &self.name
1301 }
1302
1303 #[must_use]
1305 pub const fn value(&self) -> &GeneratedString {
1306 &self.value
1307 }
1308}
1309
1310impl GeneratedEnvironment {
1311 pub fn literal(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1317 Ok(Self {
1318 name: environment_name(name.into())?,
1319 value: Some(value),
1320 })
1321 }
1322
1323 pub fn host(name: impl Into<String>) -> Result<Self, GenerationError> {
1329 Ok(Self {
1330 name: environment_name(name.into())?,
1331 value: None,
1332 })
1333 }
1334
1335 #[must_use]
1337 pub fn name(&self) -> &str {
1338 &self.name
1339 }
1340
1341 #[must_use]
1343 pub const fn value(&self) -> Option<&GeneratedString> {
1344 self.value.as_ref()
1345 }
1346}
1347
1348impl GeneratedEnvironmentFile {
1349 pub fn short(path: GeneratedString) -> Result<Self, GenerationError> {
1356 require_generated_string("environment-file path", &path)?;
1357 Ok(Self::Short(path))
1358 }
1359
1360 pub fn long(
1367 path: GeneratedString,
1368 required: Option<bool>,
1369 format: Option<GeneratedEnvironmentFileFormat>,
1370 ) -> Result<Self, GenerationError> {
1371 require_generated_string("environment-file path", &path)?;
1372 Ok(Self::Long { path, required, format })
1373 }
1374
1375 #[must_use]
1377 pub const fn path(&self) -> &GeneratedString {
1378 match self {
1379 Self::Short(path) | Self::Long { path, .. } => path,
1380 }
1381 }
1382
1383 #[must_use]
1385 pub const fn required(&self) -> Option<bool> {
1386 match self {
1387 Self::Short(_) => None,
1388 Self::Long { required, .. } => *required,
1389 }
1390 }
1391
1392 #[must_use]
1394 pub const fn format(&self) -> Option<GeneratedEnvironmentFileFormat> {
1395 match self {
1396 Self::Short(_) => None,
1397 Self::Long { format, .. } => *format,
1398 }
1399 }
1400
1401 #[must_use]
1403 pub const fn is_sensitive(&self) -> bool {
1404 self.path().is_sensitive()
1405 }
1406}
1407
1408#[derive(Clone, Debug, Eq, PartialEq)]
1410pub struct GeneratedExtraHost {
1411 hostname: String,
1412 address: String,
1413}
1414
1415impl GeneratedExtraHost {
1416 pub fn new(hostname: impl Into<String>, address: impl Into<String>) -> Result<Self, GenerationError> {
1422 let hostname = short_component("extra-host hostname", hostname.into(), '=')?;
1423 let address = short_component("extra-host address", address.into(), '=')?;
1424 Ok(Self { hostname, address })
1425 }
1426
1427 #[must_use]
1429 pub fn hostname(&self) -> &str {
1430 &self.hostname
1431 }
1432
1433 #[must_use]
1435 pub fn address(&self) -> &str {
1436 &self.address
1437 }
1438}
1439
1440#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1442#[non_exhaustive]
1443pub enum GeneratedProtocol {
1444 Tcp,
1446 Udp,
1448 Sctp,
1450}
1451
1452impl GeneratedProtocol {
1453 const fn as_str(self) -> &'static str {
1454 match self {
1455 Self::Tcp => "tcp",
1456 Self::Udp => "udp",
1457 Self::Sctp => "sctp",
1458 }
1459 }
1460}
1461
1462#[derive(Clone, Debug, Eq, PartialEq)]
1464pub struct GeneratedPort {
1465 target: u16,
1466 published: Option<u16>,
1467 host_ip: Option<String>,
1468 protocol: GeneratedProtocol,
1469}
1470
1471impl GeneratedPort {
1472 pub fn new(
1480 target: u16,
1481 published: Option<u16>,
1482 host_ip: Option<String>,
1483 protocol: GeneratedProtocol,
1484 ) -> Result<Self, GenerationError> {
1485 if target == 0 {
1486 return Err(GenerationError::InvalidPort);
1487 }
1488 if let Some(host_ip) = host_ip.as_deref() {
1489 required("port host address", host_ip.to_owned())?;
1490 if protocol == GeneratedProtocol::Sctp && published.is_none() {
1491 return Err(GenerationError::UnrepresentableSctpHostIp);
1492 }
1493 }
1494 Ok(Self {
1495 target,
1496 published,
1497 host_ip,
1498 protocol,
1499 })
1500 }
1501
1502 #[must_use]
1504 pub const fn target(&self) -> u16 {
1505 self.target
1506 }
1507
1508 #[must_use]
1510 pub const fn published(&self) -> Option<u16> {
1511 self.published
1512 }
1513
1514 #[must_use]
1516 pub fn host_ip(&self) -> Option<&str> {
1517 self.host_ip.as_deref()
1518 }
1519
1520 #[must_use]
1522 pub const fn protocol(&self) -> GeneratedProtocol {
1523 self.protocol
1524 }
1525}
1526
1527#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1529#[non_exhaustive]
1530pub enum GeneratedSelinux {
1531 Private,
1533 Shared,
1535}
1536
1537impl GeneratedSelinux {
1538 const fn as_str(self) -> &'static str {
1539 match self {
1540 Self::Private => "Z",
1541 Self::Shared => "z",
1542 }
1543 }
1544}
1545
1546#[derive(Clone, Debug, Eq, PartialEq)]
1547enum GeneratedMountKind {
1548 Volume {
1549 source: String,
1550 },
1551 Bind {
1552 source: String,
1553 selinux: Option<GeneratedSelinux>,
1554 },
1555 Anonymous,
1556}
1557
1558#[derive(Clone, Debug, Eq, PartialEq)]
1560pub struct GeneratedMount {
1561 kind: GeneratedMountKind,
1562 target: String,
1563 read_only: bool,
1564}
1565
1566impl GeneratedMount {
1567 pub fn volume(
1573 source: impl Into<String>,
1574 target: impl Into<String>,
1575 read_only: bool,
1576 ) -> Result<Self, GenerationError> {
1577 Ok(Self {
1578 kind: GeneratedMountKind::Volume {
1579 source: required("volume source", source.into())?,
1580 },
1581 target: required("mount target", target.into())?,
1582 read_only,
1583 })
1584 }
1585
1586 pub fn bind(
1593 source: impl Into<String>,
1594 target: impl Into<String>,
1595 read_only: bool,
1596 selinux: Option<GeneratedSelinux>,
1597 ) -> Result<Self, GenerationError> {
1598 let source = required("bind source", source.into())?;
1599 let target = required("mount target", target.into())?;
1600 if selinux.is_some() && (source.contains(':') || target.contains(':')) {
1601 return Err(GenerationError::InvalidSelinuxBind);
1602 }
1603 Ok(Self {
1604 kind: GeneratedMountKind::Bind { source, selinux },
1605 target,
1606 read_only,
1607 })
1608 }
1609
1610 pub fn anonymous(target: impl Into<String>, read_only: bool) -> Result<Self, GenerationError> {
1616 Ok(Self {
1617 kind: GeneratedMountKind::Anonymous,
1618 target: required("mount target", target.into())?,
1619 read_only,
1620 })
1621 }
1622
1623 #[must_use]
1625 pub fn target(&self) -> &str {
1626 &self.target
1627 }
1628
1629 #[must_use]
1631 pub const fn read_only(&self) -> bool {
1632 self.read_only
1633 }
1634}
1635
1636#[derive(Clone, Eq, PartialEq)]
1638pub struct GeneratedNetworkAttachment {
1639 name: String,
1640 aliases: Vec<String>,
1641 alias_sensitivities: Vec<bool>,
1642 ipv4_address: Option<GeneratedString>,
1643 ipv6_address: Option<GeneratedString>,
1644}
1645
1646impl GeneratedNetworkAttachment {
1647 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1653 Ok(Self {
1654 name: required("network name", name.into())?,
1655 aliases: Vec::new(),
1656 alias_sensitivities: Vec::new(),
1657 ipv4_address: None,
1658 ipv6_address: None,
1659 })
1660 }
1661
1662 pub fn add_alias(&mut self, alias: impl Into<String>) -> Result<(), GenerationError> {
1668 self.add_alias_with_sensitivity(alias.into(), false)
1669 }
1670
1671 pub fn add_alias_value(&mut self, alias: &GeneratedString) -> Result<(), GenerationError> {
1678 let sensitive = alias.is_sensitive();
1679 self.add_alias_with_sensitivity(alias.expose().to_owned(), sensitive)
1680 }
1681
1682 fn add_alias_with_sensitivity(&mut self, alias: String, sensitive: bool) -> Result<(), GenerationError> {
1683 self.aliases.push(required("network alias", alias)?);
1684 self.alias_sensitivities.push(sensitive);
1685 Ok(())
1686 }
1687
1688 pub fn set_ipv4_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1696 set_once(&mut self.ipv4_address, address, "ipv4_address")
1697 }
1698
1699 pub fn set_ipv6_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1707 set_once(&mut self.ipv6_address, address, "ipv6_address")
1708 }
1709
1710 #[must_use]
1712 pub fn name(&self) -> &str {
1713 &self.name
1714 }
1715
1716 #[must_use]
1718 pub fn aliases(&self) -> &[String] {
1719 &self.aliases
1720 }
1721
1722 #[must_use]
1724 pub fn alias_sensitivities(&self) -> &[bool] {
1725 &self.alias_sensitivities
1726 }
1727
1728 #[must_use]
1730 pub const fn ipv4_address(&self) -> Option<&GeneratedString> {
1731 self.ipv4_address.as_ref()
1732 }
1733
1734 #[must_use]
1736 pub const fn ipv6_address(&self) -> Option<&GeneratedString> {
1737 self.ipv6_address.as_ref()
1738 }
1739
1740 fn is_sensitive(&self) -> bool {
1741 self.alias_sensitivities.iter().copied().any(std::convert::identity)
1742 || self.ipv4_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1743 || self.ipv6_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1744 }
1745}
1746
1747impl fmt::Debug for GeneratedNetworkAttachment {
1748 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1749 let aliases = self
1750 .aliases
1751 .iter()
1752 .enumerate()
1753 .map(|(index, alias)| {
1754 if self.alias_sensitivities.get(index).copied().unwrap_or(false) {
1755 "<redacted>"
1756 } else {
1757 alias.as_str()
1758 }
1759 })
1760 .collect::<Vec<_>>();
1761 formatter
1762 .debug_struct("GeneratedNetworkAttachment")
1763 .field("name", &self.name)
1764 .field("aliases", &aliases)
1765 .field("ipv4_address", &self.ipv4_address)
1766 .field("ipv6_address", &self.ipv6_address)
1767 .finish()
1768 }
1769}
1770
1771#[derive(Clone, Debug, Eq, PartialEq)]
1773pub struct GeneratedResource {
1774 name: String,
1775 external: bool,
1776 custom_name: Option<String>,
1777}
1778
1779impl GeneratedResource {
1780 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
1786 Ok(Self {
1787 name: required("resource name", name.into())?,
1788 external: false,
1789 custom_name: None,
1790 })
1791 }
1792
1793 pub fn external(name: impl Into<String>) -> Result<Self, GenerationError> {
1799 Ok(Self {
1800 name: required("resource name", name.into())?,
1801 external: true,
1802 custom_name: None,
1803 })
1804 }
1805
1806 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
1814 let name = required("custom resource name", name.into())?;
1815 set_once(&mut self.custom_name, name, "resource name")
1816 }
1817
1818 #[must_use]
1820 pub fn name(&self) -> &str {
1821 &self.name
1822 }
1823
1824 #[must_use]
1826 pub const fn is_external(&self) -> bool {
1827 self.external
1828 }
1829
1830 #[must_use]
1832 pub fn custom_name(&self) -> Option<&str> {
1833 self.custom_name.as_deref()
1834 }
1835}
1836
1837#[derive(Clone, Debug, Eq, PartialEq)]
1839#[non_exhaustive]
1840pub enum GeneratedCpuRtRuntime {
1841 Microseconds(GeneratedString),
1843 Duration(GeneratedString),
1845}
1846
1847impl GeneratedCpuRtRuntime {
1848 fn is_sensitive(&self) -> bool {
1849 match self {
1850 Self::Microseconds(value) | Self::Duration(value) => value.is_sensitive(),
1851 }
1852 }
1853}
1854
1855#[derive(Clone, Debug, Eq, PartialEq)]
1859#[non_exhaustive]
1860pub enum GeneratedServiceRuntimeField {
1861 Domainname(GeneratedString),
1863 Isolation(GeneratedString),
1865 MacAddress(GeneratedString),
1867 Uts(GeneratedString),
1869 UseApiSocket(bool),
1871 GpusAll(GeneratedString),
1873 CpuRtRuntime(GeneratedCpuRtRuntime),
1875 CpuShares(GeneratedString),
1877 Cpus(GeneratedString),
1879 Cpuset(GeneratedString),
1881 DeviceCgroupRules(Vec<GeneratedString>),
1883 Ipc(GeneratedString),
1885 MemReservation(GeneratedString),
1887 MemSwappiness(GeneratedString),
1889 MemswapLimit(GeneratedString),
1891 NetworkMode(GeneratedString),
1893 OomKillDisable(bool),
1895 OomScoreAdj(GeneratedString),
1897 Pid(GeneratedString),
1899 Scale(GeneratedString),
1901 VolumesFrom(Vec<GeneratedString>),
1903}
1904
1905impl GeneratedServiceRuntimeField {
1906 fn field_name(&self) -> &'static str {
1907 match self {
1908 Self::Domainname(_) => "domainname",
1909 Self::Isolation(_) => "isolation",
1910 Self::MacAddress(_) => "mac_address",
1911 Self::Uts(_) => "uts",
1912 Self::UseApiSocket(_) => "use_api_socket",
1913 Self::GpusAll(_) => "gpus",
1914 Self::CpuRtRuntime(_) => "cpu_rt_runtime",
1915 Self::CpuShares(_) => "cpu_shares",
1916 Self::Cpus(_) => "cpus",
1917 Self::Cpuset(_) => "cpuset",
1918 Self::DeviceCgroupRules(_) => "device_cgroup_rules",
1919 Self::Ipc(_) => "ipc",
1920 Self::MemReservation(_) => "mem_reservation",
1921 Self::MemSwappiness(_) => "mem_swappiness",
1922 Self::MemswapLimit(_) => "memswap_limit",
1923 Self::NetworkMode(_) => "network_mode",
1924 Self::OomKillDisable(_) => "oom_kill_disable",
1925 Self::OomScoreAdj(_) => "oom_score_adj",
1926 Self::Pid(_) => "pid",
1927 Self::Scale(_) => "scale",
1928 Self::VolumesFrom(_) => "volumes_from",
1929 }
1930 }
1931
1932 fn is_sensitive(&self) -> bool {
1933 match self {
1934 Self::Domainname(value)
1935 | Self::Isolation(value)
1936 | Self::MacAddress(value)
1937 | Self::Uts(value)
1938 | Self::GpusAll(value)
1939 | Self::CpuShares(value)
1940 | Self::Cpus(value)
1941 | Self::Cpuset(value)
1942 | Self::Ipc(value)
1943 | Self::MemReservation(value)
1944 | Self::MemSwappiness(value)
1945 | Self::MemswapLimit(value)
1946 | Self::NetworkMode(value)
1947 | Self::OomScoreAdj(value)
1948 | Self::Pid(value)
1949 | Self::Scale(value) => value.is_sensitive(),
1950 Self::DeviceCgroupRules(values) | Self::VolumesFrom(values) => {
1951 values.iter().any(GeneratedString::is_sensitive)
1952 }
1953 Self::UseApiSocket(_) | Self::OomKillDisable(_) => false,
1954 Self::CpuRtRuntime(value) => value.is_sensitive(),
1955 }
1956 }
1957}
1958
1959#[derive(Clone, Debug, Eq, PartialEq)]
1961pub struct GeneratedService {
1962 name: String,
1963 hostname: Option<GeneratedHostname>,
1964 container_name: Option<GeneratedString>,
1965 image: Option<GeneratedString>,
1966 entrypoint: Option<GeneratedEntrypoint>,
1967 command: Option<GeneratedCommand>,
1968 init: Option<bool>,
1969 stdin_open: Option<bool>,
1970 tty: Option<bool>,
1971 privileged: Option<bool>,
1972 environment_files: Vec<GeneratedEnvironmentFile>,
1973 environment: Vec<GeneratedEnvironment>,
1974 labels: Vec<GeneratedLabel>,
1975 annotations: Option<Vec<GeneratedAnnotation>>,
1976 user: Option<GeneratedString>,
1977 userns_mode: Option<GeneratedString>,
1978 group_add: Vec<GeneratedString>,
1979 cap_add: Option<Vec<GeneratedString>>,
1980 cap_drop: Option<Vec<GeneratedString>>,
1981 devices: Option<Vec<GeneratedDevice>>,
1982 dns: Option<GeneratedDns>,
1983 dns_options: Option<Vec<GeneratedString>>,
1984 dns_search: Option<GeneratedDnsSearch>,
1985 expose: Option<Vec<GeneratedString>>,
1986 security_options: Option<Vec<GeneratedString>>,
1987 working_dir: Option<GeneratedString>,
1988 read_only: Option<bool>,
1989 pids_limit: Option<GeneratedPidsLimit>,
1990 shm_size: Option<GeneratedShmSize>,
1991 mem_limit: Option<GeneratedMemLimit>,
1992 tmpfs: Option<GeneratedTmpfs>,
1993 sysctls: Option<GeneratedSysctls>,
1994 logging: Option<GeneratedLogging>,
1995 ulimits: Option<GeneratedUlimits>,
1996 pull_policy: Option<GeneratedPullPolicy>,
1997 restart: Option<GeneratedRestartPolicy>,
1998 stop_signal: Option<GeneratedString>,
1999 stop_grace_period: Option<GeneratedString>,
2000 extra_hosts: Vec<GeneratedExtraHost>,
2001 ports: Vec<GeneratedPort>,
2002 mounts: Vec<GeneratedMount>,
2003 networks: Vec<GeneratedNetworkAttachment>,
2004 runtime_fields: Vec<GeneratedServiceRuntimeField>,
2005}
2006
2007impl GeneratedService {
2008 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
2014 Ok(Self {
2015 name: required("service name", name.into())?,
2016 hostname: None,
2017 container_name: None,
2018 image: None,
2019 entrypoint: None,
2020 command: None,
2021 init: None,
2022 stdin_open: None,
2023 tty: None,
2024 privileged: None,
2025 environment_files: Vec::new(),
2026 environment: Vec::new(),
2027 labels: Vec::new(),
2028 annotations: None,
2029 user: None,
2030 userns_mode: None,
2031 group_add: Vec::new(),
2032 cap_add: None,
2033 cap_drop: None,
2034 devices: None,
2035 dns: None,
2036 dns_options: None,
2037 dns_search: None,
2038 expose: None,
2039 security_options: None,
2040 working_dir: None,
2041 read_only: None,
2042 pids_limit: None,
2043 shm_size: None,
2044 mem_limit: None,
2045 tmpfs: None,
2046 sysctls: None,
2047 logging: None,
2048 ulimits: None,
2049 pull_policy: None,
2050 restart: None,
2051 stop_signal: None,
2052 stop_grace_period: None,
2053 extra_hosts: Vec::new(),
2054 ports: Vec::new(),
2055 mounts: Vec::new(),
2056 networks: Vec::new(),
2057 runtime_fields: Vec::new(),
2058 })
2059 }
2060
2061 #[must_use]
2063 pub fn name(&self) -> &str {
2064 &self.name
2065 }
2066
2067 pub fn add_runtime_field(&mut self, field: GeneratedServiceRuntimeField) -> Result<(), GenerationError> {
2079 if self
2080 .runtime_fields
2081 .iter()
2082 .any(|existing| existing.field_name() == field.field_name())
2083 {
2084 return Err(GenerationError::DuplicateField(field.field_name()));
2085 }
2086 if !generated_runtime_field_safe(&field) {
2087 return Err(GenerationError::InvalidServiceRuntimeField(field.field_name()));
2088 }
2089 self.runtime_fields.push(field);
2090 Ok(())
2091 }
2092
2093 #[must_use]
2095 pub fn runtime_fields(&self) -> &[GeneratedServiceRuntimeField] {
2096 &self.runtime_fields
2097 }
2098
2099 pub fn set_hostname(&mut self, hostname: GeneratedHostname) -> Result<(), GenerationError> {
2107 let GeneratedHostname::Resolved(value) = &hostname;
2108 if !valid_hostname(value.expose()) {
2109 return Err(GenerationError::InvalidHostname);
2110 }
2111 set_once(&mut self.hostname, hostname, "hostname")
2112 }
2113
2114 pub fn set_container_name(&mut self, name: GeneratedString) -> Result<(), GenerationError> {
2122 if !valid_container_name(name.expose()) {
2123 return Err(GenerationError::InvalidContainerName);
2124 }
2125 set_once(&mut self.container_name, name, "container_name")
2126 }
2127
2128 pub fn set_image(&mut self, image: GeneratedString) -> Result<(), GenerationError> {
2135 require_generated_string("service image", &image)?;
2136 set_once(&mut self.image, image, "image")
2137 }
2138
2139 pub fn set_entrypoint(&mut self, entrypoint: GeneratedEntrypoint) -> Result<(), GenerationError> {
2145 set_once(&mut self.entrypoint, entrypoint, "entrypoint")
2146 }
2147
2148 pub fn set_command(&mut self, command: GeneratedCommand) -> Result<(), GenerationError> {
2154 set_once(&mut self.command, command, "command")
2155 }
2156
2157 pub fn set_init(&mut self, init: bool) -> Result<(), GenerationError> {
2163 set_once(&mut self.init, init, "init")
2164 }
2165
2166 pub fn set_stdin_open(&mut self, stdin_open: bool) -> Result<(), GenerationError> {
2172 set_once(&mut self.stdin_open, stdin_open, "stdin_open")
2173 }
2174
2175 pub fn set_tty(&mut self, tty: bool) -> Result<(), GenerationError> {
2181 set_once(&mut self.tty, tty, "tty")
2182 }
2183
2184 pub fn set_privileged(&mut self, privileged: bool) -> Result<(), GenerationError> {
2190 set_once(&mut self.privileged, privileged, "privileged")
2191 }
2192
2193 pub fn add_environment_file(&mut self, environment_file: GeneratedEnvironmentFile) {
2195 self.environment_files.push(environment_file);
2196 }
2197
2198 pub fn add_environment(&mut self, environment: GeneratedEnvironment) {
2200 self.environment.push(environment);
2201 }
2202
2203 pub fn add_label(&mut self, label: GeneratedLabel) -> Result<(), GenerationError> {
2209 if self.labels.iter().any(|candidate| candidate.name == label.name) {
2210 return Err(GenerationError::DuplicateName {
2211 kind: "service label",
2212 name: label.name,
2213 });
2214 }
2215 self.labels.push(label);
2216 Ok(())
2217 }
2218
2219 pub fn set_annotations(&mut self, annotations: Vec<GeneratedAnnotation>) -> Result<(), GenerationError> {
2230 let mut seen = BTreeSet::new();
2231 for annotation in &annotations {
2232 if annotation.name.is_empty() || annotation.name.contains(['$', '\r', '\n', '\0']) {
2233 return Err(GenerationError::InvalidAnnotationName);
2234 }
2235 if annotation.value.expose().contains(['$', '\r', '\n', '\0']) {
2236 return Err(GenerationError::InvalidAnnotationValue);
2237 }
2238 if !seen.insert(annotation.name.as_str()) {
2239 return Err(GenerationError::DuplicateName {
2240 kind: "service annotation",
2241 name: annotation.name.clone(),
2242 });
2243 }
2244 }
2245 set_once(&mut self.annotations, annotations, "annotations")
2246 }
2247
2248 #[must_use]
2250 pub fn annotations(&self) -> Option<&[GeneratedAnnotation]> {
2251 self.annotations.as_deref()
2252 }
2253
2254 pub fn set_user(&mut self, user: GeneratedString) -> Result<(), GenerationError> {
2260 set_once(&mut self.user, user, "user")
2261 }
2262
2263 pub fn set_userns_mode(&mut self, mode: GeneratedString) -> Result<(), GenerationError> {
2270 require_generated_string("user namespace mode", &mode)?;
2271 set_once(&mut self.userns_mode, mode, "userns_mode")
2272 }
2273
2274 pub fn add_supplementary_group(&mut self, group: GeneratedString) -> Result<(), GenerationError> {
2280 require_generated_string("supplementary group", &group)?;
2281 self.group_add.push(group);
2282 Ok(())
2283 }
2284
2285 pub fn set_cap_add(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2298 let mut seen = BTreeSet::new();
2299 for capability in &capabilities {
2300 require_generated_string("cap_add item", capability)?;
2301 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2302 return Err(GenerationError::ContainsLineBreak("cap_add item"));
2303 }
2304 if !seen.insert(capability.expose()) {
2305 return Err(GenerationError::DuplicateItem("cap_add"));
2306 }
2307 }
2308 set_once(&mut self.cap_add, capabilities, "cap_add")
2309 }
2310
2311 #[must_use]
2313 pub fn cap_add(&self) -> Option<&[GeneratedString]> {
2314 self.cap_add.as_deref()
2315 }
2316
2317 pub fn set_cap_drop(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2330 let mut seen = BTreeSet::new();
2331 for capability in &capabilities {
2332 require_generated_string("cap_drop item", capability)?;
2333 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2334 return Err(GenerationError::ContainsLineBreak("cap_drop item"));
2335 }
2336 if !seen.insert(capability.expose()) {
2337 return Err(GenerationError::DuplicateItem("cap_drop"));
2338 }
2339 }
2340 set_once(&mut self.cap_drop, capabilities, "cap_drop")
2341 }
2342
2343 #[must_use]
2345 pub fn cap_drop(&self) -> Option<&[GeneratedString]> {
2346 self.cap_drop.as_deref()
2347 }
2348
2349 pub fn set_devices(&mut self, devices: Vec<GeneratedDevice>) -> Result<(), GenerationError> {
2362 for device in &devices {
2363 match device {
2364 GeneratedDevice::Short(value) => {
2365 validate_generated_device_member("short item", value, true)?;
2366 }
2367 GeneratedDevice::Long(value) => {
2368 validate_generated_device_member("source", value.source(), true)?;
2369 if let Some(target) = value.target() {
2370 validate_generated_device_member("target", target, false)?;
2371 }
2372 if let Some(permissions) = value.permissions() {
2373 validate_generated_device_member("permissions", permissions, false)?;
2374 }
2375 }
2376 }
2377 }
2378 set_once(&mut self.devices, devices, "devices")
2379 }
2380
2381 pub fn set_dns(&mut self, dns: GeneratedDns) -> Result<(), GenerationError> {
2391 let values = match &dns {
2392 GeneratedDns::Scalar(value) => std::slice::from_ref(value),
2393 GeneratedDns::List(values) => values.as_slice(),
2394 };
2395 for value in values {
2396 if value.expose().is_empty()
2397 || value.expose().contains('$')
2398 || value.expose().contains('\r')
2399 || value.expose().contains('\n')
2400 {
2401 return Err(GenerationError::InvalidDnsValue);
2402 }
2403 }
2404 set_once(&mut self.dns, dns, "dns")
2405 }
2406
2407 #[must_use]
2409 pub const fn dns(&self) -> Option<&GeneratedDns> {
2410 self.dns.as_ref()
2411 }
2412
2413 pub fn set_dns_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2425 let mut seen = BTreeSet::new();
2426 for option in &options {
2427 if option.expose().is_empty()
2428 || option.expose().contains('$')
2429 || option.expose().contains('\r')
2430 || option.expose().contains('\n')
2431 || option.expose().contains('\0')
2432 {
2433 return Err(GenerationError::InvalidDnsOptionValue);
2434 }
2435 if !seen.insert(option.expose()) {
2436 return Err(GenerationError::DuplicateItem("dns_opt"));
2437 }
2438 }
2439 set_once(&mut self.dns_options, options, "dns_opt")
2440 }
2441
2442 #[must_use]
2444 pub fn dns_options(&self) -> Option<&[GeneratedString]> {
2445 self.dns_options.as_deref()
2446 }
2447
2448 pub fn set_dns_search(&mut self, search: GeneratedDnsSearch) -> Result<(), GenerationError> {
2458 let values = match &search {
2459 GeneratedDnsSearch::Scalar(value) => std::slice::from_ref(value),
2460 GeneratedDnsSearch::List(values) => values.as_slice(),
2461 };
2462 for value in values {
2463 if value.expose().is_empty()
2464 || value.expose().contains('$')
2465 || value.expose().contains('\r')
2466 || value.expose().contains('\n')
2467 || value.expose().contains('\0')
2468 {
2469 return Err(GenerationError::InvalidDnsSearchValue);
2470 }
2471 }
2472 set_once(&mut self.dns_search, search, "dns_search")
2473 }
2474
2475 #[must_use]
2477 pub const fn dns_search(&self) -> Option<&GeneratedDnsSearch> {
2478 self.dns_search.as_ref()
2479 }
2480
2481 pub fn set_expose(&mut self, expose: Vec<GeneratedString>) -> Result<(), GenerationError> {
2492 let mut seen = BTreeSet::new();
2493 for item in &expose {
2494 if !valid_generated_expose_item(item.expose()) {
2495 return Err(GenerationError::InvalidExposeValue);
2496 }
2497 if !seen.insert(item.expose()) {
2498 return Err(GenerationError::DuplicateItem("expose"));
2499 }
2500 }
2501 set_once(&mut self.expose, expose, "expose")
2502 }
2503
2504 #[must_use]
2506 pub fn expose(&self) -> Option<&[GeneratedString]> {
2507 self.expose.as_deref()
2508 }
2509
2510 pub fn set_security_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2520 for option in &options {
2521 if option.expose().is_empty()
2522 || option.expose().contains('$')
2523 || option.expose().contains('\r')
2524 || option.expose().contains('\n')
2525 || option.expose().contains('\0')
2526 {
2527 return Err(GenerationError::InvalidSecurityOptionValue);
2528 }
2529 }
2530 set_once(&mut self.security_options, options, "security_opt")
2531 }
2532
2533 #[must_use]
2535 pub fn security_options(&self) -> Option<&[GeneratedString]> {
2536 self.security_options.as_deref()
2537 }
2538
2539 #[must_use]
2541 pub fn devices(&self) -> Option<&[GeneratedDevice]> {
2542 self.devices.as_deref()
2543 }
2544
2545 pub fn set_working_dir(&mut self, directory: GeneratedString) -> Result<(), GenerationError> {
2552 require_generated_string("working directory", &directory)?;
2553 set_once(&mut self.working_dir, directory, "working_dir")
2554 }
2555
2556 pub fn set_read_only(&mut self, read_only: bool) -> Result<(), GenerationError> {
2562 set_once(&mut self.read_only, read_only, "read_only")
2563 }
2564
2565 pub fn set_pids_limit(&mut self, limit: GeneratedPidsLimit) -> Result<(), GenerationError> {
2573 if let GeneratedPidsLimit::Finite(decimal) = &limit {
2574 if !valid_positive_pids_decimal(decimal) {
2575 return Err(GenerationError::InvalidPidsLimit);
2576 }
2577 }
2578 set_once(&mut self.pids_limit, limit, "pids_limit")
2579 }
2580
2581 pub fn set_shm_size(&mut self, size: GeneratedShmSize) -> Result<(), GenerationError> {
2589 let GeneratedShmSize::Explicit { amount, .. } = &size;
2590 if !valid_generated_shm_amount(amount.expose()) {
2591 return Err(GenerationError::InvalidShmSize);
2592 }
2593 set_once(&mut self.shm_size, size, "shm_size")
2594 }
2595
2596 pub fn set_mem_limit(&mut self, limit: GeneratedMemLimit) -> Result<(), GenerationError> {
2604 let GeneratedMemLimit::Explicit { amount, .. } = &limit;
2605 if !valid_generated_mem_amount(amount.expose()) {
2606 return Err(GenerationError::InvalidMemLimit);
2607 }
2608 set_once(&mut self.mem_limit, limit, "mem_limit")
2609 }
2610
2611 pub fn set_tmpfs(&mut self, tmpfs: GeneratedTmpfs) -> Result<(), GenerationError> {
2622 let items = match &tmpfs {
2623 GeneratedTmpfs::Scalar(item) => std::slice::from_ref(item),
2624 GeneratedTmpfs::List(items) => items.as_slice(),
2625 };
2626 for item in items {
2627 require_generated_string("tmpfs item", item)?;
2628 if item.expose().contains('\r') || item.expose().contains('\n') {
2629 return Err(GenerationError::ContainsLineBreak("tmpfs item"));
2630 }
2631 if !valid_generated_tmpfs_item(item.expose()) {
2632 return Err(GenerationError::InvalidTmpfsItem);
2633 }
2634 }
2635 set_once(&mut self.tmpfs, tmpfs, "tmpfs")
2636 }
2637
2638 #[must_use]
2640 pub const fn tmpfs(&self) -> Option<&GeneratedTmpfs> {
2641 self.tmpfs.as_ref()
2642 }
2643
2644 pub fn set_sysctls(&mut self, sysctls: GeneratedSysctls) -> Result<(), GenerationError> {
2655 let mut seen = BTreeSet::new();
2656 match &sysctls {
2657 GeneratedSysctls::Map(entries) => {
2658 for entry in entries {
2659 if !seen.insert(entry.name()) {
2660 return Err(GenerationError::DuplicateName {
2661 kind: "sysctl",
2662 name: entry.name().to_owned(),
2663 });
2664 }
2665 }
2666 }
2667 GeneratedSysctls::List(items) => {
2668 for item in items {
2669 if item.expose().contains(['\r', '\n', '$']) {
2670 return Err(GenerationError::InvalidSysctlValue);
2671 }
2672 if !seen.insert(item.expose()) {
2673 return Err(GenerationError::DuplicateItem("sysctls"));
2674 }
2675 }
2676 }
2677 }
2678 set_once(&mut self.sysctls, sysctls, "sysctls")
2679 }
2680
2681 #[must_use]
2683 pub const fn sysctls(&self) -> Option<&GeneratedSysctls> {
2684 self.sysctls.as_ref()
2685 }
2686
2687 pub fn set_logging(&mut self, logging: GeneratedLogging) -> Result<(), GenerationError> {
2694 set_once(&mut self.logging, logging, "logging")
2695 }
2696
2697 #[must_use]
2699 pub const fn logging(&self) -> Option<&GeneratedLogging> {
2700 self.logging.as_ref()
2701 }
2702
2703 pub fn set_ulimits(&mut self, ulimits: GeneratedUlimits) -> Result<(), GenerationError> {
2712 set_once(&mut self.ulimits, ulimits, "ulimits")
2713 }
2714
2715 #[must_use]
2717 pub const fn ulimits(&self) -> Option<&GeneratedUlimits> {
2718 self.ulimits.as_ref()
2719 }
2720
2721 pub fn set_pull_policy(&mut self, policy: GeneratedPullPolicy) -> Result<(), GenerationError> {
2728 if let GeneratedPullPolicy::Every(duration) = &policy {
2729 if !valid_pull_policy_duration(duration.expose()) {
2730 return Err(GenerationError::InvalidPullPolicyDuration);
2731 }
2732 }
2733 set_once(&mut self.pull_policy, policy, "pull_policy")
2734 }
2735
2736 pub fn set_restart(&mut self, restart: GeneratedRestartPolicy) -> Result<(), GenerationError> {
2742 set_once(&mut self.restart, restart, "restart")
2743 }
2744
2745 pub fn set_stop_signal(&mut self, signal: GeneratedString) -> Result<(), GenerationError> {
2752 set_once(&mut self.stop_signal, signal, "stop_signal")
2753 }
2754
2755 pub fn set_stop_grace_period(&mut self, period: GeneratedString) -> Result<(), GenerationError> {
2763 if !StopGracePeriod::parse(period.expose().to_owned()).is_valid() {
2764 return Err(GenerationError::InvalidStopGracePeriod);
2765 }
2766 set_once(&mut self.stop_grace_period, period, "stop_grace_period")
2767 }
2768
2769 pub fn add_extra_host(&mut self, host: GeneratedExtraHost) {
2771 self.extra_hosts.push(host);
2772 }
2773
2774 pub fn add_port(&mut self, port: GeneratedPort) {
2776 self.ports.push(port);
2777 }
2778
2779 pub fn add_mount(&mut self, mount: GeneratedMount) {
2781 self.mounts.push(mount);
2782 }
2783
2784 pub fn add_network(&mut self, network: GeneratedNetworkAttachment) -> Result<(), GenerationError> {
2790 if self.networks.iter().any(|candidate| candidate.name == network.name) {
2791 return Err(GenerationError::DuplicateName {
2792 kind: "service network",
2793 name: network.name,
2794 });
2795 }
2796 self.networks.push(network);
2797 Ok(())
2798 }
2799
2800 fn is_sensitive(&self) -> bool {
2801 matches!(
2802 self.hostname.as_ref(),
2803 Some(GeneratedHostname::Resolved(hostname)) if hostname.is_sensitive()
2804 ) || self.image.as_ref().is_some_and(GeneratedString::is_sensitive)
2805 || self.entrypoint.as_ref().is_some_and(entrypoint_is_sensitive)
2806 || self.command.as_ref().is_some_and(command_is_sensitive)
2807 || self
2808 .environment_files
2809 .iter()
2810 .any(GeneratedEnvironmentFile::is_sensitive)
2811 || self
2812 .environment
2813 .iter()
2814 .filter_map(GeneratedEnvironment::value)
2815 .any(GeneratedString::is_sensitive)
2816 || self.labels.iter().any(|label| label.value.is_sensitive())
2817 || self
2818 .annotations
2819 .as_ref()
2820 .is_some_and(|items| items.iter().any(|annotation| annotation.value.is_sensitive()))
2821 || matches!(
2822 self.pull_policy.as_ref(),
2823 Some(GeneratedPullPolicy::Every(duration)) if duration.is_sensitive()
2824 )
2825 || matches!(
2826 self.shm_size.as_ref(),
2827 Some(GeneratedShmSize::Explicit { amount, .. }) if amount.is_sensitive()
2828 )
2829 || matches!(
2830 self.mem_limit.as_ref(),
2831 Some(GeneratedMemLimit::Explicit { amount, .. }) if amount.is_sensitive()
2832 )
2833 || match self.tmpfs.as_ref() {
2834 Some(GeneratedTmpfs::Scalar(item)) => item.is_sensitive(),
2835 Some(GeneratedTmpfs::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2836 None => false,
2837 }
2838 || match self.dns.as_ref() {
2839 Some(GeneratedDns::Scalar(value)) => value.is_sensitive(),
2840 Some(GeneratedDns::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2841 None => false,
2842 }
2843 || self
2844 .dns_options
2845 .as_ref()
2846 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2847 || self
2848 .runtime_fields
2849 .iter()
2850 .any(GeneratedServiceRuntimeField::is_sensitive)
2851 || match self.dns_search.as_ref() {
2852 Some(GeneratedDnsSearch::Scalar(value)) => value.is_sensitive(),
2853 Some(GeneratedDnsSearch::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2854 None => false,
2855 }
2856 || self
2857 .expose
2858 .as_ref()
2859 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2860 || self
2861 .security_options
2862 .as_ref()
2863 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2864 || match self.sysctls.as_ref() {
2865 Some(GeneratedSysctls::Map(entries)) => entries.iter().any(|entry| entry.value.is_sensitive()),
2866 Some(GeneratedSysctls::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2867 None => false,
2868 }
2869 || self.logging.as_ref().is_some_and(GeneratedLogging::is_sensitive)
2870 || self
2871 .ulimits
2872 .as_ref()
2873 .is_some_and(|limits| limits.entries.iter().any(GeneratedUlimit::is_sensitive))
2874 || [
2875 self.user.as_ref(),
2876 self.userns_mode.as_ref(),
2877 self.working_dir.as_ref(),
2878 self.stop_signal.as_ref(),
2879 self.stop_grace_period.as_ref(),
2880 ]
2881 .into_iter()
2882 .flatten()
2883 .any(GeneratedString::is_sensitive)
2884 || self.group_add.iter().any(GeneratedString::is_sensitive)
2885 || self
2886 .cap_add
2887 .as_ref()
2888 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2889 || self
2890 .cap_drop
2891 .as_ref()
2892 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2893 || self
2894 .devices
2895 .as_ref()
2896 .is_some_and(|items| items.iter().any(GeneratedDevice::is_sensitive))
2897 || self.networks.iter().any(GeneratedNetworkAttachment::is_sensitive)
2898 }
2899}
2900
2901#[derive(Clone, Debug, Eq, PartialEq)]
2902enum GeneratedNetwork {
2903 Basic(GeneratedResource),
2904 Definition(GeneratedNetworkDefinition),
2905}
2906
2907#[derive(Clone, Debug, Eq, PartialEq)]
2908enum GeneratedVolume {
2909 Basic(GeneratedResource),
2910 Definition(GeneratedVolumeDefinition),
2911}
2912
2913impl GeneratedVolume {
2914 fn name(&self) -> &str {
2915 match self {
2916 Self::Basic(volume) => volume.name(),
2917 Self::Definition(volume) => volume.name(),
2918 }
2919 }
2920
2921 fn is_sensitive(&self) -> bool {
2922 match self {
2923 Self::Basic(_) => false,
2924 Self::Definition(volume) => volume.is_sensitive(),
2925 }
2926 }
2927}
2928
2929impl GeneratedNetwork {
2930 fn name(&self) -> &str {
2931 match self {
2932 Self::Basic(network) => network.name(),
2933 Self::Definition(network) => network.name(),
2934 }
2935 }
2936
2937 fn is_sensitive(&self) -> bool {
2938 match self {
2939 Self::Basic(_) => false,
2940 Self::Definition(network) => network.is_sensitive(),
2941 }
2942 }
2943}
2944
2945#[derive(Clone, Eq, PartialEq)]
2950pub struct GeneratedConfigFileDefinition {
2951 name: String,
2952 file: GeneratedString,
2953}
2954
2955impl GeneratedConfigFileDefinition {
2956 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2962 Ok(Self {
2963 name: generated_file_resource_name(name.into())?,
2964 file: generated_file_resource_path(file)?,
2965 })
2966 }
2967
2968 #[must_use]
2970 pub fn name(&self) -> &str {
2971 &self.name
2972 }
2973
2974 #[must_use]
2976 pub const fn file(&self) -> &GeneratedString {
2977 &self.file
2978 }
2979
2980 fn is_sensitive(&self) -> bool {
2981 self.file.is_sensitive()
2982 }
2983}
2984
2985impl fmt::Debug for GeneratedConfigFileDefinition {
2986 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2987 formatter
2988 .debug_struct("GeneratedConfigFileDefinition")
2989 .field("name", &self.name)
2990 .field("file", &self.file)
2991 .finish()
2992 }
2993}
2994
2995#[derive(Clone, Eq, PartialEq)]
3000pub struct GeneratedSecretFileDefinition {
3001 name: String,
3002 file: GeneratedString,
3003}
3004
3005impl GeneratedSecretFileDefinition {
3006 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
3012 Ok(Self {
3013 name: generated_file_resource_name(name.into())?,
3014 file: generated_file_resource_path(file)?,
3015 })
3016 }
3017
3018 #[must_use]
3020 pub fn name(&self) -> &str {
3021 &self.name
3022 }
3023
3024 #[must_use]
3026 pub const fn file(&self) -> &GeneratedString {
3027 &self.file
3028 }
3029
3030 fn is_sensitive(&self) -> bool {
3031 self.file.is_sensitive()
3032 }
3033}
3034
3035impl fmt::Debug for GeneratedSecretFileDefinition {
3036 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3037 formatter
3038 .debug_struct("GeneratedSecretFileDefinition")
3039 .field("name", &self.name)
3040 .field("file", &self.file)
3041 .finish()
3042 }
3043}
3044
3045#[derive(Clone, Debug, Default, Eq, PartialEq)]
3047pub struct ComposeDocumentBuilder {
3048 name: Option<String>,
3049 services: Vec<GeneratedService>,
3050 networks: Vec<GeneratedNetwork>,
3051 volumes: Vec<GeneratedVolume>,
3052 configs: Vec<GeneratedConfigFileDefinition>,
3053 secrets: Vec<GeneratedSecretFileDefinition>,
3054}
3055
3056impl ComposeDocumentBuilder {
3057 #[must_use]
3059 pub const fn new() -> Self {
3060 Self {
3061 name: None,
3062 services: Vec::new(),
3063 networks: Vec::new(),
3064 volumes: Vec::new(),
3065 configs: Vec::new(),
3066 secrets: Vec::new(),
3067 }
3068 }
3069
3070 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
3076 let name = required("project name", name.into())?;
3077 set_once(&mut self.name, name, "name")
3078 }
3079
3080 pub fn add_service(&mut self, service: GeneratedService) -> Result<(), GenerationError> {
3086 insert_named(&mut self.services, service, "service", GeneratedService::name)
3087 }
3088
3089 pub fn add_network(&mut self, network: GeneratedResource) -> Result<(), GenerationError> {
3095 insert_named(
3096 &mut self.networks,
3097 GeneratedNetwork::Basic(network),
3098 "network",
3099 GeneratedNetwork::name,
3100 )
3101 }
3102
3103 pub fn add_network_definition(&mut self, network: GeneratedNetworkDefinition) -> Result<(), GenerationError> {
3113 insert_named(
3114 &mut self.networks,
3115 GeneratedNetwork::Definition(network),
3116 "network",
3117 GeneratedNetwork::name,
3118 )
3119 }
3120
3121 pub fn add_volume(&mut self, volume: GeneratedResource) -> Result<(), GenerationError> {
3127 insert_named(
3128 &mut self.volumes,
3129 GeneratedVolume::Basic(volume),
3130 "volume",
3131 GeneratedVolume::name,
3132 )
3133 }
3134
3135 pub fn add_volume_definition(&mut self, volume: GeneratedVolumeDefinition) -> Result<(), GenerationError> {
3146 insert_named(
3147 &mut self.volumes,
3148 GeneratedVolume::Definition(volume),
3149 "volume",
3150 GeneratedVolume::name,
3151 )
3152 }
3153
3154 pub fn add_config_file(&mut self, config: GeneratedConfigFileDefinition) -> Result<(), GenerationError> {
3160 insert_named(&mut self.configs, config, "config", GeneratedConfigFileDefinition::name)
3161 }
3162
3163 pub fn add_secret_file(&mut self, secret: GeneratedSecretFileDefinition) -> Result<(), GenerationError> {
3169 insert_named(&mut self.secrets, secret, "secret", GeneratedSecretFileDefinition::name)
3170 }
3171
3172 pub fn build(self, source_id: SourceId) -> Result<GeneratedComposeDocument, GenerationError> {
3179 if self.services.is_empty() {
3180 return Err(GenerationError::MissingService);
3181 }
3182 let sensitive = self.services.iter().any(GeneratedService::is_sensitive)
3183 || self.networks.iter().any(GeneratedNetwork::is_sensitive)
3184 || self.volumes.iter().any(GeneratedVolume::is_sensitive)
3185 || self.configs.iter().any(GeneratedConfigFileDefinition::is_sensitive)
3186 || self.secrets.iter().any(GeneratedSecretFileDefinition::is_sensitive);
3187 let text = render_document(&self);
3188 let syntax = SyntaxDocument::parse(source_id, text.clone())
3189 .map_err(|_| GenerationError::InternalInvariant("syntax-tree"))?;
3190 if !syntax.is_valid() {
3191 return Err(GenerationError::InternalInvariant("syntax"));
3192 }
3193 let model = ComposeDocument::parse(syntax.document());
3194 if !model.is_valid() {
3195 return Err(GenerationError::InternalInvariant("typed-model"));
3196 }
3197 let document = model
3198 .document()
3199 .cloned()
3200 .ok_or(GenerationError::InternalInvariant("document-root"))?;
3201 Ok(GeneratedComposeDocument {
3202 text,
3203 sensitive,
3204 document,
3205 })
3206 }
3207}
3208
3209#[derive(Clone, Eq, PartialEq)]
3211pub struct GeneratedComposeDocument {
3212 text: String,
3213 sensitive: bool,
3214 document: ComposeDocument,
3215}
3216
3217impl GeneratedComposeDocument {
3218 #[must_use]
3220 pub fn text(&self) -> &str {
3221 &self.text
3222 }
3223
3224 #[must_use]
3226 pub const fn document(&self) -> &ComposeDocument {
3227 &self.document
3228 }
3229
3230 #[must_use]
3232 pub const fn is_sensitive(&self) -> bool {
3233 self.sensitive
3234 }
3235}
3236
3237impl fmt::Debug for GeneratedComposeDocument {
3238 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3239 formatter
3240 .debug_struct("GeneratedComposeDocument")
3241 .field("text", &if self.sensitive { "<redacted>" } else { &self.text })
3242 .field("sensitive", &self.sensitive)
3243 .field("document", &if self.sensitive { "<redacted>" } else { "validated" })
3244 .finish()
3245 }
3246}
3247
3248fn render_document(project: &ComposeDocumentBuilder) -> String {
3249 let mut output = String::from("---\n");
3250 if let Some(name) = &project.name {
3251 output.push_str("name: ");
3252 write_quoted(&mut output, name);
3253 output.push('\n');
3254 }
3255 output.push_str("services:\n");
3256 for service in &project.services {
3257 write_indent(&mut output, 1);
3258 write_quoted(&mut output, &service.name);
3259 output.push_str(":\n");
3260 render_service(&mut output, service);
3261 }
3262 render_network_definitions(&mut output, &project.networks);
3263 render_volume_definitions(&mut output, &project.volumes);
3264 render_file_definitions(
3265 &mut output,
3266 "configs",
3267 &project.configs,
3268 GeneratedConfigFileDefinition::name,
3269 GeneratedConfigFileDefinition::file,
3270 );
3271 render_file_definitions(
3272 &mut output,
3273 "secrets",
3274 &project.secrets,
3275 GeneratedSecretFileDefinition::name,
3276 GeneratedSecretFileDefinition::file,
3277 );
3278 output
3279}
3280
3281fn render_service(output: &mut String, service: &GeneratedService) {
3282 if let Some(GeneratedHostname::Resolved(hostname)) = &service.hostname {
3283 render_optional_string(output, "hostname", Some(hostname));
3284 }
3285 render_optional_string(output, "container_name", service.container_name.as_ref());
3286 render_optional_string(output, "image", service.image.as_ref());
3287 if let Some(entrypoint) = &service.entrypoint {
3288 render_entrypoint(output, entrypoint);
3289 }
3290 if let Some(command) = &service.command {
3291 render_command(output, command);
3292 }
3293 if let Some(init) = service.init {
3294 write_field(output, 2, "init");
3295 output.push_str(if init { "true\n" } else { "false\n" });
3296 }
3297 if let Some(stdin_open) = service.stdin_open {
3298 write_field(output, 2, "stdin_open");
3299 output.push_str(if stdin_open { "true\n" } else { "false\n" });
3300 }
3301 if let Some(tty) = service.tty {
3302 write_field(output, 2, "tty");
3303 output.push_str(if tty { "true\n" } else { "false\n" });
3304 }
3305 if let Some(privileged) = service.privileged {
3306 write_field(output, 2, "privileged");
3307 output.push_str(if privileged { "true\n" } else { "false\n" });
3308 }
3309 render_environment_files(output, &service.environment_files);
3310 render_environment(output, &service.environment);
3311 render_labels(output, &service.labels);
3312 if let Some(annotations) = &service.annotations {
3313 render_annotations(output, annotations);
3314 }
3315 render_optional_string(output, "user", service.user.as_ref());
3316 render_optional_string(output, "userns_mode", service.userns_mode.as_ref());
3317 render_string_sequence(output, "group_add", &service.group_add);
3318 if let Some(capabilities) = &service.cap_add {
3319 render_configured_string_sequence(output, "cap_add", capabilities);
3320 }
3321 if let Some(capabilities) = &service.cap_drop {
3322 render_configured_string_sequence(output, "cap_drop", capabilities);
3323 }
3324 render_optional_string(output, "working_dir", service.working_dir.as_ref());
3325 if let Some(read_only) = service.read_only {
3326 write_field(output, 2, "read_only");
3327 output.push_str(if read_only { "true\n" } else { "false\n" });
3328 }
3329 if let Some(pids_limit) = &service.pids_limit {
3330 render_pids_limit(output, pids_limit);
3331 }
3332 if let Some(shm_size) = &service.shm_size {
3333 render_shm_size(output, shm_size);
3334 }
3335 if let Some(mem_limit) = &service.mem_limit {
3336 render_mem_limit(output, mem_limit);
3337 }
3338 render_runtime_fields(output, &service.runtime_fields);
3339 if let Some(devices) = &service.devices {
3340 render_devices(output, devices);
3341 }
3342 if let Some(dns) = &service.dns {
3343 render_dns(output, dns);
3344 }
3345 if let Some(options) = &service.dns_options {
3346 render_configured_string_sequence(output, "dns_opt", options);
3347 }
3348 if let Some(search) = &service.dns_search {
3349 render_dns_search(output, search);
3350 }
3351 if let Some(expose) = &service.expose {
3352 render_configured_string_sequence(output, "expose", expose);
3353 }
3354 if let Some(options) = &service.security_options {
3355 render_configured_string_sequence(output, "security_opt", options);
3356 }
3357 if let Some(tmpfs) = &service.tmpfs {
3358 render_tmpfs(output, tmpfs);
3359 }
3360 if let Some(sysctls) = &service.sysctls {
3361 render_sysctls(output, sysctls);
3362 }
3363 if let Some(logging) = &service.logging {
3364 render_logging(output, logging);
3365 }
3366 if let Some(ulimits) = &service.ulimits {
3367 render_ulimits(output, ulimits);
3368 }
3369 if let Some(pull_policy) = &service.pull_policy {
3370 render_pull_policy(output, pull_policy);
3371 }
3372 if let Some(restart) = service.restart {
3373 render_restart(output, restart);
3374 }
3375 render_optional_string(output, "stop_signal", service.stop_signal.as_ref());
3376 render_optional_string(output, "stop_grace_period", service.stop_grace_period.as_ref());
3377 render_extra_hosts(output, &service.extra_hosts);
3378 render_ports(output, &service.ports);
3379 render_mounts(output, &service.mounts);
3380 render_networks(output, &service.networks);
3381}
3382
3383fn render_runtime_fields(output: &mut String, fields: &[GeneratedServiceRuntimeField]) {
3384 for field in fields {
3385 match field {
3386 GeneratedServiceRuntimeField::Domainname(value) => {
3387 render_optional_string(output, "domainname", Some(value));
3388 }
3389 GeneratedServiceRuntimeField::Isolation(value) => {
3390 render_optional_string(output, "isolation", Some(value));
3391 }
3392 GeneratedServiceRuntimeField::MacAddress(value) => {
3393 render_optional_string(output, "mac_address", Some(value));
3394 }
3395 GeneratedServiceRuntimeField::Uts(value) => {
3396 render_optional_string(output, "uts", Some(value));
3397 }
3398 GeneratedServiceRuntimeField::UseApiSocket(value) => {
3399 write_field(output, 2, "use_api_socket");
3400 output.push_str(if *value { "true\n" } else { "false\n" });
3401 }
3402 GeneratedServiceRuntimeField::GpusAll(value) => {
3403 render_optional_string(output, "gpus", Some(value));
3404 }
3405 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => {
3406 write_field(output, 2, "cpu_rt_runtime");
3407 output.push_str(value.expose());
3408 output.push('\n');
3409 }
3410 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3411 render_optional_string(output, "cpu_rt_runtime", Some(value));
3412 }
3413 GeneratedServiceRuntimeField::CpuShares(value) => {
3414 render_optional_string(output, "cpu_shares", Some(value));
3415 }
3416 GeneratedServiceRuntimeField::Cpus(value) => {
3417 render_optional_string(output, "cpus", Some(value));
3418 }
3419 GeneratedServiceRuntimeField::Cpuset(value) => {
3420 render_optional_string(output, "cpuset", Some(value));
3421 }
3422 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => {
3423 render_configured_string_sequence(output, "device_cgroup_rules", values);
3424 }
3425 GeneratedServiceRuntimeField::Ipc(value) => {
3426 render_optional_string(output, "ipc", Some(value));
3427 }
3428 GeneratedServiceRuntimeField::MemReservation(value) => {
3429 render_optional_string(output, "mem_reservation", Some(value));
3430 }
3431 GeneratedServiceRuntimeField::MemSwappiness(value) => {
3432 render_optional_string(output, "mem_swappiness", Some(value));
3433 }
3434 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3435 render_optional_string(output, "memswap_limit", Some(value));
3436 }
3437 GeneratedServiceRuntimeField::NetworkMode(value) => {
3438 render_optional_string(output, "network_mode", Some(value));
3439 }
3440 GeneratedServiceRuntimeField::OomKillDisable(value) => {
3441 write_field(output, 2, "oom_kill_disable");
3442 output.push_str(if *value { "true\n" } else { "false\n" });
3443 }
3444 GeneratedServiceRuntimeField::OomScoreAdj(value) => {
3445 render_optional_string(output, "oom_score_adj", Some(value));
3446 }
3447 GeneratedServiceRuntimeField::Pid(value) => {
3448 render_optional_string(output, "pid", Some(value));
3449 }
3450 GeneratedServiceRuntimeField::Scale(value) => {
3451 render_optional_string(output, "scale", Some(value));
3452 }
3453 GeneratedServiceRuntimeField::VolumesFrom(values) => {
3454 render_configured_string_sequence(output, "volumes_from", values);
3455 }
3456 }
3457 }
3458}
3459
3460fn generated_runtime_field_safe(field: &GeneratedServiceRuntimeField) -> bool {
3461 let safe = |value: &GeneratedString| !value.expose().is_empty() && !value.expose().contains(['\n', '\r', '$']);
3462 let unsigned = |value: &GeneratedString| safe(value) && value.expose().bytes().all(|byte| byte.is_ascii_digit());
3463 let bounded_unsigned = |value: &GeneratedString| unsigned(value) && value.expose().parse::<i128>().is_ok();
3464 let signed_range = |value: &GeneratedString, min: i32, max: i32| {
3465 safe(value)
3466 && value
3467 .expose()
3468 .parse::<i32>()
3469 .is_ok_and(|number| (min..=max).contains(&number))
3470 };
3471 let decimal = |value: &GeneratedString| safe(value) && normalize_generated_decimal(value.expose()).is_some();
3472 let reference = |value: &GeneratedString| {
3473 safe(value)
3474 && (!value.expose().contains(':')
3475 || value
3476 .expose()
3477 .split_once(':')
3478 .is_some_and(|(_, target)| !target.is_empty()))
3479 };
3480 match field {
3481 GeneratedServiceRuntimeField::Domainname(value)
3482 | GeneratedServiceRuntimeField::Isolation(value)
3483 | GeneratedServiceRuntimeField::MacAddress(value)
3484 | GeneratedServiceRuntimeField::Uts(value)
3485 | GeneratedServiceRuntimeField::Cpuset(value) => safe(value),
3486 GeneratedServiceRuntimeField::UseApiSocket(_) | GeneratedServiceRuntimeField::OomKillDisable(_) => true,
3487 GeneratedServiceRuntimeField::GpusAll(value) => safe(value) && value.expose() == "all",
3488 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => unsigned(value),
3489 GeneratedServiceRuntimeField::CpuShares(value) | GeneratedServiceRuntimeField::Scale(value) => {
3490 bounded_unsigned(value)
3491 }
3492 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3493 safe(value)
3494 && matches!(
3495 CpuRtRuntime::parse_string(value.expose().to_owned()),
3496 CpuRtRuntime::Duration(_)
3497 )
3498 }
3499 GeneratedServiceRuntimeField::Cpus(value) => decimal(value),
3500 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => values.iter().all(safe),
3501 GeneratedServiceRuntimeField::Ipc(value)
3502 | GeneratedServiceRuntimeField::NetworkMode(value)
3503 | GeneratedServiceRuntimeField::Pid(value) => reference(value),
3504 GeneratedServiceRuntimeField::MemReservation(value) => {
3505 safe(value) && valid_generated_runtime_memory(value.expose(), false)
3506 }
3507 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3508 safe(value) && valid_generated_runtime_memory(value.expose(), true)
3509 }
3510 GeneratedServiceRuntimeField::MemSwappiness(value) => signed_range(value, 0, 100),
3511 GeneratedServiceRuntimeField::OomScoreAdj(value) => signed_range(value, -1000, 1000),
3512 GeneratedServiceRuntimeField::VolumesFrom(values) => values.iter().all(reference),
3513 }
3514}
3515
3516fn normalize_generated_decimal(value: &str) -> Option<()> {
3517 let (whole, fraction) = value.split_once('.').unwrap_or((value, ""));
3518 let valid_shape = if value.contains('.') {
3519 !whole.is_empty() && !fraction.is_empty()
3520 } else {
3521 !whole.is_empty()
3522 };
3523 (valid_shape
3524 && whole.bytes().all(|byte| byte.is_ascii_digit())
3525 && fraction.bytes().all(|byte| byte.is_ascii_digit())
3526 && value.bytes().filter(|byte| *byte == b'.').count() <= 1)
3527 .then_some(())
3528}
3529
3530fn valid_generated_runtime_memory(value: &str, allow_unlimited: bool) -> bool {
3536 if allow_unlimited && value == "-1" {
3537 return true;
3538 }
3539 if !value.is_empty() && value.bytes().all(|byte| byte == b'0') {
3540 return true;
3541 }
3542 let Some(amount) = ["kb", "mb", "gb", "b", "k", "m", "g"]
3543 .into_iter()
3544 .find_map(|unit| value.strip_suffix(unit))
3545 else {
3546 return false;
3547 };
3548 !amount.is_empty() && amount.bytes().all(|byte| byte.is_ascii_digit())
3549}
3550
3551fn render_pids_limit(output: &mut String, limit: &GeneratedPidsLimit) {
3552 write_field(output, 2, "pids_limit");
3553 match limit {
3554 GeneratedPidsLimit::Unlimited => output.push_str("-1\n"),
3555 GeneratedPidsLimit::Finite(decimal) => {
3556 output.push_str(decimal);
3557 output.push('\n');
3558 }
3559 }
3560}
3561
3562fn render_shm_size(output: &mut String, size: &GeneratedShmSize) {
3563 let GeneratedShmSize::Explicit { amount, unit } = size;
3564 write_field(output, 2, "shm_size");
3565 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3566 output.push('\n');
3567}
3568
3569fn render_mem_limit(output: &mut String, limit: &GeneratedMemLimit) {
3570 let GeneratedMemLimit::Explicit { amount, unit } = limit;
3571 write_field(output, 2, "mem_limit");
3572 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3573 output.push('\n');
3574}
3575
3576fn render_devices(output: &mut String, devices: &[GeneratedDevice]) {
3577 if devices.is_empty() {
3578 output.push_str(" devices: []\n");
3579 return;
3580 }
3581 output.push_str(" devices:\n");
3582 for device in devices {
3583 match device {
3584 GeneratedDevice::Short(value) => {
3585 output.push_str(" - ");
3586 write_quoted(output, value.expose());
3587 output.push('\n');
3588 }
3589 GeneratedDevice::Long(value) => {
3590 output.push_str(" - source: ");
3591 write_quoted(output, value.source().expose());
3592 output.push('\n');
3593 if let Some(target) = value.target() {
3594 output.push_str(" target: ");
3595 write_quoted(output, target.expose());
3596 output.push('\n');
3597 }
3598 if let Some(permissions) = value.permissions() {
3599 output.push_str(" permissions: ");
3600 write_quoted(output, permissions.expose());
3601 output.push('\n');
3602 }
3603 }
3604 }
3605 }
3606}
3607
3608fn render_dns(output: &mut String, dns: &GeneratedDns) {
3609 match dns {
3610 GeneratedDns::Scalar(value) => render_optional_string(output, "dns", Some(value)),
3611 GeneratedDns::List(values) => render_configured_string_sequence(output, "dns", values),
3612 }
3613}
3614
3615fn render_dns_search(output: &mut String, search: &GeneratedDnsSearch) {
3616 match search {
3617 GeneratedDnsSearch::Scalar(value) => render_optional_string(output, "dns_search", Some(value)),
3618 GeneratedDnsSearch::List(values) => render_configured_string_sequence(output, "dns_search", values),
3619 }
3620}
3621
3622fn render_tmpfs(output: &mut String, tmpfs: &GeneratedTmpfs) {
3623 match tmpfs {
3624 GeneratedTmpfs::Scalar(item) => render_optional_string(output, "tmpfs", Some(item)),
3625 GeneratedTmpfs::List(items) => render_configured_string_sequence(output, "tmpfs", items),
3626 }
3627}
3628
3629fn render_sysctls(output: &mut String, sysctls: &GeneratedSysctls) {
3630 match sysctls {
3631 GeneratedSysctls::Map(entries) if entries.is_empty() => output.push_str(" sysctls: {}\n"),
3632 GeneratedSysctls::Map(entries) => {
3633 output.push_str(" sysctls:\n");
3634 for entry in entries {
3635 write_indent(output, 3);
3636 write_quoted(output, entry.name());
3637 output.push_str(": ");
3638 write_quoted(output, entry.value().expose());
3639 output.push('\n');
3640 }
3641 }
3642 GeneratedSysctls::List(items) => render_configured_string_sequence(output, "sysctls", items),
3643 }
3644}
3645
3646fn render_logging(output: &mut String, logging: &GeneratedLogging) {
3647 output.push_str(" logging:\n driver: ");
3648 write_quoted(output, logging.driver.expose());
3649 output.push('\n');
3650 if logging.options.is_empty() {
3651 output.push_str(" options: {}\n");
3652 return;
3653 }
3654 output.push_str(" options:\n");
3655 for option in &logging.options {
3656 write_indent(output, 4);
3657 write_quoted(output, option.name());
3658 output.push_str(": ");
3659 match option.value() {
3660 GeneratedLoggingOptionValue::String(value) => write_quoted(output, value.expose()),
3661 GeneratedLoggingOptionValue::Number(value) => output.push_str(value.expose()),
3662 GeneratedLoggingOptionValue::Null => output.push_str("null"),
3663 }
3664 output.push('\n');
3665 }
3666}
3667
3668fn render_ulimits(output: &mut String, ulimits: &GeneratedUlimits) {
3669 if ulimits.entries.is_empty() {
3670 output.push_str(" ulimits: {}\n");
3671 return;
3672 }
3673 output.push_str(" ulimits:\n");
3674 for limit in &ulimits.entries {
3675 write_indent(output, 3);
3676 write_quoted(output, limit.name());
3677 match limit.value() {
3678 GeneratedUlimitValue::Single(value) => {
3679 output.push_str(": ");
3680 write_quoted(output, value.expose());
3681 output.push('\n');
3682 }
3683 GeneratedUlimitValue::Range {
3684 soft: Some(soft),
3685 hard: Some(hard),
3686 } => {
3687 output.push_str(":\n");
3688 write_indent(output, 4);
3689 output.push_str("soft: ");
3690 write_quoted(output, soft.expose());
3691 output.push('\n');
3692 write_indent(output, 4);
3693 output.push_str("hard: ");
3694 write_quoted(output, hard.expose());
3695 output.push('\n');
3696 }
3697 GeneratedUlimitValue::Range { .. } => {
3698 unreachable!("generated ulimit ranges are validated during construction")
3699 }
3700 }
3701 }
3702}
3703
3704fn render_pull_policy(output: &mut String, policy: &GeneratedPullPolicy) {
3705 write_field(output, 2, "pull_policy");
3706 let value = match policy {
3707 GeneratedPullPolicy::Always => "always".to_owned(),
3708 GeneratedPullPolicy::Never => "never".to_owned(),
3709 GeneratedPullPolicy::Missing => "missing".to_owned(),
3710 GeneratedPullPolicy::IfNotPresentAlias => "if_not_present".to_owned(),
3711 GeneratedPullPolicy::Build => "build".to_owned(),
3712 GeneratedPullPolicy::Daily => "daily".to_owned(),
3713 GeneratedPullPolicy::Weekly => "weekly".to_owned(),
3714 GeneratedPullPolicy::Every(duration) => format!("every_{}", duration.expose()),
3715 };
3716 write_quoted(output, &value);
3717 output.push('\n');
3718}
3719
3720fn render_entrypoint(output: &mut String, entrypoint: &GeneratedEntrypoint) {
3721 match entrypoint {
3722 GeneratedEntrypoint::List(arguments) if arguments.is_empty() => output.push_str(" entrypoint: []\n"),
3723 GeneratedEntrypoint::List(arguments) => render_string_sequence(output, "entrypoint", arguments),
3724 GeneratedEntrypoint::String(entrypoint) => render_optional_string(output, "entrypoint", Some(entrypoint)),
3725 GeneratedEntrypoint::Empty => output.push_str(" entrypoint: []\n"),
3726 }
3727}
3728
3729fn render_restart(output: &mut String, restart: GeneratedRestartPolicy) {
3730 write_field(output, 2, "restart");
3731 let value = match restart {
3732 GeneratedRestartPolicy::No => "no".to_owned(),
3733 GeneratedRestartPolicy::Always => "always".to_owned(),
3734 GeneratedRestartPolicy::OnFailure { maximum_retries: None } => "on-failure".to_owned(),
3735 GeneratedRestartPolicy::OnFailure {
3736 maximum_retries: Some(maximum_retries),
3737 } => format!("on-failure:{maximum_retries}"),
3738 GeneratedRestartPolicy::UnlessStopped => "unless-stopped".to_owned(),
3739 };
3740 write_quoted(output, &value);
3741 output.push('\n');
3742}
3743
3744fn render_optional_string(output: &mut String, key: &str, value: Option<&GeneratedString>) {
3745 if let Some(value) = value {
3746 write_field(output, 2, key);
3747 write_quoted(output, value.expose());
3748 output.push('\n');
3749 }
3750}
3751
3752fn render_command(output: &mut String, command: &GeneratedCommand) {
3753 match command {
3754 GeneratedCommand::Exec(arguments) if arguments.is_empty() => output.push_str(" command: []\n"),
3755 GeneratedCommand::Exec(arguments) => render_string_sequence(output, "command", arguments),
3756 GeneratedCommand::Shell(command) => render_optional_string(output, "command", Some(command)),
3757 GeneratedCommand::Empty => output.push_str(" command: []\n"),
3758 }
3759}
3760
3761fn render_environment(output: &mut String, environment: &[GeneratedEnvironment]) {
3762 if environment.is_empty() {
3763 return;
3764 }
3765 output.push_str(" environment:\n");
3766 let mut variables: Vec<_> = environment.iter().collect();
3767 variables.sort_by(|left, right| left.name.cmp(&right.name));
3768 for variable in variables {
3769 output.push_str(" - ");
3770 let value = variable.value.as_ref().map_or_else(
3771 || variable.name.clone(),
3772 |value| format!("{}={}", variable.name, value.expose()),
3773 );
3774 write_quoted(output, &value);
3775 output.push('\n');
3776 }
3777}
3778
3779fn render_environment_files(output: &mut String, environment_files: &[GeneratedEnvironmentFile]) {
3780 if environment_files.is_empty() {
3781 return;
3782 }
3783 output.push_str(" env_file:\n");
3784 for environment_file in environment_files {
3785 match environment_file {
3786 GeneratedEnvironmentFile::Short(path) => {
3787 output.push_str(" - ");
3788 write_quoted(output, path.expose());
3789 output.push('\n');
3790 }
3791 GeneratedEnvironmentFile::Long { path, required, format } => {
3792 output.push_str(" - path: ");
3793 write_quoted(output, path.expose());
3794 output.push('\n');
3795 if let Some(required) = required {
3796 output.push_str(" required: ");
3797 output.push_str(if *required { "true\n" } else { "false\n" });
3798 }
3799 if let Some(format) = format {
3800 output.push_str(" format: ");
3801 write_quoted(
3802 output,
3803 match format {
3804 GeneratedEnvironmentFileFormat::Raw => "raw",
3805 },
3806 );
3807 output.push('\n');
3808 }
3809 }
3810 }
3811 }
3812}
3813
3814fn render_labels(output: &mut String, labels: &[GeneratedLabel]) {
3815 if labels.is_empty() {
3816 return;
3817 }
3818 output.push_str(" labels:\n");
3819 for label in labels {
3820 output.push_str(" ");
3821 write_quoted(output, &label.name);
3822 output.push_str(": ");
3823 write_quoted(output, label.value.expose());
3824 output.push('\n');
3825 }
3826}
3827
3828fn render_annotations(output: &mut String, annotations: &[GeneratedAnnotation]) {
3829 if annotations.is_empty() {
3830 output.push_str(" annotations: {}\n");
3831 return;
3832 }
3833 output.push_str(" annotations:\n");
3834 for annotation in annotations {
3835 output.push_str(" ");
3836 write_quoted(output, &annotation.name);
3837 output.push_str(": ");
3838 write_quoted(output, annotation.value.expose());
3839 output.push('\n');
3840 }
3841}
3842
3843fn render_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3844 if values.is_empty() {
3845 return;
3846 }
3847 write_indent(output, 2);
3848 output.push_str(key);
3849 output.push_str(":\n");
3850 for value in values {
3851 output.push_str(" - ");
3852 write_quoted(output, value.expose());
3853 output.push('\n');
3854 }
3855}
3856
3857fn render_configured_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3858 if values.is_empty() {
3859 write_indent(output, 2);
3860 output.push_str(key);
3861 output.push_str(": []\n");
3862 } else {
3863 render_string_sequence(output, key, values);
3864 }
3865}
3866
3867fn render_extra_hosts(output: &mut String, hosts: &[GeneratedExtraHost]) {
3868 if hosts.is_empty() {
3869 return;
3870 }
3871 output.push_str(" extra_hosts:\n");
3872 for host in hosts {
3873 output.push_str(" - ");
3874 write_quoted(output, &format!("{}={}", host.hostname, host.address));
3875 output.push('\n');
3876 }
3877}
3878
3879fn render_ports(output: &mut String, ports: &[GeneratedPort]) {
3880 if ports.is_empty() {
3881 return;
3882 }
3883 output.push_str(" ports:\n");
3884 for port in ports {
3885 if port.protocol == GeneratedProtocol::Sctp {
3886 render_short_sctp_port(output, port);
3887 continue;
3888 }
3889 output.push_str(" - target: ");
3890 output.push_str(&port.target.to_string());
3891 output.push('\n');
3892 if let Some(published) = port.published {
3893 output.push_str(" published: ");
3894 write_quoted(output, &published.to_string());
3895 output.push('\n');
3896 }
3897 if let Some(host_ip) = &port.host_ip {
3898 output.push_str(" host_ip: ");
3899 write_quoted(output, host_ip);
3900 output.push('\n');
3901 }
3902 output.push_str(" protocol: ");
3903 write_quoted(output, port.protocol.as_str());
3904 output.push('\n');
3905 }
3906}
3907
3908fn render_short_sctp_port(output: &mut String, port: &GeneratedPort) {
3909 let mut value = String::new();
3910 if let Some(host_ip) = &port.host_ip {
3911 if host_ip.contains(':') && !(host_ip.starts_with('[') && host_ip.ends_with(']')) {
3912 value.push('[');
3913 value.push_str(host_ip);
3914 value.push(']');
3915 } else {
3916 value.push_str(host_ip);
3917 }
3918 value.push(':');
3919 }
3920 if let Some(published) = port.published {
3921 value.push_str(&published.to_string());
3922 value.push(':');
3923 }
3924 value.push_str(&port.target.to_string());
3925 value.push_str("/sctp");
3926
3927 output.push_str(" - ");
3928 write_quoted(output, &value);
3929 output.push('\n');
3930}
3931
3932fn render_mounts(output: &mut String, mounts: &[GeneratedMount]) {
3933 if mounts.is_empty() {
3934 return;
3935 }
3936 output.push_str(" volumes:\n");
3937 for mount in mounts {
3938 match &mount.kind {
3939 GeneratedMountKind::Bind {
3940 source,
3941 selinux: Some(selinux),
3942 } => render_selinux_bind(output, source, mount, *selinux),
3943 kind => render_long_mount(output, kind, mount),
3944 }
3945 }
3946}
3947
3948fn render_selinux_bind(output: &mut String, source: &str, mount: &GeneratedMount, selinux: GeneratedSelinux) {
3949 let mut value = format!("{source}:{}:{}", mount.target, selinux.as_str());
3950 if mount.read_only {
3951 value.push_str(",ro");
3952 }
3953 output.push_str(" - ");
3954 write_quoted(output, &value);
3955 output.push('\n');
3956}
3957
3958fn render_long_mount(output: &mut String, kind: &GeneratedMountKind, mount: &GeneratedMount) {
3959 let (mount_type, source) = match kind {
3960 GeneratedMountKind::Volume { source } => ("volume", Some(source.as_str())),
3961 GeneratedMountKind::Bind { source, selinux: None } => ("bind", Some(source.as_str())),
3962 GeneratedMountKind::Anonymous => ("volume", None),
3963 GeneratedMountKind::Bind { selinux: Some(_), .. } => return,
3964 };
3965 output.push_str(" - type: ");
3966 write_quoted(output, mount_type);
3967 output.push('\n');
3968 if let Some(source) = source {
3969 output.push_str(" source: ");
3970 write_quoted(output, source);
3971 output.push('\n');
3972 }
3973 output.push_str(" target: ");
3974 write_quoted(output, &mount.target);
3975 output.push('\n');
3976 if mount.read_only {
3977 output.push_str(" read_only: true\n");
3978 }
3979}
3980
3981fn render_networks(output: &mut String, networks: &[GeneratedNetworkAttachment]) {
3982 if networks.is_empty() {
3983 return;
3984 }
3985 output.push_str(" networks:\n");
3986 for network in networks {
3987 output.push_str(" ");
3988 write_quoted(output, &network.name);
3989 if network.aliases.is_empty() && network.ipv4_address.is_none() && network.ipv6_address.is_none() {
3990 output.push_str(": {}\n");
3991 continue;
3992 }
3993 output.push_str(":\n");
3994 if !network.aliases.is_empty() {
3995 output.push_str(" aliases:\n");
3996 for alias in &network.aliases {
3997 output.push_str(" - ");
3998 write_quoted(output, alias);
3999 output.push('\n');
4000 }
4001 }
4002 for (field, address) in [
4003 ("ipv4_address", network.ipv4_address.as_ref()),
4004 ("ipv6_address", network.ipv6_address.as_ref()),
4005 ] {
4006 if let Some(address) = address {
4007 output.push_str(" ");
4008 output.push_str(field);
4009 output.push_str(": ");
4010 write_quoted(output, address.expose());
4011 output.push('\n');
4012 }
4013 }
4014 }
4015}
4016
4017fn render_network_definitions(output: &mut String, networks: &[GeneratedNetwork]) {
4018 if networks.is_empty() {
4019 return;
4020 }
4021 output.push_str("networks:\n");
4022 for network in networks {
4023 match network {
4024 GeneratedNetwork::Basic(network) => render_basic_resource(output, network),
4025 GeneratedNetwork::Definition(network) => render_network_definition(output, network),
4026 }
4027 }
4028}
4029
4030fn render_network_definition(output: &mut String, network: &GeneratedNetworkDefinition) {
4031 output.push_str(" ");
4032 write_quoted(output, &network.name);
4033 if network.custom_name.is_none()
4034 && network.driver.is_none()
4035 && network.driver_opts.is_none()
4036 && network.enable_ipv6.is_none()
4037 && network.internal.is_none()
4038 && network.labels.is_none()
4039 {
4040 output.push_str(": {}\n");
4041 return;
4042 }
4043 output.push_str(":\n");
4044 if let Some(custom_name) = &network.custom_name {
4045 output.push_str(" name: ");
4046 write_quoted(output, custom_name);
4047 output.push('\n');
4048 }
4049 if let Some(driver) = &network.driver {
4050 output.push_str(" driver: ");
4051 write_quoted(output, driver.expose());
4052 output.push('\n');
4053 }
4054 if let Some(driver_opts) = &network.driver_opts {
4055 if driver_opts.is_empty() {
4056 output.push_str(" driver_opts: {}\n");
4057 } else {
4058 output.push_str(" driver_opts:\n");
4059 for option in driver_opts {
4060 output.push_str(" ");
4061 write_quoted(output, option.name());
4062 output.push_str(": ");
4063 match option.value() {
4064 GeneratedNetworkDriverOptionValue::String(value) => {
4065 write_quoted(output, value.expose());
4066 }
4067 GeneratedNetworkDriverOptionValue::Number(value) => {
4068 output.push_str(value.expose());
4069 }
4070 }
4071 output.push('\n');
4072 }
4073 }
4074 }
4075 if let Some(enable_ipv6) = network.enable_ipv6 {
4076 output.push_str(" enable_ipv6: ");
4077 output.push_str(if enable_ipv6 { "true\n" } else { "false\n" });
4078 }
4079 if let Some(internal) = network.internal {
4080 output.push_str(" internal: ");
4081 output.push_str(if internal { "true\n" } else { "false\n" });
4082 }
4083 if let Some(labels) = &network.labels {
4084 if labels.is_empty() {
4085 output.push_str(" labels: {}\n");
4086 } else {
4087 output.push_str(" labels:\n");
4088 for label in labels {
4089 output.push_str(" ");
4090 write_quoted(output, label.name());
4091 output.push_str(": ");
4092 write_quoted(output, label.value().expose());
4093 output.push('\n');
4094 }
4095 }
4096 }
4097}
4098
4099fn render_volume_definitions(output: &mut String, volumes: &[GeneratedVolume]) {
4100 if volumes.is_empty() {
4101 return;
4102 }
4103 output.push_str("volumes:\n");
4104 for volume in volumes {
4105 match volume {
4106 GeneratedVolume::Basic(volume) => render_basic_resource(output, volume),
4107 GeneratedVolume::Definition(volume) => render_volume_definition(output, volume),
4108 }
4109 }
4110}
4111
4112fn render_file_definitions<T>(
4113 output: &mut String,
4114 field: &str,
4115 definitions: &[T],
4116 name: impl Fn(&T) -> &str,
4117 file: impl Fn(&T) -> &GeneratedString,
4118) {
4119 if definitions.is_empty() {
4120 return;
4121 }
4122 output.push_str(field);
4123 output.push_str(":\n");
4124 for definition in definitions {
4125 output.push_str(" ");
4126 write_quoted(output, name(definition));
4127 output.push_str(":\n file: ");
4128 write_quoted(output, file(definition).expose());
4129 output.push('\n');
4130 }
4131}
4132
4133fn render_volume_definition(output: &mut String, volume: &GeneratedVolumeDefinition) {
4134 output.push_str(" ");
4135 write_quoted(output, &volume.name);
4136 if volume.custom_name.is_none()
4137 && volume.driver.is_none()
4138 && volume.driver_opts.is_none()
4139 && volume.labels.is_none()
4140 {
4141 output.push_str(": {}\n");
4142 return;
4143 }
4144 output.push_str(":\n");
4145 if let Some(custom_name) = &volume.custom_name {
4146 output.push_str(" name: ");
4147 write_quoted(output, custom_name);
4148 output.push('\n');
4149 }
4150 if let Some(driver) = &volume.driver {
4151 output.push_str(" driver: ");
4152 write_quoted(output, driver.expose());
4153 output.push('\n');
4154 }
4155 if let Some(driver_opts) = &volume.driver_opts {
4156 if driver_opts.is_empty() {
4157 output.push_str(" driver_opts: {}\n");
4158 } else {
4159 output.push_str(" driver_opts:\n");
4160 for option in driver_opts {
4161 output.push_str(" ");
4162 write_quoted(output, option.name());
4163 output.push_str(": ");
4164 match option.value() {
4165 GeneratedVolumeDriverOptionValue::String(value) => write_quoted(output, value.expose()),
4166 GeneratedVolumeDriverOptionValue::Number(value) => output.push_str(value.expose()),
4167 }
4168 output.push('\n');
4169 }
4170 }
4171 }
4172 if let Some(labels) = &volume.labels {
4173 if labels.is_empty() {
4174 output.push_str(" labels: {}\n");
4175 } else {
4176 output.push_str(" labels:\n");
4177 for label in labels {
4178 output.push_str(" ");
4179 write_quoted(output, label.name());
4180 output.push_str(": ");
4181 write_quoted(output, label.value().expose());
4182 output.push('\n');
4183 }
4184 }
4185 }
4186}
4187
4188fn render_basic_resource(output: &mut String, resource: &GeneratedResource) {
4189 output.push_str(" ");
4190 write_quoted(output, &resource.name);
4191 if !resource.external && resource.custom_name.is_none() {
4192 output.push_str(": {}\n");
4193 return;
4194 }
4195 output.push_str(":\n");
4196 if let Some(custom_name) = &resource.custom_name {
4197 output.push_str(" name: ");
4198 write_quoted(output, custom_name);
4199 output.push('\n');
4200 }
4201 if resource.external {
4202 output.push_str(" external: true\n");
4203 }
4204}
4205
4206fn write_field(output: &mut String, depth: usize, key: &str) {
4207 write_indent(output, depth);
4208 output.push_str(key);
4209 output.push_str(": ");
4210}
4211
4212fn write_indent(output: &mut String, depth: usize) {
4213 for _ in 0..depth {
4214 output.push_str(" ");
4215 }
4216}
4217
4218fn required(kind: &'static str, value: String) -> Result<String, GenerationError> {
4219 if value.is_empty() {
4220 return Err(GenerationError::EmptyValue(kind));
4221 }
4222 if value.contains('\0') {
4223 return Err(GenerationError::ContainsNul(kind));
4224 }
4225 Ok(value)
4226}
4227
4228fn require_generated_string(kind: &'static str, value: &GeneratedString) -> Result<(), GenerationError> {
4229 if value.expose().is_empty() {
4230 return Err(GenerationError::EmptyValue(kind));
4231 }
4232 Ok(())
4233}
4234
4235fn generated_file_resource_name(value: String) -> Result<String, GenerationError> {
4236 if value.is_empty() || value.contains(['\0', '\r', '\n', '$']) {
4237 Err(GenerationError::InvalidFileResourceName)
4238 } else {
4239 Ok(value)
4240 }
4241}
4242
4243fn generated_file_resource_path(value: GeneratedString) -> Result<GeneratedString, GenerationError> {
4244 if value.expose().is_empty() || value.expose().contains(['\0', '\r', '\n', '$']) {
4245 Err(GenerationError::InvalidFileResourcePath)
4246 } else {
4247 Ok(value)
4248 }
4249}
4250
4251fn validate_generated_device_member(
4252 member: &'static str,
4253 value: &GeneratedString,
4254 require_non_empty: bool,
4255) -> Result<(), GenerationError> {
4256 if valid_generated_device_string(value.expose(), require_non_empty) {
4257 Ok(())
4258 } else {
4259 Err(GenerationError::InvalidDeviceValue(member))
4260 }
4261}
4262
4263fn validate_generated_ulimit_value(value: &GeneratedString) -> Result<(), GenerationError> {
4264 let value = value.expose();
4265 if value.contains(['\r', '\n', '$'])
4266 || (value != "-1" && (value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit())))
4267 {
4268 return Err(GenerationError::InvalidUlimitValue);
4269 }
4270 Ok(())
4271}
4272
4273fn valid_yaml_number(value: &str) -> bool {
4274 let ordinary = !value.is_empty()
4275 && value.bytes().any(|byte| byte.is_ascii_digit())
4276 && value.bytes().all(|byte| {
4277 byte.is_ascii_digit()
4278 || matches!(
4279 byte,
4280 b'+' | b'-'
4281 | b'.'
4282 | b'_'
4283 | b'e'
4284 | b'E'
4285 | b'x'
4286 | b'X'
4287 | b'o'
4288 | b'O'
4289 | b'a'..=b'f'
4290 | b'A'..=b'F'
4291 )
4292 });
4293 let special = matches!(
4294 value,
4295 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF" | ".nan" | ".NaN" | ".NAN"
4296 );
4297 if !ordinary && !special {
4298 return false;
4299 }
4300 let parse = YamlFile::parse(value);
4301 if !parse.ok() {
4302 return false;
4303 }
4304 let file = parse.tree();
4305 let Some(document) = file.document() else {
4306 return false;
4307 };
4308 let Some(scalar) = document.as_scalar() else {
4309 return false;
4310 };
4311 let position = scalar.byte_range();
4312 position.start == 0
4313 && position.end as usize == value.len()
4314 && matches!(
4315 ScalarValue::from_scalar(&scalar).scalar_type(),
4316 ScalarType::Integer | ScalarType::Float
4317 )
4318}
4319
4320fn environment_name(value: String) -> Result<String, GenerationError> {
4321 let value = required("environment name", value)?;
4322 if value.contains('=') {
4323 return Err(GenerationError::InvalidEnvironmentName);
4324 }
4325 Ok(value)
4326}
4327
4328fn valid_container_name(value: &str) -> bool {
4329 let mut bytes = value.bytes();
4330 bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
4331 && bytes
4332 .next()
4333 .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4334 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4335}
4336
4337fn short_component(kind: &'static str, value: String, separator: char) -> Result<String, GenerationError> {
4338 let value = required(kind, value)?;
4339 if value.contains(separator) {
4340 return Err(GenerationError::InvalidShortComponent(kind));
4341 }
4342 Ok(value)
4343}
4344
4345fn set_once<T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), GenerationError> {
4346 if slot.is_some() {
4347 return Err(GenerationError::DuplicateField(field));
4348 }
4349 *slot = Some(value);
4350 Ok(())
4351}
4352
4353fn insert_named<T>(
4354 values: &mut Vec<T>,
4355 value: T,
4356 kind: &'static str,
4357 name: impl Fn(&T) -> &str,
4358) -> Result<(), GenerationError> {
4359 let value_name = name(&value);
4360 if values.iter().any(|candidate| name(candidate) == value_name) {
4361 return Err(GenerationError::DuplicateName {
4362 kind,
4363 name: value_name.to_owned(),
4364 });
4365 }
4366 values.push(value);
4367 Ok(())
4368}
4369
4370fn command_is_sensitive(command: &GeneratedCommand) -> bool {
4371 match command {
4372 GeneratedCommand::Exec(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4373 GeneratedCommand::Shell(command) => command.is_sensitive(),
4374 GeneratedCommand::Empty => false,
4375 }
4376}
4377
4378fn entrypoint_is_sensitive(entrypoint: &GeneratedEntrypoint) -> bool {
4379 match entrypoint {
4380 GeneratedEntrypoint::List(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4381 GeneratedEntrypoint::String(entrypoint) => entrypoint.is_sensitive(),
4382 GeneratedEntrypoint::Empty => false,
4383 }
4384}