1use std::{collections::BTreeSet, error::Error, fmt};
4
5use crate::{
6 model::{
7 ComposeDocument, 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 InvalidTmpfsItem,
59 InvalidDeviceValue(&'static str),
61 InvalidSysctlName,
63 InvalidSysctlValue,
65 InvalidLoggingOptionNumber,
67 InvalidNetworkDriverOptionNumber,
69 InvalidVolumeDriverOptionNumber,
71 InvalidUlimitName,
73 InvalidUlimitValue,
75 MissingUlimitRangeMember(&'static str),
77 InvalidStopGracePeriod,
79 InvalidShortComponent(&'static str),
81 InvalidSelinuxBind,
83 DuplicateField(&'static str),
85 DuplicateName {
87 kind: &'static str,
89 name: String,
91 },
92 DuplicateItem(&'static str),
94 InvalidPort,
96 UnrepresentableSctpHostIp,
98 MissingService,
100 InternalInvariant(&'static str),
102}
103
104impl fmt::Display for GenerationError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::EmptyValue(kind) => write!(formatter, "generated {kind} must not be empty"),
108 Self::ContainsNul(kind) => write!(formatter, "generated {kind} must not contain a NUL byte"),
109 Self::ContainsLineBreak(kind) => {
110 write!(formatter, "generated {kind} must not contain a carriage return or line feed")
111 }
112 Self::InvalidEnvironmentName => formatter.write_str("generated environment name must not contain `=`"),
113 Self::InvalidContainerName => {
114 formatter.write_str("generated container name must match `[a-zA-Z0-9][a-zA-Z0-9_.-]+`")
115 }
116 Self::InvalidHostname => formatter.write_str(
117 "generated hostname must be a resolved ASCII RFC-1123 name with labels of 1 to 63 characters and total length at most 253",
118 ),
119 Self::InvalidPullPolicyDuration => formatter.write_str(
120 "generated pull policy duration must match integer `w`, `d`, `h`, `m`, and `s` components",
121 ),
122 Self::InvalidPidsLimit => {
123 formatter.write_str("generated finite PID limit must be a positive integral decimal")
124 }
125 Self::InvalidShmSize => formatter.write_str(
126 "generated shared-memory size must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
127 ),
128 Self::InvalidMemLimit => formatter.write_str(
129 "generated memory limit must use a canonical positive ASCII-integer amount and an explicit documented lowercase unit",
130 ),
131 Self::InvalidDnsValue => {
132 formatter.write_str("generated DNS server must be a non-empty resolved single-line string")
133 }
134 Self::InvalidDnsOptionValue => {
135 formatter.write_str("generated DNS option must be a non-empty resolved single-line string")
136 }
137 Self::InvalidDnsSearchValue => {
138 formatter.write_str("generated DNS search domain must be a non-empty resolved single-line string")
139 }
140 Self::InvalidExposeValue => formatter.write_str(
141 "generated expose item must be a resolved decimal port or range with an optional `tcp` or `udp` suffix",
142 ),
143 Self::InvalidSecurityOptionValue => {
144 formatter.write_str("generated security option must be a non-empty resolved single-line string")
145 }
146 Self::InvalidAnnotationName => formatter
147 .write_str("generated annotation name must be a non-empty resolved single-line string"),
148 Self::InvalidAnnotationValue => formatter
149 .write_str("generated annotation value must be a resolved single-line string"),
150 Self::InvalidTmpfsItem => formatter.write_str(
151 "generated tmpfs item must be a non-empty path optionally followed by a colon and non-empty comma-separated raw options",
152 ),
153 Self::InvalidDeviceValue(member) => write!(
154 formatter,
155 "generated device {member} must be a safe resolved single-line string{}",
156 if matches!(*member, "short item" | "source") {
157 " and must not be empty"
158 } else {
159 ""
160 }
161 ),
162 Self::InvalidSysctlName => formatter
163 .write_str("generated sysctl name must be a non-empty resolved single-line string"),
164 Self::InvalidSysctlValue => formatter
165 .write_str("generated sysctl value must be a resolved single-line string"),
166 Self::InvalidLoggingOptionNumber => formatter
167 .write_str("generated logging option number must be one complete YAML number scalar"),
168 Self::InvalidNetworkDriverOptionNumber => formatter
169 .write_str("generated network driver option number must be one complete YAML number scalar"),
170 Self::InvalidVolumeDriverOptionNumber => formatter
171 .write_str("generated volume driver option number must be one complete YAML number scalar"),
172 Self::InvalidUlimitName => formatter
173 .write_str("generated ulimit name must match lowercase ASCII `[a-z]+`"),
174 Self::InvalidUlimitValue => formatter
175 .write_str("generated ulimit value must be `-1` or a non-negative ASCII decimal"),
176 Self::MissingUlimitRangeMember(member) => {
177 write!(formatter, "generated ulimit range is missing required `{member}`")
178 }
179 Self::InvalidStopGracePeriod => formatter.write_str(
180 "generated stop grace period must match the ComposeLens duration policy using `us`, `ms`, `s`, `m`, or `h`, or contain an interpolation marker",
181 ),
182 Self::InvalidShortComponent(kind) => {
183 write!(formatter, "generated {kind} contains its reserved short-form separator")
184 }
185 Self::InvalidSelinuxBind => formatter
186 .write_str("generated SELinux bind source and target must not contain the short-syntax `:` separator"),
187 Self::DuplicateField(field) => write!(formatter, "generated field `{field}` was configured more than once"),
188 Self::DuplicateName { kind, name } => {
189 write!(formatter, "generated {kind} `{name}` was added more than once")
190 }
191 Self::DuplicateItem(kind) => write!(formatter, "generated {kind} contains an exact duplicate item"),
192 Self::InvalidPort => formatter.write_str("generated container target port must be greater than zero"),
193 Self::UnrepresentableSctpHostIp => formatter.write_str(
194 "generated SCTP port with a host address also requires a published port for Compose short syntax",
195 ),
196 Self::MissingService => formatter.write_str("generated Compose project requires at least one service"),
197 Self::InternalInvariant(stage) => write!(formatter, "generated Compose document failed {stage} validation"),
198 }
199 }
200}
201
202impl Error for GenerationError {}
203
204#[derive(Clone, Eq, PartialEq)]
206pub struct GeneratedString {
207 value: String,
208 sensitive: bool,
209}
210
211impl GeneratedString {
212 pub fn plain(value: impl Into<String>) -> Result<Self, GenerationError> {
218 Self::new(value.into(), false)
219 }
220
221 pub fn sensitive(value: impl Into<String>) -> Result<Self, GenerationError> {
227 Self::new(value.into(), true)
228 }
229
230 fn new(value: String, sensitive: bool) -> Result<Self, GenerationError> {
231 if value.contains('\0') {
232 return Err(GenerationError::ContainsNul("string"));
233 }
234 Ok(Self { value, sensitive })
235 }
236
237 #[must_use]
239 pub fn expose(&self) -> &str {
240 &self.value
241 }
242
243 #[must_use]
245 pub const fn is_sensitive(&self) -> bool {
246 self.sensitive
247 }
248}
249
250impl fmt::Debug for GeneratedString {
251 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252 formatter
253 .debug_struct("GeneratedString")
254 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
255 .field("sensitive", &self.sensitive)
256 .finish()
257 }
258}
259
260#[derive(Clone, Debug, Eq, PartialEq)]
262#[non_exhaustive]
263pub enum GeneratedCommand {
264 Exec(Vec<GeneratedString>),
266 Shell(GeneratedString),
268 Empty,
270}
271
272#[derive(Clone, Debug, Eq, PartialEq)]
274#[non_exhaustive]
275pub enum GeneratedEntrypoint {
276 List(Vec<GeneratedString>),
278 String(GeneratedString),
280 Empty,
282}
283
284#[derive(Clone, Copy, Debug, Eq, PartialEq)]
286#[non_exhaustive]
287pub enum GeneratedRestartPolicy {
288 No,
290 Always,
292 OnFailure {
294 maximum_retries: Option<u64>,
296 },
297 UnlessStopped,
299}
300
301#[derive(Clone, Debug, Eq, PartialEq)]
303#[non_exhaustive]
304pub enum GeneratedPullPolicy {
305 Always,
307 Never,
309 Missing,
311 IfNotPresentAlias,
313 Build,
315 Daily,
317 Weekly,
319 Every(GeneratedString),
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
325#[non_exhaustive]
326pub enum GeneratedPidsLimit {
327 Unlimited,
329 Finite(String),
331}
332
333#[derive(Clone, Debug, Eq, PartialEq)]
335#[non_exhaustive]
336pub enum GeneratedShmSize {
337 Explicit {
339 amount: GeneratedString,
341 unit: ShmSizeUnit,
343 },
344}
345
346#[derive(Clone, Debug, Eq, PartialEq)]
348#[non_exhaustive]
349pub enum GeneratedMemLimit {
350 Explicit {
352 amount: GeneratedString,
354 unit: MemLimitUnit,
356 },
357}
358
359#[derive(Clone, Debug, Eq, PartialEq)]
361#[non_exhaustive]
362pub enum GeneratedTmpfs {
363 Scalar(GeneratedString),
365 List(Vec<GeneratedString>),
367}
368
369#[derive(Clone, Debug, Eq, PartialEq)]
371#[non_exhaustive]
372pub enum GeneratedDns {
373 Scalar(GeneratedString),
375 List(Vec<GeneratedString>),
377}
378
379#[derive(Clone, Debug, Eq, PartialEq)]
381#[non_exhaustive]
382pub enum GeneratedDnsSearch {
383 Scalar(GeneratedString),
385 List(Vec<GeneratedString>),
387}
388
389#[derive(Clone, Debug, Eq, PartialEq)]
391pub struct GeneratedLongDevice {
392 source: GeneratedString,
393 target: Option<GeneratedString>,
394 permissions: Option<GeneratedString>,
395}
396
397impl GeneratedLongDevice {
398 pub fn new(
406 source: GeneratedString,
407 target: Option<GeneratedString>,
408 permissions: Option<GeneratedString>,
409 ) -> Result<Self, GenerationError> {
410 validate_generated_device_member("source", &source, true)?;
411 if let Some(target) = &target {
412 validate_generated_device_member("target", target, false)?;
413 }
414 if let Some(permissions) = &permissions {
415 validate_generated_device_member("permissions", permissions, false)?;
416 }
417 Ok(Self {
418 source,
419 target,
420 permissions,
421 })
422 }
423
424 #[must_use]
426 pub const fn source(&self) -> &GeneratedString {
427 &self.source
428 }
429
430 #[must_use]
432 pub const fn target(&self) -> Option<&GeneratedString> {
433 self.target.as_ref()
434 }
435
436 #[must_use]
438 pub const fn permissions(&self) -> Option<&GeneratedString> {
439 self.permissions.as_ref()
440 }
441
442 fn is_sensitive(&self) -> bool {
443 self.source.is_sensitive()
444 || self.target.as_ref().is_some_and(GeneratedString::is_sensitive)
445 || self.permissions.as_ref().is_some_and(GeneratedString::is_sensitive)
446 }
447}
448
449#[derive(Clone, Debug, Eq, PartialEq)]
451#[non_exhaustive]
452pub enum GeneratedDevice {
453 Short(GeneratedString),
455 Long(GeneratedLongDevice),
457}
458
459#[derive(Clone, Debug, Eq, PartialEq)]
461#[non_exhaustive]
462pub enum GeneratedLoggingOptionValue {
463 String(GeneratedString),
465 Number(GeneratedString),
467 Null,
469}
470
471impl GeneratedLoggingOptionValue {
472 fn is_sensitive(&self) -> bool {
473 match self {
474 Self::String(value) | Self::Number(value) => value.is_sensitive(),
475 Self::Null => false,
476 }
477 }
478}
479
480#[derive(Clone, Debug, Eq, PartialEq)]
482pub struct GeneratedLoggingOption {
483 name: String,
484 value: GeneratedLoggingOptionValue,
485}
486
487impl GeneratedLoggingOption {
488 pub fn new(name: impl Into<String>, value: GeneratedLoggingOptionValue) -> Result<Self, GenerationError> {
495 let name = required("logging option key", name.into())?;
496 if let GeneratedLoggingOptionValue::Number(number) = &value {
497 if !valid_yaml_number(number.expose()) {
498 return Err(GenerationError::InvalidLoggingOptionNumber);
499 }
500 }
501 Ok(Self { name, value })
502 }
503
504 #[must_use]
506 pub fn name(&self) -> &str {
507 &self.name
508 }
509
510 #[must_use]
512 pub const fn value(&self) -> &GeneratedLoggingOptionValue {
513 &self.value
514 }
515}
516
517#[derive(Clone, Debug, Eq, PartialEq)]
519pub struct GeneratedLogging {
520 driver: GeneratedString,
521 options: Vec<GeneratedLoggingOption>,
522}
523
524impl GeneratedLogging {
525 pub fn new(driver: GeneratedString, options: Vec<GeneratedLoggingOption>) -> Result<Self, GenerationError> {
534 let mut seen = BTreeSet::new();
535 for option in &options {
536 if !seen.insert(option.name()) {
537 return Err(GenerationError::DuplicateName {
538 kind: "logging option",
539 name: option.name().to_owned(),
540 });
541 }
542 }
543 Ok(Self { driver, options })
544 }
545
546 #[must_use]
548 pub const fn driver(&self) -> &GeneratedString {
549 &self.driver
550 }
551
552 #[must_use]
554 pub fn options(&self) -> &[GeneratedLoggingOption] {
555 &self.options
556 }
557
558 fn is_sensitive(&self) -> bool {
559 self.driver.is_sensitive() || self.options.iter().any(|option| option.value.is_sensitive())
560 }
561}
562
563#[derive(Clone, Debug, Eq, PartialEq)]
565#[non_exhaustive]
566pub enum GeneratedNetworkDriverOptionValue {
567 String(GeneratedString),
569 Number(GeneratedString),
571}
572
573impl GeneratedNetworkDriverOptionValue {
574 fn is_sensitive(&self) -> bool {
575 match self {
576 Self::String(value) | Self::Number(value) => value.is_sensitive(),
577 }
578 }
579}
580
581#[derive(Clone, Debug, Eq, PartialEq)]
583pub struct GeneratedNetworkDriverOption {
584 name: String,
585 value: GeneratedNetworkDriverOptionValue,
586}
587
588impl GeneratedNetworkDriverOption {
589 pub fn new(name: impl Into<String>, value: GeneratedNetworkDriverOptionValue) -> Result<Self, GenerationError> {
596 let name = required("network driver option key", name.into())?;
597 if let GeneratedNetworkDriverOptionValue::Number(number) = &value {
598 if !valid_yaml_number(number.expose()) {
599 return Err(GenerationError::InvalidNetworkDriverOptionNumber);
600 }
601 }
602 Ok(Self { name, value })
603 }
604
605 #[must_use]
607 pub fn name(&self) -> &str {
608 &self.name
609 }
610
611 #[must_use]
613 pub const fn value(&self) -> &GeneratedNetworkDriverOptionValue {
614 &self.value
615 }
616}
617
618#[derive(Clone, Debug, Eq, PartialEq)]
620#[non_exhaustive]
621pub enum GeneratedVolumeDriverOptionValue {
622 String(GeneratedString),
624 Number(GeneratedString),
626}
627
628impl GeneratedVolumeDriverOptionValue {
629 fn is_sensitive(&self) -> bool {
630 match self {
631 Self::String(value) | Self::Number(value) => value.is_sensitive(),
632 }
633 }
634}
635
636#[derive(Clone, Debug, Eq, PartialEq)]
638pub struct GeneratedVolumeDriverOption {
639 name: String,
640 value: GeneratedVolumeDriverOptionValue,
641}
642
643impl GeneratedVolumeDriverOption {
644 pub fn new(name: impl Into<String>, value: GeneratedVolumeDriverOptionValue) -> Result<Self, GenerationError> {
651 let name = required("volume driver option key", name.into())?;
652 if let GeneratedVolumeDriverOptionValue::Number(number) = &value {
653 if !valid_yaml_number(number.expose()) {
654 return Err(GenerationError::InvalidVolumeDriverOptionNumber);
655 }
656 }
657 Ok(Self { name, value })
658 }
659
660 #[must_use]
662 pub fn name(&self) -> &str {
663 &self.name
664 }
665
666 #[must_use]
668 pub const fn value(&self) -> &GeneratedVolumeDriverOptionValue {
669 &self.value
670 }
671}
672
673#[derive(Clone, Debug, Eq, PartialEq)]
679pub struct GeneratedVolumeDefinition {
680 name: String,
681 custom_name: Option<String>,
682 driver: Option<GeneratedString>,
683 driver_opts: Option<Vec<GeneratedVolumeDriverOption>>,
684 labels: Option<Vec<GeneratedLabel>>,
685}
686
687impl GeneratedVolumeDefinition {
688 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
694 Ok(Self {
695 name: required("volume name", name.into())?,
696 custom_name: None,
697 driver: None,
698 driver_opts: None,
699 labels: None,
700 })
701 }
702
703 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
709 let name = required("custom volume name", name.into())?;
710 set_once(&mut self.custom_name, name, "volume name")
711 }
712
713 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
721 set_once(&mut self.driver, driver, "volume driver")
722 }
723
724 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedVolumeDriverOption>) -> Result<(), GenerationError> {
734 let mut seen = BTreeSet::new();
735 for option in &driver_opts {
736 if !seen.insert(option.name()) {
737 return Err(GenerationError::DuplicateName {
738 kind: "volume driver option",
739 name: option.name().to_owned(),
740 });
741 }
742 }
743 set_once(&mut self.driver_opts, driver_opts, "volume driver_opts")
744 }
745
746 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
756 let mut seen = BTreeSet::new();
757 for label in &labels {
758 if !seen.insert(label.name()) {
759 return Err(GenerationError::DuplicateName {
760 kind: "volume label",
761 name: label.name().to_owned(),
762 });
763 }
764 }
765 set_once(&mut self.labels, labels, "volume labels")
766 }
767
768 #[must_use]
770 pub fn name(&self) -> &str {
771 &self.name
772 }
773
774 #[must_use]
776 pub fn custom_name(&self) -> Option<&str> {
777 self.custom_name.as_deref()
778 }
779
780 #[must_use]
782 pub const fn driver(&self) -> Option<&GeneratedString> {
783 self.driver.as_ref()
784 }
785
786 #[must_use]
788 pub fn driver_opts(&self) -> Option<&[GeneratedVolumeDriverOption]> {
789 self.driver_opts.as_deref()
790 }
791
792 #[must_use]
794 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
795 self.labels.as_deref()
796 }
797
798 fn is_sensitive(&self) -> bool {
799 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
800 || self
801 .driver_opts
802 .as_ref()
803 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
804 || self
805 .labels
806 .as_ref()
807 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
808 }
809}
810
811#[derive(Clone, Debug, Eq, PartialEq)]
816pub struct GeneratedNetworkDefinition {
817 name: String,
818 custom_name: Option<String>,
819 driver: Option<GeneratedString>,
820 driver_opts: Option<Vec<GeneratedNetworkDriverOption>>,
821 enable_ipv6: Option<bool>,
822 internal: Option<bool>,
823 labels: Option<Vec<GeneratedLabel>>,
824}
825
826impl GeneratedNetworkDefinition {
827 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
833 Ok(Self {
834 name: required("network name", name.into())?,
835 custom_name: None,
836 driver: None,
837 driver_opts: None,
838 enable_ipv6: None,
839 internal: None,
840 labels: None,
841 })
842 }
843
844 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
850 let name = required("custom network name", name.into())?;
851 set_once(&mut self.custom_name, name, "network name")
852 }
853
854 pub fn set_driver(&mut self, driver: GeneratedString) -> Result<(), GenerationError> {
862 set_once(&mut self.driver, driver, "network driver")
863 }
864
865 pub fn set_driver_opts(&mut self, driver_opts: Vec<GeneratedNetworkDriverOption>) -> Result<(), GenerationError> {
875 let mut seen = BTreeSet::new();
876 for option in &driver_opts {
877 if !seen.insert(option.name()) {
878 return Err(GenerationError::DuplicateName {
879 kind: "network driver option",
880 name: option.name().to_owned(),
881 });
882 }
883 }
884 set_once(&mut self.driver_opts, driver_opts, "network driver_opts")
885 }
886
887 pub fn set_enable_ipv6(&mut self, enable_ipv6: bool) -> Result<(), GenerationError> {
896 set_once(&mut self.enable_ipv6, enable_ipv6, "network enable_ipv6")
897 }
898
899 pub fn set_internal(&mut self, internal: bool) -> Result<(), GenerationError> {
908 set_once(&mut self.internal, internal, "network internal")
909 }
910
911 pub fn set_labels(&mut self, labels: Vec<GeneratedLabel>) -> Result<(), GenerationError> {
921 let mut seen = BTreeSet::new();
922 for label in &labels {
923 if !seen.insert(label.name()) {
924 return Err(GenerationError::DuplicateName {
925 kind: "network label",
926 name: label.name().to_owned(),
927 });
928 }
929 }
930 set_once(&mut self.labels, labels, "network labels")
931 }
932
933 #[must_use]
935 pub fn name(&self) -> &str {
936 &self.name
937 }
938
939 #[must_use]
941 pub fn custom_name(&self) -> Option<&str> {
942 self.custom_name.as_deref()
943 }
944
945 #[must_use]
947 pub const fn driver(&self) -> Option<&GeneratedString> {
948 self.driver.as_ref()
949 }
950
951 #[must_use]
953 pub fn driver_opts(&self) -> Option<&[GeneratedNetworkDriverOption]> {
954 self.driver_opts.as_deref()
955 }
956
957 #[must_use]
959 pub const fn enable_ipv6(&self) -> Option<bool> {
960 self.enable_ipv6
961 }
962
963 #[must_use]
965 pub const fn internal(&self) -> Option<bool> {
966 self.internal
967 }
968
969 #[must_use]
971 pub fn labels(&self) -> Option<&[GeneratedLabel]> {
972 self.labels.as_deref()
973 }
974
975 fn is_sensitive(&self) -> bool {
976 self.driver.as_ref().is_some_and(GeneratedString::is_sensitive)
977 || self
978 .driver_opts
979 .as_ref()
980 .is_some_and(|options| options.iter().any(|option| option.value.is_sensitive()))
981 || self
982 .labels
983 .as_ref()
984 .is_some_and(|labels| labels.iter().any(|label| label.value.is_sensitive()))
985 }
986}
987
988impl GeneratedDevice {
989 fn is_sensitive(&self) -> bool {
990 match self {
991 Self::Short(value) => value.is_sensitive(),
992 Self::Long(value) => value.is_sensitive(),
993 }
994 }
995}
996
997#[derive(Clone, Debug, Eq, PartialEq)]
999pub struct GeneratedSysctl {
1000 name: String,
1001 value: GeneratedString,
1002}
1003
1004impl GeneratedSysctl {
1005 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1013 let name = name.into();
1014 if name.is_empty()
1015 || name.contains(['\0', '\r', '\n'])
1016 || name.contains('$')
1017 || value.expose().contains(['\r', '\n', '$'])
1018 {
1019 return Err(if name.is_empty() || name.contains(['\0', '\r', '\n', '$']) {
1020 GenerationError::InvalidSysctlName
1021 } else {
1022 GenerationError::InvalidSysctlValue
1023 });
1024 }
1025 Ok(Self { name, value })
1026 }
1027
1028 #[must_use]
1030 pub fn name(&self) -> &str {
1031 &self.name
1032 }
1033
1034 #[must_use]
1036 pub const fn value(&self) -> &GeneratedString {
1037 &self.value
1038 }
1039}
1040
1041#[derive(Clone, Debug, Eq, PartialEq)]
1043#[non_exhaustive]
1044pub enum GeneratedSysctls {
1045 Map(Vec<GeneratedSysctl>),
1047 List(Vec<GeneratedString>),
1049}
1050
1051#[derive(Clone, Debug, Eq, PartialEq)]
1053#[non_exhaustive]
1054pub enum GeneratedUlimitValue {
1055 Single(GeneratedString),
1057 Range {
1059 soft: Option<GeneratedString>,
1061 hard: Option<GeneratedString>,
1063 },
1064}
1065
1066#[derive(Clone, Debug, Eq, PartialEq)]
1068pub struct GeneratedUlimit {
1069 name: String,
1070 value: GeneratedUlimitValue,
1071}
1072
1073impl GeneratedUlimit {
1074 pub fn new(name: impl Into<String>, value: GeneratedUlimitValue) -> Result<Self, GenerationError> {
1081 let name = name.into();
1082 if !valid_ulimit_name(&name) {
1083 return Err(GenerationError::InvalidUlimitName);
1084 }
1085 match &value {
1086 GeneratedUlimitValue::Single(value) => validate_generated_ulimit_value(value)?,
1087 GeneratedUlimitValue::Range { soft, hard } => {
1088 let soft = soft.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("soft"))?;
1089 let hard = hard.as_ref().ok_or(GenerationError::MissingUlimitRangeMember("hard"))?;
1090 validate_generated_ulimit_value(soft)?;
1091 validate_generated_ulimit_value(hard)?;
1092 }
1093 }
1094 Ok(Self { name, value })
1095 }
1096
1097 pub fn single(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1103 Self::new(name, GeneratedUlimitValue::Single(value))
1104 }
1105
1106 pub fn range(
1112 name: impl Into<String>,
1113 soft: GeneratedString,
1114 hard: GeneratedString,
1115 ) -> Result<Self, GenerationError> {
1116 Self::new(
1117 name,
1118 GeneratedUlimitValue::Range {
1119 soft: Some(soft),
1120 hard: Some(hard),
1121 },
1122 )
1123 }
1124
1125 #[must_use]
1127 pub fn name(&self) -> &str {
1128 &self.name
1129 }
1130
1131 #[must_use]
1133 pub const fn value(&self) -> &GeneratedUlimitValue {
1134 &self.value
1135 }
1136
1137 fn is_sensitive(&self) -> bool {
1138 match &self.value {
1139 GeneratedUlimitValue::Single(value) => value.is_sensitive(),
1140 GeneratedUlimitValue::Range { soft, hard } => {
1141 soft.iter().chain(hard.iter()).any(GeneratedString::is_sensitive)
1142 }
1143 }
1144 }
1145}
1146
1147#[derive(Clone, Debug, Eq, PartialEq)]
1149pub struct GeneratedUlimits {
1150 entries: Vec<GeneratedUlimit>,
1151}
1152
1153impl GeneratedUlimits {
1154 pub fn new(entries: Vec<GeneratedUlimit>) -> Result<Self, GenerationError> {
1160 let mut seen = BTreeSet::new();
1161 for entry in &entries {
1162 if !seen.insert(entry.name()) {
1163 return Err(GenerationError::DuplicateName {
1164 kind: "ulimit",
1165 name: entry.name().to_owned(),
1166 });
1167 }
1168 }
1169 Ok(Self { entries })
1170 }
1171
1172 #[must_use]
1174 pub fn entries(&self) -> &[GeneratedUlimit] {
1175 &self.entries
1176 }
1177
1178 #[must_use]
1180 pub fn is_empty(&self) -> bool {
1181 self.entries.is_empty()
1182 }
1183}
1184
1185#[derive(Clone, Debug, Eq, PartialEq)]
1187#[non_exhaustive]
1188pub enum GeneratedHostname {
1189 Resolved(GeneratedString),
1191}
1192
1193#[derive(Clone, Debug, Eq, PartialEq)]
1195pub struct GeneratedEnvironment {
1196 name: String,
1197 value: Option<GeneratedString>,
1198}
1199
1200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1202#[non_exhaustive]
1203pub enum GeneratedEnvironmentFileFormat {
1204 Raw,
1206}
1207
1208#[derive(Clone, Debug, Eq, PartialEq)]
1210#[non_exhaustive]
1211pub enum GeneratedEnvironmentFile {
1212 Short(GeneratedString),
1214 Long {
1216 path: GeneratedString,
1218 required: Option<bool>,
1220 format: Option<GeneratedEnvironmentFileFormat>,
1222 },
1223}
1224
1225#[derive(Clone, Debug, Eq, PartialEq)]
1227pub struct GeneratedLabel {
1228 name: String,
1229 value: GeneratedString,
1230}
1231
1232#[derive(Clone, Debug, Eq, PartialEq)]
1234pub struct GeneratedAnnotation {
1235 name: String,
1236 value: GeneratedString,
1237}
1238
1239impl GeneratedAnnotation {
1240 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1247 let name = name.into();
1248 if name.is_empty() || name.contains(['$', '\r', '\n', '\0']) {
1249 return Err(GenerationError::InvalidAnnotationName);
1250 }
1251 if value.expose().contains(['$', '\r', '\n', '\0']) {
1252 return Err(GenerationError::InvalidAnnotationValue);
1253 }
1254 Ok(Self { name, value })
1255 }
1256
1257 #[must_use]
1259 pub fn name(&self) -> &str {
1260 &self.name
1261 }
1262
1263 #[must_use]
1265 pub const fn value(&self) -> &GeneratedString {
1266 &self.value
1267 }
1268}
1269
1270impl GeneratedLabel {
1271 pub fn new(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1278 Ok(Self {
1279 name: required("label name", name.into())?,
1280 value,
1281 })
1282 }
1283
1284 #[must_use]
1286 pub fn name(&self) -> &str {
1287 &self.name
1288 }
1289
1290 #[must_use]
1292 pub const fn value(&self) -> &GeneratedString {
1293 &self.value
1294 }
1295}
1296
1297impl GeneratedEnvironment {
1298 pub fn literal(name: impl Into<String>, value: GeneratedString) -> Result<Self, GenerationError> {
1304 Ok(Self {
1305 name: environment_name(name.into())?,
1306 value: Some(value),
1307 })
1308 }
1309
1310 pub fn host(name: impl Into<String>) -> Result<Self, GenerationError> {
1316 Ok(Self {
1317 name: environment_name(name.into())?,
1318 value: None,
1319 })
1320 }
1321
1322 #[must_use]
1324 pub fn name(&self) -> &str {
1325 &self.name
1326 }
1327
1328 #[must_use]
1330 pub const fn value(&self) -> Option<&GeneratedString> {
1331 self.value.as_ref()
1332 }
1333}
1334
1335impl GeneratedEnvironmentFile {
1336 pub fn short(path: GeneratedString) -> Result<Self, GenerationError> {
1343 require_generated_string("environment-file path", &path)?;
1344 Ok(Self::Short(path))
1345 }
1346
1347 pub fn long(
1354 path: GeneratedString,
1355 required: Option<bool>,
1356 format: Option<GeneratedEnvironmentFileFormat>,
1357 ) -> Result<Self, GenerationError> {
1358 require_generated_string("environment-file path", &path)?;
1359 Ok(Self::Long { path, required, format })
1360 }
1361
1362 #[must_use]
1364 pub const fn path(&self) -> &GeneratedString {
1365 match self {
1366 Self::Short(path) | Self::Long { path, .. } => path,
1367 }
1368 }
1369
1370 #[must_use]
1372 pub const fn required(&self) -> Option<bool> {
1373 match self {
1374 Self::Short(_) => None,
1375 Self::Long { required, .. } => *required,
1376 }
1377 }
1378
1379 #[must_use]
1381 pub const fn format(&self) -> Option<GeneratedEnvironmentFileFormat> {
1382 match self {
1383 Self::Short(_) => None,
1384 Self::Long { format, .. } => *format,
1385 }
1386 }
1387
1388 #[must_use]
1390 pub const fn is_sensitive(&self) -> bool {
1391 self.path().is_sensitive()
1392 }
1393}
1394
1395#[derive(Clone, Debug, Eq, PartialEq)]
1397pub struct GeneratedExtraHost {
1398 hostname: String,
1399 address: String,
1400}
1401
1402impl GeneratedExtraHost {
1403 pub fn new(hostname: impl Into<String>, address: impl Into<String>) -> Result<Self, GenerationError> {
1409 let hostname = short_component("extra-host hostname", hostname.into(), '=')?;
1410 let address = short_component("extra-host address", address.into(), '=')?;
1411 Ok(Self { hostname, address })
1412 }
1413
1414 #[must_use]
1416 pub fn hostname(&self) -> &str {
1417 &self.hostname
1418 }
1419
1420 #[must_use]
1422 pub fn address(&self) -> &str {
1423 &self.address
1424 }
1425}
1426
1427#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1429#[non_exhaustive]
1430pub enum GeneratedProtocol {
1431 Tcp,
1433 Udp,
1435 Sctp,
1437}
1438
1439impl GeneratedProtocol {
1440 const fn as_str(self) -> &'static str {
1441 match self {
1442 Self::Tcp => "tcp",
1443 Self::Udp => "udp",
1444 Self::Sctp => "sctp",
1445 }
1446 }
1447}
1448
1449#[derive(Clone, Debug, Eq, PartialEq)]
1451pub struct GeneratedPort {
1452 target: u16,
1453 published: Option<u16>,
1454 host_ip: Option<String>,
1455 protocol: GeneratedProtocol,
1456}
1457
1458impl GeneratedPort {
1459 pub fn new(
1467 target: u16,
1468 published: Option<u16>,
1469 host_ip: Option<String>,
1470 protocol: GeneratedProtocol,
1471 ) -> Result<Self, GenerationError> {
1472 if target == 0 {
1473 return Err(GenerationError::InvalidPort);
1474 }
1475 if let Some(host_ip) = host_ip.as_deref() {
1476 required("port host address", host_ip.to_owned())?;
1477 if protocol == GeneratedProtocol::Sctp && published.is_none() {
1478 return Err(GenerationError::UnrepresentableSctpHostIp);
1479 }
1480 }
1481 Ok(Self {
1482 target,
1483 published,
1484 host_ip,
1485 protocol,
1486 })
1487 }
1488
1489 #[must_use]
1491 pub const fn target(&self) -> u16 {
1492 self.target
1493 }
1494
1495 #[must_use]
1497 pub const fn published(&self) -> Option<u16> {
1498 self.published
1499 }
1500
1501 #[must_use]
1503 pub fn host_ip(&self) -> Option<&str> {
1504 self.host_ip.as_deref()
1505 }
1506
1507 #[must_use]
1509 pub const fn protocol(&self) -> GeneratedProtocol {
1510 self.protocol
1511 }
1512}
1513
1514#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1516#[non_exhaustive]
1517pub enum GeneratedSelinux {
1518 Private,
1520 Shared,
1522}
1523
1524impl GeneratedSelinux {
1525 const fn as_str(self) -> &'static str {
1526 match self {
1527 Self::Private => "Z",
1528 Self::Shared => "z",
1529 }
1530 }
1531}
1532
1533#[derive(Clone, Debug, Eq, PartialEq)]
1534enum GeneratedMountKind {
1535 Volume {
1536 source: String,
1537 },
1538 Bind {
1539 source: String,
1540 selinux: Option<GeneratedSelinux>,
1541 },
1542 Anonymous,
1543}
1544
1545#[derive(Clone, Debug, Eq, PartialEq)]
1547pub struct GeneratedMount {
1548 kind: GeneratedMountKind,
1549 target: String,
1550 read_only: bool,
1551}
1552
1553impl GeneratedMount {
1554 pub fn volume(
1560 source: impl Into<String>,
1561 target: impl Into<String>,
1562 read_only: bool,
1563 ) -> Result<Self, GenerationError> {
1564 Ok(Self {
1565 kind: GeneratedMountKind::Volume {
1566 source: required("volume source", source.into())?,
1567 },
1568 target: required("mount target", target.into())?,
1569 read_only,
1570 })
1571 }
1572
1573 pub fn bind(
1580 source: impl Into<String>,
1581 target: impl Into<String>,
1582 read_only: bool,
1583 selinux: Option<GeneratedSelinux>,
1584 ) -> Result<Self, GenerationError> {
1585 let source = required("bind source", source.into())?;
1586 let target = required("mount target", target.into())?;
1587 if selinux.is_some() && (source.contains(':') || target.contains(':')) {
1588 return Err(GenerationError::InvalidSelinuxBind);
1589 }
1590 Ok(Self {
1591 kind: GeneratedMountKind::Bind { source, selinux },
1592 target,
1593 read_only,
1594 })
1595 }
1596
1597 pub fn anonymous(target: impl Into<String>, read_only: bool) -> Result<Self, GenerationError> {
1603 Ok(Self {
1604 kind: GeneratedMountKind::Anonymous,
1605 target: required("mount target", target.into())?,
1606 read_only,
1607 })
1608 }
1609
1610 #[must_use]
1612 pub fn target(&self) -> &str {
1613 &self.target
1614 }
1615
1616 #[must_use]
1618 pub const fn read_only(&self) -> bool {
1619 self.read_only
1620 }
1621}
1622
1623#[derive(Clone, Debug, Eq, PartialEq)]
1625pub struct GeneratedNetworkAttachment {
1626 name: String,
1627 aliases: Vec<String>,
1628 ipv4_address: Option<GeneratedString>,
1629 ipv6_address: Option<GeneratedString>,
1630}
1631
1632impl GeneratedNetworkAttachment {
1633 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1639 Ok(Self {
1640 name: required("network name", name.into())?,
1641 aliases: Vec::new(),
1642 ipv4_address: None,
1643 ipv6_address: None,
1644 })
1645 }
1646
1647 pub fn add_alias(&mut self, alias: impl Into<String>) -> Result<(), GenerationError> {
1653 self.aliases.push(required("network alias", alias.into())?);
1654 Ok(())
1655 }
1656
1657 pub fn set_ipv4_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1665 set_once(&mut self.ipv4_address, address, "ipv4_address")
1666 }
1667
1668 pub fn set_ipv6_address(&mut self, address: GeneratedString) -> Result<(), GenerationError> {
1676 set_once(&mut self.ipv6_address, address, "ipv6_address")
1677 }
1678
1679 #[must_use]
1681 pub fn name(&self) -> &str {
1682 &self.name
1683 }
1684
1685 #[must_use]
1687 pub fn aliases(&self) -> &[String] {
1688 &self.aliases
1689 }
1690
1691 #[must_use]
1693 pub const fn ipv4_address(&self) -> Option<&GeneratedString> {
1694 self.ipv4_address.as_ref()
1695 }
1696
1697 #[must_use]
1699 pub const fn ipv6_address(&self) -> Option<&GeneratedString> {
1700 self.ipv6_address.as_ref()
1701 }
1702
1703 fn is_sensitive(&self) -> bool {
1704 self.ipv4_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1705 || self.ipv6_address.as_ref().is_some_and(GeneratedString::is_sensitive)
1706 }
1707}
1708
1709#[derive(Clone, Debug, Eq, PartialEq)]
1711pub struct GeneratedResource {
1712 name: String,
1713 external: bool,
1714 custom_name: Option<String>,
1715}
1716
1717impl GeneratedResource {
1718 pub fn application(name: impl Into<String>) -> Result<Self, GenerationError> {
1724 Ok(Self {
1725 name: required("resource name", name.into())?,
1726 external: false,
1727 custom_name: None,
1728 })
1729 }
1730
1731 pub fn external(name: impl Into<String>) -> Result<Self, GenerationError> {
1737 Ok(Self {
1738 name: required("resource name", name.into())?,
1739 external: true,
1740 custom_name: None,
1741 })
1742 }
1743
1744 pub fn set_custom_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
1752 let name = required("custom resource name", name.into())?;
1753 set_once(&mut self.custom_name, name, "resource name")
1754 }
1755
1756 #[must_use]
1758 pub fn name(&self) -> &str {
1759 &self.name
1760 }
1761
1762 #[must_use]
1764 pub const fn is_external(&self) -> bool {
1765 self.external
1766 }
1767
1768 #[must_use]
1770 pub fn custom_name(&self) -> Option<&str> {
1771 self.custom_name.as_deref()
1772 }
1773}
1774
1775#[derive(Clone, Debug, Eq, PartialEq)]
1777pub struct GeneratedService {
1778 name: String,
1779 hostname: Option<GeneratedHostname>,
1780 container_name: Option<GeneratedString>,
1781 image: Option<GeneratedString>,
1782 entrypoint: Option<GeneratedEntrypoint>,
1783 command: Option<GeneratedCommand>,
1784 init: Option<bool>,
1785 stdin_open: Option<bool>,
1786 tty: Option<bool>,
1787 privileged: Option<bool>,
1788 environment_files: Vec<GeneratedEnvironmentFile>,
1789 environment: Vec<GeneratedEnvironment>,
1790 labels: Vec<GeneratedLabel>,
1791 annotations: Option<Vec<GeneratedAnnotation>>,
1792 user: Option<GeneratedString>,
1793 userns_mode: Option<GeneratedString>,
1794 group_add: Vec<GeneratedString>,
1795 cap_add: Option<Vec<GeneratedString>>,
1796 cap_drop: Option<Vec<GeneratedString>>,
1797 devices: Option<Vec<GeneratedDevice>>,
1798 dns: Option<GeneratedDns>,
1799 dns_options: Option<Vec<GeneratedString>>,
1800 dns_search: Option<GeneratedDnsSearch>,
1801 expose: Option<Vec<GeneratedString>>,
1802 security_options: Option<Vec<GeneratedString>>,
1803 working_dir: Option<GeneratedString>,
1804 read_only: Option<bool>,
1805 pids_limit: Option<GeneratedPidsLimit>,
1806 shm_size: Option<GeneratedShmSize>,
1807 mem_limit: Option<GeneratedMemLimit>,
1808 tmpfs: Option<GeneratedTmpfs>,
1809 sysctls: Option<GeneratedSysctls>,
1810 logging: Option<GeneratedLogging>,
1811 ulimits: Option<GeneratedUlimits>,
1812 pull_policy: Option<GeneratedPullPolicy>,
1813 restart: Option<GeneratedRestartPolicy>,
1814 stop_signal: Option<GeneratedString>,
1815 stop_grace_period: Option<GeneratedString>,
1816 extra_hosts: Vec<GeneratedExtraHost>,
1817 ports: Vec<GeneratedPort>,
1818 mounts: Vec<GeneratedMount>,
1819 networks: Vec<GeneratedNetworkAttachment>,
1820}
1821
1822impl GeneratedService {
1823 pub fn new(name: impl Into<String>) -> Result<Self, GenerationError> {
1829 Ok(Self {
1830 name: required("service name", name.into())?,
1831 hostname: None,
1832 container_name: None,
1833 image: None,
1834 entrypoint: None,
1835 command: None,
1836 init: None,
1837 stdin_open: None,
1838 tty: None,
1839 privileged: None,
1840 environment_files: Vec::new(),
1841 environment: Vec::new(),
1842 labels: Vec::new(),
1843 annotations: None,
1844 user: None,
1845 userns_mode: None,
1846 group_add: Vec::new(),
1847 cap_add: None,
1848 cap_drop: None,
1849 devices: None,
1850 dns: None,
1851 dns_options: None,
1852 dns_search: None,
1853 expose: None,
1854 security_options: None,
1855 working_dir: None,
1856 read_only: None,
1857 pids_limit: None,
1858 shm_size: None,
1859 mem_limit: None,
1860 tmpfs: None,
1861 sysctls: None,
1862 logging: None,
1863 ulimits: None,
1864 pull_policy: None,
1865 restart: None,
1866 stop_signal: None,
1867 stop_grace_period: None,
1868 extra_hosts: Vec::new(),
1869 ports: Vec::new(),
1870 mounts: Vec::new(),
1871 networks: Vec::new(),
1872 })
1873 }
1874
1875 #[must_use]
1877 pub fn name(&self) -> &str {
1878 &self.name
1879 }
1880
1881 pub fn set_hostname(&mut self, hostname: GeneratedHostname) -> Result<(), GenerationError> {
1889 let GeneratedHostname::Resolved(value) = &hostname;
1890 if !valid_hostname(value.expose()) {
1891 return Err(GenerationError::InvalidHostname);
1892 }
1893 set_once(&mut self.hostname, hostname, "hostname")
1894 }
1895
1896 pub fn set_container_name(&mut self, name: GeneratedString) -> Result<(), GenerationError> {
1904 if !valid_container_name(name.expose()) {
1905 return Err(GenerationError::InvalidContainerName);
1906 }
1907 set_once(&mut self.container_name, name, "container_name")
1908 }
1909
1910 pub fn set_image(&mut self, image: GeneratedString) -> Result<(), GenerationError> {
1917 require_generated_string("service image", &image)?;
1918 set_once(&mut self.image, image, "image")
1919 }
1920
1921 pub fn set_entrypoint(&mut self, entrypoint: GeneratedEntrypoint) -> Result<(), GenerationError> {
1927 set_once(&mut self.entrypoint, entrypoint, "entrypoint")
1928 }
1929
1930 pub fn set_command(&mut self, command: GeneratedCommand) -> Result<(), GenerationError> {
1936 set_once(&mut self.command, command, "command")
1937 }
1938
1939 pub fn set_init(&mut self, init: bool) -> Result<(), GenerationError> {
1945 set_once(&mut self.init, init, "init")
1946 }
1947
1948 pub fn set_stdin_open(&mut self, stdin_open: bool) -> Result<(), GenerationError> {
1954 set_once(&mut self.stdin_open, stdin_open, "stdin_open")
1955 }
1956
1957 pub fn set_tty(&mut self, tty: bool) -> Result<(), GenerationError> {
1963 set_once(&mut self.tty, tty, "tty")
1964 }
1965
1966 pub fn set_privileged(&mut self, privileged: bool) -> Result<(), GenerationError> {
1972 set_once(&mut self.privileged, privileged, "privileged")
1973 }
1974
1975 pub fn add_environment_file(&mut self, environment_file: GeneratedEnvironmentFile) {
1977 self.environment_files.push(environment_file);
1978 }
1979
1980 pub fn add_environment(&mut self, environment: GeneratedEnvironment) {
1982 self.environment.push(environment);
1983 }
1984
1985 pub fn add_label(&mut self, label: GeneratedLabel) -> Result<(), GenerationError> {
1991 if self.labels.iter().any(|candidate| candidate.name == label.name) {
1992 return Err(GenerationError::DuplicateName {
1993 kind: "service label",
1994 name: label.name,
1995 });
1996 }
1997 self.labels.push(label);
1998 Ok(())
1999 }
2000
2001 pub fn set_annotations(&mut self, annotations: Vec<GeneratedAnnotation>) -> Result<(), GenerationError> {
2012 let mut seen = BTreeSet::new();
2013 for annotation in &annotations {
2014 if annotation.name.is_empty() || annotation.name.contains(['$', '\r', '\n', '\0']) {
2015 return Err(GenerationError::InvalidAnnotationName);
2016 }
2017 if annotation.value.expose().contains(['$', '\r', '\n', '\0']) {
2018 return Err(GenerationError::InvalidAnnotationValue);
2019 }
2020 if !seen.insert(annotation.name.as_str()) {
2021 return Err(GenerationError::DuplicateName {
2022 kind: "service annotation",
2023 name: annotation.name.clone(),
2024 });
2025 }
2026 }
2027 set_once(&mut self.annotations, annotations, "annotations")
2028 }
2029
2030 #[must_use]
2032 pub fn annotations(&self) -> Option<&[GeneratedAnnotation]> {
2033 self.annotations.as_deref()
2034 }
2035
2036 pub fn set_user(&mut self, user: GeneratedString) -> Result<(), GenerationError> {
2042 set_once(&mut self.user, user, "user")
2043 }
2044
2045 pub fn set_userns_mode(&mut self, mode: GeneratedString) -> Result<(), GenerationError> {
2052 require_generated_string("user namespace mode", &mode)?;
2053 set_once(&mut self.userns_mode, mode, "userns_mode")
2054 }
2055
2056 pub fn add_supplementary_group(&mut self, group: GeneratedString) -> Result<(), GenerationError> {
2062 require_generated_string("supplementary group", &group)?;
2063 self.group_add.push(group);
2064 Ok(())
2065 }
2066
2067 pub fn set_cap_add(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2080 let mut seen = BTreeSet::new();
2081 for capability in &capabilities {
2082 require_generated_string("cap_add item", capability)?;
2083 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2084 return Err(GenerationError::ContainsLineBreak("cap_add item"));
2085 }
2086 if !seen.insert(capability.expose()) {
2087 return Err(GenerationError::DuplicateItem("cap_add"));
2088 }
2089 }
2090 set_once(&mut self.cap_add, capabilities, "cap_add")
2091 }
2092
2093 #[must_use]
2095 pub fn cap_add(&self) -> Option<&[GeneratedString]> {
2096 self.cap_add.as_deref()
2097 }
2098
2099 pub fn set_cap_drop(&mut self, capabilities: Vec<GeneratedString>) -> Result<(), GenerationError> {
2112 let mut seen = BTreeSet::new();
2113 for capability in &capabilities {
2114 require_generated_string("cap_drop item", capability)?;
2115 if capability.expose().contains('\r') || capability.expose().contains('\n') {
2116 return Err(GenerationError::ContainsLineBreak("cap_drop item"));
2117 }
2118 if !seen.insert(capability.expose()) {
2119 return Err(GenerationError::DuplicateItem("cap_drop"));
2120 }
2121 }
2122 set_once(&mut self.cap_drop, capabilities, "cap_drop")
2123 }
2124
2125 #[must_use]
2127 pub fn cap_drop(&self) -> Option<&[GeneratedString]> {
2128 self.cap_drop.as_deref()
2129 }
2130
2131 pub fn set_devices(&mut self, devices: Vec<GeneratedDevice>) -> Result<(), GenerationError> {
2144 for device in &devices {
2145 match device {
2146 GeneratedDevice::Short(value) => {
2147 validate_generated_device_member("short item", value, true)?;
2148 }
2149 GeneratedDevice::Long(value) => {
2150 validate_generated_device_member("source", value.source(), true)?;
2151 if let Some(target) = value.target() {
2152 validate_generated_device_member("target", target, false)?;
2153 }
2154 if let Some(permissions) = value.permissions() {
2155 validate_generated_device_member("permissions", permissions, false)?;
2156 }
2157 }
2158 }
2159 }
2160 set_once(&mut self.devices, devices, "devices")
2161 }
2162
2163 pub fn set_dns(&mut self, dns: GeneratedDns) -> Result<(), GenerationError> {
2173 let values = match &dns {
2174 GeneratedDns::Scalar(value) => std::slice::from_ref(value),
2175 GeneratedDns::List(values) => values.as_slice(),
2176 };
2177 for value in values {
2178 if value.expose().is_empty()
2179 || value.expose().contains('$')
2180 || value.expose().contains('\r')
2181 || value.expose().contains('\n')
2182 {
2183 return Err(GenerationError::InvalidDnsValue);
2184 }
2185 }
2186 set_once(&mut self.dns, dns, "dns")
2187 }
2188
2189 #[must_use]
2191 pub const fn dns(&self) -> Option<&GeneratedDns> {
2192 self.dns.as_ref()
2193 }
2194
2195 pub fn set_dns_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2207 let mut seen = BTreeSet::new();
2208 for option in &options {
2209 if option.expose().is_empty()
2210 || option.expose().contains('$')
2211 || option.expose().contains('\r')
2212 || option.expose().contains('\n')
2213 || option.expose().contains('\0')
2214 {
2215 return Err(GenerationError::InvalidDnsOptionValue);
2216 }
2217 if !seen.insert(option.expose()) {
2218 return Err(GenerationError::DuplicateItem("dns_opt"));
2219 }
2220 }
2221 set_once(&mut self.dns_options, options, "dns_opt")
2222 }
2223
2224 #[must_use]
2226 pub fn dns_options(&self) -> Option<&[GeneratedString]> {
2227 self.dns_options.as_deref()
2228 }
2229
2230 pub fn set_dns_search(&mut self, search: GeneratedDnsSearch) -> Result<(), GenerationError> {
2240 let values = match &search {
2241 GeneratedDnsSearch::Scalar(value) => std::slice::from_ref(value),
2242 GeneratedDnsSearch::List(values) => values.as_slice(),
2243 };
2244 for value in values {
2245 if value.expose().is_empty()
2246 || value.expose().contains('$')
2247 || value.expose().contains('\r')
2248 || value.expose().contains('\n')
2249 || value.expose().contains('\0')
2250 {
2251 return Err(GenerationError::InvalidDnsSearchValue);
2252 }
2253 }
2254 set_once(&mut self.dns_search, search, "dns_search")
2255 }
2256
2257 #[must_use]
2259 pub const fn dns_search(&self) -> Option<&GeneratedDnsSearch> {
2260 self.dns_search.as_ref()
2261 }
2262
2263 pub fn set_expose(&mut self, expose: Vec<GeneratedString>) -> Result<(), GenerationError> {
2273 let mut seen = BTreeSet::new();
2274 for item in &expose {
2275 if !valid_generated_expose_item(item.expose()) {
2276 return Err(GenerationError::InvalidExposeValue);
2277 }
2278 if !seen.insert(item.expose()) {
2279 return Err(GenerationError::DuplicateItem("expose"));
2280 }
2281 }
2282 set_once(&mut self.expose, expose, "expose")
2283 }
2284
2285 #[must_use]
2287 pub fn expose(&self) -> Option<&[GeneratedString]> {
2288 self.expose.as_deref()
2289 }
2290
2291 pub fn set_security_options(&mut self, options: Vec<GeneratedString>) -> Result<(), GenerationError> {
2301 for option in &options {
2302 if option.expose().is_empty()
2303 || option.expose().contains('$')
2304 || option.expose().contains('\r')
2305 || option.expose().contains('\n')
2306 || option.expose().contains('\0')
2307 {
2308 return Err(GenerationError::InvalidSecurityOptionValue);
2309 }
2310 }
2311 set_once(&mut self.security_options, options, "security_opt")
2312 }
2313
2314 #[must_use]
2316 pub fn security_options(&self) -> Option<&[GeneratedString]> {
2317 self.security_options.as_deref()
2318 }
2319
2320 #[must_use]
2322 pub fn devices(&self) -> Option<&[GeneratedDevice]> {
2323 self.devices.as_deref()
2324 }
2325
2326 pub fn set_working_dir(&mut self, directory: GeneratedString) -> Result<(), GenerationError> {
2333 require_generated_string("working directory", &directory)?;
2334 set_once(&mut self.working_dir, directory, "working_dir")
2335 }
2336
2337 pub fn set_read_only(&mut self, read_only: bool) -> Result<(), GenerationError> {
2343 set_once(&mut self.read_only, read_only, "read_only")
2344 }
2345
2346 pub fn set_pids_limit(&mut self, limit: GeneratedPidsLimit) -> Result<(), GenerationError> {
2354 if let GeneratedPidsLimit::Finite(decimal) = &limit {
2355 if !valid_positive_pids_decimal(decimal) {
2356 return Err(GenerationError::InvalidPidsLimit);
2357 }
2358 }
2359 set_once(&mut self.pids_limit, limit, "pids_limit")
2360 }
2361
2362 pub fn set_shm_size(&mut self, size: GeneratedShmSize) -> Result<(), GenerationError> {
2370 let GeneratedShmSize::Explicit { amount, .. } = &size;
2371 if !valid_generated_shm_amount(amount.expose()) {
2372 return Err(GenerationError::InvalidShmSize);
2373 }
2374 set_once(&mut self.shm_size, size, "shm_size")
2375 }
2376
2377 pub fn set_mem_limit(&mut self, limit: GeneratedMemLimit) -> Result<(), GenerationError> {
2385 let GeneratedMemLimit::Explicit { amount, .. } = &limit;
2386 if !valid_generated_mem_amount(amount.expose()) {
2387 return Err(GenerationError::InvalidMemLimit);
2388 }
2389 set_once(&mut self.mem_limit, limit, "mem_limit")
2390 }
2391
2392 pub fn set_tmpfs(&mut self, tmpfs: GeneratedTmpfs) -> Result<(), GenerationError> {
2403 let items = match &tmpfs {
2404 GeneratedTmpfs::Scalar(item) => std::slice::from_ref(item),
2405 GeneratedTmpfs::List(items) => items.as_slice(),
2406 };
2407 for item in items {
2408 require_generated_string("tmpfs item", item)?;
2409 if item.expose().contains('\r') || item.expose().contains('\n') {
2410 return Err(GenerationError::ContainsLineBreak("tmpfs item"));
2411 }
2412 if !valid_generated_tmpfs_item(item.expose()) {
2413 return Err(GenerationError::InvalidTmpfsItem);
2414 }
2415 }
2416 set_once(&mut self.tmpfs, tmpfs, "tmpfs")
2417 }
2418
2419 #[must_use]
2421 pub const fn tmpfs(&self) -> Option<&GeneratedTmpfs> {
2422 self.tmpfs.as_ref()
2423 }
2424
2425 pub fn set_sysctls(&mut self, sysctls: GeneratedSysctls) -> Result<(), GenerationError> {
2436 let mut seen = BTreeSet::new();
2437 match &sysctls {
2438 GeneratedSysctls::Map(entries) => {
2439 for entry in entries {
2440 if !seen.insert(entry.name()) {
2441 return Err(GenerationError::DuplicateName {
2442 kind: "sysctl",
2443 name: entry.name().to_owned(),
2444 });
2445 }
2446 }
2447 }
2448 GeneratedSysctls::List(items) => {
2449 for item in items {
2450 if item.expose().contains(['\r', '\n', '$']) {
2451 return Err(GenerationError::InvalidSysctlValue);
2452 }
2453 if !seen.insert(item.expose()) {
2454 return Err(GenerationError::DuplicateItem("sysctls"));
2455 }
2456 }
2457 }
2458 }
2459 set_once(&mut self.sysctls, sysctls, "sysctls")
2460 }
2461
2462 #[must_use]
2464 pub const fn sysctls(&self) -> Option<&GeneratedSysctls> {
2465 self.sysctls.as_ref()
2466 }
2467
2468 pub fn set_logging(&mut self, logging: GeneratedLogging) -> Result<(), GenerationError> {
2475 set_once(&mut self.logging, logging, "logging")
2476 }
2477
2478 #[must_use]
2480 pub const fn logging(&self) -> Option<&GeneratedLogging> {
2481 self.logging.as_ref()
2482 }
2483
2484 pub fn set_ulimits(&mut self, ulimits: GeneratedUlimits) -> Result<(), GenerationError> {
2493 set_once(&mut self.ulimits, ulimits, "ulimits")
2494 }
2495
2496 #[must_use]
2498 pub const fn ulimits(&self) -> Option<&GeneratedUlimits> {
2499 self.ulimits.as_ref()
2500 }
2501
2502 pub fn set_pull_policy(&mut self, policy: GeneratedPullPolicy) -> Result<(), GenerationError> {
2509 if let GeneratedPullPolicy::Every(duration) = &policy {
2510 if !valid_pull_policy_duration(duration.expose()) {
2511 return Err(GenerationError::InvalidPullPolicyDuration);
2512 }
2513 }
2514 set_once(&mut self.pull_policy, policy, "pull_policy")
2515 }
2516
2517 pub fn set_restart(&mut self, restart: GeneratedRestartPolicy) -> Result<(), GenerationError> {
2523 set_once(&mut self.restart, restart, "restart")
2524 }
2525
2526 pub fn set_stop_signal(&mut self, signal: GeneratedString) -> Result<(), GenerationError> {
2533 set_once(&mut self.stop_signal, signal, "stop_signal")
2534 }
2535
2536 pub fn set_stop_grace_period(&mut self, period: GeneratedString) -> Result<(), GenerationError> {
2544 if !StopGracePeriod::parse(period.expose().to_owned()).is_valid() {
2545 return Err(GenerationError::InvalidStopGracePeriod);
2546 }
2547 set_once(&mut self.stop_grace_period, period, "stop_grace_period")
2548 }
2549
2550 pub fn add_extra_host(&mut self, host: GeneratedExtraHost) {
2552 self.extra_hosts.push(host);
2553 }
2554
2555 pub fn add_port(&mut self, port: GeneratedPort) {
2557 self.ports.push(port);
2558 }
2559
2560 pub fn add_mount(&mut self, mount: GeneratedMount) {
2562 self.mounts.push(mount);
2563 }
2564
2565 pub fn add_network(&mut self, network: GeneratedNetworkAttachment) -> Result<(), GenerationError> {
2571 if self.networks.iter().any(|candidate| candidate.name == network.name) {
2572 return Err(GenerationError::DuplicateName {
2573 kind: "service network",
2574 name: network.name,
2575 });
2576 }
2577 self.networks.push(network);
2578 Ok(())
2579 }
2580
2581 fn is_sensitive(&self) -> bool {
2582 matches!(
2583 self.hostname.as_ref(),
2584 Some(GeneratedHostname::Resolved(hostname)) if hostname.is_sensitive()
2585 ) || self.image.as_ref().is_some_and(GeneratedString::is_sensitive)
2586 || self.entrypoint.as_ref().is_some_and(entrypoint_is_sensitive)
2587 || self.command.as_ref().is_some_and(command_is_sensitive)
2588 || self
2589 .environment_files
2590 .iter()
2591 .any(GeneratedEnvironmentFile::is_sensitive)
2592 || self
2593 .environment
2594 .iter()
2595 .filter_map(GeneratedEnvironment::value)
2596 .any(GeneratedString::is_sensitive)
2597 || self.labels.iter().any(|label| label.value.is_sensitive())
2598 || self
2599 .annotations
2600 .as_ref()
2601 .is_some_and(|items| items.iter().any(|annotation| annotation.value.is_sensitive()))
2602 || matches!(
2603 self.pull_policy.as_ref(),
2604 Some(GeneratedPullPolicy::Every(duration)) if duration.is_sensitive()
2605 )
2606 || matches!(
2607 self.shm_size.as_ref(),
2608 Some(GeneratedShmSize::Explicit { amount, .. }) if amount.is_sensitive()
2609 )
2610 || matches!(
2611 self.mem_limit.as_ref(),
2612 Some(GeneratedMemLimit::Explicit { amount, .. }) if amount.is_sensitive()
2613 )
2614 || match self.tmpfs.as_ref() {
2615 Some(GeneratedTmpfs::Scalar(item)) => item.is_sensitive(),
2616 Some(GeneratedTmpfs::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2617 None => false,
2618 }
2619 || match self.dns.as_ref() {
2620 Some(GeneratedDns::Scalar(value)) => value.is_sensitive(),
2621 Some(GeneratedDns::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2622 None => false,
2623 }
2624 || self
2625 .dns_options
2626 .as_ref()
2627 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2628 || match self.dns_search.as_ref() {
2629 Some(GeneratedDnsSearch::Scalar(value)) => value.is_sensitive(),
2630 Some(GeneratedDnsSearch::List(values)) => values.iter().any(GeneratedString::is_sensitive),
2631 None => false,
2632 }
2633 || self
2634 .expose
2635 .as_ref()
2636 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2637 || self
2638 .security_options
2639 .as_ref()
2640 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2641 || match self.sysctls.as_ref() {
2642 Some(GeneratedSysctls::Map(entries)) => entries.iter().any(|entry| entry.value.is_sensitive()),
2643 Some(GeneratedSysctls::List(items)) => items.iter().any(GeneratedString::is_sensitive),
2644 None => false,
2645 }
2646 || self.logging.as_ref().is_some_and(GeneratedLogging::is_sensitive)
2647 || self
2648 .ulimits
2649 .as_ref()
2650 .is_some_and(|limits| limits.entries.iter().any(GeneratedUlimit::is_sensitive))
2651 || [
2652 self.user.as_ref(),
2653 self.userns_mode.as_ref(),
2654 self.working_dir.as_ref(),
2655 self.stop_signal.as_ref(),
2656 self.stop_grace_period.as_ref(),
2657 ]
2658 .into_iter()
2659 .flatten()
2660 .any(GeneratedString::is_sensitive)
2661 || self.group_add.iter().any(GeneratedString::is_sensitive)
2662 || self
2663 .cap_add
2664 .as_ref()
2665 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2666 || self
2667 .cap_drop
2668 .as_ref()
2669 .is_some_and(|items| items.iter().any(GeneratedString::is_sensitive))
2670 || self
2671 .devices
2672 .as_ref()
2673 .is_some_and(|items| items.iter().any(GeneratedDevice::is_sensitive))
2674 || self.networks.iter().any(GeneratedNetworkAttachment::is_sensitive)
2675 }
2676}
2677
2678#[derive(Clone, Debug, Eq, PartialEq)]
2679enum GeneratedNetwork {
2680 Basic(GeneratedResource),
2681 Definition(GeneratedNetworkDefinition),
2682}
2683
2684#[derive(Clone, Debug, Eq, PartialEq)]
2685enum GeneratedVolume {
2686 Basic(GeneratedResource),
2687 Definition(GeneratedVolumeDefinition),
2688}
2689
2690impl GeneratedVolume {
2691 fn name(&self) -> &str {
2692 match self {
2693 Self::Basic(volume) => volume.name(),
2694 Self::Definition(volume) => volume.name(),
2695 }
2696 }
2697
2698 fn is_sensitive(&self) -> bool {
2699 match self {
2700 Self::Basic(_) => false,
2701 Self::Definition(volume) => volume.is_sensitive(),
2702 }
2703 }
2704}
2705
2706impl GeneratedNetwork {
2707 fn name(&self) -> &str {
2708 match self {
2709 Self::Basic(network) => network.name(),
2710 Self::Definition(network) => network.name(),
2711 }
2712 }
2713
2714 fn is_sensitive(&self) -> bool {
2715 match self {
2716 Self::Basic(_) => false,
2717 Self::Definition(network) => network.is_sensitive(),
2718 }
2719 }
2720}
2721
2722#[derive(Clone, Debug, Default, Eq, PartialEq)]
2724pub struct ComposeDocumentBuilder {
2725 name: Option<String>,
2726 services: Vec<GeneratedService>,
2727 networks: Vec<GeneratedNetwork>,
2728 volumes: Vec<GeneratedVolume>,
2729}
2730
2731impl ComposeDocumentBuilder {
2732 #[must_use]
2734 pub const fn new() -> Self {
2735 Self {
2736 name: None,
2737 services: Vec::new(),
2738 networks: Vec::new(),
2739 volumes: Vec::new(),
2740 }
2741 }
2742
2743 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), GenerationError> {
2749 let name = required("project name", name.into())?;
2750 set_once(&mut self.name, name, "name")
2751 }
2752
2753 pub fn add_service(&mut self, service: GeneratedService) -> Result<(), GenerationError> {
2759 insert_named(&mut self.services, service, "service", GeneratedService::name)
2760 }
2761
2762 pub fn add_network(&mut self, network: GeneratedResource) -> Result<(), GenerationError> {
2768 insert_named(
2769 &mut self.networks,
2770 GeneratedNetwork::Basic(network),
2771 "network",
2772 GeneratedNetwork::name,
2773 )
2774 }
2775
2776 pub fn add_network_definition(&mut self, network: GeneratedNetworkDefinition) -> Result<(), GenerationError> {
2786 insert_named(
2787 &mut self.networks,
2788 GeneratedNetwork::Definition(network),
2789 "network",
2790 GeneratedNetwork::name,
2791 )
2792 }
2793
2794 pub fn add_volume(&mut self, volume: GeneratedResource) -> Result<(), GenerationError> {
2800 insert_named(
2801 &mut self.volumes,
2802 GeneratedVolume::Basic(volume),
2803 "volume",
2804 GeneratedVolume::name,
2805 )
2806 }
2807
2808 pub fn add_volume_definition(&mut self, volume: GeneratedVolumeDefinition) -> Result<(), GenerationError> {
2819 insert_named(
2820 &mut self.volumes,
2821 GeneratedVolume::Definition(volume),
2822 "volume",
2823 GeneratedVolume::name,
2824 )
2825 }
2826
2827 pub fn build(self, source_id: SourceId) -> Result<GeneratedComposeDocument, GenerationError> {
2834 if self.services.is_empty() {
2835 return Err(GenerationError::MissingService);
2836 }
2837 let sensitive = self.services.iter().any(GeneratedService::is_sensitive)
2838 || self.networks.iter().any(GeneratedNetwork::is_sensitive)
2839 || self.volumes.iter().any(GeneratedVolume::is_sensitive);
2840 let text = render_document(&self);
2841 let syntax = SyntaxDocument::parse(source_id, text.clone())
2842 .map_err(|_| GenerationError::InternalInvariant("syntax-tree"))?;
2843 if !syntax.is_valid() {
2844 return Err(GenerationError::InternalInvariant("syntax"));
2845 }
2846 let model = ComposeDocument::parse(syntax.document());
2847 if !model.is_valid() {
2848 return Err(GenerationError::InternalInvariant("typed-model"));
2849 }
2850 let document = model
2851 .document()
2852 .cloned()
2853 .ok_or(GenerationError::InternalInvariant("document-root"))?;
2854 Ok(GeneratedComposeDocument {
2855 text,
2856 sensitive,
2857 document,
2858 })
2859 }
2860}
2861
2862#[derive(Clone, Eq, PartialEq)]
2864pub struct GeneratedComposeDocument {
2865 text: String,
2866 sensitive: bool,
2867 document: ComposeDocument,
2868}
2869
2870impl GeneratedComposeDocument {
2871 #[must_use]
2873 pub fn text(&self) -> &str {
2874 &self.text
2875 }
2876
2877 #[must_use]
2879 pub const fn document(&self) -> &ComposeDocument {
2880 &self.document
2881 }
2882
2883 #[must_use]
2885 pub const fn is_sensitive(&self) -> bool {
2886 self.sensitive
2887 }
2888}
2889
2890impl fmt::Debug for GeneratedComposeDocument {
2891 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2892 formatter
2893 .debug_struct("GeneratedComposeDocument")
2894 .field("text", &if self.sensitive { "<redacted>" } else { &self.text })
2895 .field("sensitive", &self.sensitive)
2896 .field("document", &if self.sensitive { "<redacted>" } else { "validated" })
2897 .finish()
2898 }
2899}
2900
2901fn render_document(project: &ComposeDocumentBuilder) -> String {
2902 let mut output = String::new();
2903 if let Some(name) = &project.name {
2904 output.push_str("name: ");
2905 write_quoted(&mut output, name);
2906 output.push('\n');
2907 }
2908 output.push_str("services:\n");
2909 for service in &project.services {
2910 write_indent(&mut output, 1);
2911 write_quoted(&mut output, &service.name);
2912 output.push_str(":\n");
2913 render_service(&mut output, service);
2914 }
2915 render_network_definitions(&mut output, &project.networks);
2916 render_volume_definitions(&mut output, &project.volumes);
2917 output
2918}
2919
2920fn render_service(output: &mut String, service: &GeneratedService) {
2921 if let Some(GeneratedHostname::Resolved(hostname)) = &service.hostname {
2922 render_optional_string(output, "hostname", Some(hostname));
2923 }
2924 render_optional_string(output, "container_name", service.container_name.as_ref());
2925 render_optional_string(output, "image", service.image.as_ref());
2926 if let Some(entrypoint) = &service.entrypoint {
2927 render_entrypoint(output, entrypoint);
2928 }
2929 if let Some(command) = &service.command {
2930 render_command(output, command);
2931 }
2932 if let Some(init) = service.init {
2933 write_field(output, 2, "init");
2934 output.push_str(if init { "true\n" } else { "false\n" });
2935 }
2936 if let Some(stdin_open) = service.stdin_open {
2937 write_field(output, 2, "stdin_open");
2938 output.push_str(if stdin_open { "true\n" } else { "false\n" });
2939 }
2940 if let Some(tty) = service.tty {
2941 write_field(output, 2, "tty");
2942 output.push_str(if tty { "true\n" } else { "false\n" });
2943 }
2944 if let Some(privileged) = service.privileged {
2945 write_field(output, 2, "privileged");
2946 output.push_str(if privileged { "true\n" } else { "false\n" });
2947 }
2948 render_environment_files(output, &service.environment_files);
2949 render_environment(output, &service.environment);
2950 render_labels(output, &service.labels);
2951 if let Some(annotations) = &service.annotations {
2952 render_annotations(output, annotations);
2953 }
2954 render_optional_string(output, "user", service.user.as_ref());
2955 render_optional_string(output, "userns_mode", service.userns_mode.as_ref());
2956 render_string_sequence(output, "group_add", &service.group_add);
2957 if let Some(capabilities) = &service.cap_add {
2958 render_configured_string_sequence(output, "cap_add", capabilities);
2959 }
2960 if let Some(capabilities) = &service.cap_drop {
2961 render_configured_string_sequence(output, "cap_drop", capabilities);
2962 }
2963 render_optional_string(output, "working_dir", service.working_dir.as_ref());
2964 if let Some(read_only) = service.read_only {
2965 write_field(output, 2, "read_only");
2966 output.push_str(if read_only { "true\n" } else { "false\n" });
2967 }
2968 if let Some(pids_limit) = &service.pids_limit {
2969 render_pids_limit(output, pids_limit);
2970 }
2971 if let Some(shm_size) = &service.shm_size {
2972 render_shm_size(output, shm_size);
2973 }
2974 if let Some(mem_limit) = &service.mem_limit {
2975 render_mem_limit(output, mem_limit);
2976 }
2977 if let Some(devices) = &service.devices {
2978 render_devices(output, devices);
2979 }
2980 if let Some(dns) = &service.dns {
2981 render_dns(output, dns);
2982 }
2983 if let Some(options) = &service.dns_options {
2984 render_configured_string_sequence(output, "dns_opt", options);
2985 }
2986 if let Some(search) = &service.dns_search {
2987 render_dns_search(output, search);
2988 }
2989 if let Some(expose) = &service.expose {
2990 render_configured_string_sequence(output, "expose", expose);
2991 }
2992 if let Some(options) = &service.security_options {
2993 render_configured_string_sequence(output, "security_opt", options);
2994 }
2995 if let Some(tmpfs) = &service.tmpfs {
2996 render_tmpfs(output, tmpfs);
2997 }
2998 if let Some(sysctls) = &service.sysctls {
2999 render_sysctls(output, sysctls);
3000 }
3001 if let Some(logging) = &service.logging {
3002 render_logging(output, logging);
3003 }
3004 if let Some(ulimits) = &service.ulimits {
3005 render_ulimits(output, ulimits);
3006 }
3007 if let Some(pull_policy) = &service.pull_policy {
3008 render_pull_policy(output, pull_policy);
3009 }
3010 if let Some(restart) = service.restart {
3011 render_restart(output, restart);
3012 }
3013 render_optional_string(output, "stop_signal", service.stop_signal.as_ref());
3014 render_optional_string(output, "stop_grace_period", service.stop_grace_period.as_ref());
3015 render_extra_hosts(output, &service.extra_hosts);
3016 render_ports(output, &service.ports);
3017 render_mounts(output, &service.mounts);
3018 render_networks(output, &service.networks);
3019}
3020
3021fn render_pids_limit(output: &mut String, limit: &GeneratedPidsLimit) {
3022 write_field(output, 2, "pids_limit");
3023 match limit {
3024 GeneratedPidsLimit::Unlimited => output.push_str("-1\n"),
3025 GeneratedPidsLimit::Finite(decimal) => {
3026 output.push_str(decimal);
3027 output.push('\n');
3028 }
3029 }
3030}
3031
3032fn render_shm_size(output: &mut String, size: &GeneratedShmSize) {
3033 let GeneratedShmSize::Explicit { amount, unit } = size;
3034 write_field(output, 2, "shm_size");
3035 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3036 output.push('\n');
3037}
3038
3039fn render_mem_limit(output: &mut String, limit: &GeneratedMemLimit) {
3040 let GeneratedMemLimit::Explicit { amount, unit } = limit;
3041 write_field(output, 2, "mem_limit");
3042 write_quoted(output, &format!("{}{}", amount.expose(), unit.as_str()));
3043 output.push('\n');
3044}
3045
3046fn render_devices(output: &mut String, devices: &[GeneratedDevice]) {
3047 if devices.is_empty() {
3048 output.push_str(" devices: []\n");
3049 return;
3050 }
3051 output.push_str(" devices:\n");
3052 for device in devices {
3053 match device {
3054 GeneratedDevice::Short(value) => {
3055 output.push_str(" - ");
3056 write_quoted(output, value.expose());
3057 output.push('\n');
3058 }
3059 GeneratedDevice::Long(value) => {
3060 output.push_str(" - source: ");
3061 write_quoted(output, value.source().expose());
3062 output.push('\n');
3063 if let Some(target) = value.target() {
3064 output.push_str(" target: ");
3065 write_quoted(output, target.expose());
3066 output.push('\n');
3067 }
3068 if let Some(permissions) = value.permissions() {
3069 output.push_str(" permissions: ");
3070 write_quoted(output, permissions.expose());
3071 output.push('\n');
3072 }
3073 }
3074 }
3075 }
3076}
3077
3078fn render_dns(output: &mut String, dns: &GeneratedDns) {
3079 match dns {
3080 GeneratedDns::Scalar(value) => render_optional_string(output, "dns", Some(value)),
3081 GeneratedDns::List(values) => render_configured_string_sequence(output, "dns", values),
3082 }
3083}
3084
3085fn render_dns_search(output: &mut String, search: &GeneratedDnsSearch) {
3086 match search {
3087 GeneratedDnsSearch::Scalar(value) => render_optional_string(output, "dns_search", Some(value)),
3088 GeneratedDnsSearch::List(values) => render_configured_string_sequence(output, "dns_search", values),
3089 }
3090}
3091
3092fn render_tmpfs(output: &mut String, tmpfs: &GeneratedTmpfs) {
3093 match tmpfs {
3094 GeneratedTmpfs::Scalar(item) => render_optional_string(output, "tmpfs", Some(item)),
3095 GeneratedTmpfs::List(items) => render_configured_string_sequence(output, "tmpfs", items),
3096 }
3097}
3098
3099fn render_sysctls(output: &mut String, sysctls: &GeneratedSysctls) {
3100 match sysctls {
3101 GeneratedSysctls::Map(entries) if entries.is_empty() => output.push_str(" sysctls: {}\n"),
3102 GeneratedSysctls::Map(entries) => {
3103 output.push_str(" sysctls:\n");
3104 for entry in entries {
3105 write_indent(output, 3);
3106 write_quoted(output, entry.name());
3107 output.push_str(": ");
3108 write_quoted(output, entry.value().expose());
3109 output.push('\n');
3110 }
3111 }
3112 GeneratedSysctls::List(items) => render_configured_string_sequence(output, "sysctls", items),
3113 }
3114}
3115
3116fn render_logging(output: &mut String, logging: &GeneratedLogging) {
3117 output.push_str(" logging:\n driver: ");
3118 write_quoted(output, logging.driver.expose());
3119 output.push('\n');
3120 if logging.options.is_empty() {
3121 output.push_str(" options: {}\n");
3122 return;
3123 }
3124 output.push_str(" options:\n");
3125 for option in &logging.options {
3126 write_indent(output, 4);
3127 write_quoted(output, option.name());
3128 output.push_str(": ");
3129 match option.value() {
3130 GeneratedLoggingOptionValue::String(value) => write_quoted(output, value.expose()),
3131 GeneratedLoggingOptionValue::Number(value) => output.push_str(value.expose()),
3132 GeneratedLoggingOptionValue::Null => output.push_str("null"),
3133 }
3134 output.push('\n');
3135 }
3136}
3137
3138fn render_ulimits(output: &mut String, ulimits: &GeneratedUlimits) {
3139 if ulimits.entries.is_empty() {
3140 output.push_str(" ulimits: {}\n");
3141 return;
3142 }
3143 output.push_str(" ulimits:\n");
3144 for limit in &ulimits.entries {
3145 write_indent(output, 3);
3146 write_quoted(output, limit.name());
3147 match limit.value() {
3148 GeneratedUlimitValue::Single(value) => {
3149 output.push_str(": ");
3150 write_quoted(output, value.expose());
3151 output.push('\n');
3152 }
3153 GeneratedUlimitValue::Range {
3154 soft: Some(soft),
3155 hard: Some(hard),
3156 } => {
3157 output.push_str(":\n");
3158 write_indent(output, 4);
3159 output.push_str("soft: ");
3160 write_quoted(output, soft.expose());
3161 output.push('\n');
3162 write_indent(output, 4);
3163 output.push_str("hard: ");
3164 write_quoted(output, hard.expose());
3165 output.push('\n');
3166 }
3167 GeneratedUlimitValue::Range { .. } => {
3168 unreachable!("generated ulimit ranges are validated during construction")
3169 }
3170 }
3171 }
3172}
3173
3174fn render_pull_policy(output: &mut String, policy: &GeneratedPullPolicy) {
3175 write_field(output, 2, "pull_policy");
3176 let value = match policy {
3177 GeneratedPullPolicy::Always => "always".to_owned(),
3178 GeneratedPullPolicy::Never => "never".to_owned(),
3179 GeneratedPullPolicy::Missing => "missing".to_owned(),
3180 GeneratedPullPolicy::IfNotPresentAlias => "if_not_present".to_owned(),
3181 GeneratedPullPolicy::Build => "build".to_owned(),
3182 GeneratedPullPolicy::Daily => "daily".to_owned(),
3183 GeneratedPullPolicy::Weekly => "weekly".to_owned(),
3184 GeneratedPullPolicy::Every(duration) => format!("every_{}", duration.expose()),
3185 };
3186 write_quoted(output, &value);
3187 output.push('\n');
3188}
3189
3190fn render_entrypoint(output: &mut String, entrypoint: &GeneratedEntrypoint) {
3191 match entrypoint {
3192 GeneratedEntrypoint::List(arguments) if arguments.is_empty() => output.push_str(" entrypoint: []\n"),
3193 GeneratedEntrypoint::List(arguments) => render_string_sequence(output, "entrypoint", arguments),
3194 GeneratedEntrypoint::String(entrypoint) => render_optional_string(output, "entrypoint", Some(entrypoint)),
3195 GeneratedEntrypoint::Empty => output.push_str(" entrypoint: []\n"),
3196 }
3197}
3198
3199fn render_restart(output: &mut String, restart: GeneratedRestartPolicy) {
3200 write_field(output, 2, "restart");
3201 let value = match restart {
3202 GeneratedRestartPolicy::No => "no".to_owned(),
3203 GeneratedRestartPolicy::Always => "always".to_owned(),
3204 GeneratedRestartPolicy::OnFailure { maximum_retries: None } => "on-failure".to_owned(),
3205 GeneratedRestartPolicy::OnFailure {
3206 maximum_retries: Some(maximum_retries),
3207 } => format!("on-failure:{maximum_retries}"),
3208 GeneratedRestartPolicy::UnlessStopped => "unless-stopped".to_owned(),
3209 };
3210 write_quoted(output, &value);
3211 output.push('\n');
3212}
3213
3214fn render_optional_string(output: &mut String, key: &str, value: Option<&GeneratedString>) {
3215 if let Some(value) = value {
3216 write_field(output, 2, key);
3217 write_quoted(output, value.expose());
3218 output.push('\n');
3219 }
3220}
3221
3222fn render_command(output: &mut String, command: &GeneratedCommand) {
3223 match command {
3224 GeneratedCommand::Exec(arguments) if arguments.is_empty() => output.push_str(" command: []\n"),
3225 GeneratedCommand::Exec(arguments) => render_string_sequence(output, "command", arguments),
3226 GeneratedCommand::Shell(command) => render_optional_string(output, "command", Some(command)),
3227 GeneratedCommand::Empty => output.push_str(" command: []\n"),
3228 }
3229}
3230
3231fn render_environment(output: &mut String, environment: &[GeneratedEnvironment]) {
3232 if environment.is_empty() {
3233 return;
3234 }
3235 output.push_str(" environment:\n");
3236 for variable in environment {
3237 output.push_str(" - ");
3238 let value = variable.value.as_ref().map_or_else(
3239 || variable.name.clone(),
3240 |value| format!("{}={}", variable.name, value.expose()),
3241 );
3242 write_quoted(output, &value);
3243 output.push('\n');
3244 }
3245}
3246
3247fn render_environment_files(output: &mut String, environment_files: &[GeneratedEnvironmentFile]) {
3248 if environment_files.is_empty() {
3249 return;
3250 }
3251 output.push_str(" env_file:\n");
3252 for environment_file in environment_files {
3253 match environment_file {
3254 GeneratedEnvironmentFile::Short(path) => {
3255 output.push_str(" - ");
3256 write_quoted(output, path.expose());
3257 output.push('\n');
3258 }
3259 GeneratedEnvironmentFile::Long { path, required, format } => {
3260 output.push_str(" - path: ");
3261 write_quoted(output, path.expose());
3262 output.push('\n');
3263 if let Some(required) = required {
3264 output.push_str(" required: ");
3265 output.push_str(if *required { "true\n" } else { "false\n" });
3266 }
3267 if let Some(format) = format {
3268 output.push_str(" format: ");
3269 write_quoted(
3270 output,
3271 match format {
3272 GeneratedEnvironmentFileFormat::Raw => "raw",
3273 },
3274 );
3275 output.push('\n');
3276 }
3277 }
3278 }
3279 }
3280}
3281
3282fn render_labels(output: &mut String, labels: &[GeneratedLabel]) {
3283 if labels.is_empty() {
3284 return;
3285 }
3286 output.push_str(" labels:\n");
3287 for label in labels {
3288 output.push_str(" ");
3289 write_quoted(output, &label.name);
3290 output.push_str(": ");
3291 write_quoted(output, label.value.expose());
3292 output.push('\n');
3293 }
3294}
3295
3296fn render_annotations(output: &mut String, annotations: &[GeneratedAnnotation]) {
3297 if annotations.is_empty() {
3298 output.push_str(" annotations: {}\n");
3299 return;
3300 }
3301 output.push_str(" annotations:\n");
3302 for annotation in annotations {
3303 output.push_str(" ");
3304 write_quoted(output, &annotation.name);
3305 output.push_str(": ");
3306 write_quoted(output, annotation.value.expose());
3307 output.push('\n');
3308 }
3309}
3310
3311fn render_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3312 if values.is_empty() {
3313 return;
3314 }
3315 write_indent(output, 2);
3316 output.push_str(key);
3317 output.push_str(":\n");
3318 for value in values {
3319 output.push_str(" - ");
3320 write_quoted(output, value.expose());
3321 output.push('\n');
3322 }
3323}
3324
3325fn render_configured_string_sequence(output: &mut String, key: &str, values: &[GeneratedString]) {
3326 if values.is_empty() {
3327 write_indent(output, 2);
3328 output.push_str(key);
3329 output.push_str(": []\n");
3330 } else {
3331 render_string_sequence(output, key, values);
3332 }
3333}
3334
3335fn render_extra_hosts(output: &mut String, hosts: &[GeneratedExtraHost]) {
3336 if hosts.is_empty() {
3337 return;
3338 }
3339 output.push_str(" extra_hosts:\n");
3340 for host in hosts {
3341 output.push_str(" - ");
3342 write_quoted(output, &format!("{}={}", host.hostname, host.address));
3343 output.push('\n');
3344 }
3345}
3346
3347fn render_ports(output: &mut String, ports: &[GeneratedPort]) {
3348 if ports.is_empty() {
3349 return;
3350 }
3351 output.push_str(" ports:\n");
3352 for port in ports {
3353 if port.protocol == GeneratedProtocol::Sctp {
3354 render_short_sctp_port(output, port);
3355 continue;
3356 }
3357 output.push_str(" - target: ");
3358 output.push_str(&port.target.to_string());
3359 output.push('\n');
3360 if let Some(published) = port.published {
3361 output.push_str(" published: ");
3362 write_quoted(output, &published.to_string());
3363 output.push('\n');
3364 }
3365 if let Some(host_ip) = &port.host_ip {
3366 output.push_str(" host_ip: ");
3367 write_quoted(output, host_ip);
3368 output.push('\n');
3369 }
3370 output.push_str(" protocol: ");
3371 write_quoted(output, port.protocol.as_str());
3372 output.push('\n');
3373 }
3374}
3375
3376fn render_short_sctp_port(output: &mut String, port: &GeneratedPort) {
3377 let mut value = String::new();
3378 if let Some(host_ip) = &port.host_ip {
3379 if host_ip.contains(':') && !(host_ip.starts_with('[') && host_ip.ends_with(']')) {
3380 value.push('[');
3381 value.push_str(host_ip);
3382 value.push(']');
3383 } else {
3384 value.push_str(host_ip);
3385 }
3386 value.push(':');
3387 }
3388 if let Some(published) = port.published {
3389 value.push_str(&published.to_string());
3390 value.push(':');
3391 }
3392 value.push_str(&port.target.to_string());
3393 value.push_str("/sctp");
3394
3395 output.push_str(" - ");
3396 write_quoted(output, &value);
3397 output.push('\n');
3398}
3399
3400fn render_mounts(output: &mut String, mounts: &[GeneratedMount]) {
3401 if mounts.is_empty() {
3402 return;
3403 }
3404 output.push_str(" volumes:\n");
3405 for mount in mounts {
3406 match &mount.kind {
3407 GeneratedMountKind::Bind {
3408 source,
3409 selinux: Some(selinux),
3410 } => render_selinux_bind(output, source, mount, *selinux),
3411 kind => render_long_mount(output, kind, mount),
3412 }
3413 }
3414}
3415
3416fn render_selinux_bind(output: &mut String, source: &str, mount: &GeneratedMount, selinux: GeneratedSelinux) {
3417 let mut value = format!("{source}:{}:{}", mount.target, selinux.as_str());
3418 if mount.read_only {
3419 value.push_str(",ro");
3420 }
3421 output.push_str(" - ");
3422 write_quoted(output, &value);
3423 output.push('\n');
3424}
3425
3426fn render_long_mount(output: &mut String, kind: &GeneratedMountKind, mount: &GeneratedMount) {
3427 let (mount_type, source) = match kind {
3428 GeneratedMountKind::Volume { source } => ("volume", Some(source.as_str())),
3429 GeneratedMountKind::Bind { source, selinux: None } => ("bind", Some(source.as_str())),
3430 GeneratedMountKind::Anonymous => ("volume", None),
3431 GeneratedMountKind::Bind { selinux: Some(_), .. } => return,
3432 };
3433 output.push_str(" - type: ");
3434 write_quoted(output, mount_type);
3435 output.push('\n');
3436 if let Some(source) = source {
3437 output.push_str(" source: ");
3438 write_quoted(output, source);
3439 output.push('\n');
3440 }
3441 output.push_str(" target: ");
3442 write_quoted(output, &mount.target);
3443 output.push('\n');
3444 if mount.read_only {
3445 output.push_str(" read_only: true\n");
3446 }
3447}
3448
3449fn render_networks(output: &mut String, networks: &[GeneratedNetworkAttachment]) {
3450 if networks.is_empty() {
3451 return;
3452 }
3453 output.push_str(" networks:\n");
3454 for network in networks {
3455 output.push_str(" ");
3456 write_quoted(output, &network.name);
3457 if network.aliases.is_empty() && network.ipv4_address.is_none() && network.ipv6_address.is_none() {
3458 output.push_str(": {}\n");
3459 continue;
3460 }
3461 output.push_str(":\n");
3462 if !network.aliases.is_empty() {
3463 output.push_str(" aliases:\n");
3464 for alias in &network.aliases {
3465 output.push_str(" - ");
3466 write_quoted(output, alias);
3467 output.push('\n');
3468 }
3469 }
3470 for (field, address) in [
3471 ("ipv4_address", network.ipv4_address.as_ref()),
3472 ("ipv6_address", network.ipv6_address.as_ref()),
3473 ] {
3474 if let Some(address) = address {
3475 output.push_str(" ");
3476 output.push_str(field);
3477 output.push_str(": ");
3478 write_quoted(output, address.expose());
3479 output.push('\n');
3480 }
3481 }
3482 }
3483}
3484
3485fn render_network_definitions(output: &mut String, networks: &[GeneratedNetwork]) {
3486 if networks.is_empty() {
3487 return;
3488 }
3489 output.push_str("networks:\n");
3490 for network in networks {
3491 match network {
3492 GeneratedNetwork::Basic(network) => render_basic_resource(output, network),
3493 GeneratedNetwork::Definition(network) => render_network_definition(output, network),
3494 }
3495 }
3496}
3497
3498fn render_network_definition(output: &mut String, network: &GeneratedNetworkDefinition) {
3499 output.push_str(" ");
3500 write_quoted(output, &network.name);
3501 if network.custom_name.is_none()
3502 && network.driver.is_none()
3503 && network.driver_opts.is_none()
3504 && network.enable_ipv6.is_none()
3505 && network.internal.is_none()
3506 && network.labels.is_none()
3507 {
3508 output.push_str(": {}\n");
3509 return;
3510 }
3511 output.push_str(":\n");
3512 if let Some(custom_name) = &network.custom_name {
3513 output.push_str(" name: ");
3514 write_quoted(output, custom_name);
3515 output.push('\n');
3516 }
3517 if let Some(driver) = &network.driver {
3518 output.push_str(" driver: ");
3519 write_quoted(output, driver.expose());
3520 output.push('\n');
3521 }
3522 if let Some(driver_opts) = &network.driver_opts {
3523 if driver_opts.is_empty() {
3524 output.push_str(" driver_opts: {}\n");
3525 } else {
3526 output.push_str(" driver_opts:\n");
3527 for option in driver_opts {
3528 output.push_str(" ");
3529 write_quoted(output, option.name());
3530 output.push_str(": ");
3531 match option.value() {
3532 GeneratedNetworkDriverOptionValue::String(value) => {
3533 write_quoted(output, value.expose());
3534 }
3535 GeneratedNetworkDriverOptionValue::Number(value) => {
3536 output.push_str(value.expose());
3537 }
3538 }
3539 output.push('\n');
3540 }
3541 }
3542 }
3543 if let Some(enable_ipv6) = network.enable_ipv6 {
3544 output.push_str(" enable_ipv6: ");
3545 output.push_str(if enable_ipv6 { "true\n" } else { "false\n" });
3546 }
3547 if let Some(internal) = network.internal {
3548 output.push_str(" internal: ");
3549 output.push_str(if internal { "true\n" } else { "false\n" });
3550 }
3551 if let Some(labels) = &network.labels {
3552 if labels.is_empty() {
3553 output.push_str(" labels: {}\n");
3554 } else {
3555 output.push_str(" labels:\n");
3556 for label in labels {
3557 output.push_str(" ");
3558 write_quoted(output, label.name());
3559 output.push_str(": ");
3560 write_quoted(output, label.value().expose());
3561 output.push('\n');
3562 }
3563 }
3564 }
3565}
3566
3567fn render_volume_definitions(output: &mut String, volumes: &[GeneratedVolume]) {
3568 if volumes.is_empty() {
3569 return;
3570 }
3571 output.push_str("volumes:\n");
3572 for volume in volumes {
3573 match volume {
3574 GeneratedVolume::Basic(volume) => render_basic_resource(output, volume),
3575 GeneratedVolume::Definition(volume) => render_volume_definition(output, volume),
3576 }
3577 }
3578}
3579
3580fn render_volume_definition(output: &mut String, volume: &GeneratedVolumeDefinition) {
3581 output.push_str(" ");
3582 write_quoted(output, &volume.name);
3583 if volume.custom_name.is_none()
3584 && volume.driver.is_none()
3585 && volume.driver_opts.is_none()
3586 && volume.labels.is_none()
3587 {
3588 output.push_str(": {}\n");
3589 return;
3590 }
3591 output.push_str(":\n");
3592 if let Some(custom_name) = &volume.custom_name {
3593 output.push_str(" name: ");
3594 write_quoted(output, custom_name);
3595 output.push('\n');
3596 }
3597 if let Some(driver) = &volume.driver {
3598 output.push_str(" driver: ");
3599 write_quoted(output, driver.expose());
3600 output.push('\n');
3601 }
3602 if let Some(driver_opts) = &volume.driver_opts {
3603 if driver_opts.is_empty() {
3604 output.push_str(" driver_opts: {}\n");
3605 } else {
3606 output.push_str(" driver_opts:\n");
3607 for option in driver_opts {
3608 output.push_str(" ");
3609 write_quoted(output, option.name());
3610 output.push_str(": ");
3611 match option.value() {
3612 GeneratedVolumeDriverOptionValue::String(value) => write_quoted(output, value.expose()),
3613 GeneratedVolumeDriverOptionValue::Number(value) => output.push_str(value.expose()),
3614 }
3615 output.push('\n');
3616 }
3617 }
3618 }
3619 if let Some(labels) = &volume.labels {
3620 if labels.is_empty() {
3621 output.push_str(" labels: {}\n");
3622 } else {
3623 output.push_str(" labels:\n");
3624 for label in labels {
3625 output.push_str(" ");
3626 write_quoted(output, label.name());
3627 output.push_str(": ");
3628 write_quoted(output, label.value().expose());
3629 output.push('\n');
3630 }
3631 }
3632 }
3633}
3634
3635fn render_basic_resource(output: &mut String, resource: &GeneratedResource) {
3636 output.push_str(" ");
3637 write_quoted(output, &resource.name);
3638 if !resource.external && resource.custom_name.is_none() {
3639 output.push_str(": {}\n");
3640 return;
3641 }
3642 output.push_str(":\n");
3643 if let Some(custom_name) = &resource.custom_name {
3644 output.push_str(" name: ");
3645 write_quoted(output, custom_name);
3646 output.push('\n');
3647 }
3648 if resource.external {
3649 output.push_str(" external: true\n");
3650 }
3651}
3652
3653fn write_field(output: &mut String, depth: usize, key: &str) {
3654 write_indent(output, depth);
3655 output.push_str(key);
3656 output.push_str(": ");
3657}
3658
3659fn write_indent(output: &mut String, depth: usize) {
3660 for _ in 0..depth {
3661 output.push_str(" ");
3662 }
3663}
3664
3665fn required(kind: &'static str, value: String) -> Result<String, GenerationError> {
3666 if value.is_empty() {
3667 return Err(GenerationError::EmptyValue(kind));
3668 }
3669 if value.contains('\0') {
3670 return Err(GenerationError::ContainsNul(kind));
3671 }
3672 Ok(value)
3673}
3674
3675fn require_generated_string(kind: &'static str, value: &GeneratedString) -> Result<(), GenerationError> {
3676 if value.expose().is_empty() {
3677 return Err(GenerationError::EmptyValue(kind));
3678 }
3679 Ok(())
3680}
3681
3682fn validate_generated_device_member(
3683 member: &'static str,
3684 value: &GeneratedString,
3685 require_non_empty: bool,
3686) -> Result<(), GenerationError> {
3687 if valid_generated_device_string(value.expose(), require_non_empty) {
3688 Ok(())
3689 } else {
3690 Err(GenerationError::InvalidDeviceValue(member))
3691 }
3692}
3693
3694fn validate_generated_ulimit_value(value: &GeneratedString) -> Result<(), GenerationError> {
3695 let value = value.expose();
3696 if value.contains(['\r', '\n', '$'])
3697 || (value != "-1" && (value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit())))
3698 {
3699 return Err(GenerationError::InvalidUlimitValue);
3700 }
3701 Ok(())
3702}
3703
3704fn valid_yaml_number(value: &str) -> bool {
3705 let ordinary = !value.is_empty()
3706 && value.bytes().any(|byte| byte.is_ascii_digit())
3707 && value.bytes().all(|byte| {
3708 byte.is_ascii_digit()
3709 || matches!(
3710 byte,
3711 b'+' | b'-'
3712 | b'.'
3713 | b'_'
3714 | b'e'
3715 | b'E'
3716 | b'x'
3717 | b'X'
3718 | b'o'
3719 | b'O'
3720 | b'a'..=b'f'
3721 | b'A'..=b'F'
3722 )
3723 });
3724 let special = matches!(
3725 value,
3726 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF" | ".nan" | ".NaN" | ".NAN"
3727 );
3728 if !ordinary && !special {
3729 return false;
3730 }
3731 let parse = YamlFile::parse(value);
3732 if !parse.ok() {
3733 return false;
3734 }
3735 let file = parse.tree();
3736 let Some(document) = file.document() else {
3737 return false;
3738 };
3739 let Some(scalar) = document.as_scalar() else {
3740 return false;
3741 };
3742 let position = scalar.byte_range();
3743 position.start == 0
3744 && position.end as usize == value.len()
3745 && matches!(
3746 ScalarValue::from_scalar(&scalar).scalar_type(),
3747 ScalarType::Integer | ScalarType::Float
3748 )
3749}
3750
3751fn environment_name(value: String) -> Result<String, GenerationError> {
3752 let value = required("environment name", value)?;
3753 if value.contains('=') {
3754 return Err(GenerationError::InvalidEnvironmentName);
3755 }
3756 Ok(value)
3757}
3758
3759fn valid_container_name(value: &str) -> bool {
3760 let mut bytes = value.bytes();
3761 bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
3762 && bytes
3763 .next()
3764 .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
3765 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
3766}
3767
3768fn short_component(kind: &'static str, value: String, separator: char) -> Result<String, GenerationError> {
3769 let value = required(kind, value)?;
3770 if value.contains(separator) {
3771 return Err(GenerationError::InvalidShortComponent(kind));
3772 }
3773 Ok(value)
3774}
3775
3776fn set_once<T>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), GenerationError> {
3777 if slot.is_some() {
3778 return Err(GenerationError::DuplicateField(field));
3779 }
3780 *slot = Some(value);
3781 Ok(())
3782}
3783
3784fn insert_named<T>(
3785 values: &mut Vec<T>,
3786 value: T,
3787 kind: &'static str,
3788 name: impl Fn(&T) -> &str,
3789) -> Result<(), GenerationError> {
3790 let value_name = name(&value);
3791 if values.iter().any(|candidate| name(candidate) == value_name) {
3792 return Err(GenerationError::DuplicateName {
3793 kind,
3794 name: value_name.to_owned(),
3795 });
3796 }
3797 values.push(value);
3798 Ok(())
3799}
3800
3801fn command_is_sensitive(command: &GeneratedCommand) -> bool {
3802 match command {
3803 GeneratedCommand::Exec(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
3804 GeneratedCommand::Shell(command) => command.is_sensitive(),
3805 GeneratedCommand::Empty => false,
3806 }
3807}
3808
3809fn entrypoint_is_sensitive(entrypoint: &GeneratedEntrypoint) -> bool {
3810 match entrypoint {
3811 GeneratedEntrypoint::List(arguments) => arguments.iter().any(GeneratedString::is_sensitive),
3812 GeneratedEntrypoint::String(entrypoint) => entrypoint.is_sensitive(),
3813 GeneratedEntrypoint::Empty => false,
3814 }
3815}