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> {
2442 let mut seen = BTreeSet::new();
2443 for item in &expose {
2444 if !valid_generated_expose_item(item.expose()) {
2445 return Err(GenerationError::InvalidExposeValue);
2446 }
2447 if !seen.insert(item.expose()) {
2448 return Err(GenerationError::DuplicateItem("expose"));
2449 }
2450 }
2451 set_once(&mut self.expose, expose, "expose")
2452 }
2453
2454 #[must_use]
2456 pub fn expose(&self) -> Option<&[GeneratedString]> {
2457 self.expose.as_deref()
2458 }
2459
2460 pub fn set_security_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2470 for option in &options {
2471 if option.expose().is_empty()
2472 || option.expose().contains('$')
2473 || option.expose().contains('\r')
2474 || option.expose().contains('\n')
2475 || option.expose().contains('\0')
2476 {
2477 return Err(GenerationError::InvalidSecurityOptionValue);
2478 }
2479 }
2480 set_once(&mut self.security_options, options, "security_opt")
2481 }
2482
2483 #[must_use]
2485 pub fn security_options(&self) -> Option<&[GeneratedString]> {
2486 self.security_options.as_deref()
2487 }
2488
2489 #[must_use]
2491 pub fn devices(&self) -> Option<&[GeneratedDevice]> {
2492 self.devices.as_deref()
2493 }
2494
2495 pub fn set_working_dir(&mut self, directory: GeneratedString) -> Result<(), GenerationError> {
2502 require_generated_string("working directory", &directory)?;
2503 set_once(&mut self.working_dir, directory, "working_dir")
2504 }
2505
2506 pub fn set_read_only(&mut self, read_only: bool) -> Result<(), GenerationError> {
2512 set_once(&mut self.read_only, read_only, "read_only")
2513 }
2514
2515 pub fn set_pids_limit(&mut self, limit: GeneratedPidsLimit) -> Result<(), GenerationError> {
2523 if let GeneratedPidsLimit::Finite(decimal) = &limit {
2524 if !valid_positive_pids_decimal(decimal) {
2525 return Err(GenerationError::InvalidPidsLimit);
2526 }
2527 }
2528 set_once(&mut self.pids_limit, limit, "pids_limit")
2529 }
2530
2531 pub fn set_shm_size(&mut self, size: GeneratedShmSize) -> Result<(), GenerationError> {
2539 let GeneratedShmSize::Explicit { amount, .. } = &size;
2540 if !valid_generated_shm_amount(amount.expose()) {
2541 return Err(GenerationError::InvalidShmSize);
2542 }
2543 set_once(&mut self.shm_size, size, "shm_size")
2544 }
2545
2546 pub fn set_mem_limit(&mut self, limit: GeneratedMemLimit) -> Result<(), GenerationError> {
2554 let GeneratedMemLimit::Explicit { amount, .. } = &limit;
2555 if !valid_generated_mem_amount(amount.expose()) {
2556 return Err(GenerationError::InvalidMemLimit);
2557 }
2558 set_once(&mut self.mem_limit, limit, "mem_limit")
2559 }
2560
2561 pub fn set_tmpfs(&mut self, tmpfs: GeneratedTmpfs) -> Result<(), GenerationError> {
2572 let items = match &tmpfs {
2573 GeneratedTmpfs::Scalar(item) => std::slice::from_ref(item),
2574 GeneratedTmpfs::List(items) => items.as_slice(),
2575 };
2576 for item in items {
2577 require_generated_string("tmpfs item", item)?;
2578 if item.expose().contains('\r') || item.expose().contains('\n') {
2579 return Err(GenerationError::ContainsLineBreak("tmpfs item"));
2580 }
2581 if !valid_generated_tmpfs_item(item.expose()) {
2582 return Err(GenerationError::InvalidTmpfsItem);
2583 }
2584 }
2585 set_once(&mut self.tmpfs, tmpfs, "tmpfs")
2586 }
2587
2588 #[must_use]
2590 pub const fn tmpfs(&self) -> Option<&GeneratedTmpfs> {
2591 self.tmpfs.as_ref()
2592 }
2593
2594 pub fn set_sysctls(&mut self, sysctls: GeneratedSysctls) -> Result<(), GenerationError> {
2605 let mut seen = BTreeSet::new();
2606 match &sysctls {
2607 GeneratedSysctls::Map(entries) => {
2608 for entry in entries {
2609 if !seen.insert(entry.name()) {
2610 return Err(GenerationError::DuplicateName {
2611 kind: "sysctl",
2612 name: entry.name().to_owned(),
2613 });
2614 }
2615 }
2616 }
2617 GeneratedSysctls::List(items) => {
2618 for item in items {
2619 if item.expose().contains(['\r', '\n', '$']) {
2620 return Err(GenerationError::InvalidSysctlValue);
2621 }
2622 if !seen.insert(item.expose()) {
2623 return Err(GenerationError::DuplicateItem("sysctls"));
2624 }
2625 }
2626 }
2627 }
2628 set_once(&mut self.sysctls, sysctls, "sysctls")
2629 }
2630
2631 #[must_use]
2633 pub const fn sysctls(&self) -> Option<&GeneratedSysctls> {
2634 self.sysctls.as_ref()
2635 }
2636
2637 pub fn set_logging(&mut self, logging: GeneratedLogging) -> Result<(), GenerationError> {
2644 set_once(&mut self.logging, logging, "logging")
2645 }
2646
2647 #[must_use]
2649 pub const fn logging(&self) -> Option<&GeneratedLogging> {
2650 self.logging.as_ref()
2651 }
2652
2653 pub fn set_ulimits(&mut self, ulimits: GeneratedUlimits) -> Result<(), GenerationError> {
2662 set_once(&mut self.ulimits, ulimits, "ulimits")
2663 }
2664
2665 #[must_use]
2667 pub const fn ulimits(&self) -> Option<&GeneratedUlimits> {
2668 self.ulimits.as_ref()
2669 }
2670
2671 pub fn set_pull_policy(&mut self, policy: GeneratedPullPolicy) -> Result<(), GenerationError> {
2678 if let GeneratedPullPolicy::Every(duration) = &policy {
2679 if !valid_pull_policy_duration(duration.expose()) {
2680 return Err(GenerationError::InvalidPullPolicyDuration);
2681 }
2682 }
2683 set_once(&mut self.pull_policy, policy, "pull_policy")
2684 }
2685
2686 pub fn set_restart(&mut self, restart: GeneratedRestartPolicy) -> Result<(), GenerationError> {
2692 set_once(&mut self.restart, restart, "restart")
2693 }
2694
2695 pub fn set_stop_signal(&mut self, signal: GeneratedString) -> Result<(), GenerationError> {
2702 set_once(&mut self.stop_signal, signal, "stop_signal")
2703 }
2704
2705 pub fn set_stop_grace_period(&mut self, period: GeneratedString) -> Result<(), GenerationError> {
2713 if !StopGracePeriod::parse(period.expose().to_owned()).is_valid() {
2714 return Err(GenerationError::InvalidStopGracePeriod);
2715 }
2716 set_once(&mut self.stop_grace_period, period, "stop_grace_period")
2717 }
2718
2719 pub fn add_extra_host(&mut self, host: GeneratedExtraHost) {
2721 self.extra_hosts.push(host);
2722 }
2723
2724 pub fn add_port(&mut self, port: GeneratedPort) {
2726 self.ports.push(port);
2727 }
2728
2729 pub fn add_mount(&mut self, mount: GeneratedMount) {
2731 self.mounts.push(mount);
2732 }
2733
2734 pub fn add_network(&mut self, network: GeneratedNetworkAttachment) -> Result<(), GenerationError> {
2740 if self.networks.iter().any(|candidate| candidate.name == network.name) {
2741 return Err(GenerationError::DuplicateName {
2742 kind: "service network",
2743 name: network.name,
2744 });
2745 }
2746 self.networks.push(network);
2747 Ok(())
2748 }
2749
2750 fn is_sensitive(&self) -> bool {
2751 matches!(
2752 self.hostname.as_ref(),
2753 Some(GeneratedHostname::Resolved(hostname)) if hostname.is_sensitive()
2754 ) || self.image.as_ref().is_some_and(GeneratedString::is_sensitive)
2755 || self.entrypoint.as_ref().is_some_and(entrypoint_is_sensitive)
2756 || self.command.as_ref().is_some_and(command_is_sensitive)
2757 || self
2758 .environment_files
2759 .iter()
2760 .any(GeneratedEnvironmentFile::is_sensitive)
2761 || self
2762 .environment
2763 .iter()
2764 .filter_map(GeneratedEnvironment::value)
2765 .any(GeneratedString::is_sensitive)
2766 || self.labels.iter().any(|label| label.value.is_sensitive())
2767 || self
2768 .annotations
2769 .as_ref()
2770 .is_some_and(|items| items.iter().any(|annotation| annotation.value.is_sensitive()))
2771 || matches!(
2772 self.pull_policy.as_ref(),
2773 Some(GeneratedPullPolicy::Every(duration)) if duration.is_sensitive()
2774 )
2775 || matches!(
2776 self.shm_size.as_ref(),
2777 Some(GeneratedShmSize::Explicit { amount, .. }) if amount.is_sensitive()
2778 )
2779 || matches!(
2780 self.mem_limit.as_ref(),
2781 Some(GeneratedMemLimit::Explicit { amount, .. }) if amount.is_sensitive()
2782 )
2783 || match self.tmpfs.as_ref() {
2784 Some(GeneratedTmpfs::Scalar(item)) => item.is_sensitive(),
2785 Some(GeneratedTmpfs::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2786 None => false,
2787 }
2788 || match self.dns.as_ref() {
2789 Some(GeneratedDns::Scalar(value)) => value.is_sensitive(),
2790 Some(GeneratedDns::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2791 None => false,
2792 }
2793 || self
2794 .dns_options
2795 .as_ref()
2796 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2797 || self
2798 .runtime_fields
2799 .iter()
2800 .any(GeneratedServiceRuntimeField::is_sensitive)
2801 || match self.dns_search.as_ref() {
2802 Some(GeneratedDnsSearch::Scalar(value)) => value.is_sensitive(),
2803 Some(GeneratedDnsSearch::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2804 None => false,
2805 }
2806 || self
2807 .expose
2808 .as_ref()
2809 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2810 || self
2811 .security_options
2812 .as_ref()
2813 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2814 || match self.sysctls.as_ref() {
2815 Some(GeneratedSysctls::Map(entries)) => entries.iter().any(|entry| entry.value.is_sensitive()),
2816 Some(GeneratedSysctls::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2817 None => false,
2818 }
2819 || self.logging.as_ref().is_some_and(GeneratedLogging::is_sensitive)
2820 || self
2821 .ulimits
2822 .as_ref()
2823 .is_some_and(|limits| limits.entries.iter().any(GeneratedUlimit::is_sensitive))
2824 || [
2825 self.user.as_ref(),
2826 self.userns_mode.as_ref(),
2827 self.working_dir.as_ref(),
2828 self.stop_signal.as_ref(),
2829 self.stop_grace_period.as_ref(),
2830 ]
2831 .into_iter()
2832 .flatten()
2833 .any(GeneratedString::is_sensitive)
2834 || self.group_add.iter().any(GeneratedString::is_sensitive)
2835 || self
2836 .cap_add
2837 .as_ref()
2838 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2839 || self
2840 .cap_drop
2841 .as_ref()
2842 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2843 || self
2844 .devices
2845 .as_ref()
2846 .is_some_and(|items| items.iter().any(GeneratedDevice::is_sensitive))
2847 || self.networks.iter().any(GeneratedNetworkAttachment::is_sensitive)
2848 }
2849}
2850
2851#[derive(Clone, Debug, Eq, PartialEq)]
2852enum GeneratedNetwork {
2853 Basic(GeneratedResource),
2854 Definition(GeneratedNetworkDefinition),
2855}
2856
2857#[derive(Clone, Debug, Eq, PartialEq)]
2858enum GeneratedVolume {
2859 Basic(GeneratedResource),
2860 Definition(GeneratedVolumeDefinition),
2861}
2862
2863impl GeneratedVolume {
2864 fn name(&self) -> &str {
2865 match self {
2866 Self::Basic(volume) => volume.name(),
2867 Self::Definition(volume) => volume.name(),
2868 }
2869 }
2870
2871 fn is_sensitive(&self) -> bool {
2872 match self {
2873 Self::Basic(_) => false,
2874 Self::Definition(volume) => volume.is_sensitive(),
2875 }
2876 }
2877}
2878
2879impl GeneratedNetwork {
2880 fn name(&self) -> &str {
2881 match self {
2882 Self::Basic(network) => network.name(),
2883 Self::Definition(network) => network.name(),
2884 }
2885 }
2886
2887 fn is_sensitive(&self) -> bool {
2888 match self {
2889 Self::Basic(_) => false,
2890 Self::Definition(network) => network.is_sensitive(),
2891 }
2892 }
2893}
2894
2895#[derive(Clone, Eq, PartialEq)]
2900pub struct GeneratedConfigFileDefinition {
2901 name: String,
2902 file: GeneratedString,
2903}
2904
2905impl GeneratedConfigFileDefinition {
2906 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2912 Ok(Self {
2913 name: generated_file_resource_name(name.into())?,
2914 file: generated_file_resource_path(file)?,
2915 })
2916 }
2917
2918 #[must_use]
2920 pub fn name(&self) -> &str {
2921 &self.name
2922 }
2923
2924 #[must_use]
2926 pub const fn file(&self) -> &GeneratedString {
2927 &self.file
2928 }
2929
2930 fn is_sensitive(&self) -> bool {
2931 self.file.is_sensitive()
2932 }
2933}
2934
2935impl fmt::Debug for GeneratedConfigFileDefinition {
2936 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2937 formatter
2938 .debug_struct("GeneratedConfigFileDefinition")
2939 .field("name", &self.name)
2940 .field("file", &self.file)
2941 .finish()
2942 }
2943}
2944
2945#[derive(Clone, Eq, PartialEq)]
2950pub struct GeneratedSecretFileDefinition {
2951 name: String,
2952 file: GeneratedString,
2953}
2954
2955impl GeneratedSecretFileDefinition {
2956 pub fn new(name: impl Into<String>, file: GeneratedString) -> Result<Self, GenerationError> {
2962 Ok(Self {
2963 name: generated_file_resource_name(name.into())?,
2964 file: generated_file_resource_path(file)?,
2965 })
2966 }
2967
2968 #[must_use]
2970 pub fn name(&self) -> &str {
2971 &self.name
2972 }
2973
2974 #[must_use]
2976 pub const fn file(&self) -> &GeneratedString {
2977 &self.file
2978 }
2979
2980 fn is_sensitive(&self) -> bool {
2981 self.file.is_sensitive()
2982 }
2983}
2984
2985impl fmt::Debug for GeneratedSecretFileDefinition {
2986 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2987 formatter
2988 .debug_struct("GeneratedSecretFileDefinition")
2989 .field("name", &self.name)
2990 .field("file", &self.file)
2991 .finish()
2992 }
2993}
2994
2995#[derive(Clone, Debug, Default, Eq, PartialEq)]
2997pub struct ComposeDocumentBuilder {
2998 name: Option<String>,
2999 services: Vec<GeneratedService>,
3000 networks: Vec<GeneratedNetwork>,
3001 volumes: Vec<GeneratedVolume>,
3002 configs: Vec<GeneratedConfigFileDefinition>,
3003 secrets: Vec<GeneratedSecretFileDefinition>,
3004}
3005
3006impl ComposeDocumentBuilder {
3007 #[must_use]
3009 pub const fn new() -> Self {
3010 Self {
3011 name: None,
3012 services: Vec::new(),
3013 networks: Vec::new(),
3014 volumes: Vec::new(),
3015 configs: Vec::new(),
3016 secrets: Vec::new(),
3017 }
3018 }
3019
3020 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
3026 let name = required("project name", name.into())?;
3027 set_once(&mut self.name, name, "name")
3028 }
3029
3030 pub fn add_service(&mut self, service: GeneratedService) -> Result<(), GenerationError> {
3036 insert_named(&mut self.services, service, "service", GeneratedService::name)
3037 }
3038
3039 pub fn add_network(&mut self, network: GeneratedResource) -> Result<(), GenerationError> {
3045 insert_named(
3046 &mut self.networks,
3047 GeneratedNetwork::Basic(network),
3048 "network",
3049 GeneratedNetwork::name,
3050 )
3051 }
3052
3053 pub fn add_network_definition(&mut self, network: GeneratedNetworkDefinition) -> Result<(), GenerationError> {
3063 insert_named(
3064 &mut self.networks,
3065 GeneratedNetwork::Definition(network),
3066 "network",
3067 GeneratedNetwork::name,
3068 )
3069 }
3070
3071 pub fn add_volume(&mut self, volume: GeneratedResource) -> Result<(), GenerationError> {
3077 insert_named(
3078 &mut self.volumes,
3079 GeneratedVolume::Basic(volume),
3080 "volume",
3081 GeneratedVolume::name,
3082 )
3083 }
3084
3085 pub fn add_volume_definition(&mut self, volume: GeneratedVolumeDefinition) -> Result<(), GenerationError> {
3096 insert_named(
3097 &mut self.volumes,
3098 GeneratedVolume::Definition(volume),
3099 "volume",
3100 GeneratedVolume::name,
3101 )
3102 }
3103
3104 pub fn add_config_file(&mut self, config: GeneratedConfigFileDefinition) -> Result<(), GenerationError> {
3110 insert_named(&mut self.configs, config, "config", GeneratedConfigFileDefinition::name)
3111 }
3112
3113 pub fn add_secret_file(&mut self, secret: GeneratedSecretFileDefinition) -> Result<(), GenerationError> {
3119 insert_named(&mut self.secrets, secret, "secret", GeneratedSecretFileDefinition::name)
3120 }
3121
3122 pub fn build(self, source_id: SourceId) -> Result<GeneratedComposeDocument, GenerationError> {
3129 if self.services.is_empty() {
3130 return Err(GenerationError::MissingService);
3131 }
3132 let sensitive = self.services.iter().any(GeneratedService::is_sensitive)
3133 || self.networks.iter().any(GeneratedNetwork::is_sensitive)
3134 || self.volumes.iter().any(GeneratedVolume::is_sensitive)
3135 || self.configs.iter().any(GeneratedConfigFileDefinition::is_sensitive)
3136 || self.secrets.iter().any(GeneratedSecretFileDefinition::is_sensitive);
3137 let text = render_document(&self);
3138 let syntax = SyntaxDocument::parse(source_id, text.clone())
3139 .map_err(|_| GenerationError::InternalInvariant("syntax-tree"))?;
3140 if !syntax.is_valid() {
3141 return Err(GenerationError::InternalInvariant("syntax"));
3142 }
3143 let model = ComposeDocument::parse(syntax.document());
3144 if !model.is_valid() {
3145 return Err(GenerationError::InternalInvariant("typed-model"));
3146 }
3147 let document = model
3148 .document()
3149 .cloned()
3150 .ok_or(GenerationError::InternalInvariant("document-root"))?;
3151 Ok(GeneratedComposeDocument {
3152 text,
3153 sensitive,
3154 document,
3155 })
3156 }
3157}
3158
3159#[derive(Clone, Eq, PartialEq)]
3161pub struct GeneratedComposeDocument {
3162 text: String,
3163 sensitive: bool,
3164 document: ComposeDocument,
3165}
3166
3167impl GeneratedComposeDocument {
3168 #[must_use]
3170 pub fn text(&self) -> &str {
3171 &self.text
3172 }
3173
3174 #[must_use]
3176 pub const fn document(&self) -> &ComposeDocument {
3177 &self.document
3178 }
3179
3180 #[must_use]
3182 pub const fn is_sensitive(&self) -> bool {
3183 self.sensitive
3184 }
3185}
3186
3187impl fmt::Debug for GeneratedComposeDocument {
3188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3189 formatter
3190 .debug_struct("GeneratedComposeDocument")
3191 .field("text", &if self.sensitive { "<redacted>" } else { &self.text })
3192 .field("sensitive", &self.sensitive)
3193 .field("document", &if self.sensitive { "<redacted>" } else { "validated" })
3194 .finish()
3195 }
3196}
3197
3198fn render_document(project: &ComposeDocumentBuilder) -> String {
3199 let mut output = String::new();
3200 if let Some(name) = &project.name {
3201 output.push_str("name: ");
3202 write_quoted(&mut output, name);
3203 output.push('\n');
3204 }
3205 output.push_str("services:\n");
3206 for service in &project.services {
3207 write_indent(&mut output, 1);
3208 write_quoted(&mut output, &service.name);
3209 output.push_str(":\n");
3210 render_service(&mut output, service);
3211 }
3212 render_network_definitions(&mut output, &project.networks);
3213 render_volume_definitions(&mut output, &project.volumes);
3214 render_file_definitions(
3215 &mut output,
3216 "configs",
3217 &project.configs,
3218 GeneratedConfigFileDefinition::name,
3219 GeneratedConfigFileDefinition::file,
3220 );
3221 render_file_definitions(
3222 &mut output,
3223 "secrets",
3224 &project.secrets,
3225 GeneratedSecretFileDefinition::name,
3226 GeneratedSecretFileDefinition::file,
3227 );
3228 output
3229}
3230
3231fn render_service(output: &mut String, service: &GeneratedService) {
3232 if let Some(GeneratedHostname::Resolved(hostname)) = &service.hostname {
3233 render_optional_string(output, "hostname", Some(hostname));
3234 }
3235 render_optional_string(output, "container_name", service.container_name.as_ref());
3236 render_optional_string(output, "image", service.image.as_ref());
3237 if let Some(entrypoint) = &service.entrypoint {
3238 render_entrypoint(output, entrypoint);
3239 }
3240 if let Some(command) = &service.command {
3241 render_command(output, command);
3242 }
3243 if let Some(init) = service.init {
3244 write_field(output, 2, "init");
3245 output.push_str(if init { "true\n" } else { "false\n" });
3246 }
3247 if let Some(stdin_open) = service.stdin_open {
3248 write_field(output, 2, "stdin_open");
3249 output.push_str(if stdin_open { "true\n" } else { "false\n" });
3250 }
3251 if let Some(tty) = service.tty {
3252 write_field(output, 2, "tty");
3253 output.push_str(if tty { "true\n" } else { "false\n" });
3254 }
3255 if let Some(privileged) = service.privileged {
3256 write_field(output, 2, "privileged");
3257 output.push_str(if privileged { "true\n" } else { "false\n" });
3258 }
3259 render_environment_files(output, &service.environment_files);
3260 render_environment(output, &service.environment);
3261 render_labels(output, &service.labels);
3262 if let Some(annotations) = &service.annotations {
3263 render_annotations(output, annotations);
3264 }
3265 render_optional_string(output, "user", service.user.as_ref());
3266 render_optional_string(output, "userns_mode", service.userns_mode.as_ref());
3267 render_string_sequence(output, "group_add", &service.group_add);
3268 if let Some(capabilities) = &service.cap_add {
3269 render_configured_string_sequence(output, "cap_add", capabilities);
3270 }
3271 if let Some(capabilities) = &service.cap_drop {
3272 render_configured_string_sequence(output, "cap_drop", capabilities);
3273 }
3274 render_optional_string(output, "working_dir", service.working_dir.as_ref());
3275 if let Some(read_only) = service.read_only {
3276 write_field(output, 2, "read_only");
3277 output.push_str(if read_only { "true\n" } else { "false\n" });
3278 }
3279 if let Some(pids_limit) = &service.pids_limit {
3280 render_pids_limit(output, pids_limit);
3281 }
3282 if let Some(shm_size) = &service.shm_size {
3283 render_shm_size(output, shm_size);
3284 }
3285 if let Some(mem_limit) = &service.mem_limit {
3286 render_mem_limit(output, mem_limit);
3287 }
3288 render_runtime_fields(output, &service.runtime_fields);
3289 if let Some(devices) = &service.devices {
3290 render_devices(output, devices);
3291 }
3292 if let Some(dns) = &service.dns {
3293 render_dns(output, dns);
3294 }
3295 if let Some(options) = &service.dns_options {
3296 render_configured_string_sequence(output, "dns_opt", options);
3297 }
3298 if let Some(search) = &service.dns_search {
3299 render_dns_search(output, search);
3300 }
3301 if let Some(expose) = &service.expose {
3302 render_configured_string_sequence(output, "expose", expose);
3303 }
3304 if let Some(options) = &service.security_options {
3305 render_configured_string_sequence(output, "security_opt", options);
3306 }
3307 if let Some(tmpfs) = &service.tmpfs {
3308 render_tmpfs(output, tmpfs);
3309 }
3310 if let Some(sysctls) = &service.sysctls {
3311 render_sysctls(output, sysctls);
3312 }
3313 if let Some(logging) = &service.logging {
3314 render_logging(output, logging);
3315 }
3316 if let Some(ulimits) = &service.ulimits {
3317 render_ulimits(output, ulimits);
3318 }
3319 if let Some(pull_policy) = &service.pull_policy {
3320 render_pull_policy(output, pull_policy);
3321 }
3322 if let Some(restart) = service.restart {
3323 render_restart(output, restart);
3324 }
3325 render_optional_string(output, "stop_signal", service.stop_signal.as_ref());
3326 render_optional_string(output, "stop_grace_period", service.stop_grace_period.as_ref());
3327 render_extra_hosts(output, &service.extra_hosts);
3328 render_ports(output, &service.ports);
3329 render_mounts(output, &service.mounts);
3330 render_networks(output, &service.networks);
3331}
3332
3333fn render_runtime_fields(output: &mut String, fields: &[GeneratedServiceRuntimeField]) {
3334 for field in fields {
3335 match field {
3336 GeneratedServiceRuntimeField::Domainname(value) => {
3337 render_optional_string(output, "domainname", Some(value));
3338 }
3339 GeneratedServiceRuntimeField::Isolation(value) => {
3340 render_optional_string(output, "isolation", Some(value));
3341 }
3342 GeneratedServiceRuntimeField::MacAddress(value) => {
3343 render_optional_string(output, "mac_address", Some(value));
3344 }
3345 GeneratedServiceRuntimeField::Uts(value) => {
3346 render_optional_string(output, "uts", Some(value));
3347 }
3348 GeneratedServiceRuntimeField::UseApiSocket(value) => {
3349 write_field(output, 2, "use_api_socket");
3350 output.push_str(if *value { "true\n" } else { "false\n" });
3351 }
3352 GeneratedServiceRuntimeField::GpusAll(value) => {
3353 render_optional_string(output, "gpus", Some(value));
3354 }
3355 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => {
3356 write_field(output, 2, "cpu_rt_runtime");
3357 output.push_str(value.expose());
3358 output.push('\n');
3359 }
3360 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3361 render_optional_string(output, "cpu_rt_runtime", Some(value));
3362 }
3363 GeneratedServiceRuntimeField::CpuShares(value) => {
3364 render_optional_string(output, "cpu_shares", Some(value));
3365 }
3366 GeneratedServiceRuntimeField::Cpus(value) => {
3367 render_optional_string(output, "cpus", Some(value));
3368 }
3369 GeneratedServiceRuntimeField::Cpuset(value) => {
3370 render_optional_string(output, "cpuset", Some(value));
3371 }
3372 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => {
3373 render_configured_string_sequence(output, "device_cgroup_rules", values);
3374 }
3375 GeneratedServiceRuntimeField::Ipc(value) => {
3376 render_optional_string(output, "ipc", Some(value));
3377 }
3378 GeneratedServiceRuntimeField::MemReservation(value) => {
3379 render_optional_string(output, "mem_reservation", Some(value));
3380 }
3381 GeneratedServiceRuntimeField::MemSwappiness(value) => {
3382 render_optional_string(output, "mem_swappiness", Some(value));
3383 }
3384 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3385 render_optional_string(output, "memswap_limit", Some(value));
3386 }
3387 GeneratedServiceRuntimeField::NetworkMode(value) => {
3388 render_optional_string(output, "network_mode", Some(value));
3389 }
3390 GeneratedServiceRuntimeField::OomKillDisable(value) => {
3391 write_field(output, 2, "oom_kill_disable");
3392 output.push_str(if *value { "true\n" } else { "false\n" });
3393 }
3394 GeneratedServiceRuntimeField::OomScoreAdj(value) => {
3395 render_optional_string(output, "oom_score_adj", Some(value));
3396 }
3397 GeneratedServiceRuntimeField::Pid(value) => {
3398 render_optional_string(output, "pid", Some(value));
3399 }
3400 GeneratedServiceRuntimeField::Scale(value) => {
3401 render_optional_string(output, "scale", Some(value));
3402 }
3403 GeneratedServiceRuntimeField::VolumesFrom(values) => {
3404 render_configured_string_sequence(output, "volumes_from", values);
3405 }
3406 }
3407 }
3408}
3409
3410fn generated_runtime_field_safe(field: &GeneratedServiceRuntimeField) -> bool {
3411 let safe = |value: &GeneratedString| !value.expose().is_empty() && !value.expose().contains(['\n', '\r', '$']);
3412 let unsigned = |value: &GeneratedString| safe(value) && value.expose().bytes().all(|byte| byte.is_ascii_digit());
3413 let bounded_unsigned = |value: &GeneratedString| unsigned(value) && value.expose().parse::<i128>().is_ok();
3414 let signed_range = |value: &GeneratedString, min: i32, max: i32| {
3415 safe(value)
3416 && value
3417 .expose()
3418 .parse::<i32>()
3419 .is_ok_and(|number| (min..=max).contains(&number))
3420 };
3421 let decimal = |value: &GeneratedString| safe(value) && normalize_generated_decimal(value.expose()).is_some();
3422 let reference = |value: &GeneratedString| {
3423 safe(value)
3424 && (!value.expose().contains(':')
3425 || value
3426 .expose()
3427 .split_once(':')
3428 .is_some_and(|(_, target)| !target.is_empty()))
3429 };
3430 match field {
3431 GeneratedServiceRuntimeField::Domainname(value)
3432 | GeneratedServiceRuntimeField::Isolation(value)
3433 | GeneratedServiceRuntimeField::MacAddress(value)
3434 | GeneratedServiceRuntimeField::Uts(value)
3435 | GeneratedServiceRuntimeField::Cpuset(value) => safe(value),
3436 GeneratedServiceRuntimeField::UseApiSocket(_) | GeneratedServiceRuntimeField::OomKillDisable(_) => true,
3437 GeneratedServiceRuntimeField::GpusAll(value) => safe(value) && value.expose() == "all",
3438 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Microseconds(value)) => unsigned(value),
3439 GeneratedServiceRuntimeField::CpuShares(value) | GeneratedServiceRuntimeField::Scale(value) => {
3440 bounded_unsigned(value)
3441 }
3442 GeneratedServiceRuntimeField::CpuRtRuntime(GeneratedCpuRtRuntime::Duration(value)) => {
3443 safe(value)
3444 && matches!(
3445 CpuRtRuntime::parse_string(value.expose().to_owned()),
3446 CpuRtRuntime::Duration(_)
3447 )
3448 }
3449 GeneratedServiceRuntimeField::Cpus(value) => decimal(value),
3450 GeneratedServiceRuntimeField::DeviceCgroupRules(values) => values.iter().all(safe),
3451 GeneratedServiceRuntimeField::Ipc(value)
3452 | GeneratedServiceRuntimeField::NetworkMode(value)
3453 | GeneratedServiceRuntimeField::Pid(value) => reference(value),
3454 GeneratedServiceRuntimeField::MemReservation(value) => {
3455 safe(value) && valid_generated_runtime_memory(value.expose(), false)
3456 }
3457 GeneratedServiceRuntimeField::MemswapLimit(value) => {
3458 safe(value) && valid_generated_runtime_memory(value.expose(), true)
3459 }
3460 GeneratedServiceRuntimeField::MemSwappiness(value) => signed_range(value, 0, 100),
3461 GeneratedServiceRuntimeField::OomScoreAdj(value) => signed_range(value, -1000, 1000),
3462 GeneratedServiceRuntimeField::VolumesFrom(values) => values.iter().all(reference),
3463 }
3464}
3465
3466fn normalize_generated_decimal(value: &str) -> Option<()> {
3467 let (whole, fraction) = value.split_once('.').map_or((value, ""), |parts| parts);
3468 let valid_shape = if value.contains('.') {
3469 !whole.is_empty() && !fraction.is_empty()
3470 } else {
3471 !whole.is_empty()
3472 };
3473 (valid_shape
3474 && whole.bytes().all(|byte| byte.is_ascii_digit())
3475 && fraction.bytes().all(|byte| byte.is_ascii_digit())
3476 && value.bytes().filter(|byte| *byte == b'.').count() <= 1)
3477 .then_some(())
3478}
3479
3480fn valid_generated_runtime_memory(value: &str, allow_unlimited: bool) -> bool {
3486 if allow_unlimited && value == "-1" {
3487 return true;
3488 }
3489 if !value.is_empty() && value.bytes().all(|byte| byte == b'0') {
3490 return true;
3491 }
3492 let Some(amount) = ["kb", "mb", "gb", "b", "k", "m", "g"]
3493 .into_iter()
3494 .find_map(|unit| value.strip_suffix(unit))
3495 else {
3496 return false;
3497 };
3498 !amount.is_empty() && amount.bytes().all(|byte| byte.is_ascii_digit())
3499}
3500
3501fn render_pids_limit(output: &mut String, limit: &GeneratedPidsLimit) {
3502 write_field(output, 2, "pids_limit");
3503 match limit {
3504 GeneratedPidsLimit::Unlimited => output.push_str("-1\n"),
3505 GeneratedPidsLimit::Finite(decimal) => {
3506 output.push_str(decimal);
3507 output.push('\n');
3508 }
3509 }
3510}
3511
3512fn render_shm_size(output: &mut String, size: &GeneratedShmSize) {
3513 let GeneratedShmSize::Explicit { amount, unit } = size;
3514 write_field(output, 2, "shm_size");
3515 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3516 output.push('\n');
3517}
3518
3519fn render_mem_limit(output: &mut String, limit: &GeneratedMemLimit) {
3520 let GeneratedMemLimit::Explicit { amount, unit } = limit;
3521 write_field(output, 2, "mem_limit");
3522 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3523 output.push('\n');
3524}
3525
3526fn render_devices(output: &mut String, devices: &[GeneratedDevice]) {
3527 if devices.is_empty() {
3528 output.push_str(" devices: []\n");
3529 return;
3530 }
3531 output.push_str(" devices:\n");
3532 for device in devices {
3533 match device {
3534 GeneratedDevice::Short(value) => {
3535 output.push_str(" - ");
3536 write_quoted(output, value.expose());
3537 output.push('\n');
3538 }
3539 GeneratedDevice::Long(value) => {
3540 output.push_str(" - source: ");
3541 write_quoted(output, value.source().expose());
3542 output.push('\n');
3543 if let Some(target) = value.target() {
3544 output.push_str(" target: ");
3545 write_quoted(output, target.expose());
3546 output.push('\n');
3547 }
3548 if let Some(permissions) = value.permissions() {
3549 output.push_str(" permissions: ");
3550 write_quoted(output, permissions.expose());
3551 output.push('\n');
3552 }
3553 }
3554 }
3555 }
3556}
3557
3558fn render_dns(output: &mut String, dns: &GeneratedDns) {
3559 match dns {
3560 GeneratedDns::Scalar(value) => render_optional_string(output, "dns", Some(value)),
3561 GeneratedDns::List(values) => render_configured_string_sequence(output, "dns", values),
3562 }
3563}
3564
3565fn render_dns_search(output: &mut String, search: &GeneratedDnsSearch) {
3566 match search {
3567 GeneratedDnsSearch::Scalar(value) => render_optional_string(output, "dns_search", Some(value)),
3568 GeneratedDnsSearch::List(values) => render_configured_string_sequence(output, "dns_search", values),
3569 }
3570}
3571
3572fn render_tmpfs(output: &mut String, tmpfs: &GeneratedTmpfs) {
3573 match tmpfs {
3574 GeneratedTmpfs::Scalar(item) => render_optional_string(output, "tmpfs", Some(item)),
3575 GeneratedTmpfs::List(items) => render_configured_string_sequence(output, "tmpfs", items),
3576 }
3577}
3578
3579fn render_sysctls(output: &mut String, sysctls: &GeneratedSysctls) {
3580 match sysctls {
3581 GeneratedSysctls::Map(entries) if entries.is_empty() => output.push_str(" sysctls: {}\n"),
3582 GeneratedSysctls::Map(entries) => {
3583 output.push_str(" sysctls:\n");
3584 for entry in entries {
3585 write_indent(output, 3);
3586 write_quoted(output, entry.name());
3587 output.push_str(": ");
3588 write_quoted(output, entry.value().expose());
3589 output.push('\n');
3590 }
3591 }
3592 GeneratedSysctls::List(items) => render_configured_string_sequence(output, "sysctls", items),
3593 }
3594}
3595
3596fn render_logging(output: &mut String, logging: &GeneratedLogging) {
3597 output.push_str(" logging:\n driver: ");
3598 write_quoted(output, logging.driver.expose());
3599 output.push('\n');
3600 if logging.options.is_empty() {
3601 output.push_str(" options: {}\n");
3602 return;
3603 }
3604 output.push_str(" options:\n");
3605 for option in &logging.options {
3606 write_indent(output, 4);
3607 write_quoted(output, option.name());
3608 output.push_str(": ");
3609 match option.value() {
3610 GeneratedLoggingOptionValue::String(value) => write_quoted(output, value.expose()),
3611 GeneratedLoggingOptionValue::Number(value) => output.push_str(value.expose()),
3612 GeneratedLoggingOptionValue::Null => output.push_str("null"),
3613 }
3614 output.push('\n');
3615 }
3616}
3617
3618fn render_ulimits(output: &mut String, ulimits: &GeneratedUlimits) {
3619 if ulimits.entries.is_empty() {
3620 output.push_str(" ulimits: {}\n");
3621 return;
3622 }
3623 output.push_str(" ulimits:\n");
3624 for limit in &ulimits.entries {
3625 write_indent(output, 3);
3626 write_quoted(output, limit.name());
3627 match limit.value() {
3628 GeneratedUlimitValue::Single(value) => {
3629 output.push_str(": ");
3630 write_quoted(output, value.expose());
3631 output.push('\n');
3632 }
3633 GeneratedUlimitValue::Range {
3634 soft: Some(soft),
3635 hard: Some(hard),
3636 } => {
3637 output.push_str(":\n");
3638 write_indent(output, 4);
3639 output.push_str("soft: ");
3640 write_quoted(output, soft.expose());
3641 output.push('\n');
3642 write_indent(output, 4);
3643 output.push_str("hard: ");
3644 write_quoted(output, hard.expose());
3645 output.push('\n');
3646 }
3647 GeneratedUlimitValue::Range { .. } => {
3648 unreachable!("generated ulimit ranges are validated during construction")
3649 }
3650 }
3651 }
3652}
3653
3654fn render_pull_policy(output: &mut String, policy: &GeneratedPullPolicy) {
3655 write_field(output, 2, "pull_policy");
3656 let value = match policy {
3657 GeneratedPullPolicy::Always => "always".to_owned(),
3658 GeneratedPullPolicy::Never => "never".to_owned(),
3659 GeneratedPullPolicy::Missing => "missing".to_owned(),
3660 GeneratedPullPolicy::IfNotPresentAlias => "if_not_present".to_owned(),
3661 GeneratedPullPolicy::Build => "build".to_owned(),
3662 GeneratedPullPolicy::Daily => "daily".to_owned(),
3663 GeneratedPullPolicy::Weekly => "weekly".to_owned(),
3664 GeneratedPullPolicy::Every(duration) => format!("every_{}", duration.expose()),
3665 };
3666 write_quoted(output, &value);
3667 output.push('\n');
3668}
3669
3670fn render_entrypoint(output: &mut String, entrypoint: &GeneratedEntrypoint) {
3671 match entrypoint {
3672 GeneratedEntrypoint::List(arguments) if arguments.is_empty() => output.push_str(" entrypoint: []\n"),
3673 GeneratedEntrypoint::List(arguments) => render_string_sequence(output, "entrypoint", arguments),
3674 GeneratedEntrypoint::String(entrypoint) => render_optional_string(output, "entrypoint", Some(entrypoint)),
3675 GeneratedEntrypoint::Empty => output.push_str(" entrypoint: []\n"),
3676 }
3677}
3678
3679fn render_restart(output: &mut String, restart: GeneratedRestartPolicy) {
3680 write_field(output, 2, "restart");
3681 let value = match restart {
3682 GeneratedRestartPolicy::No => "no".to_owned(),
3683 GeneratedRestartPolicy::Always => "always".to_owned(),
3684 GeneratedRestartPolicy::OnFailure { maximum_retries: None } => "on-failure".to_owned(),
3685 GeneratedRestartPolicy::OnFailure {
3686 maximum_retries: Some(maximum_retries),
3687 } => format!("on-failure:{maximum_retries}"),
3688 GeneratedRestartPolicy::UnlessStopped => "unless-stopped".to_owned(),
3689 };
3690 write_quoted(output, &value);
3691 output.push('\n');
3692}
3693
3694fn render_optional_string(output: &mut String, key: &str, value: Option<&GeneratedString>) {
3695 if let Some(value) = value {
3696 write_field(output, 2, key);
3697 write_quoted(output, value.expose());
3698 output.push('\n');
3699 }
3700}
3701
3702fn render_command(output: &mut String, command: &GeneratedCommand) {
3703 match command {
3704 GeneratedCommand::Exec(arguments) if arguments.is_empty() => output.push_str(" command: []\n"),
3705 GeneratedCommand::Exec(arguments) => render_string_sequence(output, "command", arguments),
3706 GeneratedCommand::Shell(command) => render_optional_string(output, "command", Some(command)),
3707 GeneratedCommand::Empty => output.push_str(" command: []\n"),
3708 }
3709}
3710
3711fn render_environment(output: &mut String, environment: &[GeneratedEnvironment]) {
3712 if environment.is_empty() {
3713 return;
3714 }
3715 output.push_str(" environment:\n");
3716 for variable in environment {
3717 output.push_str(" - ");
3718 let value = variable.value.as_ref().map_or_else(
3719 || variable.name.clone(),
3720 |value| format!("{}={}", variable.name, value.expose()),
3721 );
3722 write_quoted(output, &value);
3723 output.push('\n');
3724 }
3725}
3726
3727fn render_environment_files(output: &mut String, environment_files: &[GeneratedEnvironmentFile]) {
3728 if environment_files.is_empty() {
3729 return;
3730 }
3731 output.push_str(" env_file:\n");
3732 for environment_file in environment_files {
3733 match environment_file {
3734 GeneratedEnvironmentFile::Short(path) => {
3735 output.push_str(" - ");
3736 write_quoted(output, path.expose());
3737 output.push('\n');
3738 }
3739 GeneratedEnvironmentFile::Long { path, required, format } => {
3740 output.push_str(" - path: ");
3741 write_quoted(output, path.expose());
3742 output.push('\n');
3743 if let Some(required) = required {
3744 output.push_str(" required: ");
3745 output.push_str(if *required { "true\n" } else { "false\n" });
3746 }
3747 if let Some(format) = format {
3748 output.push_str(" format: ");
3749 write_quoted(
3750 output,
3751 match format {
3752 GeneratedEnvironmentFileFormat::Raw => "raw",
3753 },
3754 );
3755 output.push('\n');
3756 }
3757 }
3758 }
3759 }
3760}
3761
3762fn render_labels(output: &mut String, labels: &[GeneratedLabel]) {
3763 if labels.is_empty() {
3764 return;
3765 }
3766 output.push_str(" labels:\n");
3767 for label in labels {
3768 output.push_str(" ");
3769 write_quoted(output, &label.name);
3770 output.push_str(": ");
3771 write_quoted(output, label.value.expose());
3772 output.push('\n');
3773 }
3774}
3775
3776fn render_annotations(output: &mut String, annotations: &[GeneratedAnnotation]) {
3777 if annotations.is_empty() {
3778 output.push_str(" annotations: {}\n");
3779 return;
3780 }
3781 output.push_str(" annotations:\n");
3782 for annotation in annotations {
3783 output.push_str(" ");
3784 write_quoted(output, &annotation.name);
3785 output.push_str(": ");
3786 write_quoted(output, annotation.value.expose());
3787 output.push('\n');
3788 }
3789}
3790
3791fn render_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3792 if values.is_empty() {
3793 return;
3794 }
3795 write_indent(output, 2);
3796 output.push_str(key);
3797 output.push_str(":\n");
3798 for value in values {
3799 output.push_str(" - ");
3800 write_quoted(output, value.expose());
3801 output.push('\n');
3802 }
3803}
3804
3805fn render_configured_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3806 if values.is_empty() {
3807 write_indent(output, 2);
3808 output.push_str(key);
3809 output.push_str(": []\n");
3810 } else {
3811 render_string_sequence(output, key, values);
3812 }
3813}
3814
3815fn render_extra_hosts(output: &mut String, hosts: &[GeneratedExtraHost]) {
3816 if hosts.is_empty() {
3817 return;
3818 }
3819 output.push_str(" extra_hosts:\n");
3820 for host in hosts {
3821 output.push_str(" - ");
3822 write_quoted(output, &format!("{}={}", host.hostname, host.address));
3823 output.push('\n');
3824 }
3825}
3826
3827fn render_ports(output: &mut String, ports: &[GeneratedPort]) {
3828 if ports.is_empty() {
3829 return;
3830 }
3831 output.push_str(" ports:\n");
3832 for port in ports {
3833 if port.protocol == GeneratedProtocol::Sctp {
3834 render_short_sctp_port(output, port);
3835 continue;
3836 }
3837 output.push_str(" - target: ");
3838 output.push_str(&port.target.to_string());
3839 output.push('\n');
3840 if let Some(published) = port.published {
3841 output.push_str(" published: ");
3842 write_quoted(output, &published.to_string());
3843 output.push('\n');
3844 }
3845 if let Some(host_ip) = &port.host_ip {
3846 output.push_str(" host_ip: ");
3847 write_quoted(output, host_ip);
3848 output.push('\n');
3849 }
3850 output.push_str(" protocol: ");
3851 write_quoted(output, port.protocol.as_str());
3852 output.push('\n');
3853 }
3854}
3855
3856fn render_short_sctp_port(output: &mut String, port: &GeneratedPort) {
3857 let mut value = String::new();
3858 if let Some(host_ip) = &port.host_ip {
3859 if host_ip.contains(':') && !(host_ip.starts_with('[') && host_ip.ends_with(']')) {
3860 value.push('[');
3861 value.push_str(host_ip);
3862 value.push(']');
3863 } else {
3864 value.push_str(host_ip);
3865 }
3866 value.push(':');
3867 }
3868 if let Some(published) = port.published {
3869 value.push_str(&published.to_string());
3870 value.push(':');
3871 }
3872 value.push_str(&port.target.to_string());
3873 value.push_str("/sctp");
3874
3875 output.push_str(" - ");
3876 write_quoted(output, &value);
3877 output.push('\n');
3878}
3879
3880fn render_mounts(output: &mut String, mounts: &[GeneratedMount]) {
3881 if mounts.is_empty() {
3882 return;
3883 }
3884 output.push_str(" volumes:\n");
3885 for mount in mounts {
3886 match &mount.kind {
3887 GeneratedMountKind::Bind {
3888 source,
3889 selinux: Some(selinux),
3890 } => render_selinux_bind(output, source, mount, *selinux),
3891 kind => render_long_mount(output, kind, mount),
3892 }
3893 }
3894}
3895
3896fn render_selinux_bind(output: &mut String, source: &str, mount: &GeneratedMount, selinux: GeneratedSelinux) {
3897 let mut value = format!("{source}:{}:{}", mount.target, selinux.as_str());
3898 if mount.read_only {
3899 value.push_str(",ro");
3900 }
3901 output.push_str(" - ");
3902 write_quoted(output, &value);
3903 output.push('\n');
3904}
3905
3906fn render_long_mount(output: &mut String, kind: &GeneratedMountKind, mount: &GeneratedMount) {
3907 let (mount_type, source) = match kind {
3908 GeneratedMountKind::Volume { source } => ("volume", Some(source.as_str())),
3909 GeneratedMountKind::Bind { source, selinux: None } => ("bind", Some(source.as_str())),
3910 GeneratedMountKind::Anonymous => ("volume", None),
3911 GeneratedMountKind::Bind { selinux: Some(_), .. } => return,
3912 };
3913 output.push_str(" - type: ");
3914 write_quoted(output, mount_type);
3915 output.push('\n');
3916 if let Some(source) = source {
3917 output.push_str(" source: ");
3918 write_quoted(output, source);
3919 output.push('\n');
3920 }
3921 output.push_str(" target: ");
3922 write_quoted(output, &mount.target);
3923 output.push('\n');
3924 if mount.read_only {
3925 output.push_str(" read_only: true\n");
3926 }
3927}
3928
3929fn render_networks(output: &mut String, networks: &[GeneratedNetworkAttachment]) {
3930 if networks.is_empty() {
3931 return;
3932 }
3933 output.push_str(" networks:\n");
3934 for network in networks {
3935 output.push_str(" ");
3936 write_quoted(output, &network.name);
3937 if network.aliases.is_empty() && network.ipv4_address.is_none() && network.ipv6_address.is_none() {
3938 output.push_str(": {}\n");
3939 continue;
3940 }
3941 output.push_str(":\n");
3942 if !network.aliases.is_empty() {
3943 output.push_str(" aliases:\n");
3944 for alias in &network.aliases {
3945 output.push_str(" - ");
3946 write_quoted(output, alias);
3947 output.push('\n');
3948 }
3949 }
3950 for (field, address) in [
3951 ("ipv4_address", network.ipv4_address.as_ref()),
3952 ("ipv6_address", network.ipv6_address.as_ref()),
3953 ] {
3954 if let Some(address) = address {
3955 output.push_str(" ");
3956 output.push_str(field);
3957 output.push_str(": ");
3958 write_quoted(output, address.expose());
3959 output.push('\n');
3960 }
3961 }
3962 }
3963}
3964
3965fn render_network_definitions(output: &mut String, networks: &[GeneratedNetwork]) {
3966 if networks.is_empty() {
3967 return;
3968 }
3969 output.push_str("networks:\n");
3970 for network in networks {
3971 match network {
3972 GeneratedNetwork::Basic(network) => render_basic_resource(output, network),
3973 GeneratedNetwork::Definition(network) => render_network_definition(output, network),
3974 }
3975 }
3976}
3977
3978fn render_network_definition(output: &mut String, network: &GeneratedNetworkDefinition) {
3979 output.push_str(" ");
3980 write_quoted(output, &network.name);
3981 if network.custom_name.is_none()
3982 && network.driver.is_none()
3983 && network.driver_opts.is_none()
3984 && network.enable_ipv6.is_none()
3985 && network.internal.is_none()
3986 && network.labels.is_none()
3987 {
3988 output.push_str(": {}\n");
3989 return;
3990 }
3991 output.push_str(":\n");
3992 if let Some(custom_name) = &network.custom_name {
3993 output.push_str(" name: ");
3994 write_quoted(output, custom_name);
3995 output.push('\n');
3996 }
3997 if let Some(driver) = &network.driver {
3998 output.push_str(" driver: ");
3999 write_quoted(output, driver.expose());
4000 output.push('\n');
4001 }
4002 if let Some(driver_opts) = &network.driver_opts {
4003 if driver_opts.is_empty() {
4004 output.push_str(" driver_opts: {}\n");
4005 } else {
4006 output.push_str(" driver_opts:\n");
4007 for option in driver_opts {
4008 output.push_str(" ");
4009 write_quoted(output, option.name());
4010 output.push_str(": ");
4011 match option.value() {
4012 GeneratedNetworkDriverOptionValue::String(value) => {
4013 write_quoted(output, value.expose());
4014 }
4015 GeneratedNetworkDriverOptionValue::Number(value) => {
4016 output.push_str(value.expose());
4017 }
4018 }
4019 output.push('\n');
4020 }
4021 }
4022 }
4023 if let Some(enable_ipv6) = network.enable_ipv6 {
4024 output.push_str(" enable_ipv6: ");
4025 output.push_str(if enable_ipv6 { "true\n" } else { "false\n" });
4026 }
4027 if let Some(internal) = network.internal {
4028 output.push_str(" internal: ");
4029 output.push_str(if internal { "true\n" } else { "false\n" });
4030 }
4031 if let Some(labels) = &network.labels {
4032 if labels.is_empty() {
4033 output.push_str(" labels: {}\n");
4034 } else {
4035 output.push_str(" labels:\n");
4036 for label in labels {
4037 output.push_str(" ");
4038 write_quoted(output, label.name());
4039 output.push_str(": ");
4040 write_quoted(output, label.value().expose());
4041 output.push('\n');
4042 }
4043 }
4044 }
4045}
4046
4047fn render_volume_definitions(output: &mut String, volumes: &[GeneratedVolume]) {
4048 if volumes.is_empty() {
4049 return;
4050 }
4051 output.push_str("volumes:\n");
4052 for volume in volumes {
4053 match volume {
4054 GeneratedVolume::Basic(volume) => render_basic_resource(output, volume),
4055 GeneratedVolume::Definition(volume) => render_volume_definition(output, volume),
4056 }
4057 }
4058}
4059
4060fn render_file_definitions<T>(
4061 output: &mut String,
4062 field: &str,
4063 definitions: &[T],
4064 name: impl Fn(&T) -> &str,
4065 file: impl Fn(&T) -> &GeneratedString,
4066) {
4067 if definitions.is_empty() {
4068 return;
4069 }
4070 output.push_str(field);
4071 output.push_str(":\n");
4072 for definition in definitions {
4073 output.push_str(" ");
4074 write_quoted(output, name(definition));
4075 output.push_str(":\n file: ");
4076 write_quoted(output, file(definition).expose());
4077 output.push('\n');
4078 }
4079}
4080
4081fn render_volume_definition(output: &mut String, volume: &GeneratedVolumeDefinition) {
4082 output.push_str(" ");
4083 write_quoted(output, &volume.name);
4084 if volume.custom_name.is_none()
4085 && volume.driver.is_none()
4086 && volume.driver_opts.is_none()
4087 && volume.labels.is_none()
4088 {
4089 output.push_str(": {}\n");
4090 return;
4091 }
4092 output.push_str(":\n");
4093 if let Some(custom_name) = &volume.custom_name {
4094 output.push_str(" name: ");
4095 write_quoted(output, custom_name);
4096 output.push('\n');
4097 }
4098 if let Some(driver) = &volume.driver {
4099 output.push_str(" driver: ");
4100 write_quoted(output, driver.expose());
4101 output.push('\n');
4102 }
4103 if let Some(driver_opts) = &volume.driver_opts {
4104 if driver_opts.is_empty() {
4105 output.push_str(" driver_opts: {}\n");
4106 } else {
4107 output.push_str(" driver_opts:\n");
4108 for option in driver_opts {
4109 output.push_str(" ");
4110 write_quoted(output, option.name());
4111 output.push_str(": ");
4112 match option.value() {
4113 GeneratedVolumeDriverOptionValue::String(value) => write_quoted(output, value.expose()),
4114 GeneratedVolumeDriverOptionValue::Number(value) => output.push_str(value.expose()),
4115 }
4116 output.push('\n');
4117 }
4118 }
4119 }
4120 if let Some(labels) = &volume.labels {
4121 if labels.is_empty() {
4122 output.push_str(" labels: {}\n");
4123 } else {
4124 output.push_str(" labels:\n");
4125 for label in labels {
4126 output.push_str(" ");
4127 write_quoted(output, label.name());
4128 output.push_str(": ");
4129 write_quoted(output, label.value().expose());
4130 output.push('\n');
4131 }
4132 }
4133 }
4134}
4135
4136fn render_basic_resource(output: &mut String, resource: &GeneratedResource) {
4137 output.push_str(" ");
4138 write_quoted(output, &resource.name);
4139 if !resource.external && resource.custom_name.is_none() {
4140 output.push_str(": {}\n");
4141 return;
4142 }
4143 output.push_str(":\n");
4144 if let Some(custom_name) = &resource.custom_name {
4145 output.push_str(" name: ");
4146 write_quoted(output, custom_name);
4147 output.push('\n');
4148 }
4149 if resource.external {
4150 output.push_str(" external: true\n");
4151 }
4152}
4153
4154fn write_field(output: &mut String, depth: usize, key: &str) {
4155 write_indent(output, depth);
4156 output.push_str(key);
4157 output.push_str(": ");
4158}
4159
4160fn write_indent(output: &mut String, depth: usize) {
4161 for _ in 0..depth {
4162 output.push_str(" ");
4163 }
4164}
4165
4166fn required(kind: &'static str, value: String) -> Result<String, GenerationError> {
4167 if value.is_empty() {
4168 return Err(GenerationError::EmptyValue(kind));
4169 }
4170 if value.contains('\0') {
4171 return Err(GenerationError::ContainsNul(kind));
4172 }
4173 Ok(value)
4174}
4175
4176fn require_generated_string(kind: &'static str, value: &GeneratedString) -> Result<(), GenerationError> {
4177 if value.expose().is_empty() {
4178 return Err(GenerationError::EmptyValue(kind));
4179 }
4180 Ok(())
4181}
4182
4183fn generated_file_resource_name(value: String) -> Result<String, GenerationError> {
4184 if value.is_empty() || value.contains(['\0', '\r', '\n', '$']) {
4185 Err(GenerationError::InvalidFileResourceName)
4186 } else {
4187 Ok(value)
4188 }
4189}
4190
4191fn generated_file_resource_path(value: GeneratedString) -> Result<GeneratedString, GenerationError> {
4192 if value.expose().is_empty() || value.expose().contains(['\0', '\r', '\n', '$']) {
4193 Err(GenerationError::InvalidFileResourcePath)
4194 } else {
4195 Ok(value)
4196 }
4197}
4198
4199fn validate_generated_device_member(
4200 member: &'static str,
4201 value: &GeneratedString,
4202 require_non_empty: bool,
4203) -> Result<(), GenerationError> {
4204 if valid_generated_device_string(value.expose(), require_non_empty) {
4205 Ok(())
4206 } else {
4207 Err(GenerationError::InvalidDeviceValue(member))
4208 }
4209}
4210
4211fn validate_generated_ulimit_value(value: &GeneratedString) -> Result<(), GenerationError> {
4212 let value = value.expose();
4213 if value.contains(['\r', '\n', '$'])
4214 || (value != "-1" && (value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit())))
4215 {
4216 return Err(GenerationError::InvalidUlimitValue);
4217 }
4218 Ok(())
4219}
4220
4221fn valid_yaml_number(value: &str) -> bool {
4222 let ordinary = !value.is_empty()
4223 && value.bytes().any(|byte| byte.is_ascii_digit())
4224 && value.bytes().all(|byte| {
4225 byte.is_ascii_digit()
4226 || matches!(
4227 byte,
4228 b'+' | b'-'
4229 | b'.'
4230 | b'_'
4231 | b'e'
4232 | b'E'
4233 | b'x'
4234 | b'X'
4235 | b'o'
4236 | b'O'
4237 | b'a'..=b'f'
4238 | b'A'..=b'F'
4239 )
4240 });
4241 let special = matches!(
4242 value,
4243 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF" | ".nan" | ".NaN" | ".NAN"
4244 );
4245 if !ordinary && !special {
4246 return false;
4247 }
4248 let parse = YamlFile::parse(value);
4249 if !parse.ok() {
4250 return false;
4251 }
4252 let file = parse.tree();
4253 let Some(document) = file.document() else {
4254 return false;
4255 };
4256 let Some(scalar) = document.as_scalar() else {
4257 return false;
4258 };
4259 let position = scalar.byte_range();
4260 position.start == 0
4261 && position.end as usize == value.len()
4262 && matches!(
4263 ScalarValue::from_scalar(&scalar).scalar_type(),
4264 ScalarType::Integer | ScalarType::Float
4265 )
4266}
4267
4268fn environment_name(value: String) -> Result<String, GenerationError> {
4269 let value = required("environment name", value)?;
4270 if value.contains('=') {
4271 return Err(GenerationError::InvalidEnvironmentName);
4272 }
4273 Ok(value)
4274}
4275
4276fn valid_container_name(value: &str) -> bool {
4277 let mut bytes = value.bytes();
4278 bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
4279 && bytes
4280 .next()
4281 .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4282 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
4283}
4284
4285fn short_component(kind: &'static str, value: String, separator: char) -> Result<String, GenerationError> {
4286 let value = required(kind, value)?;
4287 if value.contains(separator) {
4288 return Err(GenerationError::InvalidShortComponent(kind));
4289 }
4290 Ok(value)
4291}
4292
4293fn set_once<T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), GenerationError> {
4294 if slot.is_some() {
4295 return Err(GenerationError::DuplicateField(field));
4296 }
4297 *slot = Some(value);
4298 Ok(())
4299}
4300
4301fn insert_named<T>(
4302 values: &mut Vec<T>,
4303 value: T,
4304 kind: &'static str,
4305 name: impl Fn(&T) -> &str,
4306) -> Result<(), GenerationError> {
4307 let value_name = name(&value);
4308 if values.iter().any(|candidate| name(candidate) == value_name) {
4309 return Err(GenerationError::DuplicateName {
4310 kind,
4311 name: value_name.to_owned(),
4312 });
4313 }
4314 values.push(value);
4315 Ok(())
4316}
4317
4318fn command_is_sensitive(command: &GeneratedCommand) -> bool {
4319 match command {
4320 GeneratedCommand::Exec(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4321 GeneratedCommand::Shell(command) => command.is_sensitive(),
4322 GeneratedCommand::Empty => false,
4323 }
4324}
4325
4326fn entrypoint_is_sensitive(entrypoint: &GeneratedEntrypoint) -> bool {
4327 match entrypoint {
4328 GeneratedEntrypoint::List(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
4329 GeneratedEntrypoint::String(entrypoint) => entrypoint.is_sensitive(),
4330 GeneratedEntrypoint::Empty => false,
4331 }
4332}