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, Debug, Eq, PartialEq)]
1638pub struct GeneratedNetworkAttachment {
1639 name: String,
1640 aliases: Vec<String>,
1641 ipv4_address: Option<GeneratedString>,
1642 ipv6_address: Option<GeneratedString>,
1643}
1644
1645impl GeneratedNetworkAttachment {
1646 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1652 Ok(Self {
1653 name: required("network name", name.into())?,
1654 aliases: Vec::new(),
1655 ipv4_address: None,
1656 ipv6_address: None,
1657 })
1658 }
1659
1660 pub fn add_alias(&mut self, alias: impl Into<String>) -> Result<(), GenerationError> {
1666 self.aliases.push(required("network alias", alias.into())?);
1667 Ok(())
1668 }
1669
1670 pub fn set_ipv4_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1678 set_once(&mut self.ipv4_address, address, "ipv4_address")
1679 }
1680
1681 pub fn set_ipv6_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1689 set_once(&mut self.ipv6_address, address, "ipv6_address")
1690 }
1691
1692 #[must_use]
1694 pub fn name(&self) -> &str {
1695 &self.name
1696 }
1697
1698 #[must_use]
1700 pub fn aliases(&self) -> &[String] {
1701 &self.aliases
1702 }
1703
1704 #[must_use]
1706 pub const fn ipv4_address(&self) -> Option<&GeneratedString> {
1707 self.ipv4_address.as_ref()
1708 }
1709
1710 #[must_use]
1712 pub const fn ipv6_address(&self) -> Option<&GeneratedString> {
1713 self.ipv6_address.as_ref()
1714 }
1715
1716 fn is_sensitive(&self) -> bool {
1717 self.ipv4_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1718 || self.ipv6_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1719 }
1720}
1721
1722#[derive(Clone, Debug, Eq, PartialEq)]
1724pub struct GeneratedResource {
1725 name: String,
1726 external: bool,
1727 custom_name: Option<String>,
1728}
1729
1730impl GeneratedResource {
1731 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
1737 Ok(Self {
1738 name: required("resource name", name.into())?,
1739 external: false,
1740 custom_name: None,
1741 })
1742 }
1743
1744 pub fn external(name: impl Into<String>) -> Result<Self, GenerationError> {
1750 Ok(Self {
1751 name: required("resource name", name.into())?,
1752 external: true,
1753 custom_name: None,
1754 })
1755 }
1756
1757 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
1765 let name = required("custom resource name", name.into())?;
1766 set_once(&mut self.custom_name, name, "resource name")
1767 }
1768
1769 #[must_use]
1771 pub fn name(&self) -> &str {
1772 &self.name
1773 }
1774
1775 #[must_use]
1777 pub const fn is_external(&self) -> bool {
1778 self.external
1779 }
1780
1781 #[must_use]
1783 pub fn custom_name(&self) -> Option<&str> {
1784 self.custom_name.as_deref()
1785 }
1786}
1787
1788#[derive(Clone, Debug, Eq, PartialEq)]
1790#[non_exhaustive]
1791pub enum GeneratedCpuRtRuntime {
1792 Microseconds(GeneratedString),
1794 Duration(GeneratedString),
1796}
1797
1798impl GeneratedCpuRtRuntime {
1799 fn is_sensitive(&self) -> bool {
1800 match self {
1801 Self::Microseconds(value) | Self::Duration(value) => value.is_sensitive(),
1802 }
1803 }
1804}
1805
1806#[derive(Clone, Debug, Eq, PartialEq)]
1810#[non_exhaustive]
1811pub enum GeneratedServiceRuntimeField {
1812 Domainname(GeneratedString),
1814 Isolation(GeneratedString),
1816 MacAddress(GeneratedString),
1818 Uts(GeneratedString),
1820 UseApiSocket(bool),
1822 GpusAll(GeneratedString),
1824 CpuRtRuntime(GeneratedCpuRtRuntime),
1826 CpuShares(GeneratedString),
1828 Cpus(GeneratedString),
1830 Cpuset(GeneratedString),
1832 DeviceCgroupRules(Vec<GeneratedString>),
1834 Ipc(GeneratedString),
1836 MemReservation(GeneratedString),
1838 MemSwappiness(GeneratedString),
1840 MemswapLimit(GeneratedString),
1842 NetworkMode(GeneratedString),
1844 OomKillDisable(bool),
1846 OomScoreAdj(GeneratedString),
1848 Pid(GeneratedString),
1850 Scale(GeneratedString),
1852 VolumesFrom(Vec<GeneratedString>),
1854}
1855
1856impl GeneratedServiceRuntimeField {
1857 fn field_name(&self) -> &'static str {
1858 match self {
1859 Self::Domainname(_) => "domainname",
1860 Self::Isolation(_) => "isolation",
1861 Self::MacAddress(_) => "mac_address",
1862 Self::Uts(_) => "uts",
1863 Self::UseApiSocket(_) => "use_api_socket",
1864 Self::GpusAll(_) => "gpus",
1865 Self::CpuRtRuntime(_) => "cpu_rt_runtime",
1866 Self::CpuShares(_) => "cpu_shares",
1867 Self::Cpus(_) => "cpus",
1868 Self::Cpuset(_) => "cpuset",
1869 Self::DeviceCgroupRules(_) => "device_cgroup_rules",
1870 Self::Ipc(_) => "ipc",
1871 Self::MemReservation(_) => "mem_reservation",
1872 Self::MemSwappiness(_) => "mem_swappiness",
1873 Self::MemswapLimit(_) => "memswap_limit",
1874 Self::NetworkMode(_) => "network_mode",
1875 Self::OomKillDisable(_) => "oom_kill_disable",
1876 Self::OomScoreAdj(_) => "oom_score_adj",
1877 Self::Pid(_) => "pid",
1878 Self::Scale(_) => "scale",
1879 Self::VolumesFrom(_) => "volumes_from",
1880 }
1881 }
1882
1883 fn is_sensitive(&self) -> bool {
1884 match self {
1885 Self::Domainname(value)
1886 | Self::Isolation(value)
1887 | Self::MacAddress(value)
1888 | Self::Uts(value)
1889 | Self::GpusAll(value)
1890 | Self::CpuShares(value)
1891 | Self::Cpus(value)
1892 | Self::Cpuset(value)
1893 | Self::Ipc(value)
1894 | Self::MemReservation(value)
1895 | Self::MemSwappiness(value)
1896 | Self::MemswapLimit(value)
1897 | Self::NetworkMode(value)
1898 | Self::OomScoreAdj(value)
1899 | Self::Pid(value)
1900 | Self::Scale(value) => value.is_sensitive(),
1901 Self::DeviceCgroupRules(values) | Self::VolumesFrom(values) => {
1902 values.iter().any(GeneratedString::is_sensitive)
1903 }
1904 Self::UseApiSocket(_) | Self::OomKillDisable(_) => false,
1905 Self::CpuRtRuntime(value) => value.is_sensitive(),
1906 }
1907 }
1908}
1909
1910#[derive(Clone, Debug, Eq, PartialEq)]
1912pub struct GeneratedService {
1913 name: String,
1914 hostname: Option<GeneratedHostname>,
1915 container_name: Option<GeneratedString>,
1916 image: Option<GeneratedString>,
1917 entrypoint: Option<GeneratedEntrypoint>,
1918 command: Option<GeneratedCommand>,
1919 init: Option<bool>,
1920 stdin_open: Option<bool>,
1921 tty: Option<bool>,
1922 privileged: Option<bool>,
1923 environment_files: Vec<GeneratedEnvironmentFile>,
1924 environment: Vec<GeneratedEnvironment>,
1925 labels: Vec<GeneratedLabel>,
1926 annotations: Option<Vec<GeneratedAnnotation>>,
1927 user: Option<GeneratedString>,
1928 userns_mode: Option<GeneratedString>,
1929 group_add: Vec<GeneratedString>,
1930 cap_add: Option<Vec<GeneratedString>>,
1931 cap_drop: Option<Vec<GeneratedString>>,
1932 devices: Option<Vec<GeneratedDevice>>,
1933 dns: Option<GeneratedDns>,
1934 dns_options: Option<Vec<GeneratedString>>,
1935 dns_search: Option<GeneratedDnsSearch>,
1936 expose: Option<Vec<GeneratedString>>,
1937 security_options: Option<Vec<GeneratedString>>,
1938 working_dir: Option<GeneratedString>,
1939 read_only: Option<bool>,
1940 pids_limit: Option<GeneratedPidsLimit>,
1941 shm_size: Option<GeneratedShmSize>,
1942 mem_limit: Option<GeneratedMemLimit>,
1943 tmpfs: Option<GeneratedTmpfs>,
1944 sysctls: Option<GeneratedSysctls>,
1945 logging: Option<GeneratedLogging>,
1946 ulimits: Option<GeneratedUlimits>,
1947 pull_policy: Option<GeneratedPullPolicy>,
1948 restart: Option<GeneratedRestartPolicy>,
1949 stop_signal: Option<GeneratedString>,
1950 stop_grace_period: Option<GeneratedString>,
1951 extra_hosts: Vec<GeneratedExtraHost>,
1952 ports: Vec<GeneratedPort>,
1953 mounts: Vec<GeneratedMount>,
1954 networks: Vec<GeneratedNetworkAttachment>,
1955 runtime_fields: Vec<GeneratedServiceRuntimeField>,
1956}
1957
1958impl GeneratedService {
1959 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1965 Ok(Self {
1966 name: required("service name", name.into())?,
1967 hostname: None,
1968 container_name: None,
1969 image: None,
1970 entrypoint: None,
1971 command: None,
1972 init: None,
1973 stdin_open: None,
1974 tty: None,
1975 privileged: None,
1976 environment_files: Vec::new(),
1977 environment: Vec::new(),
1978 labels: Vec::new(),
1979 annotations: None,
1980 user: None,
1981 userns_mode: None,
1982 group_add: Vec::new(),
1983 cap_add: None,
1984 cap_drop: None,
1985 devices: None,
1986 dns: None,
1987 dns_options: None,
1988 dns_search: None,
1989 expose: None,
1990 security_options: None,
1991 working_dir: None,
1992 read_only: None,
1993 pids_limit: None,
1994 shm_size: None,
1995 mem_limit: None,
1996 tmpfs: None,
1997 sysctls: None,
1998 logging: None,
1999 ulimits: None,
2000 pull_policy: None,
2001 restart: None,
2002 stop_signal: None,
2003 stop_grace_period: None,
2004 extra_hosts: Vec::new(),
2005 ports: Vec::new(),
2006 mounts: Vec::new(),
2007 networks: Vec::new(),
2008 runtime_fields: Vec::new(),
2009 })
2010 }
2011
2012 #[must_use]
2014 pub fn name(&self) -> &str {
2015 &self.name
2016 }
2017
2018 pub fn add_runtime_field(&mut self, field: GeneratedServiceRuntimeField) -> Result<(), GenerationError> {
2030 if self
2031 .runtime_fields
2032 .iter()
2033 .any(|existing| existing.field_name() == field.field_name())
2034 {
2035 return Err(GenerationError::DuplicateField(field.field_name()));
2036 }
2037 if !generated_runtime_field_safe(&field) {
2038 return Err(GenerationError::InvalidServiceRuntimeField(field.field_name()));
2039 }
2040 self.runtime_fields.push(field);
2041 Ok(())
2042 }
2043
2044 #[must_use]
2046 pub fn runtime_fields(&self) -> &[GeneratedServiceRuntimeField] {
2047 &self.runtime_fields
2048 }
2049
2050 pub fn set_hostname(&mut self, hostname: GeneratedHostname) -> Result<(), GenerationError> {
2058 let GeneratedHostname::Resolved(value) = &hostname;
2059 if !valid_hostname(value.expose()) {
2060 return Err(GenerationError::InvalidHostname);
2061 }
2062 set_once(&mut self.hostname, hostname, "hostname")
2063 }
2064
2065 pub fn set_container_name(&mut self, name: GeneratedString) -> Result<(), GenerationError> {
2073 if !valid_container_name(name.expose()) {
2074 return Err(GenerationError::InvalidContainerName);
2075 }
2076 set_once(&mut self.container_name, name, "container_name")
2077 }
2078
2079 pub fn set_image(&mut self, image: GeneratedString) -> Result<(), GenerationError> {
2086 require_generated_string("service image", &image)?;
2087 set_once(&mut self.image, image, "image")
2088 }
2089
2090 pub fn set_entrypoint(&mut self, entrypoint: GeneratedEntrypoint) -> Result<(), GenerationError> {
2096 set_once(&mut self.entrypoint, entrypoint, "entrypoint")
2097 }
2098
2099 pub fn set_command(&mut self, command: GeneratedCommand) -> Result<(), GenerationError> {
2105 set_once(&mut self.command, command, "command")
2106 }
2107
2108 pub fn set_init(&mut self, init: bool) -> Result<(), GenerationError> {
2114 set_once(&mut self.init, init, "init")
2115 }
2116
2117 pub fn set_stdin_open(&mut self, stdin_open: bool) -> Result<(), GenerationError> {
2123 set_once(&mut self.stdin_open, stdin_open, "stdin_open")
2124 }
2125
2126 pub fn set_tty(&mut self, tty: bool) -> Result<(), GenerationError> {
2132 set_once(&mut self.tty, tty, "tty")
2133 }
2134
2135 pub fn set_privileged(&mut self, privileged: bool) -> Result<(), GenerationError> {
2141 set_once(&mut self.privileged, privileged, "privileged")
2142 }
2143
2144 pub fn add_environment_file(&mut self, environment_file: GeneratedEnvironmentFile) {
2146 self.environment_files.push(environment_file);
2147 }
2148
2149 pub fn add_environment(&mut self, environment: GeneratedEnvironment) {
2151 self.environment.push(environment);
2152 }
2153
2154 pub fn add_label(&mut self, label: GeneratedLabel) -> Result<(), GenerationError> {
2160 if self.labels.iter().any(|candidate| candidate.name == label.name) {
2161 return Err(GenerationError::DuplicateName {
2162 kind: "service label",
2163 name: label.name,
2164 });
2165 }
2166 self.labels.push(label);
2167 Ok(())
2168 }
2169
2170 pub fn set_annotations(&mut self, annotations: Vec<GeneratedAnnotation>) -> Result<(), GenerationError> {
2181 let mut seen = BTreeSet::new();
2182 for annotation in &annotations {
2183 if annotation.name.is_empty() || annotation.name.contains(['$', '\r', '\n', '\0']) {
2184 return Err(GenerationError::InvalidAnnotationName);
2185 }
2186 if annotation.value.expose().contains(['$', '\r', '\n', '\0']) {
2187 return Err(GenerationError::InvalidAnnotationValue);
2188 }
2189 if !seen.insert(annotation.name.as_str()) {
2190 return Err(GenerationError::DuplicateName {
2191 kind: "service annotation",
2192 name: annotation.name.clone(),
2193 });
2194 }
2195 }
2196 set_once(&mut self.annotations, annotations, "annotations")
2197 }
2198
2199 #[must_use]
2201 pub fn annotations(&self) -> Option<&[GeneratedAnnotation]> {
2202 self.annotations.as_deref()
2203 }
2204
2205 pub fn set_user(&mut self, user: GeneratedString) -> Result<(), GenerationError> {
2211 set_once(&mut self.user, user, "user")
2212 }
2213
2214 pub fn set_userns_mode(&mut self, mode: GeneratedString) -> Result<(), GenerationError> {
2221 require_generated_string("user namespace mode", &mode)?;
2222 set_once(&mut self.userns_mode, mode, "userns_mode")
2223 }
2224
2225 pub fn add_supplementary_group(&mut self, group: GeneratedString) -> Result<(), GenerationError> {
2231 require_generated_string("supplementary group", &group)?;
2232 self.group_add.push(group);
2233 Ok(())
2234 }
2235
2236 pub fn set_cap_add(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2249 let mut seen = BTreeSet::new();
2250 for capability in &capabilities {
2251 require_generated_string("cap_add item", capability)?;
2252 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2253 return Err(GenerationError::ContainsLineBreak("cap_add item"));
2254 }
2255 if !seen.insert(capability.expose()) {
2256 return Err(GenerationError::DuplicateItem("cap_add"));
2257 }
2258 }
2259 set_once(&mut self.cap_add, capabilities, "cap_add")
2260 }
2261
2262 #[must_use]
2264 pub fn cap_add(&self) -> Option<&[GeneratedString]> {
2265 self.cap_add.as_deref()
2266 }
2267
2268 pub fn set_cap_drop(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2281 let mut seen = BTreeSet::new();
2282 for capability in &capabilities {
2283 require_generated_string("cap_drop item", capability)?;
2284 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2285 return Err(GenerationError::ContainsLineBreak("cap_drop item"));
2286 }
2287 if !seen.insert(capability.expose()) {
2288 return Err(GenerationError::DuplicateItem("cap_drop"));
2289 }
2290 }
2291 set_once(&mut self.cap_drop, capabilities, "cap_drop")
2292 }
2293
2294 #[must_use]
2296 pub fn cap_drop(&self) -> Option<&[GeneratedString]> {
2297 self.cap_drop.as_deref()
2298 }
2299
2300 pub fn set_devices(&mut self, devices: Vec<GeneratedDevice>) -> Result<(), GenerationError> {
2313 for device in &devices {
2314 match device {
2315 GeneratedDevice::Short(value) => {
2316 validate_generated_device_member("short item", value, true)?;
2317 }
2318 GeneratedDevice::Long(value) => {
2319 validate_generated_device_member("source", value.source(), true)?;
2320 if let Some(target) = value.target() {
2321 validate_generated_device_member("target", target, false)?;
2322 }
2323 if let Some(permissions) = value.permissions() {
2324 validate_generated_device_member("permissions", permissions, false)?;
2325 }
2326 }
2327 }
2328 }
2329 set_once(&mut self.devices, devices, "devices")
2330 }
2331
2332 pub fn set_dns(&mut self, dns: GeneratedDns) -> Result<(), GenerationError> {
2342 let values = match &dns {
2343 GeneratedDns::Scalar(value) => std::slice::from_ref(value),
2344 GeneratedDns::List(values) => values.as_slice(),
2345 };
2346 for value in values {
2347 if value.expose().is_empty()
2348 || value.expose().contains('$')
2349 || value.expose().contains('\r')
2350 || value.expose().contains('\n')
2351 {
2352 return Err(GenerationError::InvalidDnsValue);
2353 }
2354 }
2355 set_once(&mut self.dns, dns, "dns")
2356 }
2357
2358 #[must_use]
2360 pub const fn dns(&self) -> Option<&GeneratedDns> {
2361 self.dns.as_ref()
2362 }
2363
2364 pub fn set_dns_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2376 let mut seen = BTreeSet::new();
2377 for option in &options {
2378 if option.expose().is_empty()
2379 || option.expose().contains('$')
2380 || option.expose().contains('\r')
2381 || option.expose().contains('\n')
2382 || option.expose().contains('\0')
2383 {
2384 return Err(GenerationError::InvalidDnsOptionValue);
2385 }
2386 if !seen.insert(option.expose()) {
2387 return Err(GenerationError::DuplicateItem("dns_opt"));
2388 }
2389 }
2390 set_once(&mut self.dns_options, options, "dns_opt")
2391 }
2392
2393 #[must_use]
2395 pub fn dns_options(&self) -> Option<&[GeneratedString]> {
2396 self.dns_options.as_deref()
2397 }
2398
2399 pub fn set_dns_search(&mut self, search: GeneratedDnsSearch) -> Result<(), GenerationError> {
2409 let values = match &search {
2410 GeneratedDnsSearch::Scalar(value) => std::slice::from_ref(value),
2411 GeneratedDnsSearch::List(values) => values.as_slice(),
2412 };
2413 for value in values {
2414 if value.expose().is_empty()
2415 || value.expose().contains('$')
2416 || value.expose().contains('\r')
2417 || value.expose().contains('\n')
2418 || value.expose().contains('\0')
2419 {
2420 return Err(GenerationError::InvalidDnsSearchValue);
2421 }
2422 }
2423 set_once(&mut self.dns_search, search, "dns_search")
2424 }
2425
2426 #[must_use]
2428 pub const fn dns_search(&self) -> Option<&GeneratedDnsSearch> {
2429 self.dns_search.as_ref()
2430 }
2431
2432 pub fn set_expose(&mut self, expose: Vec<GeneratedString>) -> Result<(), GenerationError> {
2443 let mut seen = BTreeSet::new();
2444 for item in &expose {
2445 if !valid_generated_expose_item(item.expose()) {
2446 return Err(GenerationError::InvalidExposeValue);
2447 }
2448 if !seen.insert(item.expose()) {
2449 return Err(GenerationError::DuplicateItem("expose"));
2450 }
2451 }
2452 set_once(&mut self.expose, expose, "expose")
2453 }
2454
2455 #[must_use]
2457 pub fn expose(&self) -> Option<&[GeneratedString]> {
2458 self.expose.as_deref()
2459 }
2460
2461 pub fn set_security_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2471 for option in &options {
2472 if option.expose().is_empty()
2473 || option.expose().contains('$')
2474 || option.expose().contains('\r')
2475 || option.expose().contains('\n')
2476 || option.expose().contains('\0')
2477 {
2478 return Err(GenerationError::InvalidSecurityOptionValue);
2479 }
2480 }
2481 set_once(&mut self.security_options, options, "security_opt")
2482 }
2483
2484 #[must_use]
2486 pub fn security_options(&self) -> Option<&[GeneratedString]> {
2487 self.security_options.as_deref()
2488 }
2489
2490 #[must_use]
2492 pub fn devices(&self) -> Option<&[GeneratedDevice]> {
2493 self.devices.as_deref()
2494 }
2495
2496 pub fn set_working_dir(&mut self, directory: GeneratedString) -> Result<(), GenerationError> {
2503 require_generated_string("working directory", &directory)?;
2504 set_once(&mut self.working_dir, directory, "working_dir")
2505 }
2506
2507 pub fn set_read_only(&mut self, read_only: bool) -> Result<(), GenerationError> {
2513 set_once(&mut self.read_only, read_only, "read_only")
2514 }
2515
2516 pub fn set_pids_limit(&mut self, limit: GeneratedPidsLimit) -> Result<(), GenerationError> {
2524 if let GeneratedPidsLimit::Finite(decimal) = &limit {
2525 if !valid_positive_pids_decimal(decimal) {
2526 return Err(GenerationError::InvalidPidsLimit);
2527 }
2528 }
2529 set_once(&mut self.pids_limit, limit, "pids_limit")
2530 }
2531
2532 pub fn set_shm_size(&mut self, size: GeneratedShmSize) -> Result<(), GenerationError> {
2540 let GeneratedShmSize::Explicit { amount, .. } = &size;
2541 if !valid_generated_shm_amount(amount.expose()) {
2542 return Err(GenerationError::InvalidShmSize);
2543 }
2544 set_once(&mut self.shm_size, size, "shm_size")
2545 }
2546
2547 pub fn set_mem_limit(&mut self, limit: GeneratedMemLimit) -> Result<(), GenerationError> {
2555 let GeneratedMemLimit::Explicit { amount, .. } = &limit;
2556 if !valid_generated_mem_amount(amount.expose()) {
2557 return Err(GenerationError::InvalidMemLimit);
2558 }
2559 set_once(&mut self.mem_limit, limit, "mem_limit")
2560 }
2561
2562 pub fn set_tmpfs(&mut self, tmpfs: GeneratedTmpfs) -> Result<(), GenerationError> {
2573 let items = match &tmpfs {
2574 GeneratedTmpfs::Scalar(item) => std::slice::from_ref(item),
2575 GeneratedTmpfs::List(items) => items.as_slice(),
2576 };
2577 for item in items {
2578 require_generated_string("tmpfs item", item)?;
2579 if item.expose().contains('\r') || item.expose().contains('\n') {
2580 return Err(GenerationError::ContainsLineBreak("tmpfs item"));
2581 }
2582 if !valid_generated_tmpfs_item(item.expose()) {
2583 return Err(GenerationError::InvalidTmpfsItem);
2584 }
2585 }
2586 set_once(&mut self.tmpfs, tmpfs, "tmpfs")
2587 }
2588
2589 #[must_use]
2591 pub const fn tmpfs(&self) -> Option<&GeneratedTmpfs> {
2592 self.tmpfs.as_ref()
2593 }
2594
2595 pub fn set_sysctls(&mut self, sysctls: GeneratedSysctls) -> Result<(), GenerationError> {
2606 let mut seen = BTreeSet::new();
2607 match &sysctls {
2608 GeneratedSysctls::Map(entries) => {
2609 for entry in entries {
2610 if !seen.insert(entry.name()) {
2611 return Err(GenerationError::DuplicateName {
2612 kind: "sysctl",
2613 name: entry.name().to_owned(),
2614 });
2615 }
2616 }
2617 }
2618 GeneratedSysctls::List(items) => {
2619 for item in items {
2620 if item.expose().contains(['\r', '\n', '$']) {
2621 return Err(GenerationError::InvalidSysctlValue);
2622 }
2623 if !seen.insert(item.expose()) {
2624 return Err(GenerationError::DuplicateItem("sysctls"));
2625 }
2626 }
2627 }
2628 }
2629 set_once(&mut self.sysctls, sysctls, "sysctls")
2630 }
2631
2632 #[must_use]
2634 pub const fn sysctls(&self) -> Option<&GeneratedSysctls> {
2635 self.sysctls.as_ref()
2636 }
2637
2638 pub fn set_logging(&mut self, logging: GeneratedLogging) -> Result<(), GenerationError> {
2645 set_once(&mut self.logging, logging, "logging")
2646 }
2647
2648 #[must_use]
2650 pub const fn logging(&self) -> Option<&GeneratedLogging> {
2651 self.logging.as_ref()
2652 }
2653
2654 pub fn set_ulimits(&mut self, ulimits: GeneratedUlimits) -> Result<(), GenerationError> {
2663 set_once(&mut self.ulimits, ulimits, "ulimits")
2664 }
2665
2666 #[must_use]
2668 pub const fn ulimits(&self) -> Option<&GeneratedUlimits> {
2669 self.ulimits.as_ref()
2670 }
2671
2672 pub fn set_pull_policy(&mut self, policy: GeneratedPullPolicy) -> Result<(), GenerationError> {
2679 if let GeneratedPullPolicy::Every(duration) = &policy {
2680 if !valid_pull_policy_duration(duration.expose()) {
2681 return Err(GenerationError::InvalidPullPolicyDuration);
2682 }
2683 }
2684 set_once(&mut self.pull_policy, policy, "pull_policy")
2685 }
2686
2687 pub fn set_restart(&mut self, restart: GeneratedRestartPolicy) -> Result<(), GenerationError> {
2693 set_once(&mut self.restart, restart, "restart")
2694 }
2695
2696 pub fn set_stop_signal(&mut self, signal: GeneratedString) -> Result<(), GenerationError> {
2703 set_once(&mut self.stop_signal, signal, "stop_signal")
2704 }
2705
2706 pub fn set_stop_grace_period(&mut self, period: GeneratedString) -> Result<(), GenerationError> {
2714 if !StopGracePeriod::parse(period.expose().to_owned()).is_valid() {
2715 return Err(GenerationError::InvalidStopGracePeriod);
2716 }
2717 set_once(&mut self.stop_grace_period, period, "stop_grace_period")
2718 }
2719
2720 pub fn add_extra_host(&mut self, host: GeneratedExtraHost) {
2722 self.extra_hosts.push(host);
2723 }
2724
2725 pub fn add_port(&mut self, port: GeneratedPort) {
2727 self.ports.push(port);
2728 }
2729
2730 pub fn add_mount(&mut self, mount: GeneratedMount) {
2732 self.mounts.push(mount);
2733 }
2734
2735 pub fn add_network(&mut self, network: GeneratedNetworkAttachment) -> Result<(), GenerationError> {
2741 if self.networks.iter().any(|candidate| candidate.name == network.name) {
2742 return Err(GenerationError::DuplicateName {
2743 kind: "service network",
2744 name: network.name,
2745 });
2746 }
2747 self.networks.push(network);
2748 Ok(())
2749 }
2750
2751 fn is_sensitive(&self) -> bool {
2752 matches!(
2753 self.hostname.as_ref(),
2754 Some(GeneratedHostname::Resolved(hostname)) if hostname.is_sensitive()
2755 ) || self.image.as_ref().is_some_and(GeneratedString::is_sensitive)
2756 || self.entrypoint.as_ref().is_some_and(entrypoint_is_sensitive)
2757 || self.command.as_ref().is_some_and(command_is_sensitive)
2758 || self
2759 .environment_files
2760 .iter()
2761 .any(GeneratedEnvironmentFile::is_sensitive)
2762 || self
2763 .environment
2764 .iter()
2765 .filter_map(GeneratedEnvironment::value)
2766 .any(GeneratedString::is_sensitive)
2767 || self.labels.iter().any(|label| label.value.is_sensitive())
2768 || self
2769 .annotations
2770 .as_ref()
2771 .is_some_and(|items| items.iter().any(|annotation| annotation.value.is_sensitive()))
2772 || matches!(
2773 self.pull_policy.as_ref(),
2774 Some(GeneratedPullPolicy::Every(duration)) if duration.is_sensitive()
2775 )
2776 || matches!(
2777 self.shm_size.as_ref(),
2778 Some(GeneratedShmSize::Explicit { amount, .. }) if amount.is_sensitive()
2779 )
2780 || matches!(
2781 self.mem_limit.as_ref(),
2782 Some(GeneratedMemLimit::Explicit { amount, .. }) if amount.is_sensitive()
2783 )
2784 || match self.tmpfs.as_ref() {
2785 Some(GeneratedTmpfs::Scalar(item)) => item.is_sensitive(),
2786 Some(GeneratedTmpfs::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2787 None => false,
2788 }
2789 || match self.dns.as_ref() {
2790 Some(GeneratedDns::Scalar(value)) => value.is_sensitive(),
2791 Some(GeneratedDns::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2792 None => false,
2793 }
2794 || self
2795 .dns_options
2796 .as_ref()
2797 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2798 || self
2799 .runtime_fields
2800 .iter()
2801 .any(GeneratedServiceRuntimeField::is_sensitive)
2802 || match self.dns_search.as_ref() {
2803 Some(GeneratedDnsSearch::Scalar(value)) => value.is_sensitive(),
2804 Some(GeneratedDnsSearch::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2805 None => false,
2806 }
2807 || self
2808 .expose
2809 .as_ref()
2810 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2811 || self
2812 .security_options
2813 .as_ref()
2814 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2815 || match self.sysctls.as_ref() {
2816 Some(GeneratedSysctls::Map(entries)) => entries.iter().any(|entry| entry.value.is_sensitive()),
2817 Some(GeneratedSysctls::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2818 None => false,
2819 }
2820 || self.logging.as_ref().is_some_and(GeneratedLogging::is_sensitive)
2821 || self
2822 .ulimits
2823 .as_ref()
2824 .is_some_and(|limits| limits.entries.iter().any(GeneratedUlimit::is_sensitive))
2825 || [
2826 self.user.as_ref(),
2827 self.userns_mode.as_ref(),
2828 self.working_dir.as_ref(),
2829 self.stop_signal.as_ref(),
2830 self.stop_grace_period.as_ref(),
2831 ]
2832 .into_iter()
2833 .flatten()
2834 .any(GeneratedString::is_sensitive)
2835 || self.group_add.iter().any(GeneratedString::is_sensitive)
2836 || self
2837 .cap_add
2838 .as_ref()
2839 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2840 || self
2841 .cap_drop
2842 .as_ref()
2843 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2844 || self
2845 .devices
2846 .as_ref()
2847 .is_some_and(|items| items.iter().any(GeneratedDevice::is_sensitive))
2848 || self.networks.iter().any(GeneratedNetworkAttachment::is_sensitive)
2849 }
2850}
2851
2852#[derive(Clone, Debug, Eq, PartialEq)]
2853enum GeneratedNetwork {
2854 Basic(GeneratedResource),
2855 Definition(GeneratedNetworkDefinition),
2856}
2857
2858#[derive(Clone, Debug, Eq, PartialEq)]
2859enum GeneratedVolume {
2860 Basic(GeneratedResource),
2861 Definition(GeneratedVolumeDefinition),
2862}
2863
2864impl GeneratedVolume {
2865 fn name(&self) -> &str {
2866 match self {
2867 Self::Basic(volume) => volume.name(),
2868 Self::Definition(volume) => volume.name(),
2869 }
2870 }
2871
2872 fn is_sensitive(&self) -> bool {
2873 match self {
2874 Self::Basic(_) => false,
2875 Self::Definition(volume) => volume.is_sensitive(),
2876 }
2877 }
2878}
2879
2880impl GeneratedNetwork {
2881 fn name(&self) -> &str {
2882 match self {
2883 Self::Basic(network) => network.name(),
2884 Self::Definition(network) => network.name(),
2885 }
2886 }
2887
2888 fn is_sensitive(&self) -> bool {
2889 match self {
2890 Self::Basic(_) => false,
2891 Self::Definition(network) => network.is_sensitive(),
2892 }
2893 }
2894}
2895
2896#[derive(Clone, Eq, PartialEq)]
2901pub struct GeneratedConfigFileDefinition {
2902 name: String,
2903 file: GeneratedString,
2904}
2905
2906impl GeneratedConfigFileDefinition {
2907 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2913 Ok(Self {
2914 name: generated_file_resource_name(name.into())?,
2915 file: generated_file_resource_path(file)?,
2916 })
2917 }
2918
2919 #[must_use]
2921 pub fn name(&self) -> &str {
2922 &self.name
2923 }
2924
2925 #[must_use]
2927 pub const fn file(&self) -> &GeneratedString {
2928 &self.file
2929 }
2930
2931 fn is_sensitive(&self) -> bool {
2932 self.file.is_sensitive()
2933 }
2934}
2935
2936impl fmt::Debug for GeneratedConfigFileDefinition {
2937 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2938 formatter
2939 .debug_struct("GeneratedConfigFileDefinition")
2940 .field("name", &self.name)
2941 .field("file", &self.file)
2942 .finish()
2943 }
2944}
2945
2946#[derive(Clone, Eq, PartialEq)]
2951pub struct GeneratedSecretFileDefinition {
2952 name: String,
2953 file: GeneratedString,
2954}
2955
2956impl GeneratedSecretFileDefinition {
2957 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2963 Ok(Self {
2964 name: generated_file_resource_name(name.into())?,
2965 file: generated_file_resource_path(file)?,
2966 })
2967 }
2968
2969 #[must_use]
2971 pub fn name(&self) -> &str {
2972 &self.name
2973 }
2974
2975 #[must_use]
2977 pub const fn file(&self) -> &GeneratedString {
2978 &self.file
2979 }
2980
2981 fn is_sensitive(&self) -> bool {
2982 self.file.is_sensitive()
2983 }
2984}
2985
2986impl fmt::Debug for GeneratedSecretFileDefinition {
2987 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2988 formatter
2989 .debug_struct("GeneratedSecretFileDefinition")
2990 .field("name", &self.name)
2991 .field("file", &self.file)
2992 .finish()
2993 }
2994}
2995
2996#[derive(Clone, Debug, Default, Eq, PartialEq)]
2998pub struct ComposeDocumentBuilder {
2999 name: Option<String>,
3000 services: Vec<GeneratedService>,
3001 networks: Vec<GeneratedNetwork>,
3002 volumes: Vec<GeneratedVolume>,
3003 configs: Vec<GeneratedConfigFileDefinition>,
3004 secrets: Vec<GeneratedSecretFileDefinition>,
3005}
3006
3007impl ComposeDocumentBuilder {
3008 #[must_use]
3010 pub const fn new() -> Self {
3011 Self {
3012 name: None,
3013 services: Vec::new(),
3014 networks: Vec::new(),
3015 volumes: Vec::new(),
3016 configs: Vec::new(),
3017 secrets: Vec::new(),
3018 }
3019 }
3020
3021 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
3027 let name = required("project name", name.into())?;
3028 set_once(&mut self.name, name, "name")
3029 }
3030
3031 pub fn add_service(&mut self, service: GeneratedService) -> Result<(), GenerationError> {
3037 insert_named(&mut self.services, service, "service", GeneratedService::name)
3038 }
3039
3040 pub fn add_network(&mut self, network: GeneratedResource) -> Result<(), GenerationError> {
3046 insert_named(
3047 &mut self.networks,
3048 GeneratedNetwork::Basic(network),
3049 "network",
3050 GeneratedNetwork::name,
3051 )
3052 }
3053
3054 pub fn add_network_definition(&mut self, network: GeneratedNetworkDefinition) -> Result<(), GenerationError> {
3064 insert_named(
3065 &mut self.networks,
3066 GeneratedNetwork::Definition(network),
3067 "network",
3068 GeneratedNetwork::name,
3069 )
3070 }
3071
3072 pub fn add_volume(&mut self, volume: GeneratedResource) -> Result<(), GenerationError> {
3078 insert_named(
3079 &mut self.volumes,
3080 GeneratedVolume::Basic(volume),
3081 "volume",
3082 GeneratedVolume::name,
3083 )
3084 }
3085
3086 pub fn add_volume_definition(&mut self, volume: GeneratedVolumeDefinition) -> Result<(), GenerationError> {
3097 insert_named(
3098 &mut self.volumes,
3099 GeneratedVolume::Definition(volume),
3100 "volume",
3101 GeneratedVolume::name,
3102 )
3103 }
3104
3105 pub fn add_config_file(&mut self, config: GeneratedConfigFileDefinition) -> Result<(), GenerationError> {
3111 insert_named(&mut self.configs, config, "config", GeneratedConfigFileDefinition::name)
3112 }
3113
3114 pub fn add_secret_file(&mut self, secret: GeneratedSecretFileDefinition) -> Result<(), GenerationError> {
3120 insert_named(&mut self.secrets, secret, "secret", GeneratedSecretFileDefinition::name)
3121 }
3122
3123 pub fn build(self, source_id: SourceId) -> Result<GeneratedComposeDocument, GenerationError> {
3130 if self.services.is_empty() {
3131 return Err(GenerationError::MissingService);
3132 }
3133 let sensitive = self.services.iter().any(GeneratedService::is_sensitive)
3134 || self.networks.iter().any(GeneratedNetwork::is_sensitive)
3135 || self.volumes.iter().any(GeneratedVolume::is_sensitive)
3136 || self.configs.iter().any(GeneratedConfigFileDefinition::is_sensitive)
3137 || self.secrets.iter().any(GeneratedSecretFileDefinition::is_sensitive);
3138 let text = render_document(&self);
3139 let syntax = SyntaxDocument::parse(source_id, text.clone())
3140 .map_err(|_| GenerationError::InternalInvariant("syntax-tree"))?;
3141 if !syntax.is_valid() {
3142 return Err(GenerationError::InternalInvariant("syntax"));
3143 }
3144 let model = ComposeDocument::parse(syntax.document());
3145 if !model.is_valid() {
3146 return Err(GenerationError::InternalInvariant("typed-model"));
3147 }
3148 let document = model
3149 .document()
3150 .cloned()
3151 .ok_or(GenerationError::InternalInvariant("document-root"))?;
3152 Ok(GeneratedComposeDocument {
3153 text,
3154 sensitive,
3155 document,
3156 })
3157 }
3158}
3159
3160#[derive(Clone, Eq, PartialEq)]
3162pub struct GeneratedComposeDocument {
3163 text: String,
3164 sensitive: bool,
3165 document: ComposeDocument,
3166}
3167
3168impl GeneratedComposeDocument {
3169 #[must_use]
3171 pub fn text(&self) -> &str {
3172 &self.text
3173 }
3174
3175 #[must_use]
3177 pub const fn document(&self) -> &ComposeDocument {
3178 &self.document
3179 }
3180
3181 #[must_use]
3183 pub const fn is_sensitive(&self) -> bool {
3184 self.sensitive
3185 }
3186}
3187
3188impl fmt::Debug for GeneratedComposeDocument {
3189 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3190 formatter
3191 .debug_struct("GeneratedComposeDocument")
3192 .field("text", &if self.sensitive { "<redacted>" } else { &self.text })
3193 .field("sensitive", &self.sensitive)
3194 .field("document", &if self.sensitive { "<redacted>" } else { "validated" })
3195 .finish()
3196 }
3197}
3198
3199fn render_document(project: &ComposeDocumentBuilder) -> String {
3200 let mut output = String::from("---\n");
3201 if let Some(name) = &project.name {
3202 output.push_str("name: ");
3203 write_quoted(&mut output, name);
3204 output.push('\n');
3205 }
3206 output.push_str("services:\n");
3207 for service in &project.services {
3208 write_indent(&mut output, 1);
3209 write_quoted(&mut output, &service.name);
3210 output.push_str(":\n");
3211 render_service(&mut output, service);
3212 }
3213 render_network_definitions(&mut output, &project.networks);
3214 render_volume_definitions(&mut output, &project.volumes);
3215 render_file_definitions(
3216 &mut output,
3217 "configs",
3218 &project.configs,
3219 GeneratedConfigFileDefinition::name,
3220 GeneratedConfigFileDefinition::file,
3221 );
3222 render_file_definitions(
3223 &mut output,
3224 "secrets",
3225 &project.secrets,
3226 GeneratedSecretFileDefinition::name,
3227 GeneratedSecretFileDefinition::file,
3228 );
3229 output
3230}
3231
3232fn render_service(output: &mut String, service: &GeneratedService) {
3233 if let Some(GeneratedHostname::Resolved(hostname)) = &service.hostname {
3234 render_optional_string(output, "hostname", Some(hostname));
3235 }
3236 render_optional_string(output, "container_name", service.container_name.as_ref());
3237 render_optional_string(output, "image", service.image.as_ref());
3238 if let Some(entrypoint) = &service.entrypoint {
3239 render_entrypoint(output, entrypoint);
3240 }
3241 if let Some(command) = &service.command {
3242 render_command(output, command);
3243 }
3244 if let Some(init) = service.init {
3245 write_field(output, 2, "init");
3246 output.push_str(if init { "true\n" } else { "false\n" });
3247 }
3248 if let Some(stdin_open) = service.stdin_open {
3249 write_field(output, 2, "stdin_open");
3250 output.push_str(if stdin_open { "true\n" } else { "false\n" });
3251 }
3252 if let Some(tty) = service.tty {
3253 write_field(output, 2, "tty");
3254 output.push_str(if tty { "true\n" } else { "false\n" });
3255 }
3256 if let Some(privileged) = service.privileged {
3257 write_field(output, 2, "privileged");
3258 output.push_str(if privileged { "true\n" } else { "false\n" });
3259 }
3260 render_environment_files(output, &service.environment_files);
3261 render_environment(output, &service.environment);
3262 render_labels(output, &service.labels);
3263 if let Some(annotations) = &service.annotations {
3264 render_annotations(output, annotations);
3265 }
3266 render_optional_string(output, "user", service.user.as_ref());
3267 render_optional_string(output, "userns_mode", service.userns_mode.as_ref());
3268 render_string_sequence(output, "group_add", &service.group_add);
3269 if let Some(capabilities) = &service.cap_add {
3270 render_configured_string_sequence(output, "cap_add", capabilities);
3271 }
3272 if let Some(capabilities) = &service.cap_drop {
3273 render_configured_string_sequence(output, "cap_drop", capabilities);
3274 }
3275 render_optional_string(output, "working_dir", service.working_dir.as_ref());
3276 if let Some(read_only) = service.read_only {
3277 write_field(output, 2, "read_only");
3278 output.push_str(if read_only { "true\n" } else { "false\n" });
3279 }
3280 if let Some(pids_limit) = &service.pids_limit {
3281 render_pids_limit(output, pids_limit);
3282 }
3283 if let Some(shm_size) = &service.shm_size {
3284 render_shm_size(output, shm_size);
3285 }
3286 if let Some(mem_limit) = &service.mem_limit {
3287 render_mem_limit(output, mem_limit);
3288 }
3289 render_runtime_fields(output, &service.runtime_fields);
3290 if let Some(devices) = &service.devices {
3291 render_devices(output, devices);
3292 }
3293 if let Some(dns) = &service.dns {
3294 render_dns(output, dns);
3295 }
3296 if let Some(options) = &service.dns_options {
3297 render_configured_string_sequence(output, "dns_opt", options);
3298 }
3299 if let Some(search) = &service.dns_search {
3300 render_dns_search(output, search);
3301 }
3302 if let Some(expose) = &service.expose {
3303 render_configured_string_sequence(output, "expose", expose);
3304 }
3305 if let Some(options) = &service.security_options {
3306 render_configured_string_sequence(output, "security_opt", options);
3307 }
3308 if let Some(tmpfs) = &service.tmpfs {
3309 render_tmpfs(output, tmpfs);
3310 }
3311 if let Some(sysctls) = &service.sysctls {
3312 render_sysctls(output, sysctls);
3313 }
3314 if let Some(logging) = &service.logging {
3315 render_logging(output, logging);
3316 }
3317 if let Some(ulimits) = &service.ulimits {
3318 render_ulimits(output, ulimits);
3319 }
3320 if let Some(pull_policy) = &service.pull_policy {
3321 render_pull_policy(output, pull_policy);
3322 }
3323 if let Some(restart) = service.restart {
3324 render_restart(output, restart);
3325 }
3326 render_optional_string(output, "stop_signal", service.stop_signal.as_ref());
3327 render_optional_string(output, "stop_grace_period", service.stop_grace_period.as_ref());
3328 render_extra_hosts(output, &service.extra_hosts);
3329 render_ports(output, &service.ports);
3330 render_mounts(output, &service.mounts);
3331 render_networks(output, &service.networks);
3332}
3333
3334fn render_runtime_fields(output: &mut String, fields: &[GeneratedServiceRuntimeField]) {
3335 for field in fields {
3336 match field {
3337 GeneratedServiceRuntimeField::Domainname(value) => {
3338 render_optional_string(output, "domainname", Some(value));
3339 }
3340 GeneratedServiceRuntimeField::Isolation(value) => {
3341 render_optional_string(output, "isolation", Some(value));
3342 }
3343 GeneratedServiceRuntimeField::MacAddress(value) => {
3344 render_optional_string(output, "mac_address", Some(value));
3345 }
3346 GeneratedServiceRuntimeField::Uts(value) => {
3347 render_optional_string(output, "uts", Some(value));
3348 }
3349 GeneratedServiceRuntimeField::UseApiSocket(value) => {
3350 write_field(output, 2, "use_api_socket");
3351 output.push_str(if *value { "true\n" } else { "false\n" });
3352 }
3353 GeneratedServiceRuntimeField::GpusAll(value) => {
3354 render_optional_string(output, "gpus", Some(value));
3355 }
3356 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => {
3357 write_field(output, 2, "cpu_rt_runtime");
3358 output.push_str(value.expose());
3359 output.push('\n');
3360 }
3361 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3362 render_optional_string(output, "cpu_rt_runtime", Some(value));
3363 }
3364 GeneratedServiceRuntimeField::CpuShares(value) => {
3365 render_optional_string(output, "cpu_shares", Some(value));
3366 }
3367 GeneratedServiceRuntimeField::Cpus(value) => {
3368 render_optional_string(output, "cpus", Some(value));
3369 }
3370 GeneratedServiceRuntimeField::Cpuset(value) => {
3371 render_optional_string(output, "cpuset", Some(value));
3372 }
3373 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => {
3374 render_configured_string_sequence(output, "device_cgroup_rules", values);
3375 }
3376 GeneratedServiceRuntimeField::Ipc(value) => {
3377 render_optional_string(output, "ipc", Some(value));
3378 }
3379 GeneratedServiceRuntimeField::MemReservation(value) => {
3380 render_optional_string(output, "mem_reservation", Some(value));
3381 }
3382 GeneratedServiceRuntimeField::MemSwappiness(value) => {
3383 render_optional_string(output, "mem_swappiness", Some(value));
3384 }
3385 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3386 render_optional_string(output, "memswap_limit", Some(value));
3387 }
3388 GeneratedServiceRuntimeField::NetworkMode(value) => {
3389 render_optional_string(output, "network_mode", Some(value));
3390 }
3391 GeneratedServiceRuntimeField::OomKillDisable(value) => {
3392 write_field(output, 2, "oom_kill_disable");
3393 output.push_str(if *value { "true\n" } else { "false\n" });
3394 }
3395 GeneratedServiceRuntimeField::OomScoreAdj(value) => {
3396 render_optional_string(output, "oom_score_adj", Some(value));
3397 }
3398 GeneratedServiceRuntimeField::Pid(value) => {
3399 render_optional_string(output, "pid", Some(value));
3400 }
3401 GeneratedServiceRuntimeField::Scale(value) => {
3402 render_optional_string(output, "scale", Some(value));
3403 }
3404 GeneratedServiceRuntimeField::VolumesFrom(values) => {
3405 render_configured_string_sequence(output, "volumes_from", values);
3406 }
3407 }
3408 }
3409}
3410
3411fn generated_runtime_field_safe(field: &GeneratedServiceRuntimeField) -> bool {
3412 let safe = |value: &GeneratedString| !value.expose().is_empty() && !value.expose().contains(['\n', '\r', '$']);
3413 let unsigned = |value: &GeneratedString| safe(value) && value.expose().bytes().all(|byte| byte.is_ascii_digit());
3414 let bounded_unsigned = |value: &GeneratedString| unsigned(value) && value.expose().parse::<i128>().is_ok();
3415 let signed_range = |value: &GeneratedString, min: i32, max: i32| {
3416 safe(value)
3417 && value
3418 .expose()
3419 .parse::<i32>()
3420 .is_ok_and(|number| (min..=max).contains(&number))
3421 };
3422 let decimal = |value: &GeneratedString| safe(value) && normalize_generated_decimal(value.expose()).is_some();
3423 let reference = |value: &GeneratedString| {
3424 safe(value)
3425 && (!value.expose().contains(':')
3426 || value
3427 .expose()
3428 .split_once(':')
3429 .is_some_and(|(_, target)| !target.is_empty()))
3430 };
3431 match field {
3432 GeneratedServiceRuntimeField::Domainname(value)
3433 | GeneratedServiceRuntimeField::Isolation(value)
3434 | GeneratedServiceRuntimeField::MacAddress(value)
3435 | GeneratedServiceRuntimeField::Uts(value)
3436 | GeneratedServiceRuntimeField::Cpuset(value) => safe(value),
3437 GeneratedServiceRuntimeField::UseApiSocket(_) | GeneratedServiceRuntimeField::OomKillDisable(_) => true,
3438 GeneratedServiceRuntimeField::GpusAll(value) => safe(value) && value.expose() == "all",
3439 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => unsigned(value),
3440 GeneratedServiceRuntimeField::CpuShares(value) | GeneratedServiceRuntimeField::Scale(value) => {
3441 bounded_unsigned(value)
3442 }
3443 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3444 safe(value)
3445 && matches!(
3446 CpuRtRuntime::parse_string(value.expose().to_owned()),
3447 CpuRtRuntime::Duration(_)
3448 )
3449 }
3450 GeneratedServiceRuntimeField::Cpus(value) => decimal(value),
3451 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => values.iter().all(safe),
3452 GeneratedServiceRuntimeField::Ipc(value)
3453 | GeneratedServiceRuntimeField::NetworkMode(value)
3454 | GeneratedServiceRuntimeField::Pid(value) => reference(value),
3455 GeneratedServiceRuntimeField::MemReservation(value) => {
3456 safe(value) && valid_generated_runtime_memory(value.expose(), false)
3457 }
3458 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3459 safe(value) && valid_generated_runtime_memory(value.expose(), true)
3460 }
3461 GeneratedServiceRuntimeField::MemSwappiness(value) => signed_range(value, 0, 100),
3462 GeneratedServiceRuntimeField::OomScoreAdj(value) => signed_range(value, -1000, 1000),
3463 GeneratedServiceRuntimeField::VolumesFrom(values) => values.iter().all(reference),
3464 }
3465}
3466
3467fn normalize_generated_decimal(value: &str) -> Option<()> {
3468 let (whole, fraction) = value.split_once('.').map_or((value, ""), |parts| parts);
3469 let valid_shape = if value.contains('.') {
3470 !whole.is_empty() && !fraction.is_empty()
3471 } else {
3472 !whole.is_empty()
3473 };
3474 (valid_shape
3475 && whole.bytes().all(|byte| byte.is_ascii_digit())
3476 && fraction.bytes().all(|byte| byte.is_ascii_digit())
3477 && value.bytes().filter(|byte| *byte == b'.').count() <= 1)
3478 .then_some(())
3479}
3480
3481fn valid_generated_runtime_memory(value: &str, allow_unlimited: bool) -> bool {
3487 if allow_unlimited && value == "-1" {
3488 return true;
3489 }
3490 if !value.is_empty() && value.bytes().all(|byte| byte == b'0') {
3491 return true;
3492 }
3493 let Some(amount) = ["kb", "mb", "gb", "b", "k", "m", "g"]
3494 .into_iter()
3495 .find_map(|unit| value.strip_suffix(unit))
3496 else {
3497 return false;
3498 };
3499 !amount.is_empty() && amount.bytes().all(|byte| byte.is_ascii_digit())
3500}
3501
3502fn render_pids_limit(output: &mut String, limit: &GeneratedPidsLimit) {
3503 write_field(output, 2, "pids_limit");
3504 match limit {
3505 GeneratedPidsLimit::Unlimited => output.push_str("-1\n"),
3506 GeneratedPidsLimit::Finite(decimal) => {
3507 output.push_str(decimal);
3508 output.push('\n');
3509 }
3510 }
3511}
3512
3513fn render_shm_size(output: &mut String, size: &GeneratedShmSize) {
3514 let GeneratedShmSize::Explicit { amount, unit } = size;
3515 write_field(output, 2, "shm_size");
3516 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3517 output.push('\n');
3518}
3519
3520fn render_mem_limit(output: &mut String, limit: &GeneratedMemLimit) {
3521 let GeneratedMemLimit::Explicit { amount, unit } = limit;
3522 write_field(output, 2, "mem_limit");
3523 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3524 output.push('\n');
3525}
3526
3527fn render_devices(output: &mut String, devices: &[GeneratedDevice]) {
3528 if devices.is_empty() {
3529 output.push_str(" devices: []\n");
3530 return;
3531 }
3532 output.push_str(" devices:\n");
3533 for device in devices {
3534 match device {
3535 GeneratedDevice::Short(value) => {
3536 output.push_str(" - ");
3537 write_quoted(output, value.expose());
3538 output.push('\n');
3539 }
3540 GeneratedDevice::Long(value) => {
3541 output.push_str(" - source: ");
3542 write_quoted(output, value.source().expose());
3543 output.push('\n');
3544 if let Some(target) = value.target() {
3545 output.push_str(" target: ");
3546 write_quoted(output, target.expose());
3547 output.push('\n');
3548 }
3549 if let Some(permissions) = value.permissions() {
3550 output.push_str(" permissions: ");
3551 write_quoted(output, permissions.expose());
3552 output.push('\n');
3553 }
3554 }
3555 }
3556 }
3557}
3558
3559fn render_dns(output: &mut String, dns: &GeneratedDns) {
3560 match dns {
3561 GeneratedDns::Scalar(value) => render_optional_string(output, "dns", Some(value)),
3562 GeneratedDns::List(values) => render_configured_string_sequence(output, "dns", values),
3563 }
3564}
3565
3566fn render_dns_search(output: &mut String, search: &GeneratedDnsSearch) {
3567 match search {
3568 GeneratedDnsSearch::Scalar(value) => render_optional_string(output, "dns_search", Some(value)),
3569 GeneratedDnsSearch::List(values) => render_configured_string_sequence(output, "dns_search", values),
3570 }
3571}
3572
3573fn render_tmpfs(output: &mut String, tmpfs: &GeneratedTmpfs) {
3574 match tmpfs {
3575 GeneratedTmpfs::Scalar(item) => render_optional_string(output, "tmpfs", Some(item)),
3576 GeneratedTmpfs::List(items) => render_configured_string_sequence(output, "tmpfs", items),
3577 }
3578}
3579
3580fn render_sysctls(output: &mut String, sysctls: &GeneratedSysctls) {
3581 match sysctls {
3582 GeneratedSysctls::Map(entries) if entries.is_empty() => output.push_str(" sysctls: {}\n"),
3583 GeneratedSysctls::Map(entries) => {
3584 output.push_str(" sysctls:\n");
3585 for entry in entries {
3586 write_indent(output, 3);
3587 write_quoted(output, entry.name());
3588 output.push_str(": ");
3589 write_quoted(output, entry.value().expose());
3590 output.push('\n');
3591 }
3592 }
3593 GeneratedSysctls::List(items) => render_configured_string_sequence(output, "sysctls", items),
3594 }
3595}
3596
3597fn render_logging(output: &mut String, logging: &GeneratedLogging) {
3598 output.push_str(" logging:\n driver: ");
3599 write_quoted(output, logging.driver.expose());
3600 output.push('\n');
3601 if logging.options.is_empty() {
3602 output.push_str(" options: {}\n");
3603 return;
3604 }
3605 output.push_str(" options:\n");
3606 for option in &logging.options {
3607 write_indent(output, 4);
3608 write_quoted(output, option.name());
3609 output.push_str(": ");
3610 match option.value() {
3611 GeneratedLoggingOptionValue::String(value) => write_quoted(output, value.expose()),
3612 GeneratedLoggingOptionValue::Number(value) => output.push_str(value.expose()),
3613 GeneratedLoggingOptionValue::Null => output.push_str("null"),
3614 }
3615 output.push('\n');
3616 }
3617}
3618
3619fn render_ulimits(output: &mut String, ulimits: &GeneratedUlimits) {
3620 if ulimits.entries.is_empty() {
3621 output.push_str(" ulimits: {}\n");
3622 return;
3623 }
3624 output.push_str(" ulimits:\n");
3625 for limit in &ulimits.entries {
3626 write_indent(output, 3);
3627 write_quoted(output, limit.name());
3628 match limit.value() {
3629 GeneratedUlimitValue::Single(value) => {
3630 output.push_str(": ");
3631 write_quoted(output, value.expose());
3632 output.push('\n');
3633 }
3634 GeneratedUlimitValue::Range {
3635 soft: Some(soft),
3636 hard: Some(hard),
3637 } => {
3638 output.push_str(":\n");
3639 write_indent(output, 4);
3640 output.push_str("soft: ");
3641 write_quoted(output, soft.expose());
3642 output.push('\n');
3643 write_indent(output, 4);
3644 output.push_str("hard: ");
3645 write_quoted(output, hard.expose());
3646 output.push('\n');
3647 }
3648 GeneratedUlimitValue::Range { .. } => {
3649 unreachable!("generated ulimit ranges are validated during construction")
3650 }
3651 }
3652 }
3653}
3654
3655fn render_pull_policy(output: &mut String, policy: &GeneratedPullPolicy) {
3656 write_field(output, 2, "pull_policy");
3657 let value = match policy {
3658 GeneratedPullPolicy::Always => "always".to_owned(),
3659 GeneratedPullPolicy::Never => "never".to_owned(),
3660 GeneratedPullPolicy::Missing => "missing".to_owned(),
3661 GeneratedPullPolicy::IfNotPresentAlias => "if_not_present".to_owned(),
3662 GeneratedPullPolicy::Build => "build".to_owned(),
3663 GeneratedPullPolicy::Daily => "daily".to_owned(),
3664 GeneratedPullPolicy::Weekly => "weekly".to_owned(),
3665 GeneratedPullPolicy::Every(duration) => format!("every_{}", duration.expose()),
3666 };
3667 write_quoted(output, &value);
3668 output.push('\n');
3669}
3670
3671fn render_entrypoint(output: &mut String, entrypoint: &GeneratedEntrypoint) {
3672 match entrypoint {
3673 GeneratedEntrypoint::List(arguments) if arguments.is_empty() => output.push_str(" entrypoint: []\n"),
3674 GeneratedEntrypoint::List(arguments) => render_string_sequence(output, "entrypoint", arguments),
3675 GeneratedEntrypoint::String(entrypoint) => render_optional_string(output, "entrypoint", Some(entrypoint)),
3676 GeneratedEntrypoint::Empty => output.push_str(" entrypoint: []\n"),
3677 }
3678}
3679
3680fn render_restart(output: &mut String, restart: GeneratedRestartPolicy) {
3681 write_field(output, 2, "restart");
3682 let value = match restart {
3683 GeneratedRestartPolicy::No => "no".to_owned(),
3684 GeneratedRestartPolicy::Always => "always".to_owned(),
3685 GeneratedRestartPolicy::OnFailure { maximum_retries: None } => "on-failure".to_owned(),
3686 GeneratedRestartPolicy::OnFailure {
3687 maximum_retries: Some(maximum_retries),
3688 } => format!("on-failure:{maximum_retries}"),
3689 GeneratedRestartPolicy::UnlessStopped => "unless-stopped".to_owned(),
3690 };
3691 write_quoted(output, &value);
3692 output.push('\n');
3693}
3694
3695fn render_optional_string(output: &mut String, key: &str, value: Option<&GeneratedString>) {
3696 if let Some(value) = value {
3697 write_field(output, 2, key);
3698 write_quoted(output, value.expose());
3699 output.push('\n');
3700 }
3701}
3702
3703fn render_command(output: &mut String, command: &GeneratedCommand) {
3704 match command {
3705 GeneratedCommand::Exec(arguments) if arguments.is_empty() => output.push_str(" command: []\n"),
3706 GeneratedCommand::Exec(arguments) => render_string_sequence(output, "command", arguments),
3707 GeneratedCommand::Shell(command) => render_optional_string(output, "command", Some(command)),
3708 GeneratedCommand::Empty => output.push_str(" command: []\n"),
3709 }
3710}
3711
3712fn render_environment(output: &mut String, environment: &[GeneratedEnvironment]) {
3713 if environment.is_empty() {
3714 return;
3715 }
3716 output.push_str(" environment:\n");
3717 for variable in environment {
3718 output.push_str(" - ");
3719 let value = variable.value.as_ref().map_or_else(
3720 || variable.name.clone(),
3721 |value| format!("{}={}", variable.name, value.expose()),
3722 );
3723 write_quoted(output, &value);
3724 output.push('\n');
3725 }
3726}
3727
3728fn render_environment_files(output: &mut String, environment_files: &[GeneratedEnvironmentFile]) {
3729 if environment_files.is_empty() {
3730 return;
3731 }
3732 output.push_str(" env_file:\n");
3733 for environment_file in environment_files {
3734 match environment_file {
3735 GeneratedEnvironmentFile::Short(path) => {
3736 output.push_str(" - ");
3737 write_quoted(output, path.expose());
3738 output.push('\n');
3739 }
3740 GeneratedEnvironmentFile::Long { path, required, format } => {
3741 output.push_str(" - path: ");
3742 write_quoted(output, path.expose());
3743 output.push('\n');
3744 if let Some(required) = required {
3745 output.push_str(" required: ");
3746 output.push_str(if *required { "true\n" } else { "false\n" });
3747 }
3748 if let Some(format) = format {
3749 output.push_str(" format: ");
3750 write_quoted(
3751 output,
3752 match format {
3753 GeneratedEnvironmentFileFormat::Raw => "raw",
3754 },
3755 );
3756 output.push('\n');
3757 }
3758 }
3759 }
3760 }
3761}
3762
3763fn render_labels(output: &mut String, labels: &[GeneratedLabel]) {
3764 if labels.is_empty() {
3765 return;
3766 }
3767 output.push_str(" labels:\n");
3768 for label in labels {
3769 output.push_str(" ");
3770 write_quoted(output, &label.name);
3771 output.push_str(": ");
3772 write_quoted(output, label.value.expose());
3773 output.push('\n');
3774 }
3775}
3776
3777fn render_annotations(output: &mut String, annotations: &[GeneratedAnnotation]) {
3778 if annotations.is_empty() {
3779 output.push_str(" annotations: {}\n");
3780 return;
3781 }
3782 output.push_str(" annotations:\n");
3783 for annotation in annotations {
3784 output.push_str(" ");
3785 write_quoted(output, &annotation.name);
3786 output.push_str(": ");
3787 write_quoted(output, annotation.value.expose());
3788 output.push('\n');
3789 }
3790}
3791
3792fn render_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3793 if values.is_empty() {
3794 return;
3795 }
3796 write_indent(output, 2);
3797 output.push_str(key);
3798 output.push_str(":\n");
3799 for value in values {
3800 output.push_str(" - ");
3801 write_quoted(output, value.expose());
3802 output.push('\n');
3803 }
3804}
3805
3806fn render_configured_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3807 if values.is_empty() {
3808 write_indent(output, 2);
3809 output.push_str(key);
3810 output.push_str(": []\n");
3811 } else {
3812 render_string_sequence(output, key, values);
3813 }
3814}
3815
3816fn render_extra_hosts(output: &mut String, hosts: &[GeneratedExtraHost]) {
3817 if hosts.is_empty() {
3818 return;
3819 }
3820 output.push_str(" extra_hosts:\n");
3821 for host in hosts {
3822 output.push_str(" - ");
3823 write_quoted(output, &format!("{}={}", host.hostname, host.address));
3824 output.push('\n');
3825 }
3826}
3827
3828fn render_ports(output: &mut String, ports: &[GeneratedPort]) {
3829 if ports.is_empty() {
3830 return;
3831 }
3832 output.push_str(" ports:\n");
3833 for port in ports {
3834 if port.protocol == GeneratedProtocol::Sctp {
3835 render_short_sctp_port(output, port);
3836 continue;
3837 }
3838 output.push_str(" - target: ");
3839 output.push_str(&port.target.to_string());
3840 output.push('\n');
3841 if let Some(published) = port.published {
3842 output.push_str(" published: ");
3843 write_quoted(output, &published.to_string());
3844 output.push('\n');
3845 }
3846 if let Some(host_ip) = &port.host_ip {
3847 output.push_str(" host_ip: ");
3848 write_quoted(output, host_ip);
3849 output.push('\n');
3850 }
3851 output.push_str(" protocol: ");
3852 write_quoted(output, port.protocol.as_str());
3853 output.push('\n');
3854 }
3855}
3856
3857fn render_short_sctp_port(output: &mut String, port: &GeneratedPort) {
3858 let mut value = String::new();
3859 if let Some(host_ip) = &port.host_ip {
3860 if host_ip.contains(':') && !(host_ip.starts_with('[') && host_ip.ends_with(']')) {
3861 value.push('[');
3862 value.push_str(host_ip);
3863 value.push(']');
3864 } else {
3865 value.push_str(host_ip);
3866 }
3867 value.push(':');
3868 }
3869 if let Some(published) = port.published {
3870 value.push_str(&published.to_string());
3871 value.push(':');
3872 }
3873 value.push_str(&port.target.to_string());
3874 value.push_str("/sctp");
3875
3876 output.push_str(" - ");
3877 write_quoted(output, &value);
3878 output.push('\n');
3879}
3880
3881fn render_mounts(output: &mut String, mounts: &[GeneratedMount]) {
3882 if mounts.is_empty() {
3883 return;
3884 }
3885 output.push_str(" volumes:\n");
3886 for mount in mounts {
3887 match &mount.kind {
3888 GeneratedMountKind::Bind {
3889 source,
3890 selinux: Some(selinux),
3891 } => render_selinux_bind(output, source, mount, *selinux),
3892 kind => render_long_mount(output, kind, mount),
3893 }
3894 }
3895}
3896
3897fn render_selinux_bind(output: &mut String, source: &str, mount: &GeneratedMount, selinux: GeneratedSelinux) {
3898 let mut value = format!("{source}:{}:{}", mount.target, selinux.as_str());
3899 if mount.read_only {
3900 value.push_str(",ro");
3901 }
3902 output.push_str(" - ");
3903 write_quoted(output, &value);
3904 output.push('\n');
3905}
3906
3907fn render_long_mount(output: &mut String, kind: &GeneratedMountKind, mount: &GeneratedMount) {
3908 let (mount_type, source) = match kind {
3909 GeneratedMountKind::Volume { source } => ("volume", Some(source.as_str())),
3910 GeneratedMountKind::Bind { source, selinux: None } => ("bind", Some(source.as_str())),
3911 GeneratedMountKind::Anonymous => ("volume", None),
3912 GeneratedMountKind::Bind { selinux: Some(_), .. } => return,
3913 };
3914 output.push_str(" - type: ");
3915 write_quoted(output, mount_type);
3916 output.push('\n');
3917 if let Some(source) = source {
3918 output.push_str(" source: ");
3919 write_quoted(output, source);
3920 output.push('\n');
3921 }
3922 output.push_str(" target: ");
3923 write_quoted(output, &mount.target);
3924 output.push('\n');
3925 if mount.read_only {
3926 output.push_str(" read_only: true\n");
3927 }
3928}
3929
3930fn render_networks(output: &mut String, networks: &[GeneratedNetworkAttachment]) {
3931 if networks.is_empty() {
3932 return;
3933 }
3934 output.push_str(" networks:\n");
3935 for network in networks {
3936 output.push_str(" ");
3937 write_quoted(output, &network.name);
3938 if network.aliases.is_empty() && network.ipv4_address.is_none() && network.ipv6_address.is_none() {
3939 output.push_str(": {}\n");
3940 continue;
3941 }
3942 output.push_str(":\n");
3943 if !network.aliases.is_empty() {
3944 output.push_str(" aliases:\n");
3945 for alias in &network.aliases {
3946 output.push_str(" - ");
3947 write_quoted(output, alias);
3948 output.push('\n');
3949 }
3950 }
3951 for (field, address) in [
3952 ("ipv4_address", network.ipv4_address.as_ref()),
3953 ("ipv6_address", network.ipv6_address.as_ref()),
3954 ] {
3955 if let Some(address) = address {
3956 output.push_str(" ");
3957 output.push_str(field);
3958 output.push_str(": ");
3959 write_quoted(output, address.expose());
3960 output.push('\n');
3961 }
3962 }
3963 }
3964}
3965
3966fn render_network_definitions(output: &mut String, networks: &[GeneratedNetwork]) {
3967 if networks.is_empty() {
3968 return;
3969 }
3970 output.push_str("networks:\n");
3971 for network in networks {
3972 match network {
3973 GeneratedNetwork::Basic(network) => render_basic_resource(output, network),
3974 GeneratedNetwork::Definition(network) => render_network_definition(output, network),
3975 }
3976 }
3977}
3978
3979fn render_network_definition(output: &mut String, network: &GeneratedNetworkDefinition) {
3980 output.push_str(" ");
3981 write_quoted(output, &network.name);
3982 if network.custom_name.is_none()
3983 && network.driver.is_none()
3984 && network.driver_opts.is_none()
3985 && network.enable_ipv6.is_none()
3986 && network.internal.is_none()
3987 && network.labels.is_none()
3988 {
3989 output.push_str(": {}\n");
3990 return;
3991 }
3992 output.push_str(":\n");
3993 if let Some(custom_name) = &network.custom_name {
3994 output.push_str(" name: ");
3995 write_quoted(output, custom_name);
3996 output.push('\n');
3997 }
3998 if let Some(driver) = &network.driver {
3999 output.push_str(" driver: ");
4000 write_quoted(output, driver.expose());
4001 output.push('\n');
4002 }
4003 if let Some(driver_opts) = &network.driver_opts {
4004 if driver_opts.is_empty() {
4005 output.push_str(" driver_opts: {}\n");
4006 } else {
4007 output.push_str(" driver_opts:\n");
4008 for option in driver_opts {
4009 output.push_str(" ");
4010 write_quoted(output, option.name());
4011 output.push_str(": ");
4012 match option.value() {
4013 GeneratedNetworkDriverOptionValue::String(value) => {
4014 write_quoted(output, value.expose());
4015 }
4016 GeneratedNetworkDriverOptionValue::Number(value) => {
4017 output.push_str(value.expose());
4018 }
4019 }
4020 output.push('\n');
4021 }
4022 }
4023 }
4024 if let Some(enable_ipv6) = network.enable_ipv6 {
4025 output.push_str(" enable_ipv6: ");
4026 output.push_str(if enable_ipv6 { "true\n" } else { "false\n" });
4027 }
4028 if let Some(internal) = network.internal {
4029 output.push_str(" internal: ");
4030 output.push_str(if internal { "true\n" } else { "false\n" });
4031 }
4032 if let Some(labels) = &network.labels {
4033 if labels.is_empty() {
4034 output.push_str(" labels: {}\n");
4035 } else {
4036 output.push_str(" labels:\n");
4037 for label in labels {
4038 output.push_str(" ");
4039 write_quoted(output, label.name());
4040 output.push_str(": ");
4041 write_quoted(output, label.value().expose());
4042 output.push('\n');
4043 }
4044 }
4045 }
4046}
4047
4048fn render_volume_definitions(output: &mut String, volumes: &[GeneratedVolume]) {
4049 if volumes.is_empty() {
4050 return;
4051 }
4052 output.push_str("volumes:\n");
4053 for volume in volumes {
4054 match volume {
4055 GeneratedVolume::Basic(volume) => render_basic_resource(output, volume),
4056 GeneratedVolume::Definition(volume) => render_volume_definition(output, volume),
4057 }
4058 }
4059}
4060
4061fn render_file_definitions<T>(
4062 output: &mut String,
4063 field: &str,
4064 definitions: &[T],
4065 name: impl Fn(&T) -> &str,
4066 file: impl Fn(&T) -> &GeneratedString,
4067) {
4068 if definitions.is_empty() {
4069 return;
4070 }
4071 output.push_str(field);
4072 output.push_str(":\n");
4073 for definition in definitions {
4074 output.push_str(" ");
4075 write_quoted(output, name(definition));
4076 output.push_str(":\n file: ");
4077 write_quoted(output, file(definition).expose());
4078 output.push('\n');
4079 }
4080}
4081
4082fn render_volume_definition(output: &mut String, volume: &GeneratedVolumeDefinition) {
4083 output.push_str(" ");
4084 write_quoted(output, &volume.name);
4085 if volume.custom_name.is_none()
4086 && volume.driver.is_none()
4087 && volume.driver_opts.is_none()
4088 && volume.labels.is_none()
4089 {
4090 output.push_str(": {}\n");
4091 return;
4092 }
4093 output.push_str(":\n");
4094 if let Some(custom_name) = &volume.custom_name {
4095 output.push_str(" name: ");
4096 write_quoted(output, custom_name);
4097 output.push('\n');
4098 }
4099 if let Some(driver) = &volume.driver {
4100 output.push_str(" driver: ");
4101 write_quoted(output, driver.expose());
4102 output.push('\n');
4103 }
4104 if let Some(driver_opts) = &volume.driver_opts {
4105 if driver_opts.is_empty() {
4106 output.push_str(" driver_opts: {}\n");
4107 } else {
4108 output.push_str(" driver_opts:\n");
4109 for option in driver_opts {
4110 output.push_str(" ");
4111 write_quoted(output, option.name());
4112 output.push_str(": ");
4113 match option.value() {
4114 GeneratedVolumeDriverOptionValue::String(value) => write_quoted(output, value.expose()),
4115 GeneratedVolumeDriverOptionValue::Number(value) => output.push_str(value.expose()),
4116 }
4117 output.push('\n');
4118 }
4119 }
4120 }
4121 if let Some(labels) = &volume.labels {
4122 if labels.is_empty() {
4123 output.push_str(" labels: {}\n");
4124 } else {
4125 output.push_str(" labels:\n");
4126 for label in labels {
4127 output.push_str(" ");
4128 write_quoted(output, label.name());
4129 output.push_str(": ");
4130 write_quoted(output, label.value().expose());
4131 output.push('\n');
4132 }
4133 }
4134 }
4135}
4136
4137fn render_basic_resource(output: &mut String, resource: &GeneratedResource) {
4138 output.push_str(" ");
4139 write_quoted(output, &resource.name);
4140 if !resource.external && resource.custom_name.is_none() {
4141 output.push_str(": {}\n");
4142 return;
4143 }
4144 output.push_str(":\n");
4145 if let Some(custom_name) = &resource.custom_name {
4146 output.push_str(" name: ");
4147 write_quoted(output, custom_name);
4148 output.push('\n');
4149 }
4150 if resource.external {
4151 output.push_str(" external: true\n");
4152 }
4153}
4154
4155fn write_field(output: &mut String, depth: usize, key: &str) {
4156 write_indent(output, depth);
4157 output.push_str(key);
4158 output.push_str(": ");
4159}
4160
4161fn write_indent(output: &mut String, depth: usize) {
4162 for _ in 0..depth {
4163 output.push_str(" ");
4164 }
4165}
4166
4167fn required(kind: &'static str, value: String) -> Result<String, GenerationError> {
4168 if value.is_empty() {
4169 return Err(GenerationError::EmptyValue(kind));
4170 }
4171 if value.contains('\0') {
4172 return Err(GenerationError::ContainsNul(kind));
4173 }
4174 Ok(value)
4175}
4176
4177fn require_generated_string(kind: &'static str, value: &GeneratedString) -> Result<(), GenerationError> {
4178 if value.expose().is_empty() {
4179 return Err(GenerationError::EmptyValue(kind));
4180 }
4181 Ok(())
4182}
4183
4184fn generated_file_resource_name(value: String) -> Result<String, GenerationError> {
4185 if value.is_empty() || value.contains(['\0', '\r', '\n', '$']) {
4186 Err(GenerationError::InvalidFileResourceName)
4187 } else {
4188 Ok(value)
4189 }
4190}
4191
4192fn generated_file_resource_path(value: GeneratedString) -> Result<GeneratedString, GenerationError> {
4193 if value.expose().is_empty() || value.expose().contains(['\0', '\r', '\n', '$']) {
4194 Err(GenerationError::InvalidFileResourcePath)
4195 } else {
4196 Ok(value)
4197 }
4198}
4199
4200fn validate_generated_device_member(
4201 member: &'static str,
4202 value: &GeneratedString,
4203 require_non_empty: bool,
4204) -> Result<(), GenerationError> {
4205 if valid_generated_device_string(value.expose(), require_non_empty) {
4206 Ok(())
4207 } else {
4208 Err(GenerationError::InvalidDeviceValue(member))
4209 }
4210}
4211
4212fn validate_generated_ulimit_value(value: &GeneratedString) -> Result<(), GenerationError> {
4213 let value = value.expose();
4214 if value.contains(['\r', '\n', '$'])
4215 || (value != "-1" && (value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit())))
4216 {
4217 return Err(GenerationError::InvalidUlimitValue);
4218 }
4219 Ok(())
4220}
4221
4222fn valid_yaml_number(value: &str) -> bool {
4223 let ordinary = !value.is_empty()
4224 && value.bytes().any(|byte| byte.is_ascii_digit())
4225 && value.bytes().all(|byte| {
4226 byte.is_ascii_digit()
4227 || matches!(
4228 byte,
4229 b'+' | b'-'
4230 | b'.'
4231 | b'_'
4232 | b'e'
4233 | b'E'
4234 | b'x'
4235 | b'X'
4236 | b'o'
4237 | b'O'
4238 | b'a'..=b'f'
4239 | b'A'..=b'F'
4240 )
4241 });
4242 let special = matches!(
4243 value,
4244 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF" | ".nan" | ".NaN" | ".NAN"
4245 );
4246 if !ordinary && !special {
4247 return false;
4248 }
4249 let parse = YamlFile::parse(value);
4250 if !parse.ok() {
4251 return false;
4252 }
4253 let file = parse.tree();
4254 let Some(document) = file.document() else {
4255 return false;
4256 };
4257 let Some(scalar) = document.as_scalar() else {
4258 return false;
4259 };
4260 let position = scalar.byte_range();
4261 position.start == 0
4262 && position.end as usize == value.len()
4263 && matches!(
4264 ScalarValue::from_scalar(&scalar).scalar_type(),
4265 ScalarType::Integer | ScalarType::Float
4266 )
4267}
4268
4269fn environment_name(value: String) -> Result<String, GenerationError> {
4270 let value = required("environment name", value)?;
4271 if value.contains('=') {
4272 return Err(GenerationError::InvalidEnvironmentName);
4273 }
4274 Ok(value)
4275}
4276
4277fn valid_container_name(value: &str) -> bool {
4278 let mut bytes = value.bytes();
4279 bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
4280 && bytes
4281 .next()
4282 .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4283 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4284}
4285
4286fn short_component(kind: &'static str, value: String, separator: char) -> Result<String, GenerationError> {
4287 let value = required(kind, value)?;
4288 if value.contains(separator) {
4289 return Err(GenerationError::InvalidShortComponent(kind));
4290 }
4291 Ok(value)
4292}
4293
4294fn set_once<T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), GenerationError> {
4295 if slot.is_some() {
4296 return Err(GenerationError::DuplicateField(field));
4297 }
4298 *slot = Some(value);
4299 Ok(())
4300}
4301
4302fn insert_named<T>(
4303 values: &mut Vec<T>,
4304 value: T,
4305 kind: &'static str,
4306 name: impl Fn(&T) -> &str,
4307) -> Result<(), GenerationError> {
4308 let value_name = name(&value);
4309 if values.iter().any(|candidate| name(candidate) == value_name) {
4310 return Err(GenerationError::DuplicateName {
4311 kind,
4312 name: value_name.to_owned(),
4313 });
4314 }
4315 values.push(value);
4316 Ok(())
4317}
4318
4319fn command_is_sensitive(command: &GeneratedCommand) -> bool {
4320 match command {
4321 GeneratedCommand::Exec(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4322 GeneratedCommand::Shell(command) => command.is_sensitive(),
4323 GeneratedCommand::Empty => false,
4324 }
4325}
4326
4327fn entrypoint_is_sensitive(entrypoint: &GeneratedEntrypoint) -> bool {
4328 match entrypoint {
4329 GeneratedEntrypoint::List(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4330 GeneratedEntrypoint::String(entrypoint) => entrypoint.is_sensitive(),
4331 GeneratedEntrypoint::Empty => false,
4332 }
4333}