1use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
4use crate::merge::{
5 EntrySyntax, MergeProvenance, MergedEntry, MergedProject, MergedScalarKind, MergedValue, MergedValueKind,
6};
7use crate::model::{
8 ANNOTATIONS_DUPLICATE_NAME, ANNOTATIONS_EMPTY_NAME, ANNOTATIONS_EXPECTED_STRING, ANNOTATIONS_KEY_ONLY,
9 BUILD_DOCKERFILE_INLINE_CONFLICT, BUILD_NO_CACHE_FILTER_DUPLICATE_ITEM, BindOptions, BlkioScalar, BooleanValue,
10 BuildNoCache, BuildProvenance, BuildSbom, CAP_ADD_DUPLICATE_ITEM, CAP_DROP_DUPLICATE_ITEM, CPU_COUNT_NEGATIVE,
11 CPU_PERCENT_OUT_OF_RANGE, CPU_RT_RUNTIME_INVALID, CapabilityAddItem, CapabilityDropItem, CgroupNamespace, Command,
12 ComposeScalar, ConfigDefinition, CpuCount, CpuPercent, CpuPeriod, CpuQuota, CpuRtPeriod, CpuRtRuntime, Cpus,
13 DEPLOY_ENDPOINT_MODE_PORTABILITY, DEPLOY_MODE_PORTABILITY, DEPLOY_RESERVATION_DEVICE_ALLOCATION_SELECTOR_CONFLICT,
14 DEPLOY_RESERVATION_DEVICE_CAPABILITY_DUPLICATE_ITEM, DEPLOY_RESERVATION_DEVICE_OPTIONS_DUPLICATE_ITEM,
15 DEPLOY_RESERVATION_DEVICE_OPTIONS_INVALID_KEY, DEPLOY_ROLLBACK_CONFIG_ORDER_PORTABILITY,
16 DEPLOY_UPDATE_CONFIG_ORDER_PORTABILITY, DEVELOP_WATCH_EXEC_MISSING_COMMAND, DEVELOP_WATCH_INVALID_ACTION,
17 DEVELOP_WATCH_MISSING_ACTION, DEVELOP_WATCH_MISSING_EXEC, DEVELOP_WATCH_MISSING_PATH, DEVELOP_WATCH_MISSING_TARGET,
18 DEVICE_EXPECTED_FORM, DEVICE_EXPECTED_STRING, DNS_EXPECTED_FORM, DNS_EXPECTED_STRING, DNS_OPT_DUPLICATE_ITEM,
19 DNS_OPT_EXPECTED_SEQUENCE, DNS_OPT_EXPECTED_STRING, DNS_SEARCH_DUPLICATE_ITEM, DNS_SEARCH_EXPECTED_FORM,
20 DNS_SEARCH_EXPECTED_STRING, DependencyCondition, DeployDiscreteResourceValue, DeployEndpointMode, DeployMode,
21 DeployPlacementMaxReplicasPerNode, DeployReplicas, DeployReservationDeviceCount, DeployResourceCpus,
22 DeployResourceMemory, DeployResourcePids, DeployRestartCondition, DeployRestartDuration, DeployRestartMaxAttempts,
23 DeployRollbackMaxFailureRatio, DeployRollbackOrder, DeployRollbackParallelism, DeployUpdateMaxFailureRatio,
24 DeployUpdateOrder, DeployUpdateParallelism, Develop, DevelopWatch, DevelopWatchExec, EXPOSE_DUPLICATE_ITEM,
25 EXPOSE_EXPECTED_SCALAR, EXPOSE_EXPECTED_SEQUENCE, EXPOSE_INVALID_ITEM, EXPOSE_PROVIDER_DEPENDENT,
26 EXTENDS_MISSING_SERVICE, Entrypoint, Environment, EnvironmentFileFormat, EnvironmentFileFormatKind,
27 EnvironmentListEntry, EnvironmentMapEntry, ExposeItemKind, ExposeScalarKind, ExternalNameMapping, FieldReference,
28 GPU_MISSING_CAPABILITIES, GpuDevice, GpuOptions, Gpus, HealthcheckDuration, HealthcheckRetries, HealthcheckTest,
29 HealthcheckTestKind, HostAddress, Hostname, HostnameKind, ImageMountOptions, ImageReference, IncludeItem,
30 IncludeLong, Includes, Ipam, IpamConfig, IpcMode, KeyValueEntry, LOGGING_DRIVER_EXPECTED_STRING,
31 LOGGING_EXPECTED_MAPPING, LOGGING_OPTION_EMPTY_KEY, LOGGING_OPTION_EXPECTED_SCALAR,
32 LOGGING_OPTIONS_EXPECTED_MAPPING, Labels, LimitValue, Located, LongPort, LongVolumeMount, MEM_LIMIT_AMBIGUOUS_ZERO,
33 MEM_LIMIT_EXPECTED_VALUE, MEM_LIMIT_PROVIDER_DEPENDENT_STRING, MEM_LIMIT_SCHEMA_NUMBER,
34 MEMSWAP_LIMIT_EXPECTED_VALUE, MEMSWAP_LIMIT_INVALID, MemLimit, MemLimitKind, MemLimitScalarKind, MemswapLimit,
35 MemswapLimitKind, MemswapLimitScalarKind, ModelDefinition, ModelDefinitions, MountType, NetworkDefinition,
36 NetworkMode, PIDS_LIMIT_AMBIGUOUS_ZERO, POST_START_MISSING_COMMAND, PRE_STOP_MISSING_COMMAND,
37 PROVIDER_MISSING_TYPE, PidMode, PidsLimit, PidsLimitKind, Port, PullPolicy, ResourceExternal, RestartPolicy,
38 SECURITY_OPT_APPARMOR_CONFLICT, SECURITY_OPT_APPARMOR_NEAR_MISS, SECURITY_OPT_EMPTY_ITEM,
39 SECURITY_OPT_EXPECTED_SEQUENCE, SECURITY_OPT_EXPECTED_STRING, SECURITY_OPT_NO_NEW_PRIVILEGES_CONFLICT,
40 SECURITY_OPT_NO_NEW_PRIVILEGES_NEAR_MISS, SECURITY_OPT_SECCOMP_CONFLICT, SECURITY_OPT_SECCOMP_NEAR_MISS,
41 SECURITY_OPT_SECURITY_LABEL_DISABLE_CONFLICT, SECURITY_OPT_SECURITY_LABEL_DISABLE_NEAR_MISS,
42 SECURITY_OPT_SECURITY_LABEL_FILETYPE_CONFLICT, SECURITY_OPT_SECURITY_LABEL_FILETYPE_NEAR_MISS,
43 SECURITY_OPT_SECURITY_LABEL_LEVEL_CONFLICT, SECURITY_OPT_SECURITY_LABEL_LEVEL_NEAR_MISS,
44 SECURITY_OPT_SECURITY_LABEL_NESTED_CONFLICT, SECURITY_OPT_SECURITY_LABEL_NESTED_NEAR_MISS,
45 SECURITY_OPT_SECURITY_LABEL_TYPE_CONFLICT, SECURITY_OPT_SECURITY_LABEL_TYPE_NEAR_MISS, SHM_SIZE_AMBIGUOUS_ZERO,
46 SHM_SIZE_EXPECTED_VALUE, SHM_SIZE_PROVIDER_DEPENDENT_NUMBER, SHM_SIZE_PROVIDER_DEPENDENT_STRING,
47 SYSCTLS_DUPLICATE_ITEM, SYSCTLS_EMPTY_KEY, SYSCTLS_EXPECTED_FORM, SYSCTLS_EXPECTED_SCALAR, SYSCTLS_EXPECTED_STRING,
48 SecretDefinition, SecurityOptionCandidateCounts, SecurityOptionKind, SelinuxRelabel, ServiceInteger,
49 ServiceModelBinding, ServiceModels, ServiceNetwork, ServiceNetworks, ShmSize, ShmSizeKind, ShmSizeScalarKind,
50 ShortDevice, ShortExtraHost, ShortPort, ShortVolumeMount, StopGracePeriod, TMPFS_EXPECTED_FORM,
51 TMPFS_EXPECTED_STRING, TMPFS_PROVIDER_DEPENDENT, TmpfsItem, TmpfsItemKind, TmpfsMountOptions, ULIMIT_INVALID_NAME,
52 ULIMIT_INVALID_VALUE, ULIMIT_MISSING_RANGE_MEMBER, UserNamespaceMode, UserSpec,
53 VOLUME_EXTERNAL_DRIVER_CONFIGURATION, VOLUME_EXTERNAL_LABELS_CONFIGURATION, VolumeDefinition, VolumeMount,
54 VolumeMountOptions, VolumesFrom, classify_expose_item, classify_security_option, security_path_option_diagnostic,
55 valid_ulimit_name,
56};
57use crate::profiles::ProfileSelection;
58use crate::resolution::{SELECTION_PROJECT_MISMATCH, service_in_scope};
59use crate::source::{SourceId, SourceSpan};
60use std::collections::{BTreeMap, BTreeSet};
61use std::fmt;
62use std::path::{Path, PathBuf};
63use std::sync::Arc;
64
65pub const PROJECT_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.project.expected-form");
67
68pub const PROJECT_MISSING_FIELD: DiagnosticCode = DiagnosticCode::new("compose.project.missing-field");
70
71pub const PROJECT_INVALID_VALUE: DiagnosticCode = DiagnosticCode::new("compose.project.invalid-value");
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct ProjectInvalidServiceStringItem {
76 span: SourceSpan,
77}
78
79type ProjectStringCollection = Option<ProjectValue<Vec<ProjectValue<String>>>>;
80type ProjectVolumesFromCollection = Option<ProjectValue<Vec<ProjectValue<VolumesFrom>>>>;
81type ProjectInvalidServiceStringItems = Vec<ProjectInvalidServiceStringItem>;
82impl ProjectInvalidServiceStringItem {
83 #[must_use]
85 pub const fn span(self) -> SourceSpan {
86 self.span
87 }
88}
89pub const SERVICE_CPUS_DEPLOY_LIMIT_CONFLICT: DiagnosticCode =
91 DiagnosticCode::new("compose.service.cpus.deploy-limit-conflict");
92pub const SERVICE_MEMORY_RESERVATION_DEPLOY_CONFLICT: DiagnosticCode =
94 DiagnosticCode::new("compose.service.mem-reservation.deploy-reservation-conflict");
95pub const SERVICE_MEMSWAP_REQUIRES_MEMORY_LIMIT: DiagnosticCode =
97 DiagnosticCode::new("compose.service.memswap-limit.requires-mem-limit");
98pub const SERVICE_MEMSWAP_BELOW_MEMORY_LIMIT: DiagnosticCode =
100 DiagnosticCode::new("compose.service.memswap-limit.below-mem-limit");
101pub const SERVICE_NETWORK_MODE_NETWORKS_CONFLICT: DiagnosticCode =
103 DiagnosticCode::new("compose.service.network-mode.networks-conflict");
104pub const SERVICE_SCALE_DEPLOY_REPLICAS_CONFLICT: DiagnosticCode =
106 DiagnosticCode::new("compose.service.scale.deploy-replicas-conflict");
107pub const SERVICE_GPUS_DEPLOY_DEVICES_CONFLICT: DiagnosticCode =
109 DiagnosticCode::new("compose.service.gpus.deploy-devices-conflict");
110
111#[derive(Clone, PartialEq, Eq)]
113pub struct ProjectValue<T> {
114 value: T,
115 provenance: MergeProvenance,
116 sensitive: bool,
117}
118
119impl<T: fmt::Debug> fmt::Debug for ProjectValue<T> {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 let mut debug = formatter.debug_struct("ProjectValue");
122 if self.sensitive {
123 debug.field("value", &"<redacted>");
124 } else {
125 debug.field("value", &self.value);
126 }
127 debug
128 .field("provenance", &self.provenance)
129 .field("sensitive", &self.sensitive)
130 .finish()
131 }
132}
133
134impl<T> ProjectValue<T> {
135 fn new(value: T, source: &MergedValue) -> Self {
136 Self {
137 value,
138 provenance: source.provenance().clone(),
139 sensitive: source.is_sensitive(),
140 }
141 }
142
143 fn new_sensitive(value: T, source: &MergedValue) -> Self {
144 Self {
145 value,
146 provenance: source.provenance().clone(),
147 sensitive: true,
148 }
149 }
150
151 #[must_use]
155 pub const fn value(&self) -> &T {
156 &self.value
157 }
158
159 #[must_use]
162 pub const fn provenance(&self) -> &MergeProvenance {
163 &self.provenance
164 }
165
166 #[must_use]
169 pub fn effective_source(&self) -> Option<SourceSpan> {
170 self.provenance.effective_source()
171 }
172
173 #[must_use]
176 pub const fn is_sensitive(&self) -> bool {
177 self.sensitive
178 }
179
180 #[must_use]
183 pub fn into_value(self) -> T {
184 self.value
185 }
186}
187
188#[derive(Clone, PartialEq, Eq)]
190pub struct ProjectKey {
191 value: String,
192 sources: Vec<SourceSpan>,
193 sensitive: bool,
194}
195
196impl fmt::Debug for ProjectKey {
197 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198 formatter
199 .debug_struct("ProjectKey")
200 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
201 .field("sources", &self.sources)
202 .field("sensitive", &self.sensitive)
203 .finish()
204 }
205}
206
207impl ProjectKey {
208 fn from_entry(entry: &MergedEntry) -> Self {
209 Self {
210 value: entry.key().to_owned(),
211 sources: entry.key_sources().to_vec(),
212 sensitive: entry.is_key_sensitive(),
213 }
214 }
215
216 fn from_sensitive_entry(entry: &MergedEntry) -> Self {
217 Self {
218 value: entry.key().to_owned(),
219 sources: entry.key_sources().to_vec(),
220 sensitive: true,
221 }
222 }
223
224 fn from_value(value: String, source: &MergedValue) -> Self {
225 Self {
226 value,
227 sources: source.provenance().sources().to_vec(),
228 sensitive: source.is_sensitive(),
229 }
230 }
231
232 #[must_use]
235 pub fn value(&self) -> &str {
236 &self.value
237 }
238
239 #[must_use]
242 pub fn sources(&self) -> &[SourceSpan] {
243 &self.sources
244 }
245
246 #[must_use]
248 pub fn effective_source(&self) -> Option<SourceSpan> {
249 self.sources.last().copied()
250 }
251
252 #[must_use]
254 pub const fn is_sensitive(&self) -> bool {
255 self.sensitive
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct ProjectServiceDependency {
262 service: ProjectKey,
263 condition: Option<ProjectValue<DependencyCondition>>,
264 restart: Option<ProjectValue<BooleanValue>>,
265 required: Option<ProjectValue<BooleanValue>>,
266 unmodeled_fields: Vec<ProjectFieldReference>,
267}
268
269impl ProjectServiceDependency {
270 #[must_use]
272 pub const fn service(&self) -> &ProjectKey {
273 &self.service
274 }
275
276 #[must_use]
278 pub const fn condition(&self) -> Option<&ProjectValue<DependencyCondition>> {
279 self.condition.as_ref()
280 }
281
282 #[must_use]
284 pub const fn restart(&self) -> Option<&ProjectValue<BooleanValue>> {
285 self.restart.as_ref()
286 }
287
288 #[must_use]
290 pub const fn required(&self) -> Option<&ProjectValue<BooleanValue>> {
291 self.required.as_ref()
292 }
293
294 #[must_use]
296 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
297 &self.unmodeled_fields
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303pub enum ProjectDependsOn {
304 Short(Vec<ProjectValue<ProjectServiceDependency>>),
306 Long(Vec<ProjectValue<ProjectServiceDependency>>),
308}
309
310impl ProjectDependsOn {
311 #[must_use]
313 pub fn services(&self) -> &[ProjectValue<ProjectServiceDependency>] {
314 match self {
315 Self::Short(services) | Self::Long(services) => services,
316 }
317 }
318
319 #[must_use]
321 pub const fn is_long(&self) -> bool {
322 matches!(self, Self::Long(_))
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct ProjectFieldReference {
329 path: Vec<String>,
330 key: ProjectKey,
331 provenance: MergeProvenance,
332 extension: bool,
333 sensitive: bool,
334}
335
336impl ProjectFieldReference {
337 #[must_use]
339 pub fn path(&self) -> &[String] {
340 &self.path
341 }
342
343 #[must_use]
345 pub const fn key(&self) -> &ProjectKey {
346 &self.key
347 }
348
349 #[must_use]
351 pub const fn provenance(&self) -> &MergeProvenance {
352 &self.provenance
353 }
354
355 #[must_use]
357 pub const fn is_extension(&self) -> bool {
358 self.extension
359 }
360
361 #[must_use]
363 pub const fn is_sensitive(&self) -> bool {
364 self.sensitive
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct ProjectEnvironmentEntry {
371 name: ProjectKey,
372 value: ProjectValue<ComposeScalar>,
373 syntax: EntrySyntax,
374}
375
376impl ProjectEnvironmentEntry {
377 #[must_use]
379 pub const fn name(&self) -> &ProjectKey {
380 &self.name
381 }
382
383 #[must_use]
385 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
386 &self.value
387 }
388
389 #[must_use]
391 pub const fn syntax(&self) -> EntrySyntax {
392 self.syntax
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq)]
398pub struct ProjectEnvironment {
399 entries: Vec<ProjectEnvironmentEntry>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
404pub enum ProjectEnvironmentFile {
405 Short(String),
407 Long(Box<ProjectLongEnvironmentFile>),
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct ProjectLongEnvironmentFile {
414 path: Option<ProjectValue<String>>,
415 required: Option<ProjectValue<BooleanValue>>,
416 format: Option<ProjectValue<EnvironmentFileFormat>>,
417 unmodeled_fields: Vec<ProjectFieldReference>,
418}
419
420impl ProjectLongEnvironmentFile {
421 #[must_use]
423 pub const fn path(&self) -> Option<&ProjectValue<String>> {
424 self.path.as_ref()
425 }
426
427 #[must_use]
429 pub const fn required(&self) -> Option<&ProjectValue<BooleanValue>> {
430 self.required.as_ref()
431 }
432
433 #[must_use]
435 pub const fn format(&self) -> Option<&ProjectValue<EnvironmentFileFormat>> {
436 self.format.as_ref()
437 }
438
439 #[must_use]
441 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
442 &self.unmodeled_fields
443 }
444}
445
446#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct ProjectLabelEntry {
449 name: ProjectKey,
450 value: ProjectValue<ComposeScalar>,
451 syntax: EntrySyntax,
452}
453
454impl ProjectLabelEntry {
455 #[must_use]
457 pub const fn name(&self) -> &ProjectKey {
458 &self.name
459 }
460
461 #[must_use]
466 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
467 &self.value
468 }
469
470 #[must_use]
472 pub const fn syntax(&self) -> EntrySyntax {
473 self.syntax
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct ProjectLabels {
480 form: ProjectLabelsForm,
481 entries: Vec<ProjectLabelEntry>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486#[non_exhaustive]
487pub enum ProjectLabelsForm {
488 Map,
490 List,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
499#[non_exhaustive]
500pub enum ProjectBuildLabels {
501 Map(Vec<ProjectLabelEntry>),
503 List(Vec<ProjectValue<String>>),
505}
506
507#[derive(Debug, Clone, PartialEq, Eq)]
509pub struct ProjectBuildArgEntry {
510 name: ProjectKey,
511 value: ProjectValue<ComposeScalar>,
512}
513
514impl ProjectBuildArgEntry {
515 #[must_use]
517 pub const fn name(&self) -> &ProjectKey {
518 &self.name
519 }
520
521 #[must_use]
523 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
524 &self.value
525 }
526}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
533#[non_exhaustive]
534pub enum ProjectBuildArgs {
535 Map(Vec<ProjectBuildArgEntry>),
537 List(Vec<ProjectValue<String>>),
539}
540
541#[derive(Debug, Clone, PartialEq, Eq)]
543pub struct ProjectBuildAdditionalContextEntry {
544 name: ProjectKey,
545 value: ProjectValue<ComposeScalar>,
546}
547
548impl ProjectBuildAdditionalContextEntry {
549 #[must_use]
551 pub const fn name(&self) -> &ProjectKey {
552 &self.name
553 }
554
555 #[must_use]
557 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
558 &self.value
559 }
560}
561
562#[derive(Debug, Clone, PartialEq, Eq)]
568#[non_exhaustive]
569pub enum ProjectBuildAdditionalContexts {
570 Map(Vec<ProjectBuildAdditionalContextEntry>),
572 List(Vec<ProjectValue<String>>),
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct ProjectBuildExtraHostEntry {
579 hostname: ProjectKey,
580 addresses: ProjectBuildExtraHostAddresses,
581}
582
583impl ProjectBuildExtraHostEntry {
584 #[must_use]
586 pub const fn hostname(&self) -> &ProjectKey {
587 &self.hostname
588 }
589
590 #[must_use]
592 pub const fn addresses(&self) -> &ProjectBuildExtraHostAddresses {
593 &self.addresses
594 }
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
599#[non_exhaustive]
600pub enum ProjectBuildExtraHostAddresses {
601 Scalar(ProjectValue<String>),
603 List(Vec<ProjectValue<String>>),
605}
606
607impl ProjectBuildExtraHostAddresses {
608 #[must_use]
610 pub const fn as_scalar(&self) -> Option<&ProjectValue<String>> {
611 match self {
612 Self::Scalar(value) => Some(value),
613 Self::List(_) => None,
614 }
615 }
616
617 #[must_use]
619 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
620 let Self::List(values) = self else {
621 return None;
622 };
623 Some(values)
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq)]
632#[non_exhaustive]
633pub enum ProjectBuildExtraHosts {
634 List(Vec<ProjectValue<String>>),
636 Map(Vec<ProjectBuildExtraHostEntry>),
638}
639
640impl ProjectBuildExtraHosts {
641 #[must_use]
643 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
644 let Self::List(entries) = self else {
645 return None;
646 };
647 Some(entries)
648 }
649
650 #[must_use]
652 pub fn as_map(&self) -> Option<&[ProjectBuildExtraHostEntry]> {
653 let Self::Map(entries) = self else {
654 return None;
655 };
656 Some(entries)
657 }
658}
659
660#[derive(Clone, PartialEq, Eq)]
662pub struct ProjectBuildSshEntry {
663 name: ProjectKey,
664 value: ProjectValue<ComposeScalar>,
665}
666
667impl fmt::Debug for ProjectBuildSshEntry {
668 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
669 formatter
670 .debug_struct("ProjectBuildSshEntry")
671 .field("name", &"<redacted>")
672 .field("value", &"<redacted>")
673 .finish()
674 }
675}
676
677impl ProjectBuildSshEntry {
678 #[must_use]
680 pub const fn name(&self) -> &ProjectKey {
681 &self.name
682 }
683
684 #[must_use]
686 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
687 &self.value
688 }
689}
690
691#[derive(Clone, PartialEq, Eq)]
693#[non_exhaustive]
694pub enum ProjectBuildSsh {
695 Map(Vec<ProjectBuildSshEntry>),
697 List(Vec<ProjectValue<String>>),
699}
700
701impl fmt::Debug for ProjectBuildSsh {
702 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
703 let form = match self {
704 Self::Map(_) => "Map",
705 Self::List(_) => "List",
706 };
707 formatter
708 .debug_struct("ProjectBuildSsh")
709 .field("form", &form)
710 .field("entries", &"<redacted>")
711 .finish()
712 }
713}
714
715impl ProjectBuildSsh {
716 #[must_use]
718 pub fn as_map(&self) -> Option<&[ProjectBuildSshEntry]> {
719 let Self::Map(entries) = self else {
720 return None;
721 };
722 Some(entries)
723 }
724
725 #[must_use]
727 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
728 let Self::List(entries) = self else {
729 return None;
730 };
731 Some(entries)
732 }
733}
734
735impl ProjectBuildAdditionalContexts {
736 #[must_use]
738 pub fn as_map(&self) -> Option<&[ProjectBuildAdditionalContextEntry]> {
739 let Self::Map(entries) = self else {
740 return None;
741 };
742 Some(entries)
743 }
744
745 #[must_use]
747 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
748 let Self::List(entries) = self else {
749 return None;
750 };
751 Some(entries)
752 }
753}
754
755impl ProjectBuildArgs {
756 #[must_use]
758 pub fn as_map(&self) -> Option<&[ProjectBuildArgEntry]> {
759 let Self::Map(entries) = self else {
760 return None;
761 };
762 Some(entries)
763 }
764
765 #[must_use]
767 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
768 let Self::List(entries) = self else {
769 return None;
770 };
771 Some(entries)
772 }
773}
774
775impl ProjectBuildLabels {
776 #[must_use]
778 pub fn as_map(&self) -> Option<&[ProjectLabelEntry]> {
779 let Self::Map(entries) = self else {
780 return None;
781 };
782 Some(entries)
783 }
784
785 #[must_use]
787 pub fn as_list(&self) -> Option<&[ProjectValue<String>]> {
788 let Self::List(entries) = self else {
789 return None;
790 };
791 Some(entries)
792 }
793}
794
795#[derive(Debug, Clone, PartialEq, Eq)]
797pub struct ProjectAnnotationScalar {
798 authored: String,
799 effective: ComposeScalar,
800}
801
802impl ProjectAnnotationScalar {
803 #[must_use]
805 pub fn authored(&self) -> &str {
806 &self.authored
807 }
808
809 #[must_use]
811 pub const fn effective(&self) -> &ComposeScalar {
812 &self.effective
813 }
814}
815
816#[derive(Debug, Clone, PartialEq, Eq)]
818pub struct ProjectAnnotationEntry {
819 name: ProjectKey,
820 value: Option<ProjectValue<ProjectAnnotationScalar>>,
821 raw_list_item: Option<ProjectValue<ProjectAnnotationScalar>>,
822 syntax: EntrySyntax,
823 contributors: Vec<MergeProvenance>,
824}
825
826impl ProjectAnnotationEntry {
827 #[must_use]
829 pub const fn name(&self) -> &ProjectKey {
830 &self.name
831 }
832
833 #[must_use]
837 pub const fn value(&self) -> Option<&ProjectValue<ProjectAnnotationScalar>> {
838 self.value.as_ref()
839 }
840
841 #[must_use]
843 pub const fn raw_list_item(&self) -> Option<&ProjectValue<ProjectAnnotationScalar>> {
844 self.raw_list_item.as_ref()
845 }
846
847 #[must_use]
849 pub const fn syntax(&self) -> EntrySyntax {
850 self.syntax
851 }
852
853 #[must_use]
855 pub fn contributors(&self) -> &[MergeProvenance] {
856 &self.contributors
857 }
858}
859
860#[derive(Debug, Clone, PartialEq, Eq)]
862pub struct ProjectAnnotations {
863 entries: Vec<ProjectAnnotationEntry>,
864}
865
866impl ProjectAnnotations {
867 #[must_use]
869 pub fn entries(&self) -> &[ProjectAnnotationEntry] {
870 &self.entries
871 }
872
873 #[must_use]
875 pub fn get(&self, name: &str) -> Option<&ProjectAnnotationEntry> {
876 self.entries.iter().find(|entry| entry.name.value == name)
877 }
878}
879
880impl ProjectLabels {
881 #[must_use]
883 pub const fn form(&self) -> ProjectLabelsForm {
884 self.form
885 }
886
887 #[must_use]
889 pub fn entries(&self) -> &[ProjectLabelEntry] {
890 &self.entries
891 }
892
893 #[must_use]
895 pub fn get(&self, name: &str) -> Option<&ProjectLabelEntry> {
896 self.entries.iter().find(|entry| entry.name.value == name)
897 }
898}
899
900#[derive(Debug, Clone, PartialEq, Eq)]
902pub struct ProjectExtraHost {
903 hostname: ProjectKey,
904 address: ProjectValue<HostAddress>,
905 syntax: EntrySyntax,
906}
907
908impl ProjectExtraHost {
909 #[must_use]
911 pub const fn hostname(&self) -> &ProjectKey {
912 &self.hostname
913 }
914
915 #[must_use]
917 pub const fn address(&self) -> &ProjectValue<HostAddress> {
918 &self.address
919 }
920
921 #[must_use]
923 pub const fn syntax(&self) -> EntrySyntax {
924 self.syntax
925 }
926}
927
928#[derive(Debug, Clone, PartialEq, Eq)]
930pub struct ProjectExtraHosts {
931 entries: Vec<ProjectExtraHost>,
932}
933
934impl ProjectExtraHosts {
935 #[must_use]
937 pub fn entries(&self) -> &[ProjectExtraHost] {
938 &self.entries
939 }
940}
941
942impl ProjectEnvironment {
943 #[must_use]
945 pub fn entries(&self) -> &[ProjectEnvironmentEntry] {
946 &self.entries
947 }
948
949 #[must_use]
951 pub fn get(&self, name: &str) -> Option<&ProjectEnvironmentEntry> {
952 self.entries.iter().find(|entry| entry.name.value == name)
953 }
954}
955
956#[derive(Debug, Clone, PartialEq, Eq)]
958pub struct ProjectHealthcheck {
959 test: Option<ProjectValue<HealthcheckTest>>,
960 interval: Option<ProjectValue<HealthcheckDuration>>,
961 timeout: Option<ProjectValue<HealthcheckDuration>>,
962 retries: Option<ProjectValue<HealthcheckRetries>>,
963 start_period: Option<ProjectValue<HealthcheckDuration>>,
964 start_interval: Option<ProjectValue<HealthcheckDuration>>,
965 disable: Option<ProjectValue<BooleanValue>>,
966 unmodeled_fields: Vec<ProjectFieldReference>,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq)]
971pub struct ProjectLongGrant {
972 source: Option<ProjectValue<String>>,
973 target: Option<ProjectValue<String>>,
974 uid: Option<ProjectValue<String>>,
975 gid: Option<ProjectValue<String>>,
976 mode: Option<ProjectValue<String>>,
977 unmodeled_fields: Vec<ProjectFieldReference>,
978}
979
980impl ProjectLongGrant {
981 #[must_use]
983 pub const fn source(&self) -> Option<&ProjectValue<String>> {
984 self.source.as_ref()
985 }
986
987 #[must_use]
989 pub const fn target(&self) -> Option<&ProjectValue<String>> {
990 self.target.as_ref()
991 }
992
993 #[must_use]
995 pub const fn uid(&self) -> Option<&ProjectValue<String>> {
996 self.uid.as_ref()
997 }
998
999 #[must_use]
1001 pub const fn gid(&self) -> Option<&ProjectValue<String>> {
1002 self.gid.as_ref()
1003 }
1004
1005 #[must_use]
1007 pub const fn mode(&self) -> Option<&ProjectValue<String>> {
1008 self.mode.as_ref()
1009 }
1010
1011 #[must_use]
1013 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1014 &self.unmodeled_fields
1015 }
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq)]
1020pub enum ProjectGrant {
1021 Short(String),
1023 Long(Box<ProjectLongGrant>),
1025}
1026
1027#[derive(Debug, Clone, PartialEq, Eq)]
1029pub struct ProjectLongDevice {
1030 source: Option<ProjectValue<String>>,
1031 target: Option<ProjectValue<String>>,
1032 permissions: Option<ProjectValue<String>>,
1033 extension_fields: Vec<ProjectFieldReference>,
1034 unknown_fields: Vec<ProjectFieldReference>,
1035}
1036
1037impl ProjectLongDevice {
1038 #[must_use]
1040 pub const fn source(&self) -> Option<&ProjectValue<String>> {
1041 self.source.as_ref()
1042 }
1043
1044 #[must_use]
1046 pub const fn target(&self) -> Option<&ProjectValue<String>> {
1047 self.target.as_ref()
1048 }
1049
1050 #[must_use]
1052 pub const fn permissions(&self) -> Option<&ProjectValue<String>> {
1053 self.permissions.as_ref()
1054 }
1055
1056 #[must_use]
1058 pub fn extension_fields(&self) -> &[ProjectFieldReference] {
1059 &self.extension_fields
1060 }
1061
1062 #[must_use]
1064 pub fn unknown_fields(&self) -> &[ProjectFieldReference] {
1065 &self.unknown_fields
1066 }
1067}
1068
1069#[derive(Debug, Clone, PartialEq, Eq)]
1071#[non_exhaustive]
1072pub enum ProjectDevice {
1073 Short(ShortDevice),
1075 Long(ProjectLongDevice),
1077}
1078
1079impl ProjectHealthcheck {
1080 #[must_use]
1082 pub const fn test(&self) -> Option<&ProjectValue<HealthcheckTest>> {
1083 self.test.as_ref()
1084 }
1085
1086 #[must_use]
1088 pub const fn interval(&self) -> Option<&ProjectValue<HealthcheckDuration>> {
1089 self.interval.as_ref()
1090 }
1091
1092 #[must_use]
1094 pub const fn timeout(&self) -> Option<&ProjectValue<HealthcheckDuration>> {
1095 self.timeout.as_ref()
1096 }
1097
1098 #[must_use]
1100 pub const fn retries(&self) -> Option<&ProjectValue<HealthcheckRetries>> {
1101 self.retries.as_ref()
1102 }
1103
1104 #[must_use]
1106 pub const fn start_period(&self) -> Option<&ProjectValue<HealthcheckDuration>> {
1107 self.start_period.as_ref()
1108 }
1109
1110 #[must_use]
1112 pub const fn start_interval(&self) -> Option<&ProjectValue<HealthcheckDuration>> {
1113 self.start_interval.as_ref()
1114 }
1115
1116 #[must_use]
1118 pub const fn disable(&self) -> Option<&ProjectValue<BooleanValue>> {
1119 self.disable.as_ref()
1120 }
1121
1122 #[must_use]
1124 pub fn is_disabled(&self) -> bool {
1125 matches!(
1126 self.disable.as_ref().map(ProjectValue::value),
1127 Some(BooleanValue::Literal(true))
1128 ) || matches!(
1129 self.test.as_ref().and_then(|test| test.value().kind()),
1130 Some(HealthcheckTestKind::None)
1131 )
1132 }
1133
1134 #[must_use]
1136 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1137 &self.unmodeled_fields
1138 }
1139}
1140
1141#[derive(Debug, Clone, PartialEq, Eq)]
1143#[non_exhaustive]
1144pub enum ProjectTmpfs {
1145 Scalar(ProjectValue<TmpfsItem>),
1147 List(Vec<ProjectValue<TmpfsItem>>),
1149}
1150
1151#[derive(Debug, Clone, PartialEq, Eq)]
1153#[non_exhaustive]
1154pub enum ProjectDns {
1155 Scalar(ProjectValue<String>),
1157 List(Vec<ProjectValue<String>>),
1159}
1160
1161#[derive(Debug, Clone, PartialEq, Eq)]
1163#[non_exhaustive]
1164pub enum ProjectDnsSearch {
1165 Scalar(ProjectValue<String>),
1167 List(Vec<ProjectValue<String>>),
1169}
1170
1171#[derive(Debug, Clone, PartialEq, Eq)]
1173#[non_exhaustive]
1174pub enum ProjectLabelFiles {
1175 Scalar(ProjectValue<String>),
1177 List(Vec<ProjectValue<String>>),
1179}
1180
1181#[derive(Debug, Clone, PartialEq, Eq)]
1183pub struct ProjectLabelFileEvidence {
1184 reference: ProjectFieldReference,
1185}
1186
1187impl ProjectLabelFileEvidence {
1188 fn new(reference: ProjectFieldReference) -> Self {
1189 Self { reference }
1190 }
1191
1192 #[must_use]
1194 pub const fn reference(&self) -> &ProjectFieldReference {
1195 &self.reference
1196 }
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Eq)]
1201pub struct ProjectServiceLabelFiles {
1202 form: ProjectLabelFiles,
1203 unmodeled_items: Vec<ProjectLabelFileEvidence>,
1204}
1205
1206impl ProjectServiceLabelFiles {
1207 fn new(form: ProjectLabelFiles, unmodeled_items: Vec<ProjectLabelFileEvidence>) -> Self {
1208 Self { form, unmodeled_items }
1209 }
1210
1211 #[must_use]
1213 pub const fn form(&self) -> &ProjectLabelFiles {
1214 &self.form
1215 }
1216
1217 #[must_use]
1219 pub fn unmodeled_items(&self) -> &[ProjectLabelFileEvidence] {
1220 &self.unmodeled_items
1221 }
1222}
1223
1224#[derive(Debug, Clone, PartialEq, Eq)]
1226pub struct ProjectExposeItem {
1227 authored: String,
1228 value: String,
1229 scalar_kind: ExposeScalarKind,
1230 kind: ExposeItemKind,
1231}
1232
1233#[derive(Debug, Clone, PartialEq, Eq)]
1235pub struct ProjectSecurityOptionItem {
1236 authored: String,
1237 value: String,
1238 scalar_kind: MergedScalarKind,
1239 kind: SecurityOptionKind,
1240}
1241
1242impl ProjectSecurityOptionItem {
1243 #[must_use]
1245 pub fn authored(&self) -> &str {
1246 &self.authored
1247 }
1248
1249 #[must_use]
1251 pub fn value(&self) -> &str {
1252 &self.value
1253 }
1254
1255 #[must_use]
1257 pub const fn scalar_kind(&self) -> MergedScalarKind {
1258 self.scalar_kind
1259 }
1260
1261 #[must_use]
1263 pub const fn kind(&self) -> &SecurityOptionKind {
1264 &self.kind
1265 }
1266}
1267
1268impl ProjectExposeItem {
1269 #[must_use]
1271 pub fn authored(&self) -> &str {
1272 &self.authored
1273 }
1274
1275 #[must_use]
1277 pub fn value(&self) -> &str {
1278 &self.value
1279 }
1280
1281 #[must_use]
1283 pub const fn scalar_kind(&self) -> ExposeScalarKind {
1284 self.scalar_kind
1285 }
1286
1287 #[must_use]
1289 pub const fn kind(&self) -> &ExposeItemKind {
1290 &self.kind
1291 }
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Eq)]
1296pub struct ProjectUlimitScalar {
1297 authored: String,
1298 value: LimitValue,
1299 kind: MergedScalarKind,
1300}
1301
1302impl ProjectUlimitScalar {
1303 #[must_use]
1305 pub fn authored(&self) -> &str {
1306 &self.authored
1307 }
1308
1309 #[must_use]
1311 pub const fn value(&self) -> &LimitValue {
1312 &self.value
1313 }
1314
1315 #[must_use]
1317 pub const fn kind(&self) -> MergedScalarKind {
1318 self.kind
1319 }
1320}
1321
1322#[derive(Debug, Clone, PartialEq, Eq)]
1324pub struct ProjectUlimitRange {
1325 soft: Option<ProjectValue<ProjectUlimitScalar>>,
1326 hard: Option<ProjectValue<ProjectUlimitScalar>>,
1327 unmodeled_fields: Vec<ProjectFieldReference>,
1328}
1329
1330impl ProjectUlimitRange {
1331 #[must_use]
1333 pub const fn soft(&self) -> Option<&ProjectValue<ProjectUlimitScalar>> {
1334 self.soft.as_ref()
1335 }
1336
1337 #[must_use]
1339 pub const fn hard(&self) -> Option<&ProjectValue<ProjectUlimitScalar>> {
1340 self.hard.as_ref()
1341 }
1342
1343 #[must_use]
1345 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1346 &self.unmodeled_fields
1347 }
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Eq)]
1352#[non_exhaustive]
1353pub enum ProjectUlimitValue {
1354 Single(ProjectValue<ProjectUlimitScalar>),
1356 Range(ProjectUlimitRange),
1358}
1359
1360#[derive(Debug, Clone, PartialEq, Eq)]
1362pub struct ProjectUlimit {
1363 name: ProjectKey,
1364 value: ProjectUlimitValue,
1365}
1366
1367impl ProjectUlimit {
1368 #[must_use]
1370 pub const fn name(&self) -> &ProjectKey {
1371 &self.name
1372 }
1373
1374 #[must_use]
1376 pub const fn value(&self) -> &ProjectUlimitValue {
1377 &self.value
1378 }
1379}
1380
1381#[derive(Debug, Clone, PartialEq, Eq)]
1383pub struct ProjectUlimits {
1384 entries: Vec<ProjectValue<ProjectUlimit>>,
1385}
1386
1387impl ProjectUlimits {
1388 #[must_use]
1390 pub fn entries(&self) -> &[ProjectValue<ProjectUlimit>] {
1391 &self.entries
1392 }
1393
1394 #[must_use]
1396 pub fn is_empty(&self) -> bool {
1397 self.entries.is_empty()
1398 }
1399}
1400
1401#[derive(Debug, Clone, PartialEq, Eq)]
1403pub struct ProjectSysctl {
1404 name: ProjectKey,
1405 value: ProjectValue<ComposeScalar>,
1406}
1407
1408impl ProjectSysctl {
1409 #[must_use]
1411 pub const fn name(&self) -> &ProjectKey {
1412 &self.name
1413 }
1414
1415 #[must_use]
1417 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
1418 &self.value
1419 }
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Eq)]
1424#[non_exhaustive]
1425pub enum ProjectSysctls {
1426 Map(Vec<ProjectValue<ProjectSysctl>>),
1428 List(Vec<ProjectValue<String>>),
1430}
1431
1432#[derive(Debug, Clone, PartialEq, Eq)]
1434#[non_exhaustive]
1435pub enum ProjectLoggingOptionValue {
1436 String {
1438 authored: String,
1440 value: String,
1442 },
1443 Number(String),
1445 Null,
1447}
1448
1449#[derive(Debug, Clone, PartialEq, Eq)]
1451pub struct ProjectLoggingOption {
1452 name: ProjectKey,
1453 value: ProjectValue<ProjectLoggingOptionValue>,
1454}
1455
1456impl ProjectLoggingOption {
1457 #[must_use]
1459 pub const fn name(&self) -> &ProjectKey {
1460 &self.name
1461 }
1462
1463 #[must_use]
1465 pub const fn value(&self) -> &ProjectValue<ProjectLoggingOptionValue> {
1466 &self.value
1467 }
1468}
1469
1470#[derive(Debug, Clone, PartialEq, Eq)]
1472pub struct ProjectLoggingOptions {
1473 entries: Vec<ProjectValue<ProjectLoggingOption>>,
1474 unmodeled_entries: Vec<ProjectFieldReference>,
1475}
1476
1477impl ProjectLoggingOptions {
1478 #[must_use]
1480 pub fn entries(&self) -> &[ProjectValue<ProjectLoggingOption>] {
1481 &self.entries
1482 }
1483
1484 #[must_use]
1486 pub fn unmodeled_entries(&self) -> &[ProjectFieldReference] {
1487 &self.unmodeled_entries
1488 }
1489
1490 #[must_use]
1492 pub fn is_empty(&self) -> bool {
1493 self.entries.is_empty() && self.unmodeled_entries.is_empty()
1494 }
1495}
1496
1497#[derive(Debug, Clone, PartialEq, Eq)]
1499pub struct ProjectLogging {
1500 driver: Option<ProjectValue<String>>,
1501 options: Option<ProjectValue<ProjectLoggingOptions>>,
1502 unmodeled_fields: Vec<ProjectFieldReference>,
1503}
1504
1505#[derive(Debug, Clone, PartialEq, Eq)]
1511#[non_exhaustive]
1512pub enum ProjectBuild {
1513 Context(ProjectValue<String>),
1515 Definition(ProjectBuildDefinition),
1517}
1518#[derive(Debug, Clone, PartialEq, Eq)]
1520#[non_exhaustive]
1521pub enum ProjectBuildNoCacheFilter {
1522 Scalar(ProjectValue<String>),
1524 List(Vec<ProjectValue<String>>),
1526}
1527
1528#[derive(Debug, Clone, PartialEq, Eq)]
1530struct ProjectBuildCacheOptions {
1531 cache_from: Option<ProjectValue<Vec<ProjectValue<String>>>>,
1532 cache_to: Option<ProjectValue<Vec<ProjectValue<String>>>>,
1533 no_cache: Option<ProjectValue<BuildNoCache>>,
1534 pull: Option<ProjectValue<BooleanValue>>,
1535}
1536
1537#[derive(Debug, Clone, PartialEq, Eq)]
1539pub struct ProjectBuildDefinition {
1540 additional_contexts: Option<Box<ProjectValue<ProjectBuildAdditionalContexts>>>,
1541 context: Option<Box<ProjectValue<String>>>,
1542 args: Option<Box<ProjectValue<ProjectBuildArgs>>>,
1543 entitlements: Option<Box<ProjectValue<Vec<ProjectValue<String>>>>>,
1544 extra_hosts: Option<Box<ProjectValue<ProjectBuildExtraHosts>>>,
1545 dockerfile: Option<Box<ProjectValue<String>>>,
1546 dockerfile_inline: Option<Box<ProjectValue<String>>>,
1547 target: Option<Box<ProjectValue<String>>>,
1548 network: Option<Box<ProjectValue<String>>>,
1549 isolation: Option<Box<ProjectValue<String>>>,
1550 platforms: Option<Box<ProjectValue<Vec<ProjectValue<String>>>>>,
1551 cache_options: Option<Box<ProjectBuildCacheOptions>>,
1552 no_cache_filter: Option<Box<ProjectValue<ProjectBuildNoCacheFilter>>>,
1553 privileged: Option<Box<ProjectValue<BooleanValue>>>,
1554 sbom: Option<Box<ProjectValue<BuildSbom>>>,
1555 provenance: Option<Box<ProjectValue<BuildProvenance>>>,
1556 shm_size: Option<Box<ProjectValue<ShmSize>>>,
1557 tags: Option<Box<ProjectValue<Vec<ProjectValue<String>>>>>,
1558 labels: Option<Box<ProjectValue<ProjectBuildLabels>>>,
1559 secrets: Option<Box<ProjectValue<Vec<ProjectValue<ProjectGrant>>>>>,
1560 ssh: Option<Box<ProjectValue<ProjectBuildSsh>>>,
1561 ulimits: Option<Box<ProjectValue<ProjectUlimits>>>,
1562 unmodeled_fields: Arc<Vec<ProjectFieldReference>>,
1563}
1564
1565impl ProjectBuildDefinition {
1566 #[must_use]
1572 pub fn additional_contexts(&self) -> Option<&ProjectValue<ProjectBuildAdditionalContexts>> {
1573 self.additional_contexts.as_deref()
1574 }
1575
1576 #[must_use]
1578 pub const fn context(&self) -> Option<&ProjectValue<String>> {
1579 match &self.context {
1580 Some(context) => Some(context),
1581 None => None,
1582 }
1583 }
1584
1585 #[must_use]
1590 pub fn args(&self) -> Option<&ProjectValue<ProjectBuildArgs>> {
1591 self.args.as_deref()
1592 }
1593
1594 #[must_use]
1600 pub fn entitlements(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
1601 self.entitlements.as_deref()
1602 }
1603
1604 #[must_use]
1611 pub fn extra_hosts(&self) -> Option<&ProjectValue<ProjectBuildExtraHosts>> {
1612 self.extra_hosts.as_deref()
1613 }
1614
1615 #[must_use]
1617 pub const fn dockerfile(&self) -> Option<&ProjectValue<String>> {
1618 match &self.dockerfile {
1619 Some(dockerfile) => Some(dockerfile),
1620 None => None,
1621 }
1622 }
1623
1624 #[must_use]
1630 pub const fn dockerfile_inline(&self) -> Option<&ProjectValue<String>> {
1631 match &self.dockerfile_inline {
1632 Some(dockerfile_inline) => Some(dockerfile_inline),
1633 None => None,
1634 }
1635 }
1636
1637 #[must_use]
1641 pub const fn target(&self) -> Option<&ProjectValue<String>> {
1642 match &self.target {
1643 Some(target) => Some(target),
1644 None => None,
1645 }
1646 }
1647
1648 #[must_use]
1653 pub fn network(&self) -> Option<&ProjectValue<String>> {
1654 self.network.as_deref()
1655 }
1656
1657 #[must_use]
1664 pub fn isolation(&self) -> Option<&ProjectValue<String>> {
1665 self.isolation.as_deref()
1666 }
1667
1668 #[must_use]
1673 pub fn platforms(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
1674 self.platforms.as_deref()
1675 }
1676
1677 #[must_use]
1683 pub fn cache_from(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
1684 self.cache_options
1685 .as_ref()
1686 .and_then(|options| options.cache_from.as_ref())
1687 }
1688
1689 #[must_use]
1695 pub fn cache_to(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
1696 self.cache_options
1697 .as_ref()
1698 .and_then(|options| options.cache_to.as_ref())
1699 }
1700
1701 #[must_use]
1707 pub fn no_cache(&self) -> Option<&ProjectValue<BuildNoCache>> {
1708 self.cache_options
1709 .as_ref()
1710 .and_then(|options| options.no_cache.as_ref())
1711 }
1712 #[must_use]
1714 pub fn no_cache_filter(&self) -> Option<&ProjectValue<ProjectBuildNoCacheFilter>> {
1715 self.no_cache_filter.as_deref()
1716 }
1717 #[must_use]
1719 pub fn privileged(&self) -> Option<&ProjectValue<BooleanValue>> {
1720 self.privileged.as_deref()
1721 }
1722
1723 #[must_use]
1729 pub fn sbom(&self) -> Option<&ProjectValue<BuildSbom>> {
1730 self.sbom.as_deref()
1731 }
1732 #[must_use]
1734 pub fn provenance(&self) -> Option<&ProjectValue<BuildProvenance>> {
1735 self.provenance.as_deref()
1736 }
1737
1738 #[must_use]
1744 pub fn pull(&self) -> Option<&ProjectValue<BooleanValue>> {
1745 self.cache_options.as_ref().and_then(|options| options.pull.as_ref())
1746 }
1747
1748 #[must_use]
1755 pub fn shm_size(&self) -> Option<&ProjectValue<ShmSize>> {
1756 self.shm_size.as_deref()
1757 }
1758
1759 #[must_use]
1764 pub fn tags(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
1765 self.tags.as_deref()
1766 }
1767
1768 #[must_use]
1773 pub fn labels(&self) -> Option<&ProjectValue<ProjectBuildLabels>> {
1774 self.labels.as_deref()
1775 }
1776
1777 #[must_use]
1783 pub fn secrets(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectGrant>>>> {
1784 self.secrets.as_deref()
1785 }
1786
1787 #[must_use]
1794 pub fn ssh(&self) -> Option<&ProjectValue<ProjectBuildSsh>> {
1795 self.ssh.as_deref()
1796 }
1797
1798 #[must_use]
1805 pub fn ulimits(&self) -> Option<&ProjectValue<ProjectUlimits>> {
1806 self.ulimits.as_deref()
1807 }
1808
1809 #[must_use]
1812 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1813 self.unmodeled_fields.as_slice()
1814 }
1815
1816 fn push_unmodeled(&mut self, field: ProjectFieldReference) {
1817 Arc::make_mut(&mut self.unmodeled_fields).push(field);
1818 }
1819
1820 fn cache_options(&mut self) -> &mut ProjectBuildCacheOptions {
1821 self.cache_options.get_or_insert_with(|| {
1822 Box::new(ProjectBuildCacheOptions {
1823 cache_from: None,
1824 cache_to: None,
1825 no_cache: None,
1826 pull: None,
1827 })
1828 })
1829 }
1830}
1831
1832impl ProjectLogging {
1833 #[must_use]
1835 pub const fn driver(&self) -> Option<&ProjectValue<String>> {
1836 self.driver.as_ref()
1837 }
1838
1839 #[must_use]
1841 pub const fn options(&self) -> Option<&ProjectValue<ProjectLoggingOptions>> {
1842 self.options.as_ref()
1843 }
1844
1845 #[must_use]
1847 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1848 &self.unmodeled_fields
1849 }
1850}
1851
1852#[derive(Debug, Clone, PartialEq, Eq)]
1854pub struct ProjectDeploy {
1855 endpoint_mode: Option<ProjectValue<DeployEndpointMode>>,
1856 labels: Option<ProjectValue<ProjectLabels>>,
1857 mode: Option<ProjectValue<DeployMode>>,
1858 placement: Option<ProjectValue<ProjectDeployPlacement>>,
1859 replicas: Option<ProjectValue<DeployReplicas>>,
1860 resources: Option<ProjectValue<ProjectDeployResources>>,
1861 restart_policy: Option<ProjectValue<ProjectDeployRestartPolicy>>,
1862 rollback_config: Option<ProjectValue<ProjectDeployRollbackConfig>>,
1863 update_config: Option<ProjectValue<ProjectDeployUpdateConfig>>,
1864 unmodeled_fields: Vec<ProjectFieldReference>,
1865}
1866
1867impl ProjectDeploy {
1868 #[must_use]
1870 pub const fn endpoint_mode(&self) -> Option<&ProjectValue<DeployEndpointMode>> {
1871 self.endpoint_mode.as_ref()
1872 }
1873
1874 #[must_use]
1876 pub const fn labels(&self) -> Option<&ProjectValue<ProjectLabels>> {
1877 self.labels.as_ref()
1878 }
1879
1880 #[must_use]
1882 pub const fn mode(&self) -> Option<&ProjectValue<DeployMode>> {
1883 self.mode.as_ref()
1884 }
1885
1886 #[must_use]
1888 pub const fn placement(&self) -> Option<&ProjectValue<ProjectDeployPlacement>> {
1889 self.placement.as_ref()
1890 }
1891
1892 #[must_use]
1894 pub const fn replicas(&self) -> Option<&ProjectValue<DeployReplicas>> {
1895 self.replicas.as_ref()
1896 }
1897
1898 #[must_use]
1900 pub const fn resources(&self) -> Option<&ProjectValue<ProjectDeployResources>> {
1901 self.resources.as_ref()
1902 }
1903
1904 #[must_use]
1906 pub const fn restart_policy(&self) -> Option<&ProjectValue<ProjectDeployRestartPolicy>> {
1907 self.restart_policy.as_ref()
1908 }
1909 #[must_use]
1911 pub const fn rollback_config(&self) -> Option<&ProjectValue<ProjectDeployRollbackConfig>> {
1912 self.rollback_config.as_ref()
1913 }
1914 #[must_use]
1916 pub const fn update_config(&self) -> Option<&ProjectValue<ProjectDeployUpdateConfig>> {
1917 self.update_config.as_ref()
1918 }
1919
1920 #[must_use]
1922 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1923 &self.unmodeled_fields
1924 }
1925}
1926
1927#[derive(Debug, Clone, PartialEq, Eq)]
1929pub struct ProjectDeployResources {
1930 limits: Option<ProjectValue<ProjectDeployResourceLimits>>,
1931 reservations: Option<ProjectValue<ProjectDeployResourceReservations>>,
1932 unmodeled_fields: Vec<ProjectFieldReference>,
1933}
1934
1935impl ProjectDeployResources {
1936 #[must_use]
1938 pub const fn limits(&self) -> Option<&ProjectValue<ProjectDeployResourceLimits>> {
1939 self.limits.as_ref()
1940 }
1941
1942 #[must_use]
1944 pub const fn reservations(&self) -> Option<&ProjectValue<ProjectDeployResourceReservations>> {
1945 self.reservations.as_ref()
1946 }
1947
1948 #[must_use]
1950 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1951 &self.unmodeled_fields
1952 }
1953}
1954
1955#[derive(Debug, Clone, PartialEq, Eq)]
1957pub struct ProjectDeployResourceReservations {
1958 cpus: Option<ProjectValue<DeployResourceCpus>>,
1959 memory: Option<ProjectValue<DeployResourceMemory>>,
1960 generic_resources: Option<ProjectValue<Vec<ProjectValue<ProjectDeployGenericResource>>>>,
1961 devices: Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDevice>>>>,
1962 unmodeled_fields: Vec<ProjectFieldReference>,
1963}
1964
1965impl ProjectDeployResourceReservations {
1966 #[must_use]
1968 pub const fn cpus(&self) -> Option<&ProjectValue<DeployResourceCpus>> {
1969 self.cpus.as_ref()
1970 }
1971
1972 #[must_use]
1974 pub const fn memory(&self) -> Option<&ProjectValue<DeployResourceMemory>> {
1975 self.memory.as_ref()
1976 }
1977
1978 #[must_use]
1980 pub const fn generic_resources(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDeployGenericResource>>>> {
1981 self.generic_resources.as_ref()
1982 }
1983
1984 #[must_use]
1986 pub const fn devices(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDeployReservationDevice>>>> {
1987 self.devices.as_ref()
1988 }
1989
1990 #[must_use]
1992 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
1993 &self.unmodeled_fields
1994 }
1995}
1996
1997#[derive(Debug, Clone, PartialEq, Eq)]
1999pub struct ProjectDeployReservationDevice {
2000 form: ProjectDeployReservationDeviceForm,
2001 capabilities: Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceCapability>>>>,
2002 driver: Option<ProjectValue<String>>,
2003 count: Option<ProjectValue<DeployReservationDeviceCount>>,
2004 device_ids: Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceId>>>>,
2005 options: Option<ProjectValue<ProjectDeployReservationDeviceOptions>>,
2006 unmodeled_fields: Vec<ProjectFieldReference>,
2007}
2008
2009impl ProjectDeployReservationDevice {
2010 #[must_use]
2012 pub const fn form(&self) -> ProjectDeployReservationDeviceForm {
2013 self.form
2014 }
2015
2016 #[must_use]
2018 pub const fn capabilities(
2019 &self,
2020 ) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceCapability>>>> {
2021 self.capabilities.as_ref()
2022 }
2023
2024 #[must_use]
2026 pub const fn driver(&self) -> Option<&ProjectValue<String>> {
2027 self.driver.as_ref()
2028 }
2029
2030 #[must_use]
2032 pub const fn count(&self) -> Option<&ProjectValue<DeployReservationDeviceCount>> {
2033 self.count.as_ref()
2034 }
2035
2036 #[must_use]
2038 pub const fn device_ids(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceId>>>> {
2039 self.device_ids.as_ref()
2040 }
2041
2042 #[must_use]
2044 pub const fn options(&self) -> Option<&ProjectValue<ProjectDeployReservationDeviceOptions>> {
2045 self.options.as_ref()
2046 }
2047
2048 #[must_use]
2050 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2051 &self.unmodeled_fields
2052 }
2053}
2054
2055#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2057#[non_exhaustive]
2058pub enum ProjectDeployReservationDeviceForm {
2059 Mapping,
2061 Unmodeled,
2063}
2064
2065#[derive(Debug, Clone, PartialEq, Eq)]
2067pub struct ProjectDeployReservationDeviceId {
2068 form: ProjectDeployReservationDeviceIdForm,
2069 value: Option<ProjectValue<String>>,
2070}
2071
2072impl ProjectDeployReservationDeviceId {
2073 #[must_use]
2075 pub const fn form(&self) -> ProjectDeployReservationDeviceIdForm {
2076 self.form
2077 }
2078
2079 #[must_use]
2081 pub const fn value(&self) -> Option<&ProjectValue<String>> {
2082 self.value.as_ref()
2083 }
2084}
2085
2086#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2088#[non_exhaustive]
2089pub enum ProjectDeployReservationDeviceIdForm {
2090 String,
2092 Unmodeled,
2094}
2095
2096#[derive(Debug, Clone, PartialEq, Eq)]
2098#[non_exhaustive]
2099pub enum ProjectDeployReservationDeviceOptions {
2100 Map {
2102 entries: Vec<ProjectValue<ProjectDeployReservationDeviceOption>>,
2104 unmodeled_entries: Vec<ProjectFieldReference>,
2106 },
2107 List(Vec<ProjectValue<ProjectDeployReservationDeviceOptionItem>>),
2109}
2110
2111impl ProjectDeployReservationDeviceOptions {
2112 #[must_use]
2114 pub fn as_map(&self) -> Option<&[ProjectValue<ProjectDeployReservationDeviceOption>]> {
2115 let Self::Map { entries, .. } = self else { return None };
2116 Some(entries)
2117 }
2118 #[must_use]
2120 pub fn unmodeled_entries(&self) -> Option<&[ProjectFieldReference]> {
2121 let Self::Map { unmodeled_entries, .. } = self else {
2122 return None;
2123 };
2124 Some(unmodeled_entries)
2125 }
2126 #[must_use]
2128 pub fn as_list(&self) -> Option<&[ProjectValue<ProjectDeployReservationDeviceOptionItem>]> {
2129 let Self::List(items) = self else { return None };
2130 Some(items)
2131 }
2132}
2133
2134#[derive(Debug, Clone, PartialEq, Eq)]
2136pub struct ProjectDeployReservationDeviceOption {
2137 name: ProjectKey,
2138 value: ProjectValue<ComposeScalar>,
2139}
2140impl ProjectDeployReservationDeviceOption {
2141 #[must_use]
2143 pub const fn name(&self) -> &ProjectKey {
2144 &self.name
2145 }
2146 #[must_use]
2148 pub const fn value(&self) -> &ProjectValue<ComposeScalar> {
2149 &self.value
2150 }
2151}
2152
2153#[derive(Debug, Clone, PartialEq, Eq)]
2155pub struct ProjectDeployReservationDeviceOptionItem {
2156 form: ProjectDeployReservationDeviceOptionItemForm,
2157 value: Option<ProjectValue<String>>,
2158}
2159impl ProjectDeployReservationDeviceOptionItem {
2160 #[must_use]
2162 pub const fn form(&self) -> ProjectDeployReservationDeviceOptionItemForm {
2163 self.form
2164 }
2165 #[must_use]
2167 pub const fn value(&self) -> Option<&ProjectValue<String>> {
2168 self.value.as_ref()
2169 }
2170}
2171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2173#[non_exhaustive]
2174pub enum ProjectDeployReservationDeviceOptionItemForm {
2175 String,
2177 Unmodeled,
2179}
2180
2181#[derive(Debug, Clone, PartialEq, Eq)]
2183pub struct ProjectDeployReservationDeviceCapability {
2184 form: ProjectDeployReservationDeviceCapabilityForm,
2185 value: Option<ProjectValue<String>>,
2186}
2187
2188impl ProjectDeployReservationDeviceCapability {
2189 #[must_use]
2191 pub const fn form(&self) -> ProjectDeployReservationDeviceCapabilityForm {
2192 self.form
2193 }
2194
2195 #[must_use]
2197 pub const fn value(&self) -> Option<&ProjectValue<String>> {
2198 self.value.as_ref()
2199 }
2200}
2201
2202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2204#[non_exhaustive]
2205pub enum ProjectDeployReservationDeviceCapabilityForm {
2206 String,
2208 Unmodeled,
2210}
2211
2212#[derive(Debug, Clone, PartialEq, Eq)]
2214pub struct ProjectDeployGenericResource {
2215 form: ProjectDeployGenericResourceForm,
2216 discrete_resource_spec: Option<ProjectValue<ProjectDeployDiscreteResourceSpec>>,
2217 unmodeled_fields: Vec<ProjectFieldReference>,
2218}
2219impl ProjectDeployGenericResource {
2220 #[must_use]
2222 pub const fn form(&self) -> ProjectDeployGenericResourceForm {
2223 self.form
2224 }
2225 #[must_use]
2227 pub const fn discrete_resource_spec(&self) -> Option<&ProjectValue<ProjectDeployDiscreteResourceSpec>> {
2228 self.discrete_resource_spec.as_ref()
2229 }
2230 #[must_use]
2232 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2233 &self.unmodeled_fields
2234 }
2235}
2236
2237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2239#[non_exhaustive]
2240pub enum ProjectDeployGenericResourceForm {
2241 Mapping,
2243 Unmodeled,
2245}
2246
2247#[derive(Debug, Clone, PartialEq, Eq)]
2249pub struct ProjectDeployDiscreteResourceSpec {
2250 kind: Option<ProjectValue<String>>,
2251 value: Option<ProjectValue<DeployDiscreteResourceValue>>,
2252 unmodeled_fields: Vec<ProjectFieldReference>,
2253}
2254impl ProjectDeployDiscreteResourceSpec {
2255 #[must_use]
2257 pub const fn kind(&self) -> Option<&ProjectValue<String>> {
2258 self.kind.as_ref()
2259 }
2260 #[must_use]
2262 pub const fn value(&self) -> Option<&ProjectValue<DeployDiscreteResourceValue>> {
2263 self.value.as_ref()
2264 }
2265 #[must_use]
2267 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2268 &self.unmodeled_fields
2269 }
2270}
2271
2272#[derive(Debug, Clone, PartialEq, Eq)]
2274pub struct ProjectDeployResourceLimits {
2275 cpus: Option<ProjectValue<DeployResourceCpus>>,
2276 memory: Option<ProjectValue<DeployResourceMemory>>,
2277 pids: Option<ProjectValue<DeployResourcePids>>,
2278 unmodeled_fields: Vec<ProjectFieldReference>,
2279}
2280
2281impl ProjectDeployResourceLimits {
2282 #[must_use]
2284 pub const fn cpus(&self) -> Option<&ProjectValue<DeployResourceCpus>> {
2285 self.cpus.as_ref()
2286 }
2287
2288 #[must_use]
2290 pub const fn memory(&self) -> Option<&ProjectValue<DeployResourceMemory>> {
2291 self.memory.as_ref()
2292 }
2293
2294 #[must_use]
2296 pub const fn pids(&self) -> Option<&ProjectValue<DeployResourcePids>> {
2297 self.pids.as_ref()
2298 }
2299
2300 #[must_use]
2302 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2303 &self.unmodeled_fields
2304 }
2305}
2306
2307#[derive(Debug, Clone, PartialEq, Eq)]
2309pub struct ProjectDeployRestartPolicy {
2310 condition: Option<ProjectValue<DeployRestartCondition>>,
2311 delay: Option<ProjectValue<DeployRestartDuration>>,
2312 max_attempts: Option<ProjectValue<DeployRestartMaxAttempts>>,
2313 window: Option<ProjectValue<DeployRestartDuration>>,
2314 unmodeled_fields: Vec<ProjectFieldReference>,
2315}
2316
2317#[derive(Debug, Clone, PartialEq, Eq)]
2319pub struct ProjectDeployRollbackConfig {
2320 parallelism: Option<ProjectValue<DeployRollbackParallelism>>,
2321 delay: Option<ProjectValue<String>>,
2322 monitor: Option<ProjectValue<String>>,
2323 failure_action: Option<ProjectValue<String>>,
2324 max_failure_ratio: Option<ProjectValue<DeployRollbackMaxFailureRatio>>,
2325 order: Option<ProjectValue<DeployRollbackOrder>>,
2326 unmodeled_fields: Vec<ProjectFieldReference>,
2327}
2328impl ProjectDeployRollbackConfig {
2329 #[must_use]
2331 pub const fn parallelism(&self) -> Option<&ProjectValue<DeployRollbackParallelism>> {
2332 self.parallelism.as_ref()
2333 }
2334 #[must_use]
2336 pub const fn delay(&self) -> Option<&ProjectValue<String>> {
2337 self.delay.as_ref()
2338 }
2339 #[must_use]
2341 pub const fn monitor(&self) -> Option<&ProjectValue<String>> {
2342 self.monitor.as_ref()
2343 }
2344 #[must_use]
2346 pub const fn failure_action(&self) -> Option<&ProjectValue<String>> {
2347 self.failure_action.as_ref()
2348 }
2349 #[must_use]
2351 pub const fn max_failure_ratio(&self) -> Option<&ProjectValue<DeployRollbackMaxFailureRatio>> {
2352 self.max_failure_ratio.as_ref()
2353 }
2354 #[must_use]
2356 pub const fn order(&self) -> Option<&ProjectValue<DeployRollbackOrder>> {
2357 self.order.as_ref()
2358 }
2359 #[must_use]
2361 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2362 &self.unmodeled_fields
2363 }
2364}
2365
2366#[derive(Debug, Clone, PartialEq, Eq)]
2368pub struct ProjectDeployUpdateConfig {
2369 parallelism: Option<ProjectValue<DeployUpdateParallelism>>,
2370 delay: Option<ProjectValue<String>>,
2371 monitor: Option<ProjectValue<String>>,
2372 failure_action: Option<ProjectValue<String>>,
2373 max_failure_ratio: Option<ProjectValue<DeployUpdateMaxFailureRatio>>,
2374 order: Option<ProjectValue<DeployUpdateOrder>>,
2375 unmodeled_fields: Vec<ProjectFieldReference>,
2376}
2377impl ProjectDeployUpdateConfig {
2378 #[must_use]
2380 pub const fn parallelism(&self) -> Option<&ProjectValue<DeployUpdateParallelism>> {
2381 self.parallelism.as_ref()
2382 }
2383 #[must_use]
2385 pub const fn delay(&self) -> Option<&ProjectValue<String>> {
2386 self.delay.as_ref()
2387 }
2388 #[must_use]
2390 pub const fn monitor(&self) -> Option<&ProjectValue<String>> {
2391 self.monitor.as_ref()
2392 }
2393 #[must_use]
2395 pub const fn failure_action(&self) -> Option<&ProjectValue<String>> {
2396 self.failure_action.as_ref()
2397 }
2398 #[must_use]
2400 pub const fn max_failure_ratio(&self) -> Option<&ProjectValue<DeployUpdateMaxFailureRatio>> {
2401 self.max_failure_ratio.as_ref()
2402 }
2403 #[must_use]
2405 pub const fn order(&self) -> Option<&ProjectValue<DeployUpdateOrder>> {
2406 self.order.as_ref()
2407 }
2408 #[must_use]
2410 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2411 &self.unmodeled_fields
2412 }
2413}
2414
2415impl ProjectDeployRestartPolicy {
2416 #[must_use]
2418 pub const fn condition(&self) -> Option<&ProjectValue<DeployRestartCondition>> {
2419 self.condition.as_ref()
2420 }
2421 #[must_use]
2423 pub const fn delay(&self) -> Option<&ProjectValue<DeployRestartDuration>> {
2424 self.delay.as_ref()
2425 }
2426 #[must_use]
2428 pub const fn max_attempts(&self) -> Option<&ProjectValue<DeployRestartMaxAttempts>> {
2429 self.max_attempts.as_ref()
2430 }
2431 #[must_use]
2433 pub const fn window(&self) -> Option<&ProjectValue<DeployRestartDuration>> {
2434 self.window.as_ref()
2435 }
2436 #[must_use]
2438 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2439 &self.unmodeled_fields
2440 }
2441}
2442
2443#[derive(Debug, Clone, PartialEq, Eq)]
2445pub struct ProjectDeployPlacement {
2446 constraints: Option<ProjectValue<Vec<ProjectValue<String>>>>,
2447 preferences: Option<ProjectValue<Vec<ProjectValue<ProjectDeployPlacementPreference>>>>,
2448 max_replicas_per_node: Option<ProjectValue<DeployPlacementMaxReplicasPerNode>>,
2449 unmodeled_fields: Vec<ProjectFieldReference>,
2450}
2451
2452impl ProjectDeployPlacement {
2453 #[must_use]
2455 pub const fn constraints(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
2456 self.constraints.as_ref()
2457 }
2458
2459 #[must_use]
2461 pub const fn preferences(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDeployPlacementPreference>>>> {
2462 self.preferences.as_ref()
2463 }
2464
2465 #[must_use]
2467 pub const fn max_replicas_per_node(&self) -> Option<&ProjectValue<DeployPlacementMaxReplicasPerNode>> {
2468 self.max_replicas_per_node.as_ref()
2469 }
2470
2471 #[must_use]
2473 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2474 &self.unmodeled_fields
2475 }
2476}
2477
2478#[derive(Debug, Clone, PartialEq, Eq)]
2480pub struct ProjectDeployPlacementPreference {
2481 spread: Option<ProjectValue<String>>,
2482 unmodeled_fields: Vec<ProjectFieldReference>,
2483}
2484
2485impl ProjectDeployPlacementPreference {
2486 #[must_use]
2488 pub const fn spread(&self) -> Option<&ProjectValue<String>> {
2489 self.spread.as_ref()
2490 }
2491
2492 #[must_use]
2494 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2495 &self.unmodeled_fields
2496 }
2497}
2498
2499#[derive(Debug, Clone, PartialEq, Eq)]
2501pub struct ProjectCredentialSpec {
2502 config: Option<ProjectValue<String>>,
2503 file: Option<ProjectValue<String>>,
2504 registry: Option<ProjectValue<String>>,
2505 unmodeled_fields: Vec<ProjectFieldReference>,
2506}
2507
2508impl ProjectCredentialSpec {
2509 #[must_use]
2511 pub const fn config(&self) -> Option<&ProjectValue<String>> {
2512 self.config.as_ref()
2513 }
2514
2515 #[must_use]
2517 pub const fn file(&self) -> Option<&ProjectValue<String>> {
2518 self.file.as_ref()
2519 }
2520
2521 #[must_use]
2523 pub const fn registry(&self) -> Option<&ProjectValue<String>> {
2524 self.registry.as_ref()
2525 }
2526
2527 #[must_use]
2529 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2530 &self.unmodeled_fields
2531 }
2532}
2533
2534#[derive(Debug, Clone, PartialEq, Eq)]
2536#[non_exhaustive]
2537pub enum ProjectExtends {
2538 Short(ProjectValue<String>),
2540 Long(ProjectExtendsReference),
2542}
2543
2544#[derive(Debug, Clone, PartialEq, Eq)]
2546pub struct ProjectExtendsReference {
2547 service: Option<ProjectValue<String>>,
2548 file: Option<ProjectValue<String>>,
2549 unmodeled_fields: Vec<ProjectFieldReference>,
2550}
2551
2552impl ProjectExtendsReference {
2553 #[must_use]
2555 pub const fn service(&self) -> Option<&ProjectValue<String>> {
2556 self.service.as_ref()
2557 }
2558
2559 #[must_use]
2561 pub const fn file(&self) -> Option<&ProjectValue<String>> {
2562 self.file.as_ref()
2563 }
2564
2565 #[must_use]
2567 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2568 &self.unmodeled_fields
2569 }
2570}
2571
2572#[derive(Debug, Clone, PartialEq, Eq)]
2574#[non_exhaustive]
2575pub enum ProjectProviderOptionValue {
2576 Scalar(ComposeScalar),
2578 Sequence(Vec<ProjectValue<ProjectProviderOptionItem>>),
2580}
2581
2582#[derive(Debug, Clone, PartialEq, Eq)]
2584#[non_exhaustive]
2585pub enum ProjectProviderOptionItem {
2586 Scalar(ComposeScalar),
2588 Unmodeled,
2590}
2591
2592#[derive(Debug, Clone, PartialEq, Eq)]
2594pub struct ProjectProviderOption {
2595 name: ProjectKey,
2596 value: ProjectValue<ProjectProviderOptionValue>,
2597}
2598
2599impl ProjectProviderOption {
2600 #[must_use]
2602 pub const fn name(&self) -> &ProjectKey {
2603 &self.name
2604 }
2605
2606 #[must_use]
2608 pub const fn value(&self) -> &ProjectValue<ProjectProviderOptionValue> {
2609 &self.value
2610 }
2611}
2612
2613#[derive(Debug, Clone, PartialEq, Eq)]
2615pub struct ProjectProviderOptions {
2616 entries: Vec<ProjectValue<ProjectProviderOption>>,
2617 unmodeled_entries: Vec<ProjectFieldReference>,
2618}
2619
2620impl ProjectProviderOptions {
2621 #[must_use]
2623 pub fn entries(&self) -> &[ProjectValue<ProjectProviderOption>] {
2624 &self.entries
2625 }
2626
2627 #[must_use]
2629 pub fn unmodeled_entries(&self) -> &[ProjectFieldReference] {
2630 &self.unmodeled_entries
2631 }
2632
2633 #[must_use]
2635 pub fn is_empty(&self) -> bool {
2636 self.entries.is_empty() && self.unmodeled_entries.is_empty()
2637 }
2638}
2639
2640#[derive(Debug, Clone, PartialEq, Eq)]
2642pub struct ProjectProvider {
2643 type_: Option<ProjectValue<String>>,
2644 options: Option<ProjectValue<ProjectProviderOptions>>,
2645 unmodeled_fields: Vec<ProjectFieldReference>,
2646}
2647
2648impl ProjectProvider {
2649 #[must_use]
2651 pub const fn type_(&self) -> Option<&ProjectValue<String>> {
2652 self.type_.as_ref()
2653 }
2654
2655 #[must_use]
2657 pub const fn options(&self) -> Option<&ProjectValue<ProjectProviderOptions>> {
2658 self.options.as_ref()
2659 }
2660
2661 #[must_use]
2663 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2664 &self.unmodeled_fields
2665 }
2666}
2667
2668#[derive(Debug, Clone, PartialEq, Eq)]
2670pub struct ProjectServiceHook {
2671 command: Option<ProjectValue<Command>>,
2672 environment: Option<ProjectValue<ProjectEnvironment>>,
2673 privileged: Option<ProjectValue<BooleanValue>>,
2674 user: Option<ProjectValue<String>>,
2675 working_dir: Option<ProjectValue<String>>,
2676 unmodeled_fields: Vec<ProjectFieldReference>,
2677}
2678
2679impl ProjectServiceHook {
2680 #[must_use]
2682 pub const fn command(&self) -> Option<&ProjectValue<Command>> {
2683 self.command.as_ref()
2684 }
2685
2686 #[must_use]
2688 pub const fn environment(&self) -> Option<&ProjectValue<ProjectEnvironment>> {
2689 self.environment.as_ref()
2690 }
2691
2692 #[must_use]
2694 pub const fn privileged(&self) -> Option<&ProjectValue<BooleanValue>> {
2695 self.privileged.as_ref()
2696 }
2697
2698 #[must_use]
2700 pub const fn user(&self) -> Option<&ProjectValue<String>> {
2701 self.user.as_ref()
2702 }
2703
2704 #[must_use]
2706 pub const fn working_dir(&self) -> Option<&ProjectValue<String>> {
2707 self.working_dir.as_ref()
2708 }
2709
2710 #[must_use]
2712 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2713 &self.unmodeled_fields
2714 }
2715}
2716
2717#[derive(Debug, Clone, PartialEq, Eq)]
2719#[non_exhaustive]
2720pub enum ProjectPostStartHook {
2721 Hook(Box<ProjectServiceHook>),
2723 Unmodeled,
2725}
2726
2727#[derive(Debug, Clone, PartialEq, Eq)]
2729#[non_exhaustive]
2730pub enum ProjectPreStopHook {
2731 Hook(Box<ProjectServiceHook>),
2733 Unmodeled,
2735}
2736
2737#[derive(Debug, Clone, PartialEq, Eq)]
2739pub struct ProjectPreStartServiceHook {
2740 command: Option<ProjectValue<Command>>,
2741 image: Option<ProjectValue<String>>,
2742 environment: Option<ProjectValue<ProjectEnvironment>>,
2743 privileged: Option<ProjectValue<BooleanValue>>,
2744 per_replica: Option<ProjectValue<BooleanValue>>,
2745 user: Option<ProjectValue<String>>,
2746 working_dir: Option<ProjectValue<String>>,
2747 unmodeled_fields: Vec<ProjectFieldReference>,
2748}
2749
2750impl ProjectPreStartServiceHook {
2751 #[must_use]
2753 pub const fn command(&self) -> Option<&ProjectValue<Command>> {
2754 self.command.as_ref()
2755 }
2756
2757 #[must_use]
2759 pub const fn image(&self) -> Option<&ProjectValue<String>> {
2760 self.image.as_ref()
2761 }
2762
2763 #[must_use]
2765 pub const fn environment(&self) -> Option<&ProjectValue<ProjectEnvironment>> {
2766 self.environment.as_ref()
2767 }
2768
2769 #[must_use]
2771 pub const fn privileged(&self) -> Option<&ProjectValue<BooleanValue>> {
2772 self.privileged.as_ref()
2773 }
2774
2775 #[must_use]
2777 pub const fn per_replica(&self) -> Option<&ProjectValue<BooleanValue>> {
2778 self.per_replica.as_ref()
2779 }
2780
2781 #[must_use]
2783 pub const fn user(&self) -> Option<&ProjectValue<String>> {
2784 self.user.as_ref()
2785 }
2786
2787 #[must_use]
2789 pub const fn working_dir(&self) -> Option<&ProjectValue<String>> {
2790 self.working_dir.as_ref()
2791 }
2792
2793 #[must_use]
2795 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2796 &self.unmodeled_fields
2797 }
2798}
2799
2800#[derive(Debug, Clone, PartialEq, Eq)]
2802#[non_exhaustive]
2803pub enum ProjectPreStartHook {
2804 Hook(Box<ProjectPreStartServiceHook>),
2806 Unmodeled,
2808}
2809
2810#[derive(Debug, Clone, PartialEq, Eq)]
2815pub struct ProjectVolumeMountOptions {
2816 consistency: Option<ProjectValue<String>>,
2817 bind: Option<ProjectBindMountOptions>,
2818 image: Option<ProjectImageMountOptions>,
2819 tmpfs: Option<ProjectTmpfsMountOptions>,
2820 volume: Option<ProjectNamedVolumeMountOptions>,
2821 unmodeled_fields: Vec<ProjectFieldReference>,
2822}
2823
2824impl ProjectVolumeMountOptions {
2825 fn new() -> Self {
2826 Self {
2827 consistency: None,
2828 bind: None,
2829 image: None,
2830 tmpfs: None,
2831 volume: None,
2832 unmodeled_fields: Vec::new(),
2833 }
2834 }
2835 #[must_use]
2837 pub const fn consistency(&self) -> Option<&ProjectValue<String>> {
2838 self.consistency.as_ref()
2839 }
2840 #[must_use]
2842 pub const fn bind(&self) -> Option<&ProjectBindMountOptions> {
2843 self.bind.as_ref()
2844 }
2845 #[must_use]
2847 pub const fn image(&self) -> Option<&ProjectImageMountOptions> {
2848 self.image.as_ref()
2849 }
2850 #[must_use]
2852 pub const fn tmpfs(&self) -> Option<&ProjectTmpfsMountOptions> {
2853 self.tmpfs.as_ref()
2854 }
2855 #[must_use]
2857 pub const fn volume(&self) -> Option<&ProjectNamedVolumeMountOptions> {
2858 self.volume.as_ref()
2859 }
2860 #[must_use]
2862 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
2863 &self.unmodeled_fields
2864 }
2865}
2866
2867#[derive(Debug, Clone, PartialEq, Eq)]
2869pub struct ProjectBindMountOptions {
2870 recursive: Option<ProjectValue<String>>,
2871 extension_fields: Vec<ProjectFieldReference>,
2872 unknown_fields: Vec<ProjectFieldReference>,
2873}
2874impl ProjectBindMountOptions {
2875 #[must_use]
2877 pub const fn recursive(&self) -> Option<&ProjectValue<String>> {
2878 self.recursive.as_ref()
2879 }
2880 #[must_use]
2882 pub fn extension_fields(&self) -> &[ProjectFieldReference] {
2883 &self.extension_fields
2884 }
2885 #[must_use]
2887 pub fn unknown_fields(&self) -> &[ProjectFieldReference] {
2888 &self.unknown_fields
2889 }
2890}
2891
2892#[derive(Debug, Clone, PartialEq, Eq)]
2894pub struct ProjectImageMountOptions {
2895 subpath: Option<ProjectValue<String>>,
2896 extension_fields: Vec<ProjectFieldReference>,
2897 unknown_fields: Vec<ProjectFieldReference>,
2898}
2899impl ProjectImageMountOptions {
2900 #[must_use]
2902 pub const fn subpath(&self) -> Option<&ProjectValue<String>> {
2903 self.subpath.as_ref()
2904 }
2905 #[must_use]
2907 pub fn extension_fields(&self) -> &[ProjectFieldReference] {
2908 &self.extension_fields
2909 }
2910 #[must_use]
2912 pub fn unknown_fields(&self) -> &[ProjectFieldReference] {
2913 &self.unknown_fields
2914 }
2915}
2916
2917#[derive(Debug, Clone, PartialEq, Eq)]
2919pub struct ProjectTmpfsMountOptions {
2920 size: Option<ProjectValue<ComposeScalar>>,
2921 mode: Option<ProjectValue<ComposeScalar>>,
2922 extension_fields: Vec<ProjectFieldReference>,
2923 unknown_fields: Vec<ProjectFieldReference>,
2924}
2925impl ProjectTmpfsMountOptions {
2926 #[must_use]
2928 pub const fn size(&self) -> Option<&ProjectValue<ComposeScalar>> {
2929 self.size.as_ref()
2930 }
2931 #[must_use]
2933 pub const fn mode(&self) -> Option<&ProjectValue<ComposeScalar>> {
2934 self.mode.as_ref()
2935 }
2936 #[must_use]
2938 pub fn extension_fields(&self) -> &[ProjectFieldReference] {
2939 &self.extension_fields
2940 }
2941 #[must_use]
2943 pub fn unknown_fields(&self) -> &[ProjectFieldReference] {
2944 &self.unknown_fields
2945 }
2946}
2947
2948#[derive(Debug, Clone, PartialEq, Eq)]
2950pub struct ProjectNamedVolumeMountOptions {
2951 nocopy: Option<ProjectValue<BooleanValue>>,
2952 subpath: Option<ProjectValue<String>>,
2953 labels: Option<ProjectValue<Labels>>,
2954 extension_fields: Vec<ProjectFieldReference>,
2955 unknown_fields: Vec<ProjectFieldReference>,
2956}
2957impl ProjectNamedVolumeMountOptions {
2958 #[must_use]
2960 pub const fn nocopy(&self) -> Option<&ProjectValue<BooleanValue>> {
2961 self.nocopy.as_ref()
2962 }
2963 #[must_use]
2965 pub const fn subpath(&self) -> Option<&ProjectValue<String>> {
2966 self.subpath.as_ref()
2967 }
2968 #[must_use]
2970 pub const fn labels(&self) -> Option<&ProjectValue<Labels>> {
2971 self.labels.as_ref()
2972 }
2973 #[must_use]
2975 pub fn extension_fields(&self) -> &[ProjectFieldReference] {
2976 &self.extension_fields
2977 }
2978 #[must_use]
2980 pub fn unknown_fields(&self) -> &[ProjectFieldReference] {
2981 &self.unknown_fields
2982 }
2983}
2984
2985#[derive(Debug, Clone, PartialEq, Eq, Default)]
2986struct ProjectVolumeViews {
2987 volumes: Option<ProjectValue<Vec<ProjectValue<VolumeMount>>>>,
2988 options: Option<ProjectValue<Vec<ProjectValue<ProjectVolumeMountOptions>>>>,
2989}
2990
2991#[derive(Debug, Clone, PartialEq, Eq)]
2993pub struct ProjectService {
2994 name: ProjectKey,
2995 provenance: MergeProvenance,
2996 hostname: Option<ProjectValue<Hostname>>,
2997 domainname: Option<ProjectValue<String>>,
2998 container_name: Option<ProjectValue<String>>,
2999 image: Option<ProjectValue<ImageReference>>,
3000 platform: Option<ProjectValue<String>>,
3001 isolation: Option<ProjectValue<String>>,
3002 mac_address: Option<ProjectValue<String>>,
3003 uts: Option<ProjectValue<String>>,
3004 entrypoint: Option<ProjectValue<Entrypoint>>,
3005 command: Option<ProjectValue<Command>>,
3006 credential_spec: Option<ProjectValue<ProjectCredentialSpec>>,
3007 extends: Option<ProjectValue<ProjectExtends>>,
3008 provider: Option<ProjectValue<ProjectProvider>>,
3009 post_start: Option<ProjectValue<Vec<ProjectValue<ProjectPostStartHook>>>>,
3010 pre_stop: Option<ProjectValue<Vec<ProjectValue<ProjectPreStopHook>>>>,
3011 pre_start: Option<ProjectValue<Vec<ProjectValue<ProjectPreStartHook>>>>,
3012 blkio_config: Option<ProjectValue<ProjectBlkioConfig>>,
3013 cgroup: Option<ProjectValue<CgroupNamespace>>,
3014 cgroup_parent: Option<ProjectValue<String>>,
3015 attach: Option<ProjectValue<BooleanValue>>,
3016 init: Option<ProjectValue<BooleanValue>>,
3017 stdin_open: Option<ProjectValue<BooleanValue>>,
3018 tty: Option<ProjectValue<BooleanValue>>,
3019 privileged: Option<ProjectValue<BooleanValue>>,
3020 use_api_socket: Option<ProjectValue<BooleanValue>>,
3021 environment: Option<ProjectValue<ProjectEnvironment>>,
3022 environment_files: Option<ProjectValue<Vec<ProjectValue<ProjectEnvironmentFile>>>>,
3023 label_files: Option<ProjectValue<ProjectServiceLabelFiles>>,
3024 labels: Option<ProjectValue<ProjectLabels>>,
3025 annotations: Option<ProjectValue<ProjectAnnotations>>,
3026 extra_hosts: Option<ProjectValue<ProjectExtraHosts>>,
3027 external_links: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3028 links: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3029 storage_opt: Option<ProjectValue<Labels>>,
3030 models: Option<ProjectValue<ServiceModels>>,
3031 gpus: Option<ProjectValue<Gpus>>,
3032 develop: Option<ProjectValue<Develop>>,
3033 user: Option<ProjectValue<UserSpec>>,
3034 userns_mode: Option<ProjectValue<UserNamespaceMode>>,
3035 group_add: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3036 cap_add: Option<ProjectValue<Vec<ProjectValue<CapabilityAddItem>>>>,
3037 cap_drop: Option<ProjectValue<Vec<ProjectValue<CapabilityDropItem>>>>,
3038 devices: Option<ProjectValue<Vec<ProjectValue<ProjectDevice>>>>,
3039 dns: Option<ProjectValue<ProjectDns>>,
3040 dns_options: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3041 dns_search: Option<ProjectValue<ProjectDnsSearch>>,
3042 expose: Option<ProjectValue<Vec<ProjectValue<ProjectExposeItem>>>>,
3043 security_options: Option<ProjectValue<Vec<ProjectValue<ProjectSecurityOptionItem>>>>,
3044 working_dir: Option<ProjectValue<String>>,
3045 read_only: Option<ProjectValue<BooleanValue>>,
3046 pids_limit: Option<ProjectValue<PidsLimit>>,
3047 cpu_count: Option<ProjectValue<CpuCount>>,
3048 cpu_percent: Option<ProjectValue<CpuPercent>>,
3049 cpu_period: Option<ProjectValue<CpuPeriod>>,
3050 cpu_quota: Option<ProjectValue<CpuQuota>>,
3051 cpu_rt_period: Option<ProjectValue<CpuRtPeriod>>,
3052 cpu_rt_runtime: Option<ProjectValue<CpuRtRuntime>>,
3053 cpu_shares: Option<ProjectValue<ServiceInteger>>,
3054 cpus: Option<ProjectValue<Cpus>>,
3055 cpuset: Option<ProjectValue<String>>,
3056 device_cgroup_rules: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3057 invalid_device_cgroup_rules: Vec<ProjectInvalidServiceStringItem>,
3058 ipc: Option<ProjectValue<IpcMode>>,
3059 mem_reservation: Option<ProjectValue<MemLimit>>,
3060 mem_swappiness: Option<ProjectValue<ServiceInteger>>,
3061 memswap_limit: Option<ProjectValue<MemswapLimit>>,
3062 network_mode: Option<ProjectValue<NetworkMode>>,
3063 oom_kill_disable: Option<ProjectValue<BooleanValue>>,
3064 oom_score_adj: Option<ProjectValue<ServiceInteger>>,
3065 pid: Option<ProjectValue<PidMode>>,
3066 scale: Option<ProjectValue<ServiceInteger>>,
3067 volumes_from: Option<ProjectValue<Vec<ProjectValue<VolumesFrom>>>>,
3068 invalid_volumes_from: Vec<ProjectInvalidServiceStringItem>,
3069 shm_size: Option<ProjectValue<ShmSize>>,
3070 mem_limit: Option<ProjectValue<MemLimit>>,
3071 tmpfs: Option<ProjectValue<ProjectTmpfs>>,
3072 sysctls: Option<ProjectValue<ProjectSysctls>>,
3073 logging: Option<ProjectValue<ProjectLogging>>,
3074 ulimits: Option<ProjectValue<ProjectUlimits>>,
3075 pull_policy: Option<ProjectValue<PullPolicy>>,
3076 pull_refresh_after: Option<ProjectValue<String>>,
3077 restart: Option<ProjectValue<RestartPolicy>>,
3078 runtime: Option<ProjectValue<String>>,
3079 stop_signal: Option<ProjectValue<String>>,
3080 stop_grace_period: Option<ProjectValue<StopGracePeriod>>,
3081 healthcheck: Option<ProjectValue<ProjectHealthcheck>>,
3082 build: Option<ProjectValue<ProjectBuild>>,
3083 deploy: Option<ProjectValue<ProjectDeploy>>,
3084 depends_on: Option<ProjectValue<ProjectDependsOn>>,
3085 ports: Option<ProjectValue<Vec<ProjectValue<Port>>>>,
3086 volume_views: ProjectVolumeViews,
3087 configs: Option<ProjectValue<Vec<ProjectValue<ProjectGrant>>>>,
3088 secrets: Option<ProjectValue<Vec<ProjectValue<ProjectGrant>>>>,
3089 networks: Option<ProjectValue<ServiceNetworks>>,
3090 profiles: Option<ProjectValue<Vec<ProjectValue<String>>>>,
3091 unmodeled_fields: Vec<ProjectFieldReference>,
3092}
3093
3094impl ProjectService {
3095 fn from_entry(entry: &MergedEntry) -> Self {
3096 Self {
3097 name: ProjectKey::from_entry(entry),
3098 provenance: entry.value().provenance().clone(),
3099 hostname: None,
3100 domainname: None,
3101 container_name: None,
3102 image: None,
3103 platform: None,
3104 isolation: None,
3105 mac_address: None,
3106 uts: None,
3107 entrypoint: None,
3108 command: None,
3109 credential_spec: None,
3110 extends: None,
3111 provider: None,
3112 post_start: None,
3113 pre_stop: None,
3114 pre_start: None,
3115 blkio_config: None,
3116 cgroup: None,
3117 cgroup_parent: None,
3118 attach: None,
3119 init: None,
3120 stdin_open: None,
3121 tty: None,
3122 privileged: None,
3123 use_api_socket: None,
3124 environment: None,
3125 environment_files: None,
3126 label_files: None,
3127 labels: None,
3128 annotations: None,
3129 extra_hosts: None,
3130 external_links: None,
3131 links: None,
3132 storage_opt: None,
3133 models: None,
3134 gpus: None,
3135 develop: None,
3136 user: None,
3137 userns_mode: None,
3138 group_add: None,
3139 cap_add: None,
3140 cap_drop: None,
3141 devices: None,
3142 dns: None,
3143 dns_options: None,
3144 dns_search: None,
3145 expose: None,
3146 security_options: None,
3147 working_dir: None,
3148 read_only: None,
3149 pids_limit: None,
3150 cpu_count: None,
3151 cpu_percent: None,
3152 cpu_period: None,
3153 cpu_quota: None,
3154 cpu_rt_period: None,
3155 cpu_rt_runtime: None,
3156 cpu_shares: None,
3157 cpus: None,
3158 cpuset: None,
3159 device_cgroup_rules: None,
3160 invalid_device_cgroup_rules: Vec::new(),
3161 ipc: None,
3162 mem_reservation: None,
3163 mem_swappiness: None,
3164 memswap_limit: None,
3165 network_mode: None,
3166 oom_kill_disable: None,
3167 oom_score_adj: None,
3168 pid: None,
3169 scale: None,
3170 volumes_from: None,
3171 invalid_volumes_from: Vec::new(),
3172 shm_size: None,
3173 mem_limit: None,
3174 tmpfs: None,
3175 sysctls: None,
3176 logging: None,
3177 ulimits: None,
3178 pull_policy: None,
3179 pull_refresh_after: None,
3180 restart: None,
3181 runtime: None,
3182 stop_signal: None,
3183 stop_grace_period: None,
3184 healthcheck: None,
3185 build: None,
3186 deploy: None,
3187 depends_on: None,
3188 ports: None,
3189 volume_views: ProjectVolumeViews::default(),
3190 configs: None,
3191 secrets: None,
3192 networks: None,
3193 profiles: None,
3194 unmodeled_fields: Vec::new(),
3195 }
3196 }
3197
3198 #[must_use]
3200 pub const fn name(&self) -> &ProjectKey {
3201 &self.name
3202 }
3203
3204 #[must_use]
3206 pub const fn provenance(&self) -> &MergeProvenance {
3207 &self.provenance
3208 }
3209
3210 #[must_use]
3212 pub const fn hostname(&self) -> Option<&ProjectValue<Hostname>> {
3213 self.hostname.as_ref()
3214 }
3215
3216 #[must_use]
3218 pub const fn domainname(&self) -> Option<&ProjectValue<String>> {
3219 self.domainname.as_ref()
3220 }
3221
3222 #[must_use]
3224 pub const fn container_name(&self) -> Option<&ProjectValue<String>> {
3225 self.container_name.as_ref()
3226 }
3227
3228 #[must_use]
3230 pub const fn image(&self) -> Option<&ProjectValue<ImageReference>> {
3231 self.image.as_ref()
3232 }
3233
3234 #[must_use]
3236 pub const fn platform(&self) -> Option<&ProjectValue<String>> {
3237 self.platform.as_ref()
3238 }
3239
3240 #[must_use]
3242 pub const fn isolation(&self) -> Option<&ProjectValue<String>> {
3243 self.isolation.as_ref()
3244 }
3245
3246 #[must_use]
3248 pub const fn mac_address(&self) -> Option<&ProjectValue<String>> {
3249 self.mac_address.as_ref()
3250 }
3251
3252 #[must_use]
3254 pub const fn uts(&self) -> Option<&ProjectValue<String>> {
3255 self.uts.as_ref()
3256 }
3257
3258 #[must_use]
3260 pub const fn entrypoint(&self) -> Option<&ProjectValue<Entrypoint>> {
3261 self.entrypoint.as_ref()
3262 }
3263
3264 #[must_use]
3266 pub const fn command(&self) -> Option<&ProjectValue<Command>> {
3267 self.command.as_ref()
3268 }
3269
3270 #[must_use]
3272 pub const fn credential_spec(&self) -> Option<&ProjectValue<ProjectCredentialSpec>> {
3273 self.credential_spec.as_ref()
3274 }
3275
3276 #[must_use]
3278 pub const fn extends(&self) -> Option<&ProjectValue<ProjectExtends>> {
3279 self.extends.as_ref()
3280 }
3281
3282 #[must_use]
3284 pub const fn provider(&self) -> Option<&ProjectValue<ProjectProvider>> {
3285 self.provider.as_ref()
3286 }
3287
3288 #[must_use]
3290 pub const fn post_start(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectPostStartHook>>>> {
3291 self.post_start.as_ref()
3292 }
3293
3294 #[must_use]
3296 pub const fn pre_stop(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectPreStopHook>>>> {
3297 self.pre_stop.as_ref()
3298 }
3299
3300 #[must_use]
3302 pub const fn pre_start(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectPreStartHook>>>> {
3303 self.pre_start.as_ref()
3304 }
3305
3306 #[must_use]
3308 pub const fn blkio_config(&self) -> Option<&ProjectValue<ProjectBlkioConfig>> {
3309 self.blkio_config.as_ref()
3310 }
3311
3312 #[must_use]
3314 pub const fn cgroup(&self) -> Option<&ProjectValue<CgroupNamespace>> {
3315 self.cgroup.as_ref()
3316 }
3317
3318 #[must_use]
3320 pub const fn cgroup_parent(&self) -> Option<&ProjectValue<String>> {
3321 self.cgroup_parent.as_ref()
3322 }
3323
3324 #[must_use]
3326 pub const fn runtime(&self) -> Option<&ProjectValue<String>> {
3327 self.runtime.as_ref()
3328 }
3329
3330 #[must_use]
3332 pub const fn pull_refresh_after(&self) -> Option<&ProjectValue<String>> {
3333 self.pull_refresh_after.as_ref()
3334 }
3335
3336 #[must_use]
3338 pub const fn attach(&self) -> Option<&ProjectValue<BooleanValue>> {
3339 self.attach.as_ref()
3340 }
3341
3342 #[must_use]
3344 pub const fn init(&self) -> Option<&ProjectValue<BooleanValue>> {
3345 self.init.as_ref()
3346 }
3347
3348 #[must_use]
3350 pub const fn stdin_open(&self) -> Option<&ProjectValue<BooleanValue>> {
3351 self.stdin_open.as_ref()
3352 }
3353
3354 #[must_use]
3356 pub const fn tty(&self) -> Option<&ProjectValue<BooleanValue>> {
3357 self.tty.as_ref()
3358 }
3359
3360 #[must_use]
3362 pub const fn privileged(&self) -> Option<&ProjectValue<BooleanValue>> {
3363 self.privileged.as_ref()
3364 }
3365
3366 #[must_use]
3368 pub const fn use_api_socket(&self) -> Option<&ProjectValue<BooleanValue>> {
3369 self.use_api_socket.as_ref()
3370 }
3371
3372 #[must_use]
3374 pub const fn environment(&self) -> Option<&ProjectValue<ProjectEnvironment>> {
3375 self.environment.as_ref()
3376 }
3377
3378 #[must_use]
3380 pub const fn environment_files(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectEnvironmentFile>>>> {
3381 self.environment_files.as_ref()
3382 }
3383
3384 #[must_use]
3386 pub const fn label_files(&self) -> Option<&ProjectValue<ProjectServiceLabelFiles>> {
3387 self.label_files.as_ref()
3388 }
3389
3390 #[must_use]
3392 pub const fn labels(&self) -> Option<&ProjectValue<ProjectLabels>> {
3393 self.labels.as_ref()
3394 }
3395
3396 #[must_use]
3398 pub const fn annotations(&self) -> Option<&ProjectValue<ProjectAnnotations>> {
3399 self.annotations.as_ref()
3400 }
3401
3402 #[must_use]
3404 pub const fn extra_hosts(&self) -> Option<&ProjectValue<ProjectExtraHosts>> {
3405 self.extra_hosts.as_ref()
3406 }
3407
3408 #[must_use]
3410 pub const fn external_links(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3411 self.external_links.as_ref()
3412 }
3413
3414 #[must_use]
3416 pub const fn links(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3417 self.links.as_ref()
3418 }
3419
3420 #[must_use]
3422 pub const fn storage_opt(&self) -> Option<&ProjectValue<Labels>> {
3423 self.storage_opt.as_ref()
3424 }
3425
3426 #[must_use]
3428 pub const fn models(&self) -> Option<&ProjectValue<ServiceModels>> {
3429 self.models.as_ref()
3430 }
3431
3432 #[must_use]
3434 pub const fn gpus(&self) -> Option<&ProjectValue<Gpus>> {
3435 self.gpus.as_ref()
3436 }
3437
3438 #[must_use]
3440 pub const fn develop(&self) -> Option<&ProjectValue<Develop>> {
3441 self.develop.as_ref()
3442 }
3443
3444 #[must_use]
3446 pub const fn user(&self) -> Option<&ProjectValue<UserSpec>> {
3447 self.user.as_ref()
3448 }
3449
3450 #[must_use]
3452 pub const fn userns_mode(&self) -> Option<&ProjectValue<UserNamespaceMode>> {
3453 self.userns_mode.as_ref()
3454 }
3455
3456 #[must_use]
3458 pub const fn group_add(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3459 self.group_add.as_ref()
3460 }
3461
3462 #[must_use]
3467 pub const fn cap_add(&self) -> Option<&ProjectValue<Vec<ProjectValue<CapabilityAddItem>>>> {
3468 self.cap_add.as_ref()
3469 }
3470
3471 #[must_use]
3476 pub const fn cap_drop(&self) -> Option<&ProjectValue<Vec<ProjectValue<CapabilityDropItem>>>> {
3477 self.cap_drop.as_ref()
3478 }
3479
3480 #[must_use]
3484 pub const fn devices(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectDevice>>>> {
3485 self.devices.as_ref()
3486 }
3487
3488 #[must_use]
3490 pub const fn dns(&self) -> Option<&ProjectValue<ProjectDns>> {
3491 self.dns.as_ref()
3492 }
3493
3494 #[must_use]
3499 pub const fn dns_options(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3500 self.dns_options.as_ref()
3501 }
3502
3503 #[must_use]
3505 pub const fn dns_search(&self) -> Option<&ProjectValue<ProjectDnsSearch>> {
3506 self.dns_search.as_ref()
3507 }
3508
3509 #[must_use]
3514 pub const fn expose(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectExposeItem>>>> {
3515 self.expose.as_ref()
3516 }
3517
3518 #[must_use]
3523 pub const fn security_options(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectSecurityOptionItem>>>> {
3524 self.security_options.as_ref()
3525 }
3526
3527 #[must_use]
3529 pub const fn working_dir(&self) -> Option<&ProjectValue<String>> {
3530 self.working_dir.as_ref()
3531 }
3532
3533 #[must_use]
3535 pub const fn read_only(&self) -> Option<&ProjectValue<BooleanValue>> {
3536 self.read_only.as_ref()
3537 }
3538
3539 #[must_use]
3541 pub const fn pids_limit(&self) -> Option<&ProjectValue<PidsLimit>> {
3542 self.pids_limit.as_ref()
3543 }
3544
3545 #[must_use]
3547 pub const fn cpu_count(&self) -> Option<&ProjectValue<CpuCount>> {
3548 self.cpu_count.as_ref()
3549 }
3550
3551 #[must_use]
3553 pub const fn cpu_percent(&self) -> Option<&ProjectValue<CpuPercent>> {
3554 self.cpu_percent.as_ref()
3555 }
3556
3557 #[must_use]
3559 pub const fn cpu_period(&self) -> Option<&ProjectValue<CpuPeriod>> {
3560 self.cpu_period.as_ref()
3561 }
3562
3563 #[must_use]
3565 pub const fn cpu_quota(&self) -> Option<&ProjectValue<CpuQuota>> {
3566 self.cpu_quota.as_ref()
3567 }
3568
3569 #[must_use]
3571 pub const fn cpu_rt_period(&self) -> Option<&ProjectValue<CpuRtPeriod>> {
3572 self.cpu_rt_period.as_ref()
3573 }
3574 #[must_use]
3576 pub const fn cpu_rt_runtime(&self) -> Option<&ProjectValue<CpuRtRuntime>> {
3577 self.cpu_rt_runtime.as_ref()
3578 }
3579 #[must_use]
3581 pub const fn cpu_shares(&self) -> Option<&ProjectValue<ServiceInteger>> {
3582 self.cpu_shares.as_ref()
3583 }
3584 #[must_use]
3586 pub const fn cpus(&self) -> Option<&ProjectValue<Cpus>> {
3587 self.cpus.as_ref()
3588 }
3589 #[must_use]
3591 pub const fn cpuset(&self) -> Option<&ProjectValue<String>> {
3592 self.cpuset.as_ref()
3593 }
3594 #[must_use]
3596 pub const fn device_cgroup_rules(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3597 self.device_cgroup_rules.as_ref()
3598 }
3599 #[must_use]
3601 pub fn invalid_device_cgroup_rules(&self) -> &[ProjectInvalidServiceStringItem] {
3602 &self.invalid_device_cgroup_rules
3603 }
3604 #[must_use]
3606 pub const fn ipc(&self) -> Option<&ProjectValue<IpcMode>> {
3607 self.ipc.as_ref()
3608 }
3609 #[must_use]
3611 pub const fn mem_reservation(&self) -> Option<&ProjectValue<MemLimit>> {
3612 self.mem_reservation.as_ref()
3613 }
3614 #[must_use]
3616 pub const fn mem_swappiness(&self) -> Option<&ProjectValue<ServiceInteger>> {
3617 self.mem_swappiness.as_ref()
3618 }
3619 #[must_use]
3621 pub const fn memswap_limit(&self) -> Option<&ProjectValue<MemswapLimit>> {
3622 self.memswap_limit.as_ref()
3623 }
3624 #[must_use]
3626 pub const fn network_mode(&self) -> Option<&ProjectValue<NetworkMode>> {
3627 self.network_mode.as_ref()
3628 }
3629 #[must_use]
3631 pub const fn oom_kill_disable(&self) -> Option<&ProjectValue<BooleanValue>> {
3632 self.oom_kill_disable.as_ref()
3633 }
3634 #[must_use]
3636 pub const fn oom_score_adj(&self) -> Option<&ProjectValue<ServiceInteger>> {
3637 self.oom_score_adj.as_ref()
3638 }
3639 #[must_use]
3641 pub const fn pid(&self) -> Option<&ProjectValue<PidMode>> {
3642 self.pid.as_ref()
3643 }
3644 #[must_use]
3646 pub const fn scale(&self) -> Option<&ProjectValue<ServiceInteger>> {
3647 self.scale.as_ref()
3648 }
3649 #[must_use]
3651 pub const fn volumes_from(&self) -> Option<&ProjectValue<Vec<ProjectValue<VolumesFrom>>>> {
3652 self.volumes_from.as_ref()
3653 }
3654 #[must_use]
3656 pub fn invalid_volumes_from(&self) -> &[ProjectInvalidServiceStringItem] {
3657 &self.invalid_volumes_from
3658 }
3659
3660 #[must_use]
3662 pub const fn shm_size(&self) -> Option<&ProjectValue<ShmSize>> {
3663 self.shm_size.as_ref()
3664 }
3665
3666 #[must_use]
3668 pub const fn mem_limit(&self) -> Option<&ProjectValue<MemLimit>> {
3669 self.mem_limit.as_ref()
3670 }
3671
3672 #[must_use]
3674 pub const fn tmpfs(&self) -> Option<&ProjectValue<ProjectTmpfs>> {
3675 self.tmpfs.as_ref()
3676 }
3677
3678 #[must_use]
3680 pub const fn sysctls(&self) -> Option<&ProjectValue<ProjectSysctls>> {
3681 self.sysctls.as_ref()
3682 }
3683
3684 #[must_use]
3686 pub const fn logging(&self) -> Option<&ProjectValue<ProjectLogging>> {
3687 self.logging.as_ref()
3688 }
3689
3690 #[must_use]
3695 pub const fn ulimits(&self) -> Option<&ProjectValue<ProjectUlimits>> {
3696 self.ulimits.as_ref()
3697 }
3698
3699 #[must_use]
3701 pub const fn pull_policy(&self) -> Option<&ProjectValue<PullPolicy>> {
3702 self.pull_policy.as_ref()
3703 }
3704
3705 #[must_use]
3707 pub const fn restart(&self) -> Option<&ProjectValue<RestartPolicy>> {
3708 self.restart.as_ref()
3709 }
3710
3711 #[must_use]
3713 pub const fn stop_signal(&self) -> Option<&ProjectValue<String>> {
3714 self.stop_signal.as_ref()
3715 }
3716
3717 #[must_use]
3719 pub const fn stop_grace_period(&self) -> Option<&ProjectValue<StopGracePeriod>> {
3720 self.stop_grace_period.as_ref()
3721 }
3722
3723 #[must_use]
3725 pub const fn healthcheck(&self) -> Option<&ProjectValue<ProjectHealthcheck>> {
3726 self.healthcheck.as_ref()
3727 }
3728
3729 #[must_use]
3735 pub const fn build(&self) -> Option<&ProjectValue<ProjectBuild>> {
3736 self.build.as_ref()
3737 }
3738
3739 #[must_use]
3741 pub const fn deploy(&self) -> Option<&ProjectValue<ProjectDeploy>> {
3742 self.deploy.as_ref()
3743 }
3744
3745 #[must_use]
3747 pub const fn depends_on(&self) -> Option<&ProjectValue<ProjectDependsOn>> {
3748 self.depends_on.as_ref()
3749 }
3750
3751 #[must_use]
3753 pub const fn ports(&self) -> Option<&ProjectValue<Vec<ProjectValue<Port>>>> {
3754 self.ports.as_ref()
3755 }
3756
3757 #[must_use]
3759 pub const fn volumes(&self) -> Option<&ProjectValue<Vec<ProjectValue<VolumeMount>>>> {
3760 self.volume_views.volumes.as_ref()
3761 }
3762
3763 #[must_use]
3768 pub const fn volume_mount_options(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectVolumeMountOptions>>>> {
3769 self.volume_views.options.as_ref()
3770 }
3771
3772 #[must_use]
3774 pub const fn configs(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectGrant>>>> {
3775 self.configs.as_ref()
3776 }
3777
3778 #[must_use]
3780 pub const fn secrets(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectGrant>>>> {
3781 self.secrets.as_ref()
3782 }
3783
3784 #[must_use]
3786 pub const fn networks(&self) -> Option<&ProjectValue<ServiceNetworks>> {
3787 self.networks.as_ref()
3788 }
3789
3790 #[must_use]
3792 pub const fn profiles(&self) -> Option<&ProjectValue<Vec<ProjectValue<String>>>> {
3793 self.profiles.as_ref()
3794 }
3795
3796 #[must_use]
3798 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
3799 &self.unmodeled_fields
3800 }
3801}
3802
3803#[derive(Debug, Clone, PartialEq, Eq)]
3805pub struct ProjectBlkioConfig {
3806 weight: Option<ProjectValue<BlkioScalar>>,
3807 device_read_bps: Option<ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>>,
3808 device_read_iops: Option<ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>>,
3809 device_write_bps: Option<ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>>,
3810 device_write_iops: Option<ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>>,
3811 weight_device: Option<ProjectValue<Vec<ProjectValue<ProjectBlkioWeightDevice>>>>,
3812 unmodeled_fields: Vec<ProjectFieldReference>,
3813}
3814impl ProjectBlkioConfig {
3815 #[must_use]
3817 pub const fn weight(&self) -> Option<&ProjectValue<BlkioScalar>> {
3818 self.weight.as_ref()
3819 }
3820 #[must_use]
3822 pub const fn device_read_bps(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>> {
3823 self.device_read_bps.as_ref()
3824 }
3825 #[must_use]
3827 pub const fn device_read_iops(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>> {
3828 self.device_read_iops.as_ref()
3829 }
3830 #[must_use]
3832 pub const fn device_write_bps(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>> {
3833 self.device_write_bps.as_ref()
3834 }
3835 #[must_use]
3837 pub const fn device_write_iops(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>> {
3838 self.device_write_iops.as_ref()
3839 }
3840 #[must_use]
3842 pub const fn weight_device(&self) -> Option<&ProjectValue<Vec<ProjectValue<ProjectBlkioWeightDevice>>>> {
3843 self.weight_device.as_ref()
3844 }
3845 #[must_use]
3847 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
3848 &self.unmodeled_fields
3849 }
3850}
3851
3852#[derive(Debug, Clone, PartialEq, Eq)]
3854pub struct ProjectBlkioDeviceRate {
3855 form: ProjectBlkioDeviceRateForm,
3856 path: Option<ProjectValue<String>>,
3857 rate: Option<ProjectValue<BlkioScalar>>,
3858 unmodeled_fields: Vec<ProjectFieldReference>,
3859}
3860impl ProjectBlkioDeviceRate {
3861 #[must_use]
3863 pub const fn form(&self) -> ProjectBlkioDeviceRateForm {
3864 self.form
3865 }
3866 #[must_use]
3868 pub const fn path(&self) -> Option<&ProjectValue<String>> {
3869 self.path.as_ref()
3870 }
3871 #[must_use]
3873 pub const fn rate(&self) -> Option<&ProjectValue<BlkioScalar>> {
3874 self.rate.as_ref()
3875 }
3876 #[must_use]
3878 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
3879 &self.unmodeled_fields
3880 }
3881}
3882
3883#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3885#[non_exhaustive]
3886pub enum ProjectBlkioDeviceRateForm {
3887 Mapping,
3889 Unmodeled,
3891}
3892
3893#[derive(Debug, Clone, PartialEq, Eq)]
3895pub struct ProjectBlkioWeightDevice {
3896 form: ProjectBlkioWeightDeviceForm,
3897 path: Option<ProjectValue<String>>,
3898 weight: Option<ProjectValue<BlkioScalar>>,
3899 unmodeled_fields: Vec<ProjectFieldReference>,
3900}
3901impl ProjectBlkioWeightDevice {
3902 #[must_use]
3904 pub const fn form(&self) -> ProjectBlkioWeightDeviceForm {
3905 self.form
3906 }
3907 #[must_use]
3909 pub const fn path(&self) -> Option<&ProjectValue<String>> {
3910 self.path.as_ref()
3911 }
3912 #[must_use]
3914 pub const fn weight(&self) -> Option<&ProjectValue<BlkioScalar>> {
3915 self.weight.as_ref()
3916 }
3917 #[must_use]
3919 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
3920 &self.unmodeled_fields
3921 }
3922}
3923
3924#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3926#[non_exhaustive]
3927pub enum ProjectBlkioWeightDeviceForm {
3928 Mapping,
3930 Unmodeled,
3932}
3933
3934#[derive(Debug, Clone, PartialEq, Eq)]
3936pub struct ProjectResource<T> {
3937 name: ProjectKey,
3938 definition: ProjectValue<T>,
3939}
3940
3941impl<T> ProjectResource<T> {
3942 #[must_use]
3944 pub const fn name(&self) -> &ProjectKey {
3945 &self.name
3946 }
3947
3948 #[must_use]
3950 pub const fn definition(&self) -> &ProjectValue<T> {
3951 &self.definition
3952 }
3953}
3954
3955#[derive(Debug, Clone, PartialEq, Eq)]
3957pub struct ProjectView {
3958 source_ids: Vec<SourceId>,
3959 base_directory: PathBuf,
3960 provenance: MergeProvenance,
3961 name: Option<ProjectValue<String>>,
3962 version: Option<ProjectValue<String>>,
3963 include: Option<ProjectValue<Includes>>,
3964 models: Option<ProjectValue<ModelDefinitions>>,
3965 services: Vec<ProjectService>,
3966 networks: Vec<ProjectResource<NetworkDefinition>>,
3967 volumes: Vec<ProjectResource<VolumeDefinition>>,
3968 configs: Vec<ProjectResource<ConfigDefinition>>,
3969 secrets: Vec<ProjectResource<SecretDefinition>>,
3970 unmodeled_fields: Vec<ProjectFieldReference>,
3971}
3972
3973impl ProjectView {
3974 #[must_use]
3976 pub fn source_ids(&self) -> &[SourceId] {
3977 &self.source_ids
3978 }
3979
3980 #[must_use]
3982 pub fn base_directory(&self) -> &Path {
3983 &self.base_directory
3984 }
3985
3986 #[must_use]
3988 pub const fn provenance(&self) -> &MergeProvenance {
3989 &self.provenance
3990 }
3991
3992 #[must_use]
3994 pub const fn name(&self) -> Option<&ProjectValue<String>> {
3995 self.name.as_ref()
3996 }
3997
3998 #[must_use]
4000 pub const fn version(&self) -> Option<&ProjectValue<String>> {
4001 self.version.as_ref()
4002 }
4003
4004 #[must_use]
4006 pub const fn include(&self) -> Option<&ProjectValue<Includes>> {
4007 self.include.as_ref()
4008 }
4009
4010 #[must_use]
4012 pub const fn models(&self) -> Option<&ProjectValue<ModelDefinitions>> {
4013 self.models.as_ref()
4014 }
4015
4016 #[must_use]
4018 pub fn services(&self) -> &[ProjectService] {
4019 &self.services
4020 }
4021
4022 #[must_use]
4024 pub fn service(&self, name: &str) -> Option<&ProjectService> {
4025 self.services.iter().find(|service| service.name.value == name)
4026 }
4027
4028 #[must_use]
4030 pub fn networks(&self) -> &[ProjectResource<NetworkDefinition>] {
4031 &self.networks
4032 }
4033
4034 #[must_use]
4036 pub fn volumes(&self) -> &[ProjectResource<VolumeDefinition>] {
4037 &self.volumes
4038 }
4039
4040 #[must_use]
4042 pub fn configs(&self) -> &[ProjectResource<ConfigDefinition>] {
4043 &self.configs
4044 }
4045
4046 #[must_use]
4048 pub fn secrets(&self) -> &[ProjectResource<SecretDefinition>] {
4049 &self.secrets
4050 }
4051
4052 #[must_use]
4054 pub fn unmodeled_fields(&self) -> &[ProjectFieldReference] {
4055 &self.unmodeled_fields
4056 }
4057}
4058
4059#[derive(Debug, Clone, PartialEq, Eq)]
4061pub struct ProjectViewResult {
4062 view: Option<ProjectView>,
4063 diagnostics: Vec<Diagnostic>,
4064}
4065
4066impl ProjectViewResult {
4067 #[must_use]
4069 pub const fn view(&self) -> Option<&ProjectView> {
4070 self.view.as_ref()
4071 }
4072
4073 #[must_use]
4075 pub fn diagnostics(&self) -> &[Diagnostic] {
4076 &self.diagnostics
4077 }
4078
4079 #[must_use]
4081 pub fn is_valid(&self) -> bool {
4082 self.view.is_some()
4083 && self
4084 .diagnostics
4085 .iter()
4086 .all(|diagnostic| diagnostic.severity() != Severity::Error)
4087 }
4088
4089 #[must_use]
4091 pub fn into_parts(self) -> (Option<ProjectView>, Vec<Diagnostic>) {
4092 (self.view, self.diagnostics)
4093 }
4094}
4095
4096#[must_use]
4101pub fn build_project_view(project: &MergedProject, selection: Option<&ProfileSelection>) -> ProjectViewResult {
4102 if selection.is_some_and(|selection| !selection.belongs_to(project)) {
4103 return ProjectViewResult {
4104 view: None,
4105 diagnostics: vec![Diagnostic::new(
4106 SELECTION_PROJECT_MISMATCH,
4107 Severity::Error,
4108 "profile selection does not belong to the merged project",
4109 )],
4110 };
4111 }
4112
4113 Builder::new(project, selection).build()
4114}
4115
4116struct Builder<'a> {
4117 project: &'a MergedProject,
4118 selection: Option<&'a ProfileSelection>,
4119 diagnostics: Vec<Diagnostic>,
4120 root_unmodeled: Vec<ProjectFieldReference>,
4121 pending_unmodeled: Vec<ProjectFieldReference>,
4122}
4123
4124impl<'a> Builder<'a> {
4125 const fn new(project: &'a MergedProject, selection: Option<&'a ProfileSelection>) -> Self {
4126 Self {
4127 project,
4128 selection,
4129 diagnostics: Vec::new(),
4130 root_unmodeled: Vec::new(),
4131 pending_unmodeled: Vec::new(),
4132 }
4133 }
4134
4135 fn build(mut self) -> ProjectViewResult {
4136 let root = self.project.root();
4137 let entries = root.as_mapping().unwrap_or_default();
4138 let mut name = None;
4139 let mut version = None;
4140 let mut include = None;
4141 let mut models = None;
4142 let mut services = Vec::new();
4143 let mut networks = Vec::new();
4144 let mut volumes = Vec::new();
4145 let mut configs = Vec::new();
4146 let mut secrets = Vec::new();
4147
4148 for entry in entries {
4149 match entry.key() {
4150 "name" => name = self.project_string(entry.value(), "project name"),
4151 "version" => version = self.project_string(entry.value(), "Compose version"),
4152 "include" => {
4153 include = self.includes(entry);
4154 if include.is_none() {
4155 self.record_root_unmodeled(&[], entry);
4156 }
4157 }
4158 "models" => {
4159 models = self.model_definitions(entry.value());
4160 if models.is_none() {
4161 self.record_root_unmodeled(&[], entry);
4162 }
4163 }
4164 "services" => services = self.services(entry.value()),
4165 "networks" => networks = self.network_definitions(entry.value()),
4166 "volumes" => volumes = self.volume_definitions(entry.value()),
4167 "configs" => configs = self.config_definitions(entry.value()),
4168 "secrets" => secrets = self.secret_definitions(entry.value()),
4169 _ => self.record_root_unmodeled(&[], entry),
4170 }
4171 }
4172
4173 ProjectViewResult {
4174 view: Some(ProjectView {
4175 source_ids: self.project.source_ids().to_vec(),
4176 base_directory: self.project.base_directory().to_path_buf(),
4177 provenance: root.provenance().clone(),
4178 name,
4179 version,
4180 include,
4181 models,
4182 services,
4183 networks,
4184 volumes,
4185 configs,
4186 secrets,
4187 unmodeled_fields: self.root_unmodeled,
4188 }),
4189 diagnostics: self.diagnostics,
4190 }
4191 }
4192
4193 fn services(&mut self, value: &MergedValue) -> Vec<ProjectService> {
4194 let Some(entries) = self.mapping(value, "services must be a mapping") else {
4195 return Vec::new();
4196 };
4197 let selection = self.selection;
4198 let mut services = Vec::new();
4199 for entry in entries {
4200 if service_in_scope(selection, entry.key()) {
4201 services.extend(self.service(entry));
4202 }
4203 }
4204 services
4205 }
4206
4207 fn service(&mut self, entry: &MergedEntry) -> Option<ProjectService> {
4208 let pending_start = self.pending_unmodeled.len();
4209 let fields = self.mapping(entry.value(), "service definition must be a mapping")?;
4210 let mut service = ProjectService::from_entry(entry);
4211 let path = ["services".to_owned(), entry.key().to_owned()];
4212 for field in fields {
4213 if self.set_extra_service_field(&mut service, field, &path) {
4214 continue;
4215 }
4216 match field.key() {
4217 "hostname" => service.hostname = self.hostname(field.value()),
4218 "container_name" => {
4219 service.container_name = self.project_string(field.value(), "service container name");
4220 }
4221 "image" => {
4222 service.image = self
4223 .project_string(field.value(), "service image")
4224 .map(|value| ProjectValue {
4225 value: ImageReference::parse(value.value),
4226 provenance: value.provenance,
4227 sensitive: value.sensitive,
4228 });
4229 }
4230 "platform" => self.set_platform(&mut service, field, &path),
4231 "entrypoint" => service.entrypoint = self.entrypoint(field.value()),
4232 "command" => service.command = self.command(field.value()),
4233 "credential_spec" => self.set_credential_spec(&mut service, field, &path),
4234 "extends" => self.set_extends(&mut service, field, &path),
4235 "provider" => self.set_provider(&mut service, field, &path),
4236 "post_start" => self.set_post_start(&mut service, field, &path),
4237 "pre_stop" => self.set_pre_stop(&mut service, field, &path),
4238 "pre_start" => self.set_pre_start(&mut service, field, &path),
4239 "blkio_config" => self.set_blkio_config(&mut service, field, &path),
4240 "cgroup" | "cgroup_parent" => self.set_cgroup(&mut service, field, &path),
4241 "attach" | "init" | "stdin_open" | "tty" | "privileged" => {
4242 self.set_service_boolean(&mut service, field, &path);
4243 }
4244 "environment" => service.environment = self.environment(field.value()),
4245 "env_file" => service.environment_files = self.environment_files(field.value(), &path),
4246 "labels" => service.labels = self.service_labels(field.value()),
4247 "annotations" => service.annotations = self.service_annotations(field.value()),
4248 "extra_hosts" => service.extra_hosts = self.extra_hosts(field.value()),
4249 "user" => service.user = self.user(field.value()),
4250 "userns_mode" => service.userns_mode = self.userns_mode(field.value()),
4251 "group_add" => {
4252 service.group_add = self.string_collection(field.value(), "group_add must be a sequence");
4253 }
4254 "cap_add" => service.cap_add = self.capability_add(field.value()),
4255 "cap_drop" => service.cap_drop = self.capability_drop(field.value()),
4256 "devices" => service.devices = self.devices(field.value(), &path),
4257 "dns" => service.dns = self.dns(field.value()),
4258 "dns_opt" => service.dns_options = self.dns_options(field.value()),
4259 "dns_search" => service.dns_search = self.dns_search(field.value()),
4260 "expose" => service.expose = self.expose(field.value()),
4261 "security_opt" => service.security_options = self.security_options(field.value()),
4262 "working_dir" => service.working_dir = self.project_string(field.value(), "service working directory"),
4263 "read_only" => service.read_only = self.read_only(field.value()),
4264 "shm_size" => service.shm_size = self.shm_size(field.value()),
4265 "mem_limit" => service.mem_limit = self.mem_limit(field.value()),
4266 "tmpfs" => service.tmpfs = self.tmpfs(field.value()),
4267 "sysctls" => service.sysctls = self.sysctls(field.value()),
4268 "logging" => {
4269 service.logging = self.logging(field.value(), &path);
4270 if service.logging.is_none() {
4271 service.unmodeled_fields.push(field_reference(&path, field));
4272 }
4273 }
4274 "ulimits" => {
4275 let (ulimits, unmodeled) = self.ulimits(field.value(), &path);
4276 service.ulimits = ulimits;
4277 service.unmodeled_fields.extend(unmodeled);
4278 if service.ulimits.is_none() {
4279 service.unmodeled_fields.push(field_reference(&path, field));
4280 }
4281 }
4282 "pull_policy" => service.pull_policy = self.pull_policy(field.value()),
4283 "pull_refresh_after" => self.set_pull_refresh_after(&mut service, field, &path),
4284 "restart" => service.restart = self.restart_policy(field.value()),
4285 "runtime" => self.set_runtime(&mut service, field, &path),
4286 "stop_signal" => service.stop_signal = self.project_string(field.value(), "service stop signal"),
4287 "stop_grace_period" => {
4288 service.stop_grace_period = self.stop_grace_period(field.value());
4289 }
4290 "healthcheck" => service.healthcheck = self.healthcheck(field.value(), &path),
4291 "build" => service.build = self.service_build(field.value(), &path),
4292 "deploy" => self.set_service_deploy(&mut service, field, &path),
4293 "depends_on" => service.depends_on = self.depends_on(field.value(), &path),
4294 "ports" => service.ports = self.ports(field.value(), &path),
4295 "volumes" => self.set_volumes(&mut service, field, &path),
4296 "configs" => service.configs = self.grants(field.value(), &path, "config"),
4297 "secrets" => service.secrets = self.grants(field.value(), &path, "secret"),
4298 "networks" => service.networks = self.service_networks(field.value(), &path),
4299 "profiles" => service.profiles = self.string_collection(field.value(), "profiles must be a sequence"),
4300 _ => service.unmodeled_fields.push(field_reference(&path, field)),
4301 }
4302 }
4303 service
4304 .unmodeled_fields
4305 .extend(self.pending_unmodeled.drain(pending_start..));
4306 self.validate_service_cross_fields(&service);
4307 Some(service)
4308 }
4309
4310 fn set_extra_service_field(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) -> bool {
4311 self.set_service_runtime_field(service, field, path) || self.set_service_remaining_field(service, field, path)
4312 }
4313
4314 fn set_service_remaining_field(
4315 &mut self,
4316 service: &mut ProjectService,
4317 field: &MergedEntry,
4318 path: &[String],
4319 ) -> bool {
4320 match field.key() {
4321 "domainname" => {
4322 service.domainname =
4323 self.strict_project_string(field.value(), "service domainname must be a YAML string scalar");
4324 }
4325 "isolation" => {
4326 service.isolation =
4327 self.strict_project_string(field.value(), "service isolation must be a YAML string scalar");
4328 }
4329 "mac_address" => {
4330 service.mac_address =
4331 self.strict_project_string(field.value(), "service mac_address must be a YAML string scalar");
4332 }
4333 "uts" => {
4334 service.uts = self.strict_project_string(field.value(), "service uts must be a YAML string scalar");
4335 }
4336 "gpus" => service.gpus = self.gpus(field.value(), path),
4337 "use_api_socket" => {
4338 service.use_api_socket = self.boolean(field.value(), "service use_api_socket must be a boolean");
4339 }
4340 "label_file" => {
4341 service.label_files = self.label_files(field.value(), path);
4342 if service.label_files.is_none() {
4343 service.unmodeled_fields.push(field_reference(path, field));
4344 }
4345 }
4346 "external_links" => {
4347 service.external_links = self.string_collection(field.value(), "external_links must be a sequence");
4348 }
4349 "links" => service.links = self.string_collection(field.value(), "links must be a sequence"),
4350 "storage_opt" => {
4351 service.storage_opt = self
4352 .labels(field.value())
4353 .map(|value| ProjectValue::new(value, field.value()));
4354 }
4355 "models" => service.models = self.service_models(field, path),
4356 "develop" if field.value().as_mapping().is_some() => service.develop = self.develop(field.value(), path),
4357 "develop" => {
4358 self.expected(field.value(), "service develop must be a mapping");
4359 service.unmodeled_fields.push(field_reference(path, field));
4360 }
4361 _ => return false,
4362 }
4363 true
4364 }
4365
4366 fn includes(&mut self, entry: &MergedEntry) -> Option<ProjectValue<Includes>> {
4367 let value = entry.value();
4368 let sequence = value.as_sequence()?;
4369 let mut items = Vec::new();
4370 for item in sequence {
4371 match item.kind() {
4372 MergedValueKind::Scalar(_) => {
4373 if let Some(path) = self.strict_located_string(item, "include item must be a YAML string scalar") {
4374 items.push(IncludeItem::Short(path));
4375 } else {
4376 self.record_root_unmodeled(&[], entry);
4377 items.push(IncludeItem::Unmodeled);
4378 }
4379 }
4380 MergedValueKind::Mapping(fields) => {
4381 let span = effective_span(item);
4382 let mut paths = Vec::new();
4383 let mut env_files = Vec::new();
4384 let mut project_directory = None;
4385 let mut unmodeled_fields = Vec::new();
4386 for field in fields {
4387 match field.key() {
4388 "path" => {
4389 paths = self.strict_string_or_sequence(
4390 field.value(),
4391 "include path must be a YAML string or sequence",
4392 );
4393 if !Self::is_strict_string_or_sequence(field.value()) {
4394 self.record_root_unmodeled(&["include".to_owned()], field);
4395 unmodeled_fields.push(FieldReference::generated(
4396 Located::new(field.key().to_owned(), entry_span(field)),
4397 entry_span(field),
4398 Some(effective_span(field.value())),
4399 ));
4400 }
4401 }
4402 "env_file" => {
4403 env_files = self.strict_string_or_sequence(
4404 field.value(),
4405 "include env_file must be a YAML string or sequence",
4406 );
4407 if !Self::is_strict_string_or_sequence(field.value()) {
4408 self.record_root_unmodeled(&["include".to_owned()], field);
4409 unmodeled_fields.push(FieldReference::generated(
4410 Located::new(field.key().to_owned(), entry_span(field)),
4411 entry_span(field),
4412 Some(effective_span(field.value())),
4413 ));
4414 }
4415 }
4416 "project_directory" => {
4417 project_directory = self.strict_located_string(
4418 field.value(),
4419 "include project_directory must be a YAML string",
4420 );
4421 if project_directory.is_none() {
4422 self.record_root_unmodeled(&["include".to_owned()], field);
4423 unmodeled_fields.push(FieldReference::generated(
4424 Located::new(field.key().to_owned(), entry_span(field)),
4425 entry_span(field),
4426 Some(effective_span(field.value())),
4427 ));
4428 }
4429 }
4430 _ => {
4431 self.record_root_unmodeled(&["include".to_owned()], field);
4432 unmodeled_fields.push(FieldReference::generated(
4433 Located::new(field.key().to_owned(), entry_span(field)),
4434 entry_span(field),
4435 Some(effective_span(field.value())),
4436 ));
4437 }
4438 }
4439 }
4440 if paths.is_empty() {
4441 self.diagnostics.push(
4442 Diagnostic::new(PROJECT_MISSING_FIELD, Severity::Error, "include mapping requires path")
4443 .with_label(DiagnosticLabel::primary(span, "path is missing")),
4444 );
4445 }
4446 items.push(IncludeItem::Long(IncludeLong::new(
4447 span,
4448 paths,
4449 env_files,
4450 project_directory,
4451 unmodeled_fields,
4452 )));
4453 }
4454 _ => {
4455 self.expected(item, "include items must be strings or mappings");
4456 self.record_root_unmodeled(&[], entry);
4457 items.push(IncludeItem::Unmodeled);
4458 }
4459 }
4460 }
4461 Some(ProjectValue::new(
4462 Includes::new(effective_span(value), items, Vec::new()),
4463 value,
4464 ))
4465 }
4466
4467 fn model_definitions(&mut self, value: &MergedValue) -> Option<ProjectValue<ModelDefinitions>> {
4468 let entries = value.as_mapping()?;
4469 let mut definitions = Vec::new();
4470 for entry in entries {
4471 let span = effective_span(entry.value());
4472 let mut definition = ModelDefinition::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
4473 let Some(members) = entry.value().as_mapping() else {
4474 self.expected(entry.value(), "model definition must be a mapping");
4475 self.record_root_unmodeled(&["models".to_owned()], entry);
4476 continue;
4477 };
4478 for member in members {
4479 match member.key() {
4480 "name" => {
4481 let parsed = self.strict_located_string(member.value(), "model name must be a YAML string");
4482 if let Some(value) = parsed {
4483 definition.set_name(value);
4484 } else {
4485 definition.push_unmodeled(FieldReference::generated(
4486 Located::new(member.key().to_owned(), entry_span(member)),
4487 entry_span(member),
4488 Some(effective_span(member.value())),
4489 ));
4490 self.record_root_unmodeled(&["models".to_owned(), entry.key().to_owned()], member);
4491 }
4492 }
4493 "model" => {
4494 let parsed =
4495 self.strict_located_string(member.value(), "model reference must be a YAML string");
4496 if let Some(value) = parsed {
4497 definition.set_model(value);
4498 } else {
4499 definition.push_unmodeled(FieldReference::generated(
4500 Located::new(member.key().to_owned(), entry_span(member)),
4501 entry_span(member),
4502 Some(effective_span(member.value())),
4503 ));
4504 self.record_root_unmodeled(&["models".to_owned(), entry.key().to_owned()], member);
4505 }
4506 }
4507 "context_size" => {
4508 if let Some(value) =
4509 self.located_yaml_integer(member.value(), "model context_size must be a YAML integer")
4510 {
4511 definition.set_context_size(value);
4512 } else {
4513 self.record_root_unmodeled(&["models".to_owned(), entry.key().to_owned()], member);
4514 }
4515 }
4516 "runtime_flags" => {
4517 let flags = self.strict_string_or_sequence(
4518 member.value(),
4519 "model runtime_flags must be a string or sequence",
4520 );
4521 if !Self::is_strict_string_or_sequence(member.value()) {
4522 definition.push_unmodeled(FieldReference::generated(
4523 Located::new(member.key().to_owned(), entry_span(member)),
4524 entry_span(member),
4525 Some(effective_span(member.value())),
4526 ));
4527 self.record_root_unmodeled(&["models".to_owned(), entry.key().to_owned()], member);
4528 }
4529 definition.set_runtime_flags(flags);
4530 }
4531 _ => {
4532 definition.push_unmodeled(FieldReference::generated(
4533 Located::new(member.key().to_owned(), entry_span(member)),
4534 entry_span(member),
4535 Some(effective_span(member.value())),
4536 ));
4537 self.record_root_unmodeled(&["models".to_owned(), entry.key().to_owned()], member);
4538 }
4539 }
4540 }
4541 if definition.model().is_none() {
4542 self.diagnostics.push(
4543 Diagnostic::new(
4544 PROJECT_MISSING_FIELD,
4545 Severity::Error,
4546 "model definition requires model",
4547 )
4548 .with_label(DiagnosticLabel::primary(span, "model is missing")),
4549 );
4550 }
4551 definitions.push(definition);
4552 }
4553 Some(ProjectValue::new(
4554 ModelDefinitions::new(effective_span(value), definitions, Vec::new()),
4555 value,
4556 ))
4557 }
4558
4559 fn service_models(&mut self, field: &MergedEntry, path: &[String]) -> Option<ProjectValue<ServiceModels>> {
4560 let value = field.value();
4561 let mut bindings = Vec::new();
4562 match value.kind() {
4563 MergedValueKind::Sequence(items) => {
4564 for item in items {
4565 if let Some(model) = self.strict_located_string(item, "service model binding must be a YAML string")
4566 {
4567 bindings.push(ServiceModelBinding::new(model, effective_span(item)));
4568 } else {
4569 self.record_pending_unmodeled(path, field);
4570 }
4571 }
4572 }
4573 MergedValueKind::Mapping(entries) => {
4574 for entry in entries {
4575 let mut binding = ServiceModelBinding::new(
4576 Located::new(entry.key().to_owned(), entry_span(entry)),
4577 effective_span(entry.value()),
4578 );
4579 if let Some(members) = entry.value().as_mapping() {
4580 for member in members {
4581 match member.key() {
4582 "endpoint_var" => {
4583 let parsed = self.strict_located_string(
4584 member.value(),
4585 "model endpoint_var must be a YAML string",
4586 );
4587 if let Some(value) = parsed {
4588 binding.set_endpoint_var(value);
4589 } else {
4590 binding.push_unmodeled(FieldReference::generated(
4591 Located::new(member.key().to_owned(), entry_span(member)),
4592 entry_span(member),
4593 Some(effective_span(member.value())),
4594 ));
4595 }
4596 }
4597 "model_var" => {
4598 let parsed = self
4599 .strict_located_string(member.value(), "model model_var must be a YAML string");
4600 if let Some(value) = parsed {
4601 binding.set_model_var(value);
4602 } else {
4603 binding.push_unmodeled(FieldReference::generated(
4604 Located::new(member.key().to_owned(), entry_span(member)),
4605 entry_span(member),
4606 Some(effective_span(member.value())),
4607 ));
4608 }
4609 }
4610 _ => binding.push_unmodeled(FieldReference::generated(
4611 Located::new(member.key().to_owned(), entry_span(member)),
4612 entry_span(member),
4613 Some(effective_span(member.value())),
4614 )),
4615 }
4616 }
4617 } else if !matches!(entry.value().kind(), MergedValueKind::Null(_)) {
4618 self.expected(entry.value(), "service model binding must be null or a mapping");
4619 binding.push_unmodeled(FieldReference::generated(
4620 Located::new(entry.key().to_owned(), entry_span(entry)),
4621 entry_span(entry),
4622 Some(effective_span(entry.value())),
4623 ));
4624 }
4625 if !binding.unmodeled_fields().is_empty() {
4626 self.record_pending_unmodeled(path, field);
4627 }
4628 bindings.push(binding);
4629 }
4630 }
4631 _ => return None,
4632 }
4633 Some(ProjectValue::new(
4634 ServiceModels::new(effective_span(value), bindings, Vec::new()),
4635 value,
4636 ))
4637 }
4638
4639 #[expect(
4640 clippy::too_many_lines,
4641 reason = "GPU selectors retain every supported field and malformed source evidence"
4642 )]
4643 fn gpus(&mut self, value: &MergedValue, path: &[String]) -> Option<ProjectValue<Gpus>> {
4644 let mut gpu_path = path.to_vec();
4645 gpu_path.push("gpus".to_owned());
4646 let result = match value.kind() {
4647 MergedValueKind::Scalar(_) => self
4648 .located_string(value, "service gpus must be a string or sequence")
4649 .and_then(|selector| {
4650 if selector.value() == "all" {
4651 Some(Gpus::All(selector))
4652 } else {
4653 self.expected(value, "service gpus scalar must be exactly `all`");
4654 None
4655 }
4656 }),
4657 MergedValueKind::Sequence(items) => {
4658 let mut devices = Vec::new();
4659 for item in items {
4660 let Some(members) = item.as_mapping() else {
4661 self.expected(item, "GPU selector must be a mapping");
4662 self.pending_unmodeled.push(value_reference(&gpu_path, item));
4663 continue;
4664 };
4665 let mut device = GpuDevice::new(effective_span(item));
4666 for member in members {
4667 match member.key() {
4668 "capabilities" => {
4669 let capabilities = self.strict_string_sequence(
4670 member.value(),
4671 "GPU capabilities must be a YAML sequence of strings",
4672 );
4673 if !Self::is_strict_string_sequence(member.value()) {
4674 self.record_pending_unmodeled(&gpu_path, member);
4675 }
4676 device.set_capabilities(capabilities);
4677 }
4678 "count" => {
4679 let parsed = self.located_integer_or_string_scalar(
4680 member.value(),
4681 "GPU count must be a YAML integer or string",
4682 );
4683 if let Some(value) = parsed {
4684 device.set_count(value);
4685 } else {
4686 self.record_pending_unmodeled(&gpu_path, member);
4687 }
4688 }
4689 "device_ids" => {
4690 let ids = self.strict_string_or_sequence(
4691 member.value(),
4692 "GPU device_ids must be a string or sequence",
4693 );
4694 if !Self::is_strict_string_or_sequence(member.value()) {
4695 self.record_pending_unmodeled(&gpu_path, member);
4696 }
4697 device.set_device_ids(ids);
4698 }
4699 "driver" => {
4700 let parsed =
4701 self.strict_located_string(member.value(), "GPU driver must be a YAML string");
4702 if let Some(value) = parsed {
4703 device.set_driver(value);
4704 } else {
4705 self.record_pending_unmodeled(&gpu_path, member);
4706 }
4707 }
4708 "options" => match member.value().kind() {
4709 MergedValueKind::Mapping(_) => self
4710 .key_value_mapping(member.value(), "GPU options must be a scalar mapping")
4711 .map(GpuOptions::Mapping)
4712 .into_iter()
4713 .for_each(|value| device.set_options(value)),
4714 MergedValueKind::Sequence(_) => {
4715 let options = self.strict_string_or_sequence(
4716 member.value(),
4717 "GPU options must be a string sequence",
4718 );
4719 if !Self::is_strict_string_or_sequence(member.value()) {
4720 self.record_pending_unmodeled(&gpu_path, member);
4721 }
4722 device.set_options(GpuOptions::List(options));
4723 }
4724 _ => {
4725 self.expected(member.value(), "GPU options must be a mapping or sequence");
4726 self.record_pending_unmodeled(&gpu_path, member);
4727 }
4728 },
4729 _ => self.record_pending_unmodeled(&gpu_path, member),
4730 }
4731 }
4732 if device.count().is_some() && !device.device_ids().is_empty() {
4733 self.diagnostics.push(
4734 Diagnostic::new(
4735 crate::model::GPU_COUNT_DEVICE_IDS_CONFLICT,
4736 Severity::Warning,
4737 "GPU selector cannot use count and device_ids together",
4738 )
4739 .with_label(DiagnosticLabel::primary(
4740 device.span(),
4741 "conflicting GPU allocation selectors",
4742 )),
4743 );
4744 }
4745 if !device.capabilities().iter().any(|value| !value.value().is_empty()) {
4746 self.diagnostics.push(
4747 Diagnostic::new(
4748 GPU_MISSING_CAPABILITIES,
4749 Severity::Error,
4750 "GPU selector requires a non-empty capabilities declaration",
4751 )
4752 .with_label(DiagnosticLabel::primary(
4753 device.span(),
4754 "capabilities are missing, empty, or malformed",
4755 )),
4756 );
4757 }
4758 devices.push(device);
4759 }
4760 Some(Gpus::Devices {
4761 span: effective_span(value),
4762 devices,
4763 unmodeled_items: Vec::new(),
4764 })
4765 }
4766 _ => None,
4767 }?;
4768 Some(ProjectValue::new(result, value))
4769 }
4770
4771 #[expect(
4772 clippy::too_many_lines,
4773 reason = "develop watch retains each documented member and nested source evidence explicitly"
4774 )]
4775 fn develop(&mut self, value: &MergedValue, path: &[String]) -> Option<ProjectValue<Develop>> {
4776 let entries = value.as_mapping()?;
4777 let mut develop_path = path.to_vec();
4778 develop_path.push("develop".to_owned());
4779 let mut watch = Vec::new();
4780 for entry in entries {
4781 if entry.key() != "watch" {
4782 self.record_pending_unmodeled(&develop_path, entry);
4783 continue;
4784 }
4785 let Some(items) = entry.value().as_sequence() else {
4786 self.expected(entry.value(), "develop watch must be a sequence");
4787 self.record_pending_unmodeled(&develop_path, entry);
4788 continue;
4789 };
4790 for item_value in items {
4791 let Some(members) = item_value.as_mapping() else {
4792 self.expected(item_value, "develop watch item must be a mapping");
4793 self.pending_unmodeled.push(value_reference(&develop_path, item_value));
4794 continue;
4795 };
4796 let mut item = DevelopWatch::new(effective_span(item_value));
4797 let mut has_exec = false;
4798 for member in members {
4799 match member.key() {
4800 "action" => {
4801 let parsed =
4802 self.strict_located_string(member.value(), "develop action must be a YAML string");
4803 if let Some(value) = parsed {
4804 item.set_action(value);
4805 } else {
4806 self.record_pending_unmodeled(&develop_path, member);
4807 }
4808 }
4809 "path" => {
4810 let parsed =
4811 self.strict_located_string(member.value(), "develop path must be a YAML string");
4812 if let Some(value) = parsed {
4813 item.set_path(value);
4814 } else {
4815 self.record_pending_unmodeled(&develop_path, member);
4816 }
4817 }
4818 "target" => {
4819 let parsed =
4820 self.strict_located_string(member.value(), "develop target must be a YAML string");
4821 if let Some(value) = parsed {
4822 item.set_target(value);
4823 } else {
4824 self.record_pending_unmodeled(&develop_path, member);
4825 }
4826 }
4827 "ignore" => {
4828 let values = self.strict_string_or_sequence(
4829 member.value(),
4830 "develop ignore must be a string or sequence",
4831 );
4832 if !Self::is_strict_string_or_sequence(member.value()) {
4833 self.record_pending_unmodeled(&develop_path, member);
4834 }
4835 item.set_ignore(values);
4836 }
4837 "include" => {
4838 let values = self.strict_string_or_sequence(
4839 member.value(),
4840 "develop include must be a string or sequence",
4841 );
4842 if !Self::is_strict_string_or_sequence(member.value()) {
4843 self.record_pending_unmodeled(&develop_path, member);
4844 }
4845 item.set_include(values);
4846 }
4847 "initial_sync" => {
4848 let parsed = self.located_boolean(member.value(), "develop initial_sync must be a boolean");
4849 if let Some(value) = parsed {
4850 item.set_initial_sync(value);
4851 } else {
4852 self.record_pending_unmodeled(&develop_path, member);
4853 }
4854 }
4855 "exec" if matches!(member.value().kind(), MergedValueKind::Mapping(_)) => {
4856 let exec = self.develop_exec(member.value(), &develop_path);
4857 has_exec = exec.command().is_some_and(project_command_is_non_empty);
4858 if !has_exec {
4859 self.diagnostics.push(
4860 Diagnostic::new(
4861 DEVELOP_WATCH_EXEC_MISSING_COMMAND,
4862 Severity::Error,
4863 "develop watch exec mapping requires a non-empty command member",
4864 )
4865 .with_label(DiagnosticLabel::primary(
4866 entry_span(member),
4867 "exec command is missing",
4868 )),
4869 );
4870 }
4871 item.set_exec(exec);
4872 }
4873 "exec" => {
4874 self.expected(member.value(), "develop exec must be a mapping");
4875 self.record_pending_unmodeled(&develop_path, member);
4876 }
4877 _ => {
4878 item.push_unmodeled(FieldReference::generated(
4879 Located::new(member.key().to_owned(), entry_span(member)),
4880 entry_span(member),
4881 Some(effective_span(member.value())),
4882 ));
4883 self.record_pending_unmodeled(&develop_path, member);
4884 }
4885 }
4886 }
4887 self.validate_develop_watch(&item, has_exec);
4888 watch.push(item);
4889 }
4890 }
4891 if watch.is_empty() {
4892 self.diagnostics.push(
4893 Diagnostic::new(
4894 crate::model::DEVELOP_MISSING_WATCH,
4895 Severity::Error,
4896 "develop requires a watch sequence",
4897 )
4898 .with_label(DiagnosticLabel::primary(effective_span(value), "watch is missing")),
4899 );
4900 }
4901 Some(ProjectValue::new(
4902 Develop::new(effective_span(value), watch, Vec::new(), Vec::new()),
4903 value,
4904 ))
4905 }
4906
4907 fn develop_exec(&mut self, value: &MergedValue, path: &[String]) -> DevelopWatchExec {
4908 let mut exec = DevelopWatchExec::new(effective_span(value));
4909 let Some(entries) = value.as_mapping() else {
4910 return exec;
4911 };
4912 for entry in entries {
4913 match entry.key() {
4914 "command" => match self.command(entry.value()) {
4915 Some(value) => exec.set_command(value.into_value()),
4916 None => self.record_pending_unmodeled(path, entry),
4917 },
4918 "user" => match self.strict_located_string(entry.value(), "develop exec user must be a YAML string") {
4919 Some(value) => exec.set_user(value),
4920 None => self.record_pending_unmodeled(path, entry),
4921 },
4922 "privileged" => {
4923 match self.located_boolean(entry.value(), "develop exec privileged must be a boolean") {
4924 Some(value) => exec.set_privileged(value),
4925 None => self.record_pending_unmodeled(path, entry),
4926 }
4927 }
4928 "working_dir" => {
4929 match self.strict_located_string(entry.value(), "develop exec working_dir must be a YAML string") {
4930 Some(value) => exec.set_working_dir(value),
4931 None => self.record_pending_unmodeled(path, entry),
4932 }
4933 }
4934 "environment" => match self.develop_exec_environment(entry.value()) {
4935 Some(value) => exec.set_environment(value),
4936 None => self.record_pending_unmodeled(path, entry),
4937 },
4938 _ => exec.push_unmodeled(FieldReference::generated(
4939 Located::new(entry.key().to_owned(), entry_span(entry)),
4940 entry_span(entry),
4941 Some(effective_span(entry.value())),
4942 )),
4943 }
4944 }
4945 exec
4946 }
4947
4948 fn develop_exec_environment(&mut self, value: &MergedValue) -> Option<Environment> {
4949 let span = effective_span(value);
4950 match value.kind() {
4951 MergedValueKind::Sequence(items) => {
4952 let mut entries = Vec::new();
4953 for item in items {
4954 entries.push(EnvironmentListEntry::parse(
4955 self.located_string(item, "develop exec environment list item must be a scalar")?,
4956 ));
4957 }
4958 Some(Environment::List { span, entries })
4959 }
4960 MergedValueKind::Mapping(items) => {
4961 let mut entries = Vec::new();
4962 for item in items {
4963 let scalar = self.compose_scalar(
4964 item.value(),
4965 "develop exec environment mapping value must be a scalar or null",
4966 )?;
4967 entries.push(EnvironmentMapEntry::new(
4968 Located::new(item.key().to_owned(), entry_span(item)),
4969 Located::new(scalar, effective_span(item.value())),
4970 entry_span(item),
4971 ));
4972 }
4973 Some(Environment::Map { span, entries })
4974 }
4975 _ => {
4976 self.expected(value, "develop exec environment must be a sequence or mapping");
4977 None
4978 }
4979 }
4980 }
4981
4982 fn validate_develop_watch(&mut self, item: &DevelopWatch, has_exec: bool) {
4983 if item.action().is_none() {
4984 self.diagnostics.push(
4985 Diagnostic::new(
4986 DEVELOP_WATCH_MISSING_ACTION,
4987 Severity::Error,
4988 "develop watch item requires an action",
4989 )
4990 .with_label(DiagnosticLabel::primary(item.span(), "action is missing")),
4991 );
4992 }
4993 if item.path().is_none() {
4994 self.diagnostics.push(
4995 Diagnostic::new(
4996 DEVELOP_WATCH_MISSING_PATH,
4997 Severity::Error,
4998 "develop watch item requires a path",
4999 )
5000 .with_label(DiagnosticLabel::primary(item.span(), "path is missing")),
5001 );
5002 }
5003 let Some(action) = item.action() else {
5004 return;
5005 };
5006 let action_value = action.value();
5007 let synchronizes = matches!(action_value.as_str(), "sync" | "sync+restart" | "sync+exec");
5008 if !matches!(
5009 action_value.as_str(),
5010 "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"
5011 ) {
5012 self.diagnostics.push(
5013 Diagnostic::new(
5014 DEVELOP_WATCH_INVALID_ACTION,
5015 Severity::Error,
5016 "develop watch action is not one of rebuild, sync, restart, sync+restart, or sync+exec",
5017 )
5018 .with_label(DiagnosticLabel::primary(action.span(), "unknown watch action")),
5019 );
5020 }
5021 if synchronizes && item.target().is_none() {
5022 self.diagnostics.push(
5023 Diagnostic::new(
5024 DEVELOP_WATCH_MISSING_TARGET,
5025 Severity::Error,
5026 "synchronizing develop watch action requires a target",
5027 )
5028 .with_label(DiagnosticLabel::primary(item.span(), "target is missing")),
5029 );
5030 }
5031 if action_value == "sync+exec" && !has_exec {
5032 self.diagnostics.push(
5033 Diagnostic::new(
5034 DEVELOP_WATCH_MISSING_EXEC,
5035 Severity::Error,
5036 "sync+exec develop watch action requires an exec command mapping",
5037 )
5038 .with_label(DiagnosticLabel::primary(item.span(), "exec is missing")),
5039 );
5040 }
5041 }
5042
5043 fn strict_string_or_sequence(&mut self, value: &MergedValue, message: &str) -> Vec<Located<String>> {
5044 match value.kind() {
5045 MergedValueKind::Scalar(_) => self.strict_located_string(value, message).into_iter().collect(),
5046 MergedValueKind::Sequence(items) => items
5047 .iter()
5048 .filter_map(|item| self.strict_located_string(item, message))
5049 .collect(),
5050 _ => {
5051 self.expected(value, message);
5052 Vec::new()
5053 }
5054 }
5055 }
5056
5057 fn strict_string_sequence(&mut self, value: &MergedValue, message: &str) -> Vec<Located<String>> {
5058 let MergedValueKind::Sequence(items) = value.kind() else {
5059 self.expected(value, message);
5060 return Vec::new();
5061 };
5062 items
5063 .iter()
5064 .filter_map(|item| self.strict_located_string(item, message))
5065 .collect()
5066 }
5067
5068 fn is_strict_string_or_sequence(value: &MergedValue) -> bool {
5069 match value.kind() {
5070 MergedValueKind::Scalar(scalar) => {
5071 scalar.kind() == MergedScalarKind::String && scalar.is_strict_yaml_string()
5072 }
5073 MergedValueKind::Sequence(items) => items.iter().all(|item| {
5074 matches!(
5075 item.kind(),
5076 MergedValueKind::Scalar(scalar)
5077 if scalar.kind() == MergedScalarKind::String && scalar.is_strict_yaml_string()
5078 )
5079 }),
5080 _ => false,
5081 }
5082 }
5083
5084 fn is_strict_string_sequence(value: &MergedValue) -> bool {
5085 matches!(value.kind(), MergedValueKind::Sequence(items) if items.iter().all(|item| {
5086 matches!(
5087 item.kind(),
5088 MergedValueKind::Scalar(scalar)
5089 if scalar.kind() == MergedScalarKind::String && scalar.is_strict_yaml_string()
5090 )
5091 }))
5092 }
5093
5094 fn label_files(
5095 &mut self,
5096 value: &MergedValue,
5097 service_path: &[String],
5098 ) -> Option<ProjectValue<ProjectServiceLabelFiles>> {
5099 let form = match value.kind() {
5100 MergedValueKind::Scalar(_) => {
5101 let path =
5102 self.strict_project_string(value, "service label_file must be a YAML string scalar or sequence")?;
5103 ProjectLabelFiles::Scalar(path)
5104 }
5105 MergedValueKind::Sequence(items) => {
5106 let mut paths = Vec::new();
5107 let mut unmodeled_items = Vec::new();
5108 for (index, item) in items.iter().enumerate() {
5109 let Some(path) =
5110 self.strict_project_string(item, "service label_file entries must be YAML string scalars")
5111 else {
5112 let mut item_path = service_path.to_vec();
5113 item_path.push("label_file".to_owned());
5114 item_path.push(index.to_string());
5115 unmodeled_items.push(ProjectLabelFileEvidence::new(value_reference(&item_path, item)));
5116 continue;
5117 };
5118 paths.push(path);
5119 }
5120 return Some(ProjectValue::new(
5121 ProjectServiceLabelFiles::new(ProjectLabelFiles::List(paths), unmodeled_items),
5122 value,
5123 ));
5124 }
5125 _ => {
5126 self.expected(value, "service label_file must be a YAML string scalar or sequence");
5127 return None;
5128 }
5129 };
5130 Some(ProjectValue::new(
5131 ProjectServiceLabelFiles::new(form, Vec::new()),
5132 value,
5133 ))
5134 }
5135
5136 fn located_integer_or_string_scalar(
5137 &mut self,
5138 value: &MergedValue,
5139 message: &str,
5140 ) -> Option<Located<ComposeScalar>> {
5141 let scalar = value.as_scalar()?;
5142 let parsed = match scalar.kind() {
5143 MergedScalarKind::Number if CpuCount::yaml_integer_spelling(scalar.raw()) => {
5144 ComposeScalar::Number(scalar.value().to_owned())
5145 }
5146 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
5147 ComposeScalar::String(scalar.value().to_owned())
5148 }
5149 _ => {
5150 self.expected(value, message);
5151 return None;
5152 }
5153 };
5154 Some(Located::new(parsed, effective_span(value)))
5155 }
5156
5157 fn located_yaml_integer(&mut self, value: &MergedValue, message: &str) -> Option<Located<ComposeScalar>> {
5158 let scalar = value.as_scalar()?;
5159 if scalar.kind() != MergedScalarKind::Number || !CpuCount::yaml_integer_spelling(scalar.raw()) {
5160 self.expected(value, message);
5161 return None;
5162 }
5163 Some(Located::new(
5164 ComposeScalar::Number(scalar.value().to_owned()),
5165 effective_span(value),
5166 ))
5167 }
5168
5169 fn set_service_runtime_field(
5170 &mut self,
5171 service: &mut ProjectService,
5172 field: &MergedEntry,
5173 path: &[String],
5174 ) -> bool {
5175 match field.key() {
5176 "cpu_count" | "cpu_percent" | "cpu_period" | "cpu_quota" | "cpu_rt_period" | "cpu_rt_runtime"
5177 | "cpu_shares" | "cpus" | "mem_swappiness" | "oom_score_adj" | "scale" | "pids_limit" => {
5178 self.set_service_count(service, field, path);
5179 }
5180 "cpuset" => {
5181 service.cpuset = self.project_string(field.value(), "service cpuset must be a YAML string scalar");
5182 if service.cpuset.is_none() {
5183 service.unmodeled_fields.push(field_reference(path, field));
5184 }
5185 }
5186 "device_cgroup_rules" => {
5187 if field.value().as_sequence().is_none() {
5188 service.unmodeled_fields.push(field_reference(path, field));
5189 }
5190 let (items, invalid) = self.strict_string_collection(field.value(), "device_cgroup_rules");
5191 service.device_cgroup_rules = items;
5192 service.invalid_device_cgroup_rules = invalid;
5193 }
5194 "ipc" => {
5195 service.ipc = self
5196 .project_string(field.value(), "service ipc must be a YAML string scalar")
5197 .map(|raw| ProjectValue {
5198 value: IpcMode::parse(raw.value),
5199 provenance: raw.provenance,
5200 sensitive: raw.sensitive,
5201 });
5202 if service.ipc.is_none() {
5203 service.unmodeled_fields.push(field_reference(path, field));
5204 }
5205 }
5206 "mem_reservation" => {
5207 service.mem_reservation = self.mem_limit(field.value());
5208 if service.mem_reservation.is_none() {
5209 service.unmodeled_fields.push(field_reference(path, field));
5210 }
5211 }
5212 "memswap_limit" => {
5213 service.memswap_limit = self.memswap_limit(field.value());
5214 if service.memswap_limit.is_none() {
5215 service.unmodeled_fields.push(field_reference(path, field));
5216 }
5217 }
5218 "network_mode" => {
5219 service.network_mode = self
5220 .project_string(field.value(), "service network_mode must be a YAML string scalar")
5221 .map(|raw| ProjectValue {
5222 value: NetworkMode::parse(raw.value),
5223 provenance: raw.provenance,
5224 sensitive: raw.sensitive,
5225 });
5226 if service.network_mode.is_none() {
5227 service.unmodeled_fields.push(field_reference(path, field));
5228 }
5229 }
5230 "oom_kill_disable" => {
5231 service.oom_kill_disable = self.boolean(field.value(), "service oom_kill_disable must be a boolean");
5232 if service.oom_kill_disable.is_none() {
5233 service.unmodeled_fields.push(field_reference(path, field));
5234 }
5235 }
5236 "pid" => {
5237 service.pid = self
5238 .project_string(field.value(), "service pid must be a YAML string scalar")
5239 .map(|raw| ProjectValue {
5240 value: PidMode::parse(raw.value),
5241 provenance: raw.provenance,
5242 sensitive: raw.sensitive,
5243 });
5244 if service.pid.is_none() {
5245 service.unmodeled_fields.push(field_reference(path, field));
5246 }
5247 }
5248 "volumes_from" => {
5249 if field.value().as_sequence().is_none() {
5250 service.unmodeled_fields.push(field_reference(path, field));
5251 }
5252 let (items, invalid) = self.volumes_from(field.value());
5253 service.volumes_from = items;
5254 service.invalid_volumes_from = invalid;
5255 }
5256 _ => return false,
5257 }
5258 true
5259 }
5260
5261 fn validate_service_cross_fields(&mut self, service: &ProjectService) {
5262 if let (Some(network_mode), Some(networks)) = (service.network_mode.as_ref(), service.networks.as_ref()) {
5263 self.diagnostics.push(
5264 Diagnostic::new(
5265 SERVICE_NETWORK_MODE_NETWORKS_CONFLICT,
5266 Severity::Error,
5267 "network_mode and networks are mutually exclusive service settings",
5268 )
5269 .with_label(DiagnosticLabel::primary(
5270 project_value_span(network_mode),
5271 "network_mode retained",
5272 ))
5273 .with_label(DiagnosticLabel::secondary(
5274 project_value_span(networks),
5275 "networks retained",
5276 )),
5277 );
5278 }
5279 let deploy = service.deploy.as_ref().map(ProjectValue::value);
5280 self.validate_service_memory_cross_fields(service, deploy);
5281 self.validate_service_scale_cross_fields(service, deploy);
5282 self.validate_service_cpu_cross_fields(service, deploy);
5283 if let (Some(gpus), Some(devices)) = (
5284 service.gpus.as_ref(),
5285 deploy
5286 .and_then(ProjectDeploy::resources)
5287 .and_then(|value| value.value().reservations())
5288 .and_then(|value| value.value().devices()),
5289 ) {
5290 self.diagnostics.push(
5291 Diagnostic::new(
5292 SERVICE_GPUS_DEPLOY_DEVICES_CONFLICT,
5293 Severity::Warning,
5294 "service gpus and deploy reservation devices are both retained for caller reconciliation",
5295 )
5296 .with_label(DiagnosticLabel::primary(
5297 project_value_span(gpus),
5298 "service gpus retained",
5299 ))
5300 .with_label(DiagnosticLabel::secondary(
5301 project_value_span(devices),
5302 "deploy reservation devices retained",
5303 )),
5304 );
5305 }
5306 }
5307
5308 fn validate_service_memory_cross_fields(&mut self, service: &ProjectService, deploy: Option<&ProjectDeploy>) {
5309 if let (Some(reservation), Some(deploy_reservation)) = (
5310 service.mem_reservation.as_ref(),
5311 deploy
5312 .and_then(ProjectDeploy::resources)
5313 .and_then(|value| value.value().reservations())
5314 .and_then(|value| value.value().memory()),
5315 ) {
5316 if same_memory_value(reservation.value(), deploy_reservation.value()) == Some(false) {
5317 self.diagnostics.push(
5318 Diagnostic::new(
5319 SERVICE_MEMORY_RESERVATION_DEPLOY_CONFLICT,
5320 Severity::Warning,
5321 "service mem_reservation and deploy reservation memory are both retained for caller reconciliation",
5322 )
5323 .with_label(DiagnosticLabel::primary(project_value_span(reservation), "service mem_reservation retained"))
5324 .with_label(DiagnosticLabel::secondary(project_value_span(deploy_reservation), "deploy reservation memory retained")),
5325 );
5326 }
5327 }
5328 if let Some(memswap) = service
5329 .memswap_limit
5330 .as_ref()
5331 .filter(|value| service.mem_limit.is_none() && value.value().is_positive())
5332 {
5333 self.diagnostics.push(
5334 Diagnostic::new(
5335 SERVICE_MEMSWAP_REQUIRES_MEMORY_LIMIT,
5336 Severity::Error,
5337 "memswap_limit requires an explicit service mem_limit; no host default is inferred",
5338 )
5339 .with_label(DiagnosticLabel::primary(
5340 project_value_span(memswap),
5341 "memory-plus-swap value retained without memory limit",
5342 )),
5343 );
5344 }
5345 if let (Some(memswap), Some(memory)) = (service.memswap_limit.as_ref(), service.mem_limit.as_ref()) {
5346 if memswap.value().is_positive()
5347 && memory_compare_same_unit(memswap.value().raw().value(), memory.value().raw().value())
5348 == Some(std::cmp::Ordering::Less)
5349 {
5350 self.diagnostics.push(
5351 Diagnostic::new(
5352 SERVICE_MEMSWAP_BELOW_MEMORY_LIMIT,
5353 Severity::Error,
5354 "positive memswap_limit is below the comparable service mem_limit",
5355 )
5356 .with_label(DiagnosticLabel::primary(
5357 project_value_span(memswap),
5358 "memory-plus-swap limit retained",
5359 ))
5360 .with_label(DiagnosticLabel::secondary(
5361 project_value_span(memory),
5362 "service memory limit retained",
5363 )),
5364 );
5365 }
5366 }
5367 }
5368
5369 fn validate_service_scale_cross_fields(&mut self, service: &ProjectService, deploy: Option<&ProjectDeploy>) {
5370 if let (Some(scale), Some(replicas)) = (service.scale.as_ref(), deploy.and_then(ProjectDeploy::replicas)) {
5371 let scale_raw = match scale.value() {
5372 ServiceInteger::Valid(raw) | ServiceInteger::OutOfRange(raw) | ServiceInteger::Other(raw) => raw,
5373 };
5374 let replica_raw = match replicas.value() {
5375 DeployReplicas::YamlNumber(raw) | DeployReplicas::String(raw) => raw,
5376 };
5377 if let (Some(scale_number), Some(replica_number)) =
5378 (normalize_integer(scale_raw), normalize_integer(replica_raw))
5379 {
5380 if scale_number != replica_number {
5381 self.diagnostics.push(
5382 Diagnostic::new(
5383 SERVICE_SCALE_DEPLOY_REPLICAS_CONFLICT,
5384 Severity::Warning,
5385 "scale and deploy.replicas have different proven integer values",
5386 )
5387 .with_label(DiagnosticLabel::primary(
5388 project_value_span(scale),
5389 "service scale retained",
5390 ))
5391 .with_label(DiagnosticLabel::secondary(
5392 project_value_span(replicas),
5393 "deploy replicas retained",
5394 )),
5395 );
5396 }
5397 }
5398 }
5399 }
5400
5401 fn validate_service_cpu_cross_fields(&mut self, service: &ProjectService, deploy: Option<&ProjectDeploy>) {
5402 if let (Some(cpus), Some(limit)) = (
5403 service.cpus.as_ref(),
5404 deploy
5405 .and_then(ProjectDeploy::resources)
5406 .and_then(|value| value.value().limits())
5407 .and_then(|value| value.value().cpus()),
5408 ) {
5409 let service_raw = match cpus.value() {
5410 Cpus::Decimal(raw) | Cpus::Expression(raw) | Cpus::Other(raw) => raw,
5411 };
5412 let limit_raw = match limit.value() {
5413 DeployResourceCpus::YamlNumber(raw) | DeployResourceCpus::String(raw) => raw,
5414 };
5415 if let (Some(service_number), Some(limit_number)) =
5416 (normalize_decimal(service_raw), normalize_decimal(limit_raw))
5417 {
5418 if service_number != limit_number {
5419 self.diagnostics.push(
5420 Diagnostic::new(
5421 SERVICE_CPUS_DEPLOY_LIMIT_CONFLICT,
5422 Severity::Warning,
5423 "cpus and deploy.resources.limits.cpus have different proven decimal values",
5424 )
5425 .with_label(DiagnosticLabel::primary(
5426 project_value_span(cpus),
5427 "service cpus retained",
5428 ))
5429 .with_label(DiagnosticLabel::secondary(
5430 project_value_span(limit),
5431 "deploy CPU limit retained",
5432 )),
5433 );
5434 }
5435 }
5436 }
5437 }
5438
5439 fn boolean(&mut self, field: &MergedValue, message: &str) -> Option<ProjectValue<BooleanValue>> {
5440 self.located_boolean(field, message)
5441 .map(|value| ProjectValue::new(value.into_value(), field))
5442 }
5443
5444 fn read_only(&mut self, field: &MergedValue) -> Option<ProjectValue<BooleanValue>> {
5445 self.located_boolean(field, "service read_only must be a boolean")
5446 .map(|value| ProjectValue::new(value.into_value(), field))
5447 }
5448
5449 fn set_service_deploy(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5450 service.deploy = self.service_deploy(field, path);
5451 if service.deploy.is_none() {
5452 service.unmodeled_fields.push(field_reference(path, field));
5453 }
5454 }
5455
5456 fn set_volumes(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5457 service.volume_views.volumes = self.volumes(field.value(), path);
5458 service.volume_views.options = Self::project_volume_mount_options_collection(field.value(), path);
5459 }
5460
5461 fn set_credential_spec(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5462 service.credential_spec = self.credential_spec(field.value(), path);
5463 if service.credential_spec.is_none() {
5464 service.unmodeled_fields.push(field_reference(path, field));
5465 }
5466 }
5467
5468 fn set_extends(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5469 service.extends = self.extends(field.value(), path);
5470 if service.extends.is_none() {
5471 service.unmodeled_fields.push(field_reference(path, field));
5472 }
5473 }
5474
5475 fn set_provider(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5476 service.provider = self.provider(field.value(), path);
5477 if service.provider.is_none() {
5478 service.unmodeled_fields.push(field_reference(path, field));
5479 }
5480 }
5481
5482 fn set_post_start(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5483 service.post_start = self.post_start(field.value(), path);
5484 if service.post_start.is_none() {
5485 service.unmodeled_fields.push(field_reference(path, field));
5486 }
5487 }
5488
5489 fn set_pre_stop(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5490 service.pre_stop = self.pre_stop(field.value(), path);
5491 if service.pre_stop.is_none() {
5492 service.unmodeled_fields.push(field_reference(path, field));
5493 }
5494 }
5495
5496 fn set_pre_start(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5497 service.pre_start = self.pre_start(field.value(), path);
5498 if service.pre_start.is_none() {
5499 service.unmodeled_fields.push(field_reference(path, field));
5500 }
5501 }
5502
5503 fn set_runtime(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5504 service.runtime = self.strict_project_string(field.value(), "service runtime must be a YAML string scalar");
5505 if service.runtime.is_none() {
5506 service.unmodeled_fields.push(field_reference(path, field));
5507 }
5508 }
5509
5510 fn set_pull_refresh_after(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5511 service.pull_refresh_after =
5512 self.strict_project_string(field.value(), "service pull_refresh_after must be a YAML string scalar");
5513 if service.pull_refresh_after.is_none() {
5514 service.unmodeled_fields.push(field_reference(path, field));
5515 }
5516 }
5517
5518 fn set_platform(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5519 service.platform = self.strict_project_string(field.value(), "service platform must be a YAML string scalar");
5520 if service.platform.is_none() {
5521 service.unmodeled_fields.push(field_reference(path, field));
5522 }
5523 }
5524
5525 fn set_attach(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5526 service.attach = self.boolean(field.value(), "service attach must be a boolean");
5527 if service.attach.is_none() {
5528 service.unmodeled_fields.push(field_reference(path, field));
5529 }
5530 }
5531
5532 fn set_service_boolean(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5533 match field.key() {
5534 "attach" => self.set_attach(service, field, path),
5535 "init" => service.init = self.boolean(field.value(), "service init must be a boolean"),
5536 "stdin_open" => {
5537 service.stdin_open = self.boolean(field.value(), "service stdin_open must be a boolean");
5538 }
5539 "tty" => service.tty = self.boolean(field.value(), "service tty must be a boolean"),
5540 "privileged" => {
5541 service.privileged = self.boolean(field.value(), "service privileged must be a boolean");
5542 }
5543 "use_api_socket" => {
5544 service.use_api_socket = self.boolean(field.value(), "service use_api_socket must be a boolean");
5545 }
5546 _ => {}
5547 }
5548 }
5549
5550 fn set_service_count(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5551 match field.key() {
5552 "cpu_count" => {
5553 service.cpu_count = self.cpu_count(field.value());
5554 if service.cpu_count.is_none() {
5555 service.unmodeled_fields.push(field_reference(path, field));
5556 }
5557 }
5558 "cpu_percent" => {
5559 service.cpu_percent = self.cpu_percent(field.value());
5560 if service.cpu_percent.is_none() {
5561 service.unmodeled_fields.push(field_reference(path, field));
5562 }
5563 }
5564 "cpu_period" => {
5565 service.cpu_period = self.cpu_period(field.value());
5566 if service.cpu_period.is_none() {
5567 service.unmodeled_fields.push(field_reference(path, field));
5568 }
5569 }
5570 "cpu_quota" => {
5571 service.cpu_quota = self.cpu_quota(field.value());
5572 if service.cpu_quota.is_none() {
5573 service.unmodeled_fields.push(field_reference(path, field));
5574 }
5575 }
5576 "cpu_rt_period" => {
5577 service.cpu_rt_period = self.cpu_rt_period(field.value());
5578 if service.cpu_rt_period.is_none() {
5579 service.unmodeled_fields.push(field_reference(path, field));
5580 }
5581 }
5582 "cpu_rt_runtime" => {
5583 service.cpu_rt_runtime = self.cpu_rt_runtime(field.value());
5584 if service.cpu_rt_runtime.is_none() {
5585 service.unmodeled_fields.push(field_reference(path, field));
5586 }
5587 }
5588 "cpu_shares" => {
5589 service.cpu_shares = self.service_integer(field.value(), 0, i128::MAX, "cpu_shares");
5590 if service.cpu_shares.is_none() {
5591 service.unmodeled_fields.push(field_reference(path, field));
5592 }
5593 }
5594 "cpus" => {
5595 service.cpus = self.cpus(field.value());
5596 if service.cpus.is_none() {
5597 service.unmodeled_fields.push(field_reference(path, field));
5598 }
5599 }
5600 "mem_swappiness" => {
5601 service.mem_swappiness = self.service_integer(field.value(), 0, 100, "mem_swappiness");
5602 if service.mem_swappiness.is_none() {
5603 service.unmodeled_fields.push(field_reference(path, field));
5604 }
5605 }
5606 "oom_score_adj" => {
5607 service.oom_score_adj = self.service_integer(field.value(), -1000, 1000, "oom_score_adj");
5608 if service.oom_score_adj.is_none() {
5609 service.unmodeled_fields.push(field_reference(path, field));
5610 }
5611 }
5612 "scale" => {
5613 service.scale = self.service_integer(field.value(), 0, i128::MAX, "scale");
5614 if service.scale.is_none() {
5615 service.unmodeled_fields.push(field_reference(path, field));
5616 }
5617 }
5618 "pids_limit" => service.pids_limit = self.pids_limit(field.value()),
5619 _ => {}
5620 }
5621 }
5622
5623 fn set_blkio_config(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5624 service.blkio_config = self.blkio_config(field.value(), path);
5625 if service.blkio_config.is_none() {
5626 service.unmodeled_fields.push(field_reference(path, field));
5627 }
5628 }
5629
5630 fn set_cgroup(&mut self, service: &mut ProjectService, field: &MergedEntry, path: &[String]) {
5631 match field.key() {
5632 "cgroup" => {
5633 service.cgroup = self.cgroup_namespace(field.value());
5634 if service.cgroup.is_none() {
5635 service.unmodeled_fields.push(field_reference(path, field));
5636 }
5637 }
5638 "cgroup_parent" => {
5639 service.cgroup_parent =
5640 self.strict_project_string(field.value(), "service cgroup_parent must be a YAML string scalar");
5641 if service.cgroup_parent.is_none() {
5642 service.unmodeled_fields.push(field_reference(path, field));
5643 }
5644 }
5645 _ => {}
5646 }
5647 }
5648
5649 fn cgroup_namespace(&mut self, value: &MergedValue) -> Option<ProjectValue<CgroupNamespace>> {
5650 let raw = self.strict_project_string(value, "service cgroup must be a YAML string scalar")?;
5651 let cgroup = CgroupNamespace::parse(Located::new(raw.value, effective_span(value)));
5652 if !cgroup.is_valid() {
5653 self.diagnostics.push(
5654 Diagnostic::new(
5655 crate::model::CGROUP_NAMESPACE_INVALID,
5656 Severity::Warning,
5657 "service cgroup must be `host`, `private`, or a deferred expression",
5658 )
5659 .with_label(DiagnosticLabel::primary(
5660 cgroup.raw().span(),
5661 "retained unsupported cgroup namespace",
5662 )),
5663 );
5664 }
5665 Some(ProjectValue {
5666 value: cgroup,
5667 provenance: raw.provenance,
5668 sensitive: raw.sensitive,
5669 })
5670 }
5671
5672 fn blkio_config(&mut self, value: &MergedValue, path: &[String]) -> Option<ProjectValue<ProjectBlkioConfig>> {
5673 let fields = self.mapping(value, "blkio_config must be a mapping")?;
5674 let mut config = ProjectBlkioConfig {
5675 weight: None,
5676 device_read_bps: None,
5677 device_read_iops: None,
5678 device_write_bps: None,
5679 device_write_iops: None,
5680 weight_device: None,
5681 unmodeled_fields: Vec::new(),
5682 };
5683 let mut config_path = path.to_vec();
5684 config_path.push("blkio_config".to_owned());
5685 for field in fields {
5686 match field.key() {
5687 "weight" => {
5688 if let Some(value) = self.blkio_scalar(field.value()) {
5689 config.weight = Some(value);
5690 } else {
5691 config.unmodeled_fields.push(field_reference(&config_path, field));
5692 }
5693 }
5694 "device_read_bps" | "device_read_iops" | "device_write_bps" | "device_write_iops" => {
5695 let rates = self.blkio_rates(field.value(), &config_path, field.key());
5696 let malformed = rates.is_none();
5697 match field.key() {
5698 "device_read_bps" => config.device_read_bps = rates,
5699 "device_read_iops" => config.device_read_iops = rates,
5700 "device_write_bps" => config.device_write_bps = rates,
5701 "device_write_iops" => config.device_write_iops = rates,
5702 _ => unreachable!("blkio rate key was matched above"),
5703 }
5704 if malformed {
5705 config.unmodeled_fields.push(field_reference(&config_path, field));
5706 }
5707 }
5708 "weight_device" => {
5709 config.weight_device = self.blkio_weight_devices(field.value(), &config_path, field.key());
5710 if config.weight_device.is_none() {
5711 config.unmodeled_fields.push(field_reference(&config_path, field));
5712 }
5713 }
5714 _ => config.unmodeled_fields.push(field_reference(&config_path, field)),
5715 }
5716 }
5717 Some(ProjectValue::new(config, value))
5718 }
5719
5720 fn blkio_scalar(&mut self, value: &MergedValue) -> Option<ProjectValue<BlkioScalar>> {
5721 let Some(scalar) = value.as_scalar() else {
5722 self.expected(value, "blkio value must be a YAML integer or string scalar");
5723 return None;
5724 };
5725 let parsed = match scalar.kind() {
5726 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
5727 BlkioScalar::YamlInteger(scalar.value().to_owned())
5728 }
5729 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
5730 BlkioScalar::String(scalar.value().to_owned())
5731 }
5732 _ => {
5733 self.expected(value, "blkio value must be a YAML integer or string scalar");
5734 return None;
5735 }
5736 };
5737 Some(ProjectValue::new(parsed, value))
5738 }
5739
5740 fn blkio_rates(
5741 &mut self,
5742 value: &MergedValue,
5743 path: &[String],
5744 sequence_name: &str,
5745 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectBlkioDeviceRate>>>> {
5746 let Some(values) = value.as_sequence() else {
5747 self.expected(value, "blkio device rates must be a sequence");
5748 return None;
5749 };
5750 let mut item_path = path.to_vec();
5751 item_path.push(sequence_name.to_owned());
5752 let mut items = Vec::new();
5753 for item in values {
5754 let mut rate = ProjectBlkioDeviceRate {
5755 form: ProjectBlkioDeviceRateForm::Unmodeled,
5756 path: None,
5757 rate: None,
5758 unmodeled_fields: Vec::new(),
5759 };
5760 if let Some(fields) = self.mapping(item, "blkio device rate entries must be mappings") {
5761 rate.form = ProjectBlkioDeviceRateForm::Mapping;
5762 for field in fields {
5763 match field.key() {
5764 "path" => {
5765 rate.path = self
5766 .strict_project_string(field.value(), "blkio device path must be a YAML string scalar");
5767 if rate.path.is_none() {
5768 rate.unmodeled_fields.push(field_reference(&item_path, field));
5769 }
5770 }
5771 "rate" => {
5772 rate.rate = self.blkio_scalar(field.value());
5773 if rate.rate.is_none() {
5774 rate.unmodeled_fields.push(field_reference(&item_path, field));
5775 }
5776 }
5777 _ => rate.unmodeled_fields.push(field_reference(&item_path, field)),
5778 }
5779 }
5780 }
5781 items.push(ProjectValue::new(rate, item));
5782 }
5783 Some(ProjectValue::new(items, value))
5784 }
5785
5786 fn blkio_weight_devices(
5787 &mut self,
5788 value: &MergedValue,
5789 path: &[String],
5790 sequence_name: &str,
5791 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectBlkioWeightDevice>>>> {
5792 let Some(values) = value.as_sequence() else {
5793 self.expected(value, "blkio weight devices must be a sequence");
5794 return None;
5795 };
5796 let mut item_path = path.to_vec();
5797 item_path.push(sequence_name.to_owned());
5798 let mut items = Vec::new();
5799 for item in values {
5800 let mut weight = ProjectBlkioWeightDevice {
5801 form: ProjectBlkioWeightDeviceForm::Unmodeled,
5802 path: None,
5803 weight: None,
5804 unmodeled_fields: Vec::new(),
5805 };
5806 if let Some(fields) = self.mapping(item, "blkio weight device entries must be mappings") {
5807 weight.form = ProjectBlkioWeightDeviceForm::Mapping;
5808 for field in fields {
5809 match field.key() {
5810 "path" => {
5811 weight.path = self.strict_project_string(
5812 field.value(),
5813 "blkio weight device path must be a YAML string scalar",
5814 );
5815 if weight.path.is_none() {
5816 weight.unmodeled_fields.push(field_reference(&item_path, field));
5817 }
5818 }
5819 "weight" => {
5820 weight.weight = self.blkio_scalar(field.value());
5821 if weight.weight.is_none() {
5822 weight.unmodeled_fields.push(field_reference(&item_path, field));
5823 }
5824 }
5825 _ => weight.unmodeled_fields.push(field_reference(&item_path, field)),
5826 }
5827 }
5828 }
5829 items.push(ProjectValue::new(weight, item));
5830 }
5831 Some(ProjectValue::new(items, value))
5832 }
5833
5834 fn service_deploy(&mut self, field: &MergedEntry, path: &[String]) -> Option<ProjectValue<ProjectDeploy>> {
5835 let mut deploy_path = path.to_vec();
5836 deploy_path.push(field.key().to_owned());
5837 let fields = self.mapping(field.value(), "deploy must be a mapping")?;
5838 let mut deploy = ProjectDeploy {
5839 endpoint_mode: None,
5840 labels: None,
5841 mode: None,
5842 placement: None,
5843 replicas: None,
5844 resources: None,
5845 restart_policy: None,
5846 rollback_config: None,
5847 update_config: None,
5848 unmodeled_fields: Vec::new(),
5849 };
5850 for nested in fields {
5851 match nested.key() {
5852 "endpoint_mode" => {
5853 if let Some(endpoint_mode) = self.deploy_endpoint_mode(nested.value()) {
5854 deploy.endpoint_mode = Some(endpoint_mode);
5855 } else {
5856 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5857 }
5858 }
5859 "labels" => {
5860 let (labels, malformed) = self.deploy_labels(nested.value());
5861 if let Some(labels) = labels {
5862 deploy.labels = Some(labels);
5863 }
5864 if malformed {
5865 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5866 }
5867 }
5868 "mode" => {
5869 if let Some(mode) = self.deploy_mode(nested.value()) {
5870 deploy.mode = Some(mode);
5871 } else {
5872 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5873 }
5874 }
5875 "placement" => {
5876 if let Some(placement) = self.deploy_placement(nested.value(), &deploy_path) {
5877 deploy.placement = Some(placement);
5878 } else {
5879 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5880 }
5881 }
5882 "replicas" => {
5883 if let Some(replicas) = self.deploy_replicas(nested.value()) {
5884 deploy.replicas = Some(replicas);
5885 } else {
5886 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5887 }
5888 }
5889 "resources" => {
5890 if let Some(resources) = self.deploy_resources(nested.value(), &deploy_path) {
5891 deploy.resources = Some(resources);
5892 } else {
5893 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5894 }
5895 }
5896 "restart_policy" => {
5897 if let Some(policy) = self.deploy_restart_policy(nested.value(), &deploy_path) {
5898 deploy.restart_policy = Some(policy);
5899 } else {
5900 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5901 }
5902 }
5903 "rollback_config" => {
5904 if let Some(config) = self.deploy_rollback_config(nested.value(), &deploy_path) {
5905 deploy.rollback_config = Some(config);
5906 } else {
5907 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5908 }
5909 }
5910 "update_config" => {
5911 if let Some(config) = self.deploy_update_config(nested.value(), &deploy_path) {
5912 deploy.update_config = Some(config);
5913 } else {
5914 deploy.unmodeled_fields.push(field_reference(&deploy_path, nested));
5915 }
5916 }
5917 _ => deploy.unmodeled_fields.push(field_reference(&deploy_path, nested)),
5918 }
5919 }
5920 Some(ProjectValue::new(deploy, field.value()))
5921 }
5922
5923 fn deploy_labels(&mut self, value: &MergedValue) -> (Option<ProjectValue<ProjectLabels>>, bool) {
5924 let mut entries = Vec::new();
5925 let mut malformed = false;
5926 let form = match value.kind() {
5927 MergedValueKind::Mapping(values) => {
5928 for entry in values {
5929 let scalar = if entry.syntax() == EntrySyntax::ListKeyOnly {
5930 Some(ComposeScalar::String(String::new()))
5931 } else {
5932 self.compose_scalar(entry.value(), "deploy label value must be a scalar or null")
5933 };
5934 let Some(scalar) = scalar else {
5935 malformed = true;
5936 continue;
5937 };
5938 entries.push(ProjectLabelEntry {
5939 name: ProjectKey::from_entry(entry),
5940 value: ProjectValue::new(scalar, entry.value()),
5941 syntax: entry.syntax(),
5942 });
5943 }
5944 ProjectLabelsForm::Map
5945 }
5946 MergedValueKind::Sequence(values) => {
5947 for item in values {
5948 let Some(raw) = self.located_string(item, "deploy label list item must be a scalar") else {
5949 malformed = true;
5950 continue;
5951 };
5952 let (name, value, syntax) = raw.value().split_once('=').map_or_else(
5953 || (raw.value().clone(), String::new(), EntrySyntax::ListKeyOnly),
5954 |(name, value)| (name.to_owned(), value.to_owned(), EntrySyntax::ListKeyValue),
5955 );
5956 entries.push(ProjectLabelEntry {
5957 name: ProjectKey::from_value(name, item),
5958 value: ProjectValue::new(ComposeScalar::String(value), item),
5959 syntax,
5960 });
5961 }
5962 ProjectLabelsForm::List
5963 }
5964 _ => {
5965 self.expected(value, "deploy labels must be a mapping or sequence");
5966 return (None, true);
5967 }
5968 };
5969 (
5970 Some(ProjectValue::new(ProjectLabels { form, entries }, value)),
5971 malformed,
5972 )
5973 }
5974
5975 fn deploy_endpoint_mode(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployEndpointMode>> {
5976 let scalar = match value.kind() {
5977 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
5978 _ => {
5979 self.expected(value, "deploy endpoint_mode must be a YAML string scalar");
5980 return None;
5981 }
5982 };
5983 let endpoint_mode = DeployEndpointMode::parse(scalar.value().to_owned());
5984 if !endpoint_mode.is_documented() {
5985 self.diagnostics.push(
5986 Diagnostic::new(
5987 DEPLOY_ENDPOINT_MODE_PORTABILITY,
5988 Severity::Warning,
5989 "deploy endpoint_mode is outside Compose's documented portable values",
5990 )
5991 .with_label(DiagnosticLabel::primary(
5992 effective_span(value),
5993 "retained provider-specific endpoint mode",
5994 )),
5995 );
5996 }
5997 Some(ProjectValue::new(endpoint_mode, value))
5998 }
5999
6000 fn deploy_mode(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployMode>> {
6001 let scalar = match value.kind() {
6002 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
6003 _ => {
6004 self.expected(value, "deploy mode must be a YAML string scalar");
6005 return None;
6006 }
6007 };
6008 let mode = DeployMode::parse(scalar.value().to_owned());
6009 if !mode.is_documented() {
6010 self.diagnostics.push(
6011 Diagnostic::new(
6012 DEPLOY_MODE_PORTABILITY,
6013 Severity::Warning,
6014 "deploy mode is outside Compose's documented portable values",
6015 )
6016 .with_label(DiagnosticLabel::primary(
6017 effective_span(value),
6018 "retained provider-specific deploy mode",
6019 )),
6020 );
6021 }
6022 Some(ProjectValue::new(mode, value))
6023 }
6024
6025 fn deploy_replicas(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployReplicas>> {
6026 let MergedValueKind::Scalar(scalar) = value.kind() else {
6027 self.expected(value, "deploy replicas must be a YAML number or string scalar");
6028 return None;
6029 };
6030 let replicas = match scalar.kind() {
6031 MergedScalarKind::Number => DeployReplicas::YamlNumber(scalar.value().to_owned()),
6032 MergedScalarKind::String => DeployReplicas::String(scalar.value().to_owned()),
6033 MergedScalarKind::Boolean => {
6034 self.expected(value, "deploy replicas must be a YAML number or string scalar");
6035 return None;
6036 }
6037 };
6038 Some(ProjectValue::new(replicas, value))
6039 }
6040
6041 fn deploy_resources(
6042 &mut self,
6043 value: &MergedValue,
6044 path: &[String],
6045 ) -> Option<ProjectValue<ProjectDeployResources>> {
6046 let fields = self.mapping(value, "deploy resources must be a mapping")?;
6047 let mut resources_path = path.to_vec();
6048 resources_path.push("resources".to_owned());
6049 let mut resources = ProjectDeployResources {
6050 limits: None,
6051 reservations: None,
6052 unmodeled_fields: Vec::new(),
6053 };
6054 for field in fields {
6055 match field.key() {
6056 "limits" => {
6057 if let Some(limits) = self.deploy_resource_limits(field.value(), &resources_path) {
6058 resources.limits = Some(limits);
6059 } else {
6060 resources.unmodeled_fields.push(field_reference(&resources_path, field));
6061 }
6062 }
6063 "reservations" => {
6064 if let Some(reservations) = self.deploy_resource_reservations(field.value(), &resources_path) {
6065 resources.reservations = Some(reservations);
6066 } else {
6067 resources.unmodeled_fields.push(field_reference(&resources_path, field));
6068 }
6069 }
6070 _ => resources.unmodeled_fields.push(field_reference(&resources_path, field)),
6071 }
6072 }
6073 Some(ProjectValue::new(resources, value))
6074 }
6075
6076 fn deploy_resource_limits(
6077 &mut self,
6078 value: &MergedValue,
6079 path: &[String],
6080 ) -> Option<ProjectValue<ProjectDeployResourceLimits>> {
6081 let fields = self.mapping(value, "deploy resource limits must be a mapping")?;
6082 let mut limits_path = path.to_vec();
6083 limits_path.push("limits".to_owned());
6084 let mut limits = ProjectDeployResourceLimits {
6085 cpus: None,
6086 memory: None,
6087 pids: None,
6088 unmodeled_fields: Vec::new(),
6089 };
6090 for field in fields {
6091 match field.key() {
6092 "cpus" => {
6093 if let Some(cpus) = self.deploy_resource_cpus(
6094 field.value(),
6095 "deploy resource limits cpus must be a YAML number or string scalar",
6096 ) {
6097 limits.cpus = Some(cpus);
6098 } else {
6099 limits.unmodeled_fields.push(field_reference(&limits_path, field));
6100 }
6101 }
6102 "memory" => {
6103 if let Some(memory) = self.deploy_resource_memory(
6104 field.value(),
6105 "deploy resource limits memory must be a YAML string scalar",
6106 ) {
6107 limits.memory = Some(memory);
6108 } else {
6109 limits.unmodeled_fields.push(field_reference(&limits_path, field));
6110 }
6111 }
6112 "pids" => {
6113 if let Some(pids) = self.deploy_resource_pids(field.value()) {
6114 limits.pids = Some(pids);
6115 } else {
6116 limits.unmodeled_fields.push(field_reference(&limits_path, field));
6117 }
6118 }
6119 _ => limits.unmodeled_fields.push(field_reference(&limits_path, field)),
6120 }
6121 }
6122 Some(ProjectValue::new(limits, value))
6123 }
6124
6125 fn deploy_resource_reservations(
6126 &mut self,
6127 value: &MergedValue,
6128 path: &[String],
6129 ) -> Option<ProjectValue<ProjectDeployResourceReservations>> {
6130 let fields = self.mapping(value, "deploy resource reservations must be a mapping")?;
6131 let mut reservations_path = path.to_vec();
6132 reservations_path.push("reservations".to_owned());
6133 let mut reservations = ProjectDeployResourceReservations {
6134 cpus: None,
6135 memory: None,
6136 generic_resources: None,
6137 devices: None,
6138 unmodeled_fields: Vec::new(),
6139 };
6140 for field in fields {
6141 match field.key() {
6142 "cpus" => {
6143 if let Some(cpus) = self.deploy_resource_cpus(
6144 field.value(),
6145 "deploy resource reservations cpus must be a YAML number or string scalar",
6146 ) {
6147 reservations.cpus = Some(cpus);
6148 } else {
6149 reservations
6150 .unmodeled_fields
6151 .push(field_reference(&reservations_path, field));
6152 }
6153 }
6154 "memory" => {
6155 if let Some(memory) = self.deploy_resource_memory(
6156 field.value(),
6157 "deploy resource reservations memory must be a YAML string scalar",
6158 ) {
6159 reservations.memory = Some(memory);
6160 } else {
6161 reservations
6162 .unmodeled_fields
6163 .push(field_reference(&reservations_path, field));
6164 }
6165 }
6166 "generic_resources" => {
6167 if let Some(values) = self.deploy_generic_resources(field.value(), &reservations_path) {
6168 reservations.generic_resources = Some(values);
6169 } else {
6170 reservations
6171 .unmodeled_fields
6172 .push(field_reference(&reservations_path, field));
6173 }
6174 }
6175 "devices" => {
6176 if let Some(values) = self.deploy_reservation_devices(field.value(), &reservations_path) {
6177 reservations.devices = Some(values);
6178 } else {
6179 reservations
6180 .unmodeled_fields
6181 .push(field_reference(&reservations_path, field));
6182 }
6183 }
6184 _ => reservations
6185 .unmodeled_fields
6186 .push(field_reference(&reservations_path, field)),
6187 }
6188 }
6189 Some(ProjectValue::new(reservations, value))
6190 }
6191
6192 fn deploy_reservation_devices(
6193 &mut self,
6194 value: &MergedValue,
6195 path: &[String],
6196 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDevice>>>> {
6197 let Some(values) = value.as_sequence() else {
6198 self.expected(value, "deploy resource reservation devices must be a sequence");
6199 return None;
6200 };
6201 let mut item_path = path.to_vec();
6202 item_path.push("devices".to_owned());
6203 let mut items = Vec::new();
6204 for item in values {
6205 let Some(fields) = self.mapping(item, "deploy resource reservation device entries must be mappings") else {
6206 items.push(ProjectValue::new(
6207 ProjectDeployReservationDevice {
6208 form: ProjectDeployReservationDeviceForm::Unmodeled,
6209 capabilities: None,
6210 driver: None,
6211 count: None,
6212 device_ids: None,
6213 options: None,
6214 unmodeled_fields: Vec::new(),
6215 },
6216 item,
6217 ));
6218 continue;
6219 };
6220 let mut device = ProjectDeployReservationDevice {
6221 form: ProjectDeployReservationDeviceForm::Mapping,
6222 capabilities: None,
6223 driver: None,
6224 count: None,
6225 device_ids: None,
6226 options: None,
6227 unmodeled_fields: Vec::new(),
6228 };
6229 let mut has_capabilities = false;
6230 let mut count_field = None;
6231 let mut device_ids_field = None;
6232 for field in fields {
6233 match field.key() {
6234 "capabilities" => {
6235 has_capabilities = true;
6236 if let Some(capabilities) = self.deploy_reservation_device_capabilities(field.value()) {
6237 device.capabilities = Some(capabilities);
6238 } else {
6239 device.unmodeled_fields.push(field_reference(&item_path, field));
6240 }
6241 }
6242 "driver" => {
6243 let Some(scalar) = field.value().as_scalar() else {
6244 self.expected(
6245 field.value(),
6246 "deploy resource reservation device driver must be a YAML string scalar",
6247 );
6248 device.unmodeled_fields.push(field_reference(&item_path, field));
6249 continue;
6250 };
6251 if !scalar.is_strict_yaml_string() {
6252 self.expected(
6253 field.value(),
6254 "deploy resource reservation device driver must be a YAML string scalar",
6255 );
6256 device.unmodeled_fields.push(field_reference(&item_path, field));
6257 continue;
6258 }
6259 device.driver = Some(ProjectValue::new(scalar.value().to_owned(), field.value()));
6260 }
6261 "count" => {
6262 count_field.get_or_insert(field);
6263 if let Some(count) = self.deploy_reservation_device_count(field.value()) {
6264 device.count = Some(count);
6265 } else {
6266 device.unmodeled_fields.push(field_reference(&item_path, field));
6267 }
6268 }
6269 "device_ids" => {
6270 device_ids_field.get_or_insert(field);
6271 if let Some(device_ids) = self.deploy_reservation_device_ids(field.value()) {
6272 device.device_ids = Some(device_ids);
6273 } else {
6274 device.unmodeled_fields.push(field_reference(&item_path, field));
6275 }
6276 }
6277 "options" => {
6278 if let Some(options) = self.deploy_reservation_device_options(field.value(), &item_path) {
6279 device.options = Some(options);
6280 } else {
6281 device.unmodeled_fields.push(field_reference(&item_path, field));
6282 }
6283 }
6284 _ => device.unmodeled_fields.push(field_reference(&item_path, field)),
6285 }
6286 }
6287 self.reservation_device_allocation_selector_conflict(count_field, device_ids_field);
6288 if !has_capabilities {
6289 self.missing(
6290 item,
6291 "deploy resource reservation device is missing required `capabilities`",
6292 );
6293 }
6294 items.push(ProjectValue::new(device, item));
6295 }
6296 Some(ProjectValue::new(items, value))
6297 }
6298
6299 fn reservation_device_allocation_selector_conflict(
6300 &mut self,
6301 count: Option<&MergedEntry>,
6302 device_ids: Option<&MergedEntry>,
6303 ) {
6304 let (Some(count), Some(device_ids)) = (count, device_ids) else {
6305 return;
6306 };
6307 self.diagnostics.push(
6308 Diagnostic::new(
6309 DEPLOY_RESERVATION_DEVICE_ALLOCATION_SELECTOR_CONFLICT,
6310 Severity::Error,
6311 "deploy resource reservation device count and device_ids are mutually exclusive",
6312 )
6313 .with_label(DiagnosticLabel::primary(entry_span(count), "count retained"))
6314 .with_label(DiagnosticLabel::secondary(
6315 entry_span(device_ids),
6316 "device_ids retained",
6317 )),
6318 );
6319 }
6320
6321 fn deploy_reservation_device_count(
6322 &mut self,
6323 value: &MergedValue,
6324 ) -> Option<ProjectValue<DeployReservationDeviceCount>> {
6325 let MergedValueKind::Scalar(scalar) = value.kind() else {
6326 self.expected(
6327 value,
6328 "deploy resource reservation device count must be a YAML integer or string scalar",
6329 );
6330 return None;
6331 };
6332 let count = match scalar.kind() {
6333 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
6334 DeployReservationDeviceCount::YamlInteger(scalar.value().to_owned())
6335 }
6336 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
6337 DeployReservationDeviceCount::String(scalar.value().to_owned())
6338 }
6339 MergedScalarKind::Number | MergedScalarKind::String | MergedScalarKind::Boolean => {
6340 self.expected(
6341 value,
6342 "deploy resource reservation device count must be a YAML integer or string scalar",
6343 );
6344 return None;
6345 }
6346 };
6347 Some(ProjectValue::new(count, value))
6348 }
6349
6350 fn deploy_reservation_device_ids(
6351 &mut self,
6352 value: &MergedValue,
6353 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceId>>>> {
6354 let Some(values) = value.as_sequence() else {
6355 self.expected(
6356 value,
6357 "deploy resource reservation device device_ids must be a sequence",
6358 );
6359 return None;
6360 };
6361 let mut items = Vec::new();
6362 for item in values {
6363 let MergedValueKind::Scalar(scalar) = item.kind() else {
6364 self.expected(
6365 item,
6366 "deploy resource reservation device device_ids must be string scalars",
6367 );
6368 items.push(ProjectValue::new(
6369 ProjectDeployReservationDeviceId {
6370 form: ProjectDeployReservationDeviceIdForm::Unmodeled,
6371 value: None,
6372 },
6373 item,
6374 ));
6375 continue;
6376 };
6377 if scalar.kind() != MergedScalarKind::String || !scalar.is_strict_yaml_string() {
6378 self.expected(
6379 item,
6380 "deploy resource reservation device device_ids must be string scalars",
6381 );
6382 items.push(ProjectValue::new(
6383 ProjectDeployReservationDeviceId {
6384 form: ProjectDeployReservationDeviceIdForm::Unmodeled,
6385 value: None,
6386 },
6387 item,
6388 ));
6389 continue;
6390 }
6391 items.push(ProjectValue::new(
6392 ProjectDeployReservationDeviceId {
6393 form: ProjectDeployReservationDeviceIdForm::String,
6394 value: Some(ProjectValue::new(scalar.value().to_owned(), item)),
6395 },
6396 item,
6397 ));
6398 }
6399 Some(ProjectValue::new(items, value))
6400 }
6401
6402 fn deploy_reservation_device_options(
6403 &mut self,
6404 value: &MergedValue,
6405 path: &[String],
6406 ) -> Option<ProjectValue<ProjectDeployReservationDeviceOptions>> {
6407 match value.kind() {
6408 MergedValueKind::Mapping(entries) => {
6409 let mut items = Vec::new();
6410 let mut unmodeled_entries = Vec::new();
6411 let mut option_path = path.to_vec();
6412 option_path.push("options".to_owned());
6413 for entry in entries {
6414 if entry.key().is_empty() || !entry.is_strict_yaml_string() {
6415 self.diagnostics.push(Diagnostic::new(DEPLOY_RESERVATION_DEVICE_OPTIONS_INVALID_KEY, Severity::Error,
6416 "deploy resource reservation device options mapping keys must be non-empty strict YAML strings")
6417 .with_label(DiagnosticLabel::primary(entry_span(entry), "invalid option key")));
6418 unmodeled_entries.push(field_reference(&option_path, entry));
6419 continue;
6420 }
6421 let Some(option_value) = self.compose_scalar(entry.value(),
6422 "deploy resource reservation device options mapping values must be scalar strings, numbers, booleans, or null") else {
6423 unmodeled_entries.push(field_reference(&option_path, entry));
6424 continue;
6425 };
6426 items.push(ProjectValue::new(
6427 ProjectDeployReservationDeviceOption {
6428 name: ProjectKey::from_entry(entry),
6429 value: ProjectValue::new(option_value, entry.value()),
6430 },
6431 entry.value(),
6432 ));
6433 }
6434 Some(ProjectValue::new(
6435 ProjectDeployReservationDeviceOptions::Map {
6436 entries: items,
6437 unmodeled_entries,
6438 },
6439 value,
6440 ))
6441 }
6442 MergedValueKind::Sequence(values) => {
6443 let mut items = Vec::new();
6444 let mut seen = BTreeMap::new();
6445 for item in values {
6446 let MergedValueKind::Scalar(scalar) = item.kind() else {
6447 self.expected(item, "deploy resource reservation device options list entries must be strict YAML string scalars");
6448 items.push(ProjectValue::new(
6449 ProjectDeployReservationDeviceOptionItem {
6450 form: ProjectDeployReservationDeviceOptionItemForm::Unmodeled,
6451 value: None,
6452 },
6453 item,
6454 ));
6455 continue;
6456 };
6457 if scalar.kind() != MergedScalarKind::String || !scalar.is_strict_yaml_string() {
6458 self.expected(item, "deploy resource reservation device options list entries must be strict YAML string scalars");
6459 items.push(ProjectValue::new(
6460 ProjectDeployReservationDeviceOptionItem {
6461 form: ProjectDeployReservationDeviceOptionItemForm::Unmodeled,
6462 value: None,
6463 },
6464 item,
6465 ));
6466 continue;
6467 }
6468 let span = effective_span(item);
6469 if let Some(first) = seen.get(scalar.value()) {
6470 self.diagnostics.push(
6471 Diagnostic::new(
6472 DEPLOY_RESERVATION_DEVICE_OPTIONS_DUPLICATE_ITEM,
6473 Severity::Error,
6474 "deploy resource reservation device options list entries must be unique exact strings",
6475 )
6476 .with_label(DiagnosticLabel::primary(span, "duplicate option string"))
6477 .with_label(DiagnosticLabel::secondary(*first, "first identical string")),
6478 );
6479 } else {
6480 seen.insert(scalar.value().to_owned(), span);
6481 }
6482 items.push(ProjectValue::new(
6483 ProjectDeployReservationDeviceOptionItem {
6484 form: ProjectDeployReservationDeviceOptionItemForm::String,
6485 value: Some(ProjectValue::new(scalar.value().to_owned(), item)),
6486 },
6487 item,
6488 ));
6489 }
6490 Some(ProjectValue::new(
6491 ProjectDeployReservationDeviceOptions::List(items),
6492 value,
6493 ))
6494 }
6495 _ => {
6496 self.expected(
6497 value,
6498 "deploy resource reservation device options must be a mapping or sequence",
6499 );
6500 None
6501 }
6502 }
6503 }
6504
6505 fn deploy_reservation_device_capabilities(
6506 &mut self,
6507 value: &MergedValue,
6508 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectDeployReservationDeviceCapability>>>> {
6509 let Some(values) = value.as_sequence() else {
6510 self.expected(
6511 value,
6512 "deploy resource reservation device capabilities must be a sequence of strings",
6513 );
6514 return None;
6515 };
6516 let mut items = Vec::new();
6517 let mut seen = BTreeMap::new();
6518 for item in values {
6519 let MergedValueKind::Scalar(scalar) = item.kind() else {
6520 self.expected(
6521 item,
6522 "deploy resource reservation device capabilities must be string scalars",
6523 );
6524 items.push(ProjectValue::new(
6525 ProjectDeployReservationDeviceCapability {
6526 form: ProjectDeployReservationDeviceCapabilityForm::Unmodeled,
6527 value: None,
6528 },
6529 item,
6530 ));
6531 continue;
6532 };
6533 if scalar.kind() != MergedScalarKind::String {
6534 self.expected(
6535 item,
6536 "deploy resource reservation device capabilities must be string scalars",
6537 );
6538 items.push(ProjectValue::new(
6539 ProjectDeployReservationDeviceCapability {
6540 form: ProjectDeployReservationDeviceCapabilityForm::Unmodeled,
6541 value: None,
6542 },
6543 item,
6544 ));
6545 continue;
6546 }
6547 let span = effective_span(item);
6548 if let Some(first) = seen.get(scalar.value()) {
6549 self.diagnostics.push(
6550 Diagnostic::new(
6551 DEPLOY_RESERVATION_DEVICE_CAPABILITY_DUPLICATE_ITEM,
6552 Severity::Error,
6553 "deploy resource reservation device capabilities must be unique exact strings",
6554 )
6555 .with_label(DiagnosticLabel::primary(span, "duplicate capability string"))
6556 .with_label(DiagnosticLabel::secondary(*first, "first identical string")),
6557 );
6558 } else {
6559 seen.insert(scalar.value().to_owned(), span);
6560 }
6561 items.push(ProjectValue::new(
6562 ProjectDeployReservationDeviceCapability {
6563 form: ProjectDeployReservationDeviceCapabilityForm::String,
6564 value: Some(ProjectValue::new(scalar.value().to_owned(), item)),
6565 },
6566 item,
6567 ));
6568 }
6569 Some(ProjectValue::new(items, value))
6570 }
6571
6572 fn deploy_generic_resources(
6573 &mut self,
6574 value: &MergedValue,
6575 path: &[String],
6576 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectDeployGenericResource>>>> {
6577 let Some(values) = value.as_sequence() else {
6578 self.expected(
6579 value,
6580 "deploy resource reservations generic_resources must be a sequence",
6581 );
6582 return None;
6583 };
6584 let mut item_path = path.to_vec();
6585 item_path.push("generic_resources".to_owned());
6586 let mut items = Vec::new();
6587 for item in values {
6588 let Some(fields) = self.mapping(item, "deploy resource generic-resource entries must be mappings") else {
6589 items.push(ProjectValue::new(
6590 ProjectDeployGenericResource {
6591 form: ProjectDeployGenericResourceForm::Unmodeled,
6592 discrete_resource_spec: None,
6593 unmodeled_fields: Vec::new(),
6594 },
6595 item,
6596 ));
6597 continue;
6598 };
6599 let mut resource = ProjectDeployGenericResource {
6600 form: ProjectDeployGenericResourceForm::Mapping,
6601 discrete_resource_spec: None,
6602 unmodeled_fields: Vec::new(),
6603 };
6604 for field in fields {
6605 match field.key() {
6606 "discrete_resource_spec" => {
6607 if let Some(spec) = self.deploy_discrete_resource_spec(field.value(), &item_path) {
6608 resource.discrete_resource_spec = Some(spec);
6609 } else {
6610 resource.unmodeled_fields.push(field_reference(&item_path, field));
6611 }
6612 }
6613 _ => resource.unmodeled_fields.push(field_reference(&item_path, field)),
6614 }
6615 }
6616 items.push(ProjectValue::new(resource, item));
6617 }
6618 Some(ProjectValue::new(items, value))
6619 }
6620
6621 fn deploy_discrete_resource_spec(
6622 &mut self,
6623 value: &MergedValue,
6624 path: &[String],
6625 ) -> Option<ProjectValue<ProjectDeployDiscreteResourceSpec>> {
6626 let fields = self.mapping(value, "deploy discrete_resource_spec must be a mapping")?;
6627 let mut spec_path = path.to_vec();
6628 spec_path.push("discrete_resource_spec".to_owned());
6629 let mut spec = ProjectDeployDiscreteResourceSpec {
6630 kind: None,
6631 value: None,
6632 unmodeled_fields: Vec::new(),
6633 };
6634 for field in fields {
6635 match field.key() {
6636 "kind" => {
6637 let MergedValueKind::Scalar(scalar) = field.value().kind() else {
6638 self.expected(
6639 field.value(),
6640 "deploy discrete_resource_spec kind must be a YAML string scalar",
6641 );
6642 spec.unmodeled_fields.push(field_reference(&spec_path, field));
6643 continue;
6644 };
6645 if scalar.kind() == MergedScalarKind::String {
6646 spec.kind = Some(ProjectValue::new(scalar.value().to_owned(), field.value()));
6647 } else {
6648 self.expected(
6649 field.value(),
6650 "deploy discrete_resource_spec kind must be a YAML string scalar",
6651 );
6652 spec.unmodeled_fields.push(field_reference(&spec_path, field));
6653 }
6654 }
6655 "value" => {
6656 if let Some(number) = self.deploy_discrete_resource_value(field.value()) {
6657 spec.value = Some(number);
6658 } else {
6659 spec.unmodeled_fields.push(field_reference(&spec_path, field));
6660 }
6661 }
6662 _ => spec.unmodeled_fields.push(field_reference(&spec_path, field)),
6663 }
6664 }
6665 Some(ProjectValue::new(spec, value))
6666 }
6667
6668 fn deploy_discrete_resource_value(
6669 &mut self,
6670 value: &MergedValue,
6671 ) -> Option<ProjectValue<DeployDiscreteResourceValue>> {
6672 let MergedValueKind::Scalar(scalar) = value.kind() else {
6673 self.expected(
6674 value,
6675 "deploy discrete_resource_spec value must be a YAML number or string scalar",
6676 );
6677 return None;
6678 };
6679 let scalar_value = match scalar.kind() {
6680 MergedScalarKind::Number => DeployDiscreteResourceValue::YamlNumber(scalar.value().to_owned()),
6681 MergedScalarKind::String => DeployDiscreteResourceValue::String(scalar.value().to_owned()),
6682 MergedScalarKind::Boolean => {
6683 self.expected(
6684 value,
6685 "deploy discrete_resource_spec value must be a YAML number or string scalar",
6686 );
6687 return None;
6688 }
6689 };
6690 Some(ProjectValue::new(scalar_value, value))
6691 }
6692
6693 fn deploy_resource_cpus(
6694 &mut self,
6695 value: &MergedValue,
6696 message: &'static str,
6697 ) -> Option<ProjectValue<DeployResourceCpus>> {
6698 let MergedValueKind::Scalar(scalar) = value.kind() else {
6699 self.expected(value, message);
6700 return None;
6701 };
6702 let cpus = match scalar.kind() {
6703 MergedScalarKind::Number => DeployResourceCpus::YamlNumber(scalar.value().to_owned()),
6704 MergedScalarKind::String => DeployResourceCpus::String(scalar.value().to_owned()),
6705 MergedScalarKind::Boolean => {
6706 self.expected(value, message);
6707 return None;
6708 }
6709 };
6710 Some(ProjectValue::new(cpus, value))
6711 }
6712
6713 fn deploy_resource_memory(
6714 &mut self,
6715 value: &MergedValue,
6716 message: &'static str,
6717 ) -> Option<ProjectValue<DeployResourceMemory>> {
6718 let MergedValueKind::Scalar(scalar) = value.kind() else {
6719 self.expected(value, message);
6720 return None;
6721 };
6722 if scalar.kind() != MergedScalarKind::String {
6723 self.expected(value, message);
6724 return None;
6725 }
6726 Some(ProjectValue::new(
6727 DeployResourceMemory::parse(scalar.value().to_owned()),
6728 value,
6729 ))
6730 }
6731
6732 fn deploy_resource_pids(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployResourcePids>> {
6733 let MergedValueKind::Scalar(scalar) = value.kind() else {
6734 self.expected(
6735 value,
6736 "deploy resource limits pids must be a YAML integer or string scalar",
6737 );
6738 return None;
6739 };
6740 let pids = match scalar.kind() {
6741 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
6742 DeployResourcePids::YamlInteger(scalar.value().to_owned())
6743 }
6744 MergedScalarKind::String => DeployResourcePids::String(scalar.value().to_owned()),
6745 MergedScalarKind::Number | MergedScalarKind::Boolean => {
6746 self.expected(
6747 value,
6748 "deploy resource limits pids must be a YAML integer or string scalar",
6749 );
6750 return None;
6751 }
6752 };
6753 Some(ProjectValue::new(pids, value))
6754 }
6755
6756 fn deploy_restart_policy(
6757 &mut self,
6758 value: &MergedValue,
6759 path: &[String],
6760 ) -> Option<ProjectValue<ProjectDeployRestartPolicy>> {
6761 let fields = self.mapping(value, "deploy restart_policy must be a mapping")?;
6762 let mut member_path = path.to_vec();
6763 member_path.push("restart_policy".to_owned());
6764 let mut policy = ProjectDeployRestartPolicy {
6765 condition: None,
6766 delay: None,
6767 max_attempts: None,
6768 window: None,
6769 unmodeled_fields: Vec::new(),
6770 };
6771 for field in fields {
6772 let typed = match field.key() {
6773 "condition" => self
6774 .project_string(field.value(), "deploy restart-policy condition")
6775 .map(|raw| ProjectValue {
6776 value: DeployRestartCondition::parse(raw.value),
6777 provenance: raw.provenance,
6778 sensitive: raw.sensitive,
6779 })
6780 .map(|value| {
6781 policy.condition = Some(value);
6782 }),
6783 "delay" => self
6784 .project_string(field.value(), "deploy restart-policy delay")
6785 .map(|raw| ProjectValue {
6786 value: DeployRestartDuration::new(raw.value),
6787 provenance: raw.provenance,
6788 sensitive: raw.sensitive,
6789 })
6790 .map(|value| {
6791 policy.delay = Some(value);
6792 }),
6793 "window" => self
6794 .project_string(field.value(), "deploy restart-policy window")
6795 .map(|raw| ProjectValue {
6796 value: DeployRestartDuration::new(raw.value),
6797 provenance: raw.provenance,
6798 sensitive: raw.sensitive,
6799 })
6800 .map(|value| {
6801 policy.window = Some(value);
6802 }),
6803 "max_attempts" => self.deploy_restart_max_attempts(field.value()).map(|value| {
6804 policy.max_attempts = Some(value);
6805 }),
6806 _ => {
6807 policy.unmodeled_fields.push(field_reference(&member_path, field));
6808 Some(())
6809 }
6810 };
6811 if typed.is_none() {
6812 policy.unmodeled_fields.push(field_reference(&member_path, field));
6813 }
6814 }
6815 Some(ProjectValue::new(policy, value))
6816 }
6817
6818 fn deploy_restart_max_attempts(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployRestartMaxAttempts>> {
6819 let MergedValueKind::Scalar(scalar) = value.kind() else {
6820 self.expected(
6821 value,
6822 "deploy restart-policy max_attempts must be a YAML integer or string scalar",
6823 );
6824 return None;
6825 };
6826 let parsed = match scalar.kind() {
6827 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
6828 DeployRestartMaxAttempts::YamlNumber(scalar.value().to_owned())
6829 }
6830 MergedScalarKind::String => DeployRestartMaxAttempts::String(scalar.value().to_owned()),
6831 MergedScalarKind::Number | MergedScalarKind::Boolean => {
6832 self.expected(
6833 value,
6834 "deploy restart-policy max_attempts must be a YAML integer or string scalar",
6835 );
6836 return None;
6837 }
6838 };
6839 Some(ProjectValue::new(parsed, value))
6840 }
6841
6842 fn deploy_update_config(
6843 &mut self,
6844 value: &MergedValue,
6845 path: &[String],
6846 ) -> Option<ProjectValue<ProjectDeployUpdateConfig>> {
6847 let fields = self.mapping(value, "deploy update_config must be a mapping")?;
6848 let mut member_path = path.to_vec();
6849 member_path.push("update_config".to_owned());
6850 let mut config = ProjectDeployUpdateConfig {
6851 parallelism: None,
6852 delay: None,
6853 monitor: None,
6854 failure_action: None,
6855 max_failure_ratio: None,
6856 order: None,
6857 unmodeled_fields: Vec::new(),
6858 };
6859 for field in fields {
6860 let typed = match field.key() {
6861 "parallelism" => self.deploy_update_parallelism(field.value()).map(|value| {
6862 config.parallelism = Some(value);
6863 }),
6864 "delay" => self
6865 .strict_project_string(field.value(), "deploy update_config delay must be a YAML string scalar")
6866 .map(|value| {
6867 config.delay = Some(value);
6868 }),
6869 "monitor" => self
6870 .strict_project_string(
6871 field.value(),
6872 "deploy update_config monitor must be a YAML string scalar",
6873 )
6874 .map(|value| {
6875 config.monitor = Some(value);
6876 }),
6877 "failure_action" => self
6878 .strict_project_string(
6879 field.value(),
6880 "deploy update_config failure_action must be a YAML string scalar",
6881 )
6882 .map(|value| {
6883 config.failure_action = Some(value);
6884 }),
6885 "max_failure_ratio" => self.deploy_update_max_failure_ratio(field.value()).map(|value| {
6886 config.max_failure_ratio = Some(value);
6887 }),
6888 "order" => self.deploy_update_order(field.value()).map(|value| {
6889 config.order = Some(value);
6890 }),
6891 _ => {
6892 config.unmodeled_fields.push(field_reference(&member_path, field));
6893 Some(())
6894 }
6895 };
6896 if typed.is_none() {
6897 config.unmodeled_fields.push(field_reference(&member_path, field));
6898 }
6899 }
6900 Some(ProjectValue::new(config, value))
6901 }
6902
6903 fn deploy_rollback_config(
6904 &mut self,
6905 value: &MergedValue,
6906 path: &[String],
6907 ) -> Option<ProjectValue<ProjectDeployRollbackConfig>> {
6908 let fields = self.mapping(value, "deploy rollback_config must be a mapping")?;
6909 let mut member_path = path.to_vec();
6910 member_path.push("rollback_config".to_owned());
6911 let mut config = ProjectDeployRollbackConfig {
6912 parallelism: None,
6913 delay: None,
6914 monitor: None,
6915 failure_action: None,
6916 max_failure_ratio: None,
6917 order: None,
6918 unmodeled_fields: Vec::new(),
6919 };
6920 for field in fields {
6921 let typed = match field.key() {
6922 "parallelism" => self.deploy_rollback_parallelism(field.value()).map(|value| {
6923 config.parallelism = Some(value);
6924 }),
6925 "delay" => self
6926 .strict_project_string(
6927 field.value(),
6928 "deploy rollback_config delay must be a YAML string scalar",
6929 )
6930 .map(|value| {
6931 config.delay = Some(value);
6932 }),
6933 "monitor" => self
6934 .strict_project_string(
6935 field.value(),
6936 "deploy rollback_config monitor must be a YAML string scalar",
6937 )
6938 .map(|value| {
6939 config.monitor = Some(value);
6940 }),
6941 "failure_action" => self
6942 .strict_project_string(
6943 field.value(),
6944 "deploy rollback_config failure_action must be a YAML string scalar",
6945 )
6946 .map(|value| {
6947 config.failure_action = Some(value);
6948 }),
6949 "max_failure_ratio" => self.deploy_rollback_max_failure_ratio(field.value()).map(|value| {
6950 config.max_failure_ratio = Some(value);
6951 }),
6952 "order" => self.deploy_rollback_order(field.value()).map(|value| {
6953 config.order = Some(value);
6954 }),
6955 _ => {
6956 config.unmodeled_fields.push(field_reference(&member_path, field));
6957 Some(())
6958 }
6959 };
6960 if typed.is_none() {
6961 config.unmodeled_fields.push(field_reference(&member_path, field));
6962 }
6963 }
6964 Some(ProjectValue::new(config, value))
6965 }
6966
6967 fn deploy_rollback_parallelism(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployRollbackParallelism>> {
6968 let MergedValueKind::Scalar(scalar) = value.kind() else {
6969 self.expected(
6970 value,
6971 "deploy rollback_config parallelism must be a YAML integer or string scalar",
6972 );
6973 return None;
6974 };
6975 let parsed = match scalar.kind() {
6976 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
6977 DeployRollbackParallelism::YamlInteger(scalar.value().to_owned())
6978 }
6979 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
6980 DeployRollbackParallelism::String(scalar.value().to_owned())
6981 }
6982 _ => {
6983 self.expected(
6984 value,
6985 "deploy rollback_config parallelism must be a YAML integer or string scalar",
6986 );
6987 return None;
6988 }
6989 };
6990 Some(ProjectValue::new(parsed, value))
6991 }
6992
6993 fn deploy_rollback_max_failure_ratio(
6994 &mut self,
6995 value: &MergedValue,
6996 ) -> Option<ProjectValue<DeployRollbackMaxFailureRatio>> {
6997 let MergedValueKind::Scalar(scalar) = value.kind() else {
6998 self.expected(
6999 value,
7000 "deploy rollback_config max_failure_ratio must be a YAML number or string scalar",
7001 );
7002 return None;
7003 };
7004 let parsed = match scalar.kind() {
7005 MergedScalarKind::Number => DeployRollbackMaxFailureRatio::YamlNumber(scalar.value().to_owned()),
7006 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
7007 DeployRollbackMaxFailureRatio::String(scalar.value().to_owned())
7008 }
7009 _ => {
7010 self.expected(
7011 value,
7012 "deploy rollback_config max_failure_ratio must be a YAML number or string scalar",
7013 );
7014 return None;
7015 }
7016 };
7017 Some(ProjectValue::new(parsed, value))
7018 }
7019
7020 fn deploy_rollback_order(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployRollbackOrder>> {
7021 let raw = self.strict_project_string(value, "deploy rollback_config order must be a YAML string scalar")?;
7022 let order = DeployRollbackOrder::parse(raw.value().to_owned());
7023 if !order.is_documented() {
7024 self.diagnostics.push(
7025 Diagnostic::new(
7026 DEPLOY_ROLLBACK_CONFIG_ORDER_PORTABILITY,
7027 Severity::Warning,
7028 "deploy rollback_config order is outside Compose's documented portable values",
7029 )
7030 .with_label(DiagnosticLabel::primary(
7031 effective_span(value),
7032 "retained provider-specific rollback order",
7033 )),
7034 );
7035 }
7036 Some(ProjectValue {
7037 value: order,
7038 provenance: raw.provenance,
7039 sensitive: raw.sensitive,
7040 })
7041 }
7042
7043 fn strict_project_string(&mut self, value: &MergedValue, message: &str) -> Option<ProjectValue<String>> {
7044 let MergedValueKind::Scalar(scalar) = value.kind() else {
7045 self.expected(value, message);
7046 return None;
7047 };
7048 if scalar.kind() != MergedScalarKind::String || !scalar.is_strict_yaml_string() {
7049 self.expected(value, message);
7050 return None;
7051 }
7052 Some(ProjectValue::new(scalar.value().to_owned(), value))
7053 }
7054 fn deploy_update_parallelism(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployUpdateParallelism>> {
7055 let MergedValueKind::Scalar(scalar) = value.kind() else {
7056 self.expected(
7057 value,
7058 "deploy update_config parallelism must be a YAML integer or string scalar",
7059 );
7060 return None;
7061 };
7062 let parsed = match scalar.kind() {
7063 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
7064 DeployUpdateParallelism::YamlInteger(scalar.value().to_owned())
7065 }
7066 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
7067 DeployUpdateParallelism::String(scalar.value().to_owned())
7068 }
7069 _ => {
7070 self.expected(
7071 value,
7072 "deploy update_config parallelism must be a YAML integer or string scalar",
7073 );
7074 return None;
7075 }
7076 };
7077 Some(ProjectValue::new(parsed, value))
7078 }
7079 fn deploy_update_max_failure_ratio(
7080 &mut self,
7081 value: &MergedValue,
7082 ) -> Option<ProjectValue<DeployUpdateMaxFailureRatio>> {
7083 let MergedValueKind::Scalar(scalar) = value.kind() else {
7084 self.expected(
7085 value,
7086 "deploy update_config max_failure_ratio must be a YAML number or string scalar",
7087 );
7088 return None;
7089 };
7090 let parsed = match scalar.kind() {
7091 MergedScalarKind::Number => DeployUpdateMaxFailureRatio::YamlNumber(scalar.value().to_owned()),
7092 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
7093 DeployUpdateMaxFailureRatio::String(scalar.value().to_owned())
7094 }
7095 _ => {
7096 self.expected(
7097 value,
7098 "deploy update_config max_failure_ratio must be a YAML number or string scalar",
7099 );
7100 return None;
7101 }
7102 };
7103 Some(ProjectValue::new(parsed, value))
7104 }
7105 fn deploy_update_order(&mut self, value: &MergedValue) -> Option<ProjectValue<DeployUpdateOrder>> {
7106 let raw = self.strict_project_string(value, "deploy update_config order must be a YAML string scalar")?;
7107 let order = DeployUpdateOrder::parse(raw.value().to_owned());
7108 if !order.is_documented() {
7109 self.diagnostics.push(
7110 Diagnostic::new(
7111 DEPLOY_UPDATE_CONFIG_ORDER_PORTABILITY,
7112 Severity::Warning,
7113 "deploy update_config order is outside Compose's documented portable values",
7114 )
7115 .with_label(DiagnosticLabel::primary(
7116 effective_span(value),
7117 "retained provider-specific update order",
7118 )),
7119 );
7120 }
7121 Some(ProjectValue {
7122 value: order,
7123 provenance: raw.provenance,
7124 sensitive: raw.sensitive,
7125 })
7126 }
7127
7128 fn deploy_placement(
7129 &mut self,
7130 value: &MergedValue,
7131 path: &[String],
7132 ) -> Option<ProjectValue<ProjectDeployPlacement>> {
7133 let fields = self.mapping(value, "deploy placement must be a mapping")?;
7134 let mut placement_path = path.to_vec();
7135 placement_path.push("placement".to_owned());
7136 let mut placement = ProjectDeployPlacement {
7137 constraints: None,
7138 preferences: None,
7139 max_replicas_per_node: None,
7140 unmodeled_fields: Vec::new(),
7141 };
7142 for field in fields {
7143 match field.key() {
7144 "constraints" => {
7145 let (constraints, malformed) = self.deploy_placement_constraints(field.value());
7146 placement.constraints = constraints;
7147 if malformed {
7148 placement.unmodeled_fields.push(field_reference(&placement_path, field));
7149 }
7150 }
7151 "preferences" => {
7152 let (preferences, malformed) = self.deploy_placement_preferences(field.value(), &placement_path);
7153 placement.preferences = preferences;
7154 if malformed {
7155 placement.unmodeled_fields.push(field_reference(&placement_path, field));
7156 }
7157 }
7158 "max_replicas_per_node" => {
7159 if let Some(maximum) = self.deploy_placement_max_replicas_per_node(field.value()) {
7160 placement.max_replicas_per_node = Some(maximum);
7161 } else {
7162 placement.unmodeled_fields.push(field_reference(&placement_path, field));
7163 }
7164 }
7165 _ => placement.unmodeled_fields.push(field_reference(&placement_path, field)),
7166 }
7167 }
7168 Some(ProjectValue::new(placement, value))
7169 }
7170
7171 fn deploy_placement_constraints(
7172 &mut self,
7173 value: &MergedValue,
7174 ) -> (Option<ProjectValue<Vec<ProjectValue<String>>>>, bool) {
7175 let Some(values) = value.as_sequence() else {
7176 self.expected(
7177 value,
7178 "deploy placement constraints must be a sequence of YAML string scalars",
7179 );
7180 return (None, true);
7181 };
7182 let mut constraints = Vec::new();
7183 let mut malformed = false;
7184 for item in values {
7185 match item.kind() {
7186 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7187 constraints.push(ProjectValue::new(scalar.value().to_owned(), item));
7188 }
7189 _ => {
7190 self.expected(item, "deploy placement constraints must contain YAML string scalars");
7191 malformed = true;
7192 }
7193 }
7194 }
7195 (Some(ProjectValue::new(constraints, value)), malformed)
7196 }
7197
7198 fn deploy_placement_preferences(
7199 &mut self,
7200 value: &MergedValue,
7201 path: &[String],
7202 ) -> (
7203 Option<ProjectValue<Vec<ProjectValue<ProjectDeployPlacementPreference>>>>,
7204 bool,
7205 ) {
7206 let Some(values) = value.as_sequence() else {
7207 self.expected(value, "deploy placement preferences must be a sequence of mappings");
7208 return (None, true);
7209 };
7210 let mut preferences = Vec::new();
7211 let mut malformed = false;
7212 for (index, item) in values.iter().enumerate() {
7213 let Some(fields) = item.as_mapping() else {
7214 self.expected(item, "deploy placement preferences must contain mappings");
7215 malformed = true;
7216 continue;
7217 };
7218 let mut preference_path = path.to_vec();
7219 preference_path.extend(["preferences".to_owned(), index.to_string()]);
7220 let mut preference = ProjectDeployPlacementPreference {
7221 spread: None,
7222 unmodeled_fields: Vec::new(),
7223 };
7224 for field in fields {
7225 match field.key() {
7226 "spread" => match field.value().kind() {
7227 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7228 preference.spread = Some(ProjectValue::new(scalar.value().to_owned(), field.value()));
7229 }
7230 _ => {
7231 self.expected(
7232 field.value(),
7233 "deploy placement preference spread must be a YAML string scalar",
7234 );
7235 preference
7236 .unmodeled_fields
7237 .push(field_reference(&preference_path, field));
7238 }
7239 },
7240 _ => preference
7241 .unmodeled_fields
7242 .push(field_reference(&preference_path, field)),
7243 }
7244 }
7245 preferences.push(ProjectValue::new(preference, item));
7246 }
7247 (Some(ProjectValue::new(preferences, value)), malformed)
7248 }
7249
7250 fn deploy_placement_max_replicas_per_node(
7251 &mut self,
7252 value: &MergedValue,
7253 ) -> Option<ProjectValue<DeployPlacementMaxReplicasPerNode>> {
7254 let MergedValueKind::Scalar(scalar) = value.kind() else {
7255 self.expected(
7256 value,
7257 "deploy placement max_replicas_per_node must be a YAML integer or string scalar",
7258 );
7259 return None;
7260 };
7261 let maximum = match scalar.kind() {
7262 MergedScalarKind::Number if Self::yaml_integer_spelling(scalar.value()) => {
7263 DeployPlacementMaxReplicasPerNode::YamlInteger(scalar.value().to_owned())
7264 }
7265 MergedScalarKind::String => DeployPlacementMaxReplicasPerNode::String(scalar.value().to_owned()),
7266 MergedScalarKind::Number | MergedScalarKind::Boolean => {
7267 self.expected(
7268 value,
7269 "deploy placement max_replicas_per_node must be a YAML integer or string scalar",
7270 );
7271 return None;
7272 }
7273 };
7274 Some(ProjectValue::new(maximum, value))
7275 }
7276
7277 fn yaml_integer_spelling(value: &str) -> bool {
7283 let digits = value
7284 .strip_prefix('+')
7285 .or_else(|| value.strip_prefix('-'))
7286 .unwrap_or(value);
7287 let (radix, digits) = if let Some(value) = digits.strip_prefix("0b") {
7288 (2, value)
7289 } else if let Some(value) = digits.strip_prefix("0o") {
7290 (8, value)
7291 } else if let Some(value) = digits.strip_prefix("0x") {
7292 (16, value)
7293 } else {
7294 (10, digits)
7295 };
7296 let mut saw_digit = false;
7297 let mut previous_separator = false;
7298 for byte in digits.bytes() {
7299 if byte == b'_' {
7300 if !saw_digit || previous_separator {
7301 return false;
7302 }
7303 previous_separator = true;
7304 } else if if radix == 16 {
7305 byte.is_ascii_hexdigit()
7306 } else {
7307 byte.is_ascii_digit() && (byte - b'0') < radix
7308 } {
7309 saw_digit = true;
7310 previous_separator = false;
7311 } else {
7312 return false;
7313 }
7314 }
7315 saw_digit && !previous_separator
7316 }
7317
7318 fn hostname(&mut self, value: &MergedValue) -> Option<ProjectValue<Hostname>> {
7319 let scalar = match value.kind() {
7320 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
7321 _ => {
7322 self.expected(value, "hostname must be a YAML string scalar");
7323 return None;
7324 }
7325 };
7326 let hostname = Hostname::parse(Located::new(scalar.value().to_owned(), effective_span(value)));
7327 if hostname.kind() == &HostnameKind::Invalid {
7328 self.invalid(
7329 effective_span(value),
7330 "hostname must be an ASCII RFC-1123 name of 1 to 253 characters with dot-separated labels of 1 to 63 alphanumeric or hyphen characters; each label must start and end alphanumeric",
7331 );
7332 }
7333 Some(ProjectValue::new(hostname, value))
7334 }
7335
7336 fn restart_policy(&mut self, value: &MergedValue) -> Option<ProjectValue<RestartPolicy>> {
7337 let policy = RestartPolicy::parse(self.located_string(value, "restart must be a non-null scalar")?);
7338 if !policy.is_valid() {
7339 self.invalid(
7340 effective_span(value),
7341 "restart must be `no`, `always`, `on-failure[:max-retries]`, or `unless-stopped`",
7342 );
7343 }
7344 Some(ProjectValue::new(policy, value))
7345 }
7346
7347 fn service_build(&mut self, value: &MergedValue, path: &[String]) -> Option<ProjectValue<ProjectBuild>> {
7348 let build = match value.kind() {
7349 MergedValueKind::Scalar(_) => ProjectBuild::Context(self.project_string(value, "build context")?),
7350 MergedValueKind::Mapping(fields) => {
7351 let mut definition = ProjectBuildDefinition {
7352 additional_contexts: None,
7353 context: None,
7354 args: None,
7355 entitlements: None,
7356 extra_hosts: None,
7357 dockerfile: None,
7358 dockerfile_inline: None,
7359 target: None,
7360 network: None,
7361 isolation: None,
7362 platforms: None,
7363 cache_options: None,
7364 no_cache_filter: None,
7365 privileged: None,
7366 sbom: None,
7367 provenance: None,
7368 shm_size: None,
7369 tags: None,
7370 labels: None,
7371 secrets: None,
7372 ssh: None,
7373 ulimits: None,
7374 unmodeled_fields: Arc::new(Vec::new()),
7375 };
7376 let (mut dockerfile, mut dockerfile_inline) = (None, None);
7377 for field in fields {
7378 match field.key() {
7379 "additional_contexts" => self.set_build_additional_contexts(&mut definition, field, path),
7380 "args" => {
7381 let (args, malformed) = self.build_args(field.value());
7382 definition.args = args.map(Box::new);
7383 if malformed {
7384 definition.push_unmodeled(field_reference(path, field));
7385 }
7386 }
7387 "cache_from" | "cache_to" => self.cache_locations(&mut definition, field, path),
7388 "context" => {
7389 if let Some(context) = self.project_string(field.value(), "build context") {
7390 definition.context = Some(Box::new(context));
7391 } else {
7392 definition.push_unmodeled(field_reference(path, field));
7393 }
7394 }
7395 "dockerfile" => {
7396 dockerfile = Some(field);
7397 if let Some(dockerfile) = self.non_empty_project_string(field.value(), "build dockerfile") {
7398 definition.dockerfile = Some(Box::new(dockerfile));
7399 } else {
7400 definition.push_unmodeled(field_reference(path, field));
7401 }
7402 }
7403 "dockerfile_inline" => self.set_inline(&mut definition, field, path, &mut dockerfile_inline),
7404 "entitlements" => self.set_build_entitlements(&mut definition, field, path),
7405 "extra_hosts" => self.set_build_extra_hosts(&mut definition, field, path),
7406 "target" => {
7407 if let Some(target) = self.project_string(field.value(), "build target") {
7408 definition.target = Some(Box::new(target));
7409 } else {
7410 definition.push_unmodeled(field_reference(path, field));
7411 }
7412 }
7413 "network" => self.set_build_network(&mut definition, field, path),
7414 "isolation" => self.set_build_isolation(&mut definition, field, path),
7415 "platforms" => self.set_build_platforms(&mut definition, field, path),
7416 "no_cache" => self.set_build_no_cache(&mut definition, field, path),
7417 "no_cache_filter" => self.set_build_no_cache_filter(&mut definition, field, path),
7418 "privileged" => self.set_build_privileged(&mut definition, field, path),
7419 "sbom" | "provenance" => self.set_build_attestation(&mut definition, field, path),
7420 "pull" => self.set_build_pull(&mut definition, field, path),
7421 "shm_size" => self.set_build_shm_size(&mut definition, field, path),
7422 "tags" => {
7423 let (tags, malformed) = self.build_tags(field.value());
7424 definition.tags = tags.map(Box::new);
7425 if malformed {
7426 definition.push_unmodeled(field_reference(path, field));
7427 }
7428 }
7429 "labels" => {
7430 let (labels, malformed) = self.build_labels(field.value());
7431 definition.labels = labels.map(Box::new);
7432 if malformed {
7433 definition.push_unmodeled(field_reference(path, field));
7434 }
7435 }
7436 "secrets" => self.set_build_secrets(&mut definition, field, path),
7437 "ssh" => self.set_build_ssh(&mut definition, field, path),
7438 "ulimits" => self.set_build_ulimits(&mut definition, field, path),
7439 _ => definition.push_unmodeled(field_reference(path, field)),
7440 }
7441 }
7442 self.report_build_dockerfile_conflict(dockerfile, dockerfile_inline);
7443 ProjectBuild::Definition(definition)
7444 }
7445 _ => return self.invalid_project_build_form(value),
7446 };
7447 Some(ProjectValue::new(build, value))
7448 }
7449
7450 fn invalid_project_build_form(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectBuild>> {
7451 self.expected(value, "build must be a scalar context or mapping");
7452 None
7453 }
7454
7455 fn report_build_dockerfile_conflict(
7456 &mut self,
7457 dockerfile: Option<&MergedEntry>,
7458 dockerfile_inline: Option<&MergedEntry>,
7459 ) {
7460 let (Some(dockerfile), Some(dockerfile_inline)) = (dockerfile, dockerfile_inline) else {
7461 return;
7462 };
7463 self.diagnostics.push(
7464 Diagnostic::new(
7465 BUILD_DOCKERFILE_INLINE_CONFLICT,
7466 Severity::Error,
7467 "build `dockerfile` and `dockerfile_inline` are mutually exclusive",
7468 )
7469 .with_label(DiagnosticLabel::primary(entry_span(dockerfile), "dockerfile retained"))
7470 .with_label(DiagnosticLabel::secondary(
7471 entry_span(dockerfile_inline),
7472 "dockerfile_inline retained",
7473 )),
7474 );
7475 }
7476
7477 fn set_build_secrets(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7478 let (secrets, malformed) = self.build_secret_grants(field.value(), path);
7479 definition.secrets = secrets.map(Box::new);
7480 if malformed {
7481 definition.push_unmodeled(field_reference(path, field));
7482 }
7483 }
7484
7485 fn set_build_ssh(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7486 let (ssh, malformed) = self.build_ssh(field.value());
7487 definition.ssh = ssh.map(Box::new);
7488 if malformed {
7489 definition.push_unmodeled(field_reference(path, field));
7490 }
7491 }
7492
7493 fn set_build_ulimits(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7494 let (ulimits, unmodeled) = self.ulimits(field.value(), path);
7495 definition.ulimits = ulimits.map(Box::new);
7496 for field in unmodeled {
7497 definition.push_unmodeled(field);
7498 }
7499 if definition.ulimits.is_none() {
7500 definition.push_unmodeled(field_reference(path, field));
7501 }
7502 }
7503
7504 fn set_build_additional_contexts(
7505 &mut self,
7506 definition: &mut ProjectBuildDefinition,
7507 field: &MergedEntry,
7508 path: &[String],
7509 ) {
7510 let (additional_contexts, malformed) = self.build_additional_contexts(field.value());
7511 definition.additional_contexts = additional_contexts.map(Box::new);
7512 if malformed {
7513 definition.push_unmodeled(field_reference(path, field));
7514 }
7515 }
7516
7517 fn set_build_entitlements(
7518 &mut self,
7519 definition: &mut ProjectBuildDefinition,
7520 field: &MergedEntry,
7521 path: &[String],
7522 ) {
7523 let (entitlements, malformed) = self.build_entitlements(field.value());
7524 definition.entitlements = entitlements.map(Box::new);
7525 if malformed {
7526 definition.push_unmodeled(field_reference(path, field));
7527 }
7528 }
7529
7530 fn set_build_dockerfile_inline<'field>(
7531 &mut self,
7532 definition: &mut ProjectBuildDefinition,
7533 field: &'field MergedEntry,
7534 path: &[String],
7535 dockerfile_inline: &mut Option<&'field MergedEntry>,
7536 ) {
7537 *dockerfile_inline = Some(field);
7538 let dockerfile_inline = match field.value().kind() {
7539 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7540 Some(ProjectValue::new(scalar.value().to_owned(), field.value()))
7541 }
7542 MergedValueKind::Scalar(_) => {
7543 self.invalid(
7544 effective_span(field.value()),
7545 "build dockerfile_inline must be a YAML string scalar",
7546 );
7547 None
7548 }
7549 _ => {
7550 self.expected(field.value(), "build dockerfile_inline must be a YAML string scalar");
7551 None
7552 }
7553 };
7554 if let Some(dockerfile_inline) = dockerfile_inline {
7555 definition.dockerfile_inline = Some(Box::new(dockerfile_inline));
7556 } else {
7557 definition.push_unmodeled(field_reference(path, field));
7558 }
7559 }
7560 fn set_inline<'field>(
7561 &mut self,
7562 definition: &mut ProjectBuildDefinition,
7563 field: &'field MergedEntry,
7564 path: &[String],
7565 inline: &mut Option<&'field MergedEntry>,
7566 ) {
7567 self.set_build_dockerfile_inline(definition, field, path, inline);
7568 }
7569
7570 fn set_build_extra_hosts(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7571 let pending_start = self.pending_unmodeled.len();
7572 let (extra_hosts, malformed) = self.build_extra_hosts(field.value(), path);
7573 definition.extra_hosts = extra_hosts.map(Box::new);
7574 for nested in self.pending_unmodeled.drain(pending_start..) {
7575 definition.push_unmodeled(nested);
7576 }
7577 if malformed {
7578 definition.push_unmodeled(field_reference(path, field));
7579 }
7580 }
7581
7582 fn set_build_platforms(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7583 let (platforms, malformed) = self.build_platforms(field.value());
7584 definition.platforms = platforms.map(Box::new);
7585 if malformed {
7586 definition.push_unmodeled(field_reference(path, field));
7587 }
7588 }
7589
7590 fn set_build_cache_locations(
7591 &mut self,
7592 definition: &mut ProjectBuildDefinition,
7593 field: &MergedEntry,
7594 path: &[String],
7595 name: &str,
7596 ) {
7597 let (locations, malformed) = self.build_cache_locations(field.value(), name);
7598 if name == "cache_from" {
7599 definition.cache_options().cache_from = locations;
7600 } else {
7601 definition.cache_options().cache_to = locations;
7602 }
7603 if malformed {
7604 definition.push_unmodeled(field_reference(path, field));
7605 }
7606 }
7607 fn cache_locations(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7608 self.set_build_cache_locations(definition, field, path, field.key());
7609 }
7610
7611 fn set_build_network(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7612 if let Some(network) = self.project_string(field.value(), "build network") {
7613 definition.network = Some(Box::new(network));
7614 } else {
7615 definition.push_unmodeled(field_reference(path, field));
7616 }
7617 }
7618
7619 fn set_build_isolation(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7620 let isolation = match field.value().kind() {
7621 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7622 Some(ProjectValue::new(scalar.value().to_owned(), field.value()))
7623 }
7624 MergedValueKind::Scalar(_) => {
7625 self.invalid(
7626 effective_span(field.value()),
7627 "build isolation must be a YAML string scalar",
7628 );
7629 None
7630 }
7631 _ => {
7632 self.expected(field.value(), "build isolation must be a YAML string scalar");
7633 None
7634 }
7635 };
7636 if let Some(isolation) = isolation {
7637 definition.isolation = Some(Box::new(isolation));
7638 } else {
7639 definition.push_unmodeled(field_reference(path, field));
7640 }
7641 }
7642
7643 fn set_build_pull(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7644 if let Some(pull) = self.boolean(field.value(), "build pull must be a boolean") {
7645 definition.cache_options().pull = Some(pull);
7646 } else {
7647 definition.push_unmodeled(field_reference(path, field));
7648 }
7649 }
7650
7651 fn set_build_no_cache(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7652 let value = match field.value().kind() {
7653 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Boolean => {
7654 Some(BuildNoCache::Boolean(scalar.value().eq_ignore_ascii_case("true")))
7655 }
7656 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7657 Some(BuildNoCache::String(scalar.value().to_owned()))
7658 }
7659 MergedValueKind::Scalar(_) => {
7660 self.invalid(
7661 effective_span(field.value()),
7662 "build no_cache must be a YAML boolean or string scalar",
7663 );
7664 None
7665 }
7666 _ => {
7667 self.expected(field.value(), "build no_cache must be a YAML boolean or string scalar");
7668 None
7669 }
7670 };
7671 if let Some(value) = value {
7672 definition.cache_options().no_cache = Some(ProjectValue::new(value, field.value()));
7673 } else {
7674 definition.push_unmodeled(field_reference(path, field));
7675 }
7676 }
7677 fn set_build_no_cache_filter(
7678 &mut self,
7679 definition: &mut ProjectBuildDefinition,
7680 field: &MergedEntry,
7681 path: &[String],
7682 ) {
7683 let value = match field.value().kind() {
7684 MergedValueKind::Scalar(s) if s.kind() == MergedScalarKind::String => Some(
7685 ProjectBuildNoCacheFilter::Scalar(ProjectValue::new(s.value().to_owned(), field.value())),
7686 ),
7687 MergedValueKind::Sequence(items) => {
7688 let mut values = Vec::new();
7689 let mut bad = false;
7690 for item in items {
7691 if let MergedValueKind::Scalar(s) = item.kind() {
7692 if s.kind() == MergedScalarKind::String {
7693 if values
7694 .iter()
7695 .any(|value: &ProjectValue<String>| value.value() == s.value())
7696 {
7697 self.diagnostics.push(
7698 Diagnostic::new(
7699 BUILD_NO_CACHE_FILTER_DUPLICATE_ITEM,
7700 Severity::Warning,
7701 "build no_cache_filter retains duplicate stage",
7702 )
7703 .with_label(DiagnosticLabel::primary(effective_span(item), "duplicate retained")),
7704 );
7705 }
7706 values.push(ProjectValue::new(s.value().to_owned(), item));
7707 continue;
7708 }
7709 }
7710 self.expected(item, "build no_cache_filter entries must be string scalars");
7711 bad = true;
7712 }
7713 if bad {
7714 definition.push_unmodeled(field_reference(path, field));
7715 }
7716 Some(ProjectBuildNoCacheFilter::List(values))
7717 }
7718 _ => {
7719 self.expected(
7720 field.value(),
7721 "build no_cache_filter must be a string scalar or sequence",
7722 );
7723 None
7724 }
7725 };
7726 if let Some(value) = value {
7727 definition.no_cache_filter = Some(Box::new(ProjectValue::new(value, field.value())));
7728 } else {
7729 definition.push_unmodeled(field_reference(path, field));
7730 }
7731 }
7732 fn set_build_privileged(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7733 if let Some(value) = self.boolean(field.value(), "build privileged must be a boolean") {
7734 definition.privileged = Some(Box::new(value));
7735 } else {
7736 definition.push_unmodeled(field_reference(path, field));
7737 }
7738 }
7739
7740 fn set_build_sbom(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7741 let value = match field.value().kind() {
7742 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Boolean => {
7743 Some(BuildSbom::Boolean(scalar.value().eq_ignore_ascii_case("true")))
7744 }
7745 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
7746 Some(BuildSbom::String(scalar.value().to_owned()))
7747 }
7748 MergedValueKind::Scalar(_) => {
7749 self.invalid(
7750 effective_span(field.value()),
7751 "build sbom must be a YAML boolean or string scalar",
7752 );
7753 None
7754 }
7755 _ => {
7756 self.expected(field.value(), "build sbom must be a YAML boolean or string scalar");
7757 None
7758 }
7759 };
7760 if let Some(value) = value {
7761 definition.sbom = Some(Box::new(ProjectValue::new(value, field.value())));
7762 } else {
7763 definition.push_unmodeled(field_reference(path, field));
7764 }
7765 }
7766
7767 fn set_build_attestation(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7768 if field.key() == "sbom" {
7769 self.set_build_sbom(definition, field, path);
7770 } else {
7771 self.set_build_provenance(definition, field, path);
7772 }
7773 }
7774 fn set_build_provenance(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7775 let value = match field.value().kind() {
7776 MergedValueKind::Scalar(s) if s.kind() == MergedScalarKind::Boolean => {
7777 Some(BuildProvenance::Boolean(s.value().eq_ignore_ascii_case("true")))
7778 }
7779 MergedValueKind::Scalar(s) if s.kind() == MergedScalarKind::String => {
7780 Some(BuildProvenance::String(s.value().to_owned()))
7781 }
7782 MergedValueKind::Scalar(_) => {
7783 self.invalid(
7784 effective_span(field.value()),
7785 "build provenance must be a YAML boolean or string scalar",
7786 );
7787 None
7788 }
7789 _ => {
7790 self.expected(
7791 field.value(),
7792 "build provenance must be a YAML boolean or string scalar",
7793 );
7794 None
7795 }
7796 };
7797 if let Some(value) = value {
7798 definition.provenance = Some(Box::new(ProjectValue::new(value, field.value())));
7799 } else {
7800 definition.push_unmodeled(field_reference(path, field));
7801 }
7802 }
7803
7804 fn set_build_shm_size(&mut self, definition: &mut ProjectBuildDefinition, field: &MergedEntry, path: &[String]) {
7805 if let Some(shm_size) = self.shm_size(field.value()) {
7806 definition.shm_size = Some(Box::new(shm_size));
7807 } else {
7808 definition.push_unmodeled(field_reference(path, field));
7809 }
7810 }
7811
7812 fn build_tags(&mut self, value: &MergedValue) -> (Option<ProjectValue<Vec<ProjectValue<String>>>>, bool) {
7813 let Some(values) = value.as_sequence() else {
7814 self.expected(value, "build tags must be a sequence of non-null scalars");
7815 return (None, true);
7816 };
7817 let mut tags = Vec::new();
7818 let mut malformed = false;
7819 for item in values {
7820 let Some(scalar) = item.as_scalar() else {
7821 self.expected(item, "build tag entries must be non-null scalars");
7822 malformed = true;
7823 continue;
7824 };
7825 tags.push(ProjectValue::new(scalar.value().to_owned(), item));
7826 }
7827 (Some(ProjectValue::new(tags, value)), malformed)
7828 }
7829
7830 fn build_entitlements(&mut self, value: &MergedValue) -> (Option<ProjectValue<Vec<ProjectValue<String>>>>, bool) {
7831 let Some(values) = value.as_sequence() else {
7832 self.expected(value, "build entitlements must be a sequence of string scalars");
7833 return (None, true);
7834 };
7835 let mut entitlements = Vec::new();
7836 let mut malformed = false;
7837 for item in values {
7838 let MergedValueKind::Scalar(scalar) = item.kind() else {
7839 self.expected(item, "build entitlement entries must be string scalars");
7840 malformed = true;
7841 continue;
7842 };
7843 if scalar.kind() != MergedScalarKind::String {
7844 self.expected(item, "build entitlement entries must be string scalars");
7845 malformed = true;
7846 continue;
7847 }
7848 entitlements.push(ProjectValue::new(scalar.value().to_owned(), item));
7849 }
7850 (Some(ProjectValue::new(entitlements, value)), malformed)
7851 }
7852
7853 fn build_platforms(&mut self, value: &MergedValue) -> (Option<ProjectValue<Vec<ProjectValue<String>>>>, bool) {
7854 let Some(values) = value.as_sequence() else {
7855 self.expected(value, "build platforms must be a sequence of non-null scalars");
7856 return (None, true);
7857 };
7858 let mut platforms = Vec::new();
7859 let mut malformed = false;
7860 for item in values {
7861 let Some(scalar) = item.as_scalar() else {
7862 self.expected(item, "build platform entries must be non-null scalars");
7863 malformed = true;
7864 continue;
7865 };
7866 platforms.push(ProjectValue::new(scalar.value().to_owned(), item));
7867 }
7868 (Some(ProjectValue::new(platforms, value)), malformed)
7869 }
7870
7871 fn build_cache_locations(
7872 &mut self,
7873 value: &MergedValue,
7874 name: &str,
7875 ) -> (Option<ProjectValue<Vec<ProjectValue<String>>>>, bool) {
7876 let Some(values) = value.as_sequence() else {
7877 self.expected(value, &format!("build {name} must be a sequence of string scalars"));
7878 return (None, true);
7879 };
7880 let mut locations = Vec::new();
7881 let mut malformed = false;
7882 for item in values {
7883 let MergedValueKind::Scalar(scalar) = item.kind() else {
7884 self.expected(item, &format!("build {name} entries must be string scalars"));
7885 malformed = true;
7886 continue;
7887 };
7888 if scalar.kind() != MergedScalarKind::String {
7889 self.expected(item, &format!("build {name} entries must be string scalars"));
7890 malformed = true;
7891 continue;
7892 }
7893 locations.push(ProjectValue::new(scalar.value().to_owned(), item));
7894 }
7895 (Some(ProjectValue::new(locations, value)), malformed)
7896 }
7897
7898 fn build_args(&mut self, value: &MergedValue) -> (Option<ProjectValue<ProjectBuildArgs>>, bool) {
7899 match value.kind() {
7900 MergedValueKind::Mapping(entries) => {
7901 let mut args = Vec::new();
7902 let mut malformed = false;
7903 for entry in entries {
7904 let Some(value) =
7905 self.compose_scalar(entry.value(), "build argument value must be a scalar or null")
7906 else {
7907 malformed = true;
7908 continue;
7909 };
7910 args.push(ProjectBuildArgEntry {
7911 name: ProjectKey::from_entry(entry),
7912 value: ProjectValue::new(value, entry.value()),
7913 });
7914 }
7915 (Some(ProjectValue::new(ProjectBuildArgs::Map(args), value)), malformed)
7916 }
7917 MergedValueKind::Sequence(values) => {
7918 let mut args = Vec::new();
7919 let mut malformed = false;
7920 for item in values {
7921 let MergedValueKind::Scalar(argument) = item.kind() else {
7922 self.expected(item, "build argument list item must be a string scalar");
7923 malformed = true;
7924 continue;
7925 };
7926 if argument.kind() != MergedScalarKind::String {
7927 self.expected(item, "build argument list item must be a string scalar");
7928 malformed = true;
7929 continue;
7930 }
7931 args.push(ProjectValue::new(argument.value().to_owned(), item));
7932 }
7933 (Some(ProjectValue::new(ProjectBuildArgs::List(args), value)), malformed)
7934 }
7935 _ => {
7936 self.expected(value, "build args must be a mapping or sequence");
7937 (None, true)
7938 }
7939 }
7940 }
7941
7942 fn build_additional_contexts(
7943 &mut self,
7944 value: &MergedValue,
7945 ) -> (Option<ProjectValue<ProjectBuildAdditionalContexts>>, bool) {
7946 match value.kind() {
7947 MergedValueKind::Mapping(entries) => {
7948 let mut contexts = Vec::new();
7949 let mut malformed = false;
7950 let mut names = BTreeSet::new();
7951 for entry in entries {
7952 if !names.insert(entry.key().to_owned()) {
7953 self.invalid(
7954 entry_span(entry),
7955 "build additional context mapping names must be unique",
7956 );
7957 malformed = true;
7958 continue;
7959 }
7960 let Some(value) = self.compose_scalar(
7961 entry.value(),
7962 "build additional context mapping values must be scalars or null",
7963 ) else {
7964 malformed = true;
7965 continue;
7966 };
7967 contexts.push(ProjectBuildAdditionalContextEntry {
7968 name: ProjectKey::from_entry(entry),
7969 value: ProjectValue::new(value, entry.value()),
7970 });
7971 }
7972 (
7973 Some(ProjectValue::new(ProjectBuildAdditionalContexts::Map(contexts), value)),
7974 malformed,
7975 )
7976 }
7977 MergedValueKind::Sequence(values) => {
7978 let mut contexts = Vec::new();
7979 let mut malformed = false;
7980 for item in values {
7981 let MergedValueKind::Scalar(context) = item.kind() else {
7982 self.expected(item, "build additional context list items must be string scalars");
7983 malformed = true;
7984 continue;
7985 };
7986 if context.kind() != MergedScalarKind::String {
7987 self.expected(item, "build additional context list items must be string scalars");
7988 malformed = true;
7989 continue;
7990 }
7991 contexts.push(ProjectValue::new(context.value().to_owned(), item));
7992 }
7993 (
7994 Some(ProjectValue::new(ProjectBuildAdditionalContexts::List(contexts), value)),
7995 malformed,
7996 )
7997 }
7998 _ => {
7999 self.expected(value, "build additional_contexts must be a mapping or sequence");
8000 (None, true)
8001 }
8002 }
8003 }
8004
8005 fn build_extra_hosts(
8006 &mut self,
8007 value: &MergedValue,
8008 path: &[String],
8009 ) -> (Option<ProjectValue<ProjectBuildExtraHosts>>, bool) {
8010 match value.kind() {
8011 MergedValueKind::Sequence(values) => {
8012 let mut entries = Vec::new();
8013 let mut malformed = false;
8014 for item in values {
8015 let MergedValueKind::Scalar(scalar) = item.kind() else {
8016 self.expected(item, "build extra_hosts list entries must be string scalars");
8017 malformed = true;
8018 continue;
8019 };
8020 if scalar.kind() != MergedScalarKind::String {
8021 self.expected(item, "build extra_hosts list entries must be string scalars");
8022 malformed = true;
8023 continue;
8024 }
8025 entries.push(ProjectValue::new(scalar.value().to_owned(), item));
8026 }
8027 (
8028 Some(ProjectValue::new(ProjectBuildExtraHosts::List(entries), value)),
8029 malformed,
8030 )
8031 }
8032 MergedValueKind::Mapping(entries) => {
8033 let mut hosts = Vec::new();
8034 let mut malformed = false;
8035 for entry in entries {
8036 let mut entry_path = path.to_vec();
8037 entry_path.push("extra_hosts".to_owned());
8038 entry_path.push(entry.key().to_owned());
8039 let (addresses, invalid_addresses) = self.build_extra_host_addresses(entry.value(), &entry_path);
8040 malformed |= invalid_addresses;
8041 let Some(addresses) = addresses else {
8042 continue;
8043 };
8044 hosts.push(ProjectBuildExtraHostEntry {
8045 hostname: ProjectKey::from_entry(entry),
8046 addresses,
8047 });
8048 }
8049 (
8050 Some(ProjectValue::new(ProjectBuildExtraHosts::Map(hosts), value)),
8051 malformed,
8052 )
8053 }
8054 _ => {
8055 self.expected(value, "build extra_hosts must be a mapping or sequence");
8056 (None, true)
8057 }
8058 }
8059 }
8060
8061 fn build_extra_host_addresses(
8062 &mut self,
8063 value: &MergedValue,
8064 path: &[String],
8065 ) -> (Option<ProjectBuildExtraHostAddresses>, bool) {
8066 match value.kind() {
8067 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => (
8068 Some(ProjectBuildExtraHostAddresses::Scalar(ProjectValue::new(
8069 scalar.value().to_owned(),
8070 value,
8071 ))),
8072 false,
8073 ),
8074 MergedValueKind::Sequence(values) => {
8075 let mut addresses = Vec::new();
8076 let mut malformed = false;
8077 for (index, item) in values.iter().enumerate() {
8078 let MergedValueKind::Scalar(scalar) = item.kind() else {
8079 self.expected(item, "build extra_hosts address-list entries must be string scalars");
8080 let mut item_path = path.to_vec();
8081 item_path.push(index.to_string());
8082 self.pending_unmodeled.push(value_reference(&item_path, item));
8083 malformed = true;
8084 continue;
8085 };
8086 if scalar.kind() != MergedScalarKind::String {
8087 self.expected(item, "build extra_hosts address-list entries must be string scalars");
8088 let mut item_path = path.to_vec();
8089 item_path.push(index.to_string());
8090 self.pending_unmodeled.push(value_reference(&item_path, item));
8091 malformed = true;
8092 continue;
8093 }
8094 addresses.push(ProjectValue::new(scalar.value().to_owned(), item));
8095 }
8096 (Some(ProjectBuildExtraHostAddresses::List(addresses)), malformed)
8097 }
8098 _ => {
8099 self.expected(
8100 value,
8101 "build extra_hosts mapping addresses must be string scalars or sequences of string scalars",
8102 );
8103 self.pending_unmodeled.push(value_reference(path, value));
8104 (None, true)
8105 }
8106 }
8107 }
8108
8109 fn build_ssh(&mut self, value: &MergedValue) -> (Option<ProjectValue<ProjectBuildSsh>>, bool) {
8110 match value.kind() {
8111 MergedValueKind::Mapping(entries) => {
8112 let mut ssh = Vec::new();
8113 let mut malformed = false;
8114 for entry in entries {
8115 let Some(value) =
8116 self.compose_scalar(entry.value(), "build ssh mapping values must be scalars or null")
8117 else {
8118 malformed = true;
8119 continue;
8120 };
8121 ssh.push(ProjectBuildSshEntry {
8122 name: ProjectKey::from_sensitive_entry(entry),
8123 value: ProjectValue::new_sensitive(value, entry.value()),
8124 });
8125 }
8126 (
8127 Some(ProjectValue::new_sensitive(ProjectBuildSsh::Map(ssh), value)),
8128 malformed,
8129 )
8130 }
8131 MergedValueKind::Sequence(values) => {
8132 let mut ssh = Vec::new();
8133 let mut malformed = false;
8134 for item in values {
8135 let MergedValueKind::Scalar(scalar) = item.kind() else {
8136 self.expected(item, "build ssh list entries must be string scalars");
8137 malformed = true;
8138 continue;
8139 };
8140 if scalar.kind() != MergedScalarKind::String {
8141 self.expected(item, "build ssh list entries must be string scalars");
8142 malformed = true;
8143 continue;
8144 }
8145 ssh.push(ProjectValue::new_sensitive(scalar.value().to_owned(), item));
8146 }
8147 (
8148 Some(ProjectValue::new_sensitive(ProjectBuildSsh::List(ssh), value)),
8149 malformed,
8150 )
8151 }
8152 _ => {
8153 self.expected(value, "build ssh must be a mapping or sequence");
8154 (None, true)
8155 }
8156 }
8157 }
8158
8159 fn build_labels(&mut self, value: &MergedValue) -> (Option<ProjectValue<ProjectBuildLabels>>, bool) {
8160 match value.kind() {
8161 MergedValueKind::Mapping(entries) => {
8162 let mut labels = Vec::new();
8163 let mut malformed = false;
8164 for entry in entries {
8165 let Some(value) = self.compose_scalar(entry.value(), "build label value must be a scalar or null")
8166 else {
8167 malformed = true;
8168 continue;
8169 };
8170 labels.push(ProjectLabelEntry {
8171 name: ProjectKey::from_entry(entry),
8172 value: ProjectValue::new(value, entry.value()),
8173 syntax: entry.syntax(),
8174 });
8175 }
8176 (
8177 Some(ProjectValue::new(ProjectBuildLabels::Map(labels), value)),
8178 malformed,
8179 )
8180 }
8181 MergedValueKind::Sequence(values) => {
8182 let mut labels = Vec::new();
8183 let mut malformed = false;
8184 for item in values {
8185 let Some(label) = self.located_string(item, "build label list item must be a scalar") else {
8186 malformed = true;
8187 continue;
8188 };
8189 labels.push(ProjectValue::new(label.value().to_owned(), item));
8190 }
8191 (
8192 Some(ProjectValue::new(ProjectBuildLabels::List(labels), value)),
8193 malformed,
8194 )
8195 }
8196 _ => {
8197 self.expected(value, "build labels must be a mapping or sequence");
8198 (None, true)
8199 }
8200 }
8201 }
8202
8203 fn cpu_count(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuCount>> {
8204 let MergedValueKind::Scalar(scalar) = value.kind() else {
8205 self.expected(value, "cpu_count must be a YAML integer or string scalar");
8206 return None;
8207 };
8208 let count = match scalar.kind() {
8209 MergedScalarKind::Number if CpuCount::yaml_integer_spelling(scalar.raw()) => {
8210 CpuCount::yaml_integer(scalar.raw().to_owned())
8211 }
8212 MergedScalarKind::String
8213 if scalar.is_strict_yaml_string()
8214 && scalar.is_plain_style()
8215 && CpuCount::yaml_integer_spelling(scalar.raw()) =>
8216 {
8217 CpuCount::yaml_integer(scalar.raw().to_owned())
8218 }
8219 MergedScalarKind::String if scalar.is_strict_yaml_string() => CpuCount::String(scalar.value().to_owned()),
8220 MergedScalarKind::String | MergedScalarKind::Number | MergedScalarKind::Boolean => {
8221 self.expected(value, "cpu_count must be a YAML integer or string scalar");
8222 return None;
8223 }
8224 };
8225 if !count.is_valid() {
8226 self.diagnostics.push(
8227 Diagnostic::new(
8228 CPU_COUNT_NEGATIVE,
8229 Severity::Error,
8230 "cpu_count YAML integers must be nonnegative",
8231 )
8232 .with_label(DiagnosticLabel::primary(
8233 effective_span(value),
8234 "negative CPU count retained as invalid evidence",
8235 )),
8236 );
8237 }
8238 Some(ProjectValue::new(count, value))
8239 }
8240
8241 fn cpu_percent(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuPercent>> {
8242 let MergedValueKind::Scalar(scalar) = value.kind() else {
8243 self.expected(value, "cpu_percent must be a YAML integer or string scalar");
8244 return None;
8245 };
8246 let percent = match scalar.kind() {
8247 MergedScalarKind::Number if CpuPercent::yaml_integer_spelling(scalar.raw()) => {
8248 CpuPercent::yaml_integer(scalar.raw().to_owned())
8249 }
8250 MergedScalarKind::String
8251 if scalar.is_strict_yaml_string()
8252 && scalar.is_plain_style()
8253 && CpuPercent::yaml_integer_spelling(scalar.raw()) =>
8254 {
8255 CpuPercent::yaml_integer(scalar.raw().to_owned())
8256 }
8257 MergedScalarKind::String if scalar.is_strict_yaml_string() => CpuPercent::String(scalar.value().to_owned()),
8258 MergedScalarKind::String | MergedScalarKind::Number | MergedScalarKind::Boolean => {
8259 self.expected(value, "cpu_percent must be a YAML integer or string scalar");
8260 return None;
8261 }
8262 };
8263 if !percent.is_valid() {
8264 self.diagnostics.push(
8265 Diagnostic::new(
8266 CPU_PERCENT_OUT_OF_RANGE,
8267 Severity::Error,
8268 "cpu_percent YAML integers must be between 0 and 100 inclusive",
8269 )
8270 .with_label(DiagnosticLabel::primary(
8271 effective_span(value),
8272 "out-of-range CPU percentage retained as invalid evidence",
8273 )),
8274 );
8275 }
8276 Some(ProjectValue::new(percent, value))
8277 }
8278
8279 fn cpu_period(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuPeriod>> {
8280 let MergedValueKind::Scalar(scalar) = value.kind() else {
8281 self.expected(value, "cpu_period must be a YAML number or string scalar");
8282 return None;
8283 };
8284 let period = match scalar.kind() {
8285 MergedScalarKind::Number => CpuPeriod::YamlNumber(scalar.raw().to_owned()),
8286 MergedScalarKind::String
8287 if scalar.is_strict_yaml_string()
8288 && scalar.is_plain_style()
8289 && CpuPeriod::yaml_number_spelling(scalar.raw()) =>
8290 {
8291 CpuPeriod::YamlNumber(scalar.raw().to_owned())
8292 }
8293 MergedScalarKind::String if scalar.is_strict_yaml_string() => CpuPeriod::String(scalar.value().to_owned()),
8294 MergedScalarKind::String | MergedScalarKind::Boolean => {
8295 self.expected(value, "cpu_period must be a YAML number or string scalar");
8296 return None;
8297 }
8298 };
8299 Some(ProjectValue::new(period, value))
8300 }
8301
8302 fn cpu_quota(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuQuota>> {
8303 let MergedValueKind::Scalar(scalar) = value.kind() else {
8304 self.expected(value, "cpu_quota must be a YAML number or string scalar");
8305 return None;
8306 };
8307 let quota = match scalar.kind() {
8308 MergedScalarKind::Number => CpuQuota::YamlNumber(scalar.raw().to_owned()),
8309 MergedScalarKind::String
8310 if scalar.is_strict_yaml_string()
8311 && scalar.is_plain_style()
8312 && CpuPeriod::yaml_number_spelling(scalar.raw()) =>
8313 {
8314 CpuQuota::YamlNumber(scalar.raw().to_owned())
8315 }
8316 MergedScalarKind::String if scalar.is_strict_yaml_string() => CpuQuota::String(scalar.value().to_owned()),
8317 MergedScalarKind::String | MergedScalarKind::Boolean => {
8318 self.expected(value, "cpu_quota must be a YAML number or string scalar");
8319 return None;
8320 }
8321 };
8322 Some(ProjectValue::new(quota, value))
8323 }
8324
8325 fn cpu_rt_period(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuRtPeriod>> {
8326 let MergedValueKind::Scalar(scalar) = value.kind() else {
8327 self.expected(value, "cpu_rt_period must be a YAML number or string scalar");
8328 return None;
8329 };
8330 let period = match scalar.kind() {
8331 MergedScalarKind::Number => CpuRtPeriod::YamlNumber(scalar.raw().to_owned()),
8332 MergedScalarKind::String
8333 if scalar.is_strict_yaml_string()
8334 && scalar.is_plain_style()
8335 && CpuPeriod::yaml_number_spelling(scalar.raw()) =>
8336 {
8337 CpuRtPeriod::YamlNumber(scalar.raw().to_owned())
8338 }
8339 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
8340 CpuRtPeriod::parse_string(scalar.value().to_owned())
8341 }
8342 MergedScalarKind::String | MergedScalarKind::Boolean => {
8343 self.expected(value, "cpu_rt_period must be a YAML number or string scalar");
8344 return None;
8345 }
8346 };
8347 if !period.is_valid() {
8348 self.invalid(
8349 effective_span(value),
8350 "cpu_rt_period must match the ComposeLens duration policy using `us`, `ms`, `s`, `m`, or `h`, or contain an interpolation marker",
8351 );
8352 }
8353 Some(ProjectValue::new(period, value))
8354 }
8355
8356 fn cpu_rt_runtime(&mut self, value: &MergedValue) -> Option<ProjectValue<CpuRtRuntime>> {
8357 let MergedValueKind::Scalar(scalar) = value.kind() else {
8358 self.expected(
8359 value,
8360 "cpu_rt_runtime must be an integer microsecond or duration string scalar",
8361 );
8362 return None;
8363 };
8364 let runtime = match scalar.kind() {
8365 MergedScalarKind::Number if scalar.raw().bytes().all(|byte| byte.is_ascii_digit()) => {
8366 CpuRtRuntime::Microseconds(scalar.raw().to_owned())
8367 }
8368 MergedScalarKind::Number => CpuRtRuntime::Other(scalar.raw().to_owned()),
8369 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
8370 CpuRtRuntime::parse_string(scalar.value().to_owned())
8371 }
8372 MergedScalarKind::String | MergedScalarKind::Boolean => {
8373 self.expected(
8374 value,
8375 "cpu_rt_runtime must be an integer microsecond or duration string scalar",
8376 );
8377 return None;
8378 }
8379 };
8380 if !runtime.is_valid() {
8381 self.diagnostics.push(
8382 Diagnostic::new(
8383 CPU_RT_RUNTIME_INVALID,
8384 Severity::Error,
8385 "cpu_rt_runtime must be an integer microsecond value, Compose duration, or interpolation expression",
8386 )
8387 .with_label(DiagnosticLabel::primary(
8388 effective_span(value),
8389 "invalid service real-time CPU runtime",
8390 )),
8391 );
8392 }
8393 Some(ProjectValue::new(runtime, value))
8394 }
8395
8396 fn service_integer(
8397 &mut self,
8398 value: &MergedValue,
8399 min: i128,
8400 max: i128,
8401 description: &str,
8402 ) -> Option<ProjectValue<ServiceInteger>> {
8403 let MergedValueKind::Scalar(scalar) = value.kind() else {
8404 self.expected(value, &format!("{description} must be an integer scalar"));
8405 return None;
8406 };
8407 if matches!(scalar.kind(), MergedScalarKind::Boolean) {
8408 self.expected(value, &format!("{description} must be an integer scalar"));
8409 return None;
8410 }
8411 let parsed = ServiceInteger::parse(scalar.value().to_owned(), min, max);
8412 if !parsed.is_valid() {
8413 let _ = description;
8414 self.invalid(effective_span(value), "service integer must be in its documented range");
8415 }
8416 Some(ProjectValue::new(parsed, value))
8417 }
8418
8419 fn cpus(&mut self, value: &MergedValue) -> Option<ProjectValue<Cpus>> {
8420 let MergedValueKind::Scalar(scalar) = value.kind() else {
8421 self.expected(value, "cpus must be a decimal scalar");
8422 return None;
8423 };
8424 if matches!(scalar.kind(), MergedScalarKind::Boolean) {
8425 self.expected(value, "cpus must be a decimal scalar");
8426 return None;
8427 }
8428 let parsed = Cpus::parse(scalar.value().to_owned());
8429 if !parsed.is_valid() {
8430 self.invalid(
8431 effective_span(value),
8432 "cpus must be a decimal allocation or interpolation expression",
8433 );
8434 }
8435 Some(ProjectValue::new(parsed, value))
8436 }
8437
8438 fn volumes_from(
8439 &mut self,
8440 value: &MergedValue,
8441 ) -> (ProjectVolumesFromCollection, ProjectInvalidServiceStringItems) {
8442 let MergedValueKind::Sequence(items) = value.kind() else {
8443 self.expected(value, "volumes_from must be a sequence");
8444 return (None, Vec::new());
8445 };
8446 let mut result = Vec::new();
8447 let mut invalid = Vec::new();
8448 for item in items {
8449 let MergedValueKind::Scalar(scalar) = item.kind() else {
8450 self.expected(item, "volumes_from entries must be string scalars");
8451 invalid.push(ProjectInvalidServiceStringItem {
8452 span: effective_span(item),
8453 });
8454 continue;
8455 };
8456 if !matches!(scalar.kind(), MergedScalarKind::String) {
8457 self.expected(item, "volumes_from entries must be string scalars");
8458 invalid.push(ProjectInvalidServiceStringItem {
8459 span: effective_span(item),
8460 });
8461 continue;
8462 }
8463 let raw = Located::new(scalar.value().to_owned(), effective_span(item));
8464 result.push(ProjectValue::new(VolumesFrom::parse(raw), item));
8465 }
8466 (Some(ProjectValue::new(result, value)), invalid)
8467 }
8468
8469 fn pids_limit(&mut self, value: &MergedValue) -> Option<ProjectValue<PidsLimit>> {
8470 let scalar = match value.kind() {
8471 MergedValueKind::Scalar(scalar) if scalar.kind() != MergedScalarKind::Boolean => scalar,
8472 _ => {
8473 self.expected(value, "pids_limit must be a number or string scalar");
8474 return None;
8475 }
8476 };
8477 let limit = PidsLimit::parse(Located::new(scalar.value().to_owned(), effective_span(value)));
8478 match limit.kind() {
8479 PidsLimitKind::Zero => self.diagnostics.push(
8480 Diagnostic::new(
8481 PIDS_LIMIT_AMBIGUOUS_ZERO,
8482 Severity::Warning,
8483 "pids_limit zero is preserved as an ambiguous and unportable native state",
8484 )
8485 .with_label(DiagnosticLabel::primary(
8486 effective_span(value),
8487 "ambiguous zero PID limit",
8488 )),
8489 ),
8490 PidsLimitKind::Other => self.invalid(
8491 effective_span(value),
8492 "pids_limit must be `-1`, a positive integral decimal, or interpolation",
8493 ),
8494 _ => {}
8495 }
8496 Some(ProjectValue::new(limit, value))
8497 }
8498
8499 fn shm_size(&mut self, value: &MergedValue) -> Option<ProjectValue<ShmSize>> {
8500 let scalar = match value.kind() {
8501 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Number => {
8502 (scalar, ShmSizeScalarKind::Number)
8503 }
8504 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
8505 (scalar, ShmSizeScalarKind::String)
8506 }
8507 _ => {
8508 self.diagnostics.push(
8509 Diagnostic::new(
8510 SHM_SIZE_EXPECTED_VALUE,
8511 Severity::Error,
8512 "shm_size must be a YAML number or string scalar",
8513 )
8514 .with_label(DiagnosticLabel::primary(
8515 effective_span(value),
8516 "unexpected shared-memory-size form",
8517 )),
8518 );
8519 return None;
8520 }
8521 };
8522 let size = ShmSize::parse(
8523 Located::new(scalar.0.value().to_owned(), effective_span(value)),
8524 scalar.1,
8525 );
8526 let (code, message, label, note) = match size.kind() {
8527 ShmSizeKind::Zero { .. } => (
8528 SHM_SIZE_AMBIGUOUS_ZERO,
8529 "shm_size zero is preserved because Compose does not define its semantics",
8530 "ambiguous zero shared-memory size",
8531 "choose a positive size with an explicit documented lowercase unit",
8532 ),
8533 ShmSizeKind::ProviderDependentNumber => (
8534 SHM_SIZE_PROVIDER_DEPENDENT_NUMBER,
8535 "numeric shm_size is schema-accepted but lacks a documented explicit unit",
8536 "provider-dependent numeric shared-memory size",
8537 "use a positive quoted value with `b`, `k`, `kb`, `m`, `mb`, `g`, or `gb` for portable intent",
8538 ),
8539 ShmSizeKind::ProviderDependentString => (
8540 SHM_SIZE_PROVIDER_DEPENDENT_STRING,
8541 "string shm_size is schema-accepted but falls outside the documented lowercase suffix family",
8542 "provider-dependent string shared-memory size",
8543 "use an explicit lowercase `b`, `k`, `kb`, `m`, `mb`, `g`, or `gb` suffix when that is the intended unit",
8544 ),
8545 ShmSizeKind::Documented { .. } | ShmSizeKind::Expression => {
8546 return Some(ProjectValue::new(size, value));
8547 }
8548 };
8549 self.diagnostics.push(
8550 Diagnostic::new(code, Severity::Warning, message)
8551 .with_label(DiagnosticLabel::primary(effective_span(value), label))
8552 .with_note(note),
8553 );
8554 Some(ProjectValue::new(size, value))
8555 }
8556
8557 fn mem_limit(&mut self, value: &MergedValue) -> Option<ProjectValue<MemLimit>> {
8558 let scalar = match value.kind() {
8559 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Number => {
8560 (scalar, MemLimitScalarKind::Number)
8561 }
8562 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
8563 (scalar, MemLimitScalarKind::String)
8564 }
8565 _ => {
8566 self.diagnostics.push(
8567 Diagnostic::new(
8568 MEM_LIMIT_EXPECTED_VALUE,
8569 Severity::Error,
8570 "mem_limit must be a YAML number or string scalar",
8571 )
8572 .with_label(DiagnosticLabel::primary(
8573 effective_span(value),
8574 "unexpected memory-limit form",
8575 )),
8576 );
8577 return None;
8578 }
8579 };
8580 let limit = MemLimit::parse(
8581 Located::new(scalar.0.value().to_owned(), effective_span(value)),
8582 scalar.1,
8583 );
8584 let (code, message, label, note) = match limit.kind() {
8585 MemLimitKind::Zero { .. } => (
8586 MEM_LIMIT_AMBIGUOUS_ZERO,
8587 "mem_limit zero is preserved without inferring portable runtime behavior",
8588 "ambiguous zero memory limit",
8589 "choose a positive size with an explicit documented lowercase unit",
8590 ),
8591 MemLimitKind::SchemaNumber => (
8592 MEM_LIMIT_SCHEMA_NUMBER,
8593 "numeric mem_limit is schema-accepted but lacks a documented explicit unit",
8594 "schema-only numeric memory limit",
8595 "use a positive quoted value with `b`, `k`, `kb`, `m`, `mb`, `g`, or `gb` for explicit intent",
8596 ),
8597 MemLimitKind::ProviderDependentString => (
8598 MEM_LIMIT_PROVIDER_DEPENDENT_STRING,
8599 "string mem_limit is schema-accepted but falls outside the documented lowercase suffix family",
8600 "provider-dependent string memory limit",
8601 "use an explicit lowercase `b`, `k`, `kb`, `m`, `mb`, `g`, or `gb` suffix when that is the intended unit",
8602 ),
8603 MemLimitKind::Documented { .. } | MemLimitKind::Expression => {
8604 return Some(ProjectValue::new(limit, value));
8605 }
8606 };
8607 self.diagnostics.push(
8608 Diagnostic::new(code, Severity::Warning, message)
8609 .with_label(DiagnosticLabel::primary(effective_span(value), label))
8610 .with_note(note),
8611 );
8612 Some(ProjectValue::new(limit, value))
8613 }
8614
8615 fn memswap_limit(&mut self, value: &MergedValue) -> Option<ProjectValue<MemswapLimit>> {
8616 let scalar = match value.kind() {
8617 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Number => {
8618 (scalar, MemswapLimitScalarKind::Number)
8619 }
8620 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
8621 (scalar, MemswapLimitScalarKind::String)
8622 }
8623 _ => {
8624 self.diagnostics.push(
8625 Diagnostic::new(
8626 MEMSWAP_LIMIT_EXPECTED_VALUE,
8627 Severity::Error,
8628 "memswap_limit must be a YAML number or string scalar",
8629 )
8630 .with_label(DiagnosticLabel::primary(
8631 effective_span(value),
8632 "unexpected memory-plus-swap-limit form",
8633 )),
8634 );
8635 return None;
8636 }
8637 };
8638 let limit = MemswapLimit::parse(
8639 Located::new(scalar.0.value().to_owned(), effective_span(value)),
8640 scalar.1,
8641 );
8642 if matches!(limit.kind(), MemswapLimitKind::Other(_)) {
8643 self.diagnostics.push(
8644 Diagnostic::new(
8645 MEMSWAP_LIMIT_INVALID,
8646 Severity::Error,
8647 "memswap_limit must be `-1`, a decimal byte quantity, or an interpolation expression",
8648 )
8649 .with_label(DiagnosticLabel::primary(
8650 effective_span(value),
8651 "invalid memory-plus-swap limit retained",
8652 )),
8653 );
8654 }
8655 Some(ProjectValue::new(limit, value))
8656 }
8657
8658 fn dns(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectDns>> {
8659 let form = match value.kind() {
8660 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
8661 ProjectDns::Scalar(ProjectValue::new(scalar.value().to_owned(), value))
8662 }
8663 MergedValueKind::Sequence(values) => {
8664 let mut items = Vec::new();
8665 for item_value in values {
8666 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
8667 self.diagnostics.push(
8668 Diagnostic::new(
8669 DNS_EXPECTED_STRING,
8670 Severity::Error,
8671 "dns entries must be string scalars",
8672 )
8673 .with_label(DiagnosticLabel::primary(
8674 effective_span(item_value),
8675 "unexpected DNS server list item",
8676 )),
8677 );
8678 continue;
8679 };
8680 if scalar.kind() != MergedScalarKind::String {
8681 self.diagnostics.push(
8682 Diagnostic::new(
8683 DNS_EXPECTED_STRING,
8684 Severity::Error,
8685 "dns entries must be string scalars",
8686 )
8687 .with_label(DiagnosticLabel::primary(
8688 effective_span(item_value),
8689 "unexpected DNS server list item",
8690 )),
8691 );
8692 continue;
8693 }
8694 items.push(ProjectValue::new(scalar.value().to_owned(), item_value));
8695 }
8696 ProjectDns::List(items)
8697 }
8698 _ => {
8699 self.diagnostics.push(
8700 Diagnostic::new(
8701 DNS_EXPECTED_FORM,
8702 Severity::Error,
8703 "dns must be a string scalar or a sequence of string scalars",
8704 )
8705 .with_label(DiagnosticLabel::primary(
8706 effective_span(value),
8707 "unexpected service DNS form",
8708 )),
8709 );
8710 return None;
8711 }
8712 };
8713 Some(ProjectValue::new(form, value))
8714 }
8715
8716 fn dns_options(&mut self, value: &MergedValue) -> Option<ProjectValue<Vec<ProjectValue<String>>>> {
8717 let MergedValueKind::Sequence(values) = value.kind() else {
8718 self.diagnostics.push(
8719 Diagnostic::new(
8720 DNS_OPT_EXPECTED_SEQUENCE,
8721 Severity::Error,
8722 "dns_opt must be a sequence of string scalars",
8723 )
8724 .with_label(DiagnosticLabel::primary(
8725 effective_span(value),
8726 "unexpected service DNS option form",
8727 )),
8728 );
8729 return None;
8730 };
8731 let mut items = Vec::new();
8732 let mut seen = BTreeSet::new();
8733 for item_value in values {
8734 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
8735 self.diagnostics.push(
8736 Diagnostic::new(
8737 DNS_OPT_EXPECTED_STRING,
8738 Severity::Error,
8739 "dns_opt entries must be string scalars",
8740 )
8741 .with_label(DiagnosticLabel::primary(
8742 effective_span(item_value),
8743 "unexpected DNS option list item",
8744 )),
8745 );
8746 continue;
8747 };
8748 if scalar.kind() != MergedScalarKind::String {
8749 self.diagnostics.push(
8750 Diagnostic::new(
8751 DNS_OPT_EXPECTED_STRING,
8752 Severity::Error,
8753 "dns_opt entries must be string scalars",
8754 )
8755 .with_label(DiagnosticLabel::primary(
8756 effective_span(item_value),
8757 "unexpected DNS option list item",
8758 )),
8759 );
8760 continue;
8761 }
8762 if !seen.insert(scalar.value().to_owned()) {
8763 self.diagnostics.push(
8764 Diagnostic::new(
8765 DNS_OPT_DUPLICATE_ITEM,
8766 Severity::Warning,
8767 "dns_opt entries must be unique exact strings",
8768 )
8769 .with_label(DiagnosticLabel::primary(
8770 effective_span(item_value),
8771 "duplicate DNS option retained",
8772 )),
8773 );
8774 }
8775 items.push(ProjectValue::new(scalar.value().to_owned(), item_value));
8776 }
8777 Some(ProjectValue::new(items, value))
8778 }
8779
8780 fn dns_search(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectDnsSearch>> {
8781 let form = match value.kind() {
8782 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
8783 ProjectDnsSearch::Scalar(ProjectValue::new(scalar.value().to_owned(), value))
8784 }
8785 MergedValueKind::Sequence(values) => {
8786 let mut items = Vec::new();
8787 let mut seen = BTreeSet::new();
8788 for item_value in values {
8789 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
8790 self.diagnostics.push(
8791 Diagnostic::new(
8792 DNS_SEARCH_EXPECTED_STRING,
8793 Severity::Error,
8794 "dns_search entries must be string scalars",
8795 )
8796 .with_label(DiagnosticLabel::primary(
8797 effective_span(item_value),
8798 "unexpected DNS search-domain list item",
8799 )),
8800 );
8801 continue;
8802 };
8803 if scalar.kind() != MergedScalarKind::String {
8804 self.diagnostics.push(
8805 Diagnostic::new(
8806 DNS_SEARCH_EXPECTED_STRING,
8807 Severity::Error,
8808 "dns_search entries must be string scalars",
8809 )
8810 .with_label(DiagnosticLabel::primary(
8811 effective_span(item_value),
8812 "unexpected DNS search-domain list item",
8813 )),
8814 );
8815 continue;
8816 }
8817 if !seen.insert(scalar.value().to_owned()) {
8818 self.diagnostics.push(
8819 Diagnostic::new(
8820 DNS_SEARCH_DUPLICATE_ITEM,
8821 Severity::Warning,
8822 "dns_search schema entries are unique, but duplicate merge behavior is ambiguous",
8823 )
8824 .with_label(DiagnosticLabel::primary(
8825 effective_span(item_value),
8826 "duplicate DNS search domain retained",
8827 )),
8828 );
8829 }
8830 items.push(ProjectValue::new(scalar.value().to_owned(), item_value));
8831 }
8832 ProjectDnsSearch::List(items)
8833 }
8834 _ => {
8835 self.diagnostics.push(
8836 Diagnostic::new(
8837 DNS_SEARCH_EXPECTED_FORM,
8838 Severity::Error,
8839 "dns_search must be a string scalar or a sequence of string scalars",
8840 )
8841 .with_label(DiagnosticLabel::primary(
8842 effective_span(value),
8843 "unexpected service DNS search-domain form",
8844 )),
8845 );
8846 return None;
8847 }
8848 };
8849 Some(ProjectValue::new(form, value))
8850 }
8851
8852 fn expose(&mut self, value: &MergedValue) -> Option<ProjectValue<Vec<ProjectValue<ProjectExposeItem>>>> {
8853 let MergedValueKind::Sequence(values) = value.kind() else {
8854 self.diagnostics.push(
8855 Diagnostic::new(
8856 EXPOSE_EXPECTED_SEQUENCE,
8857 Severity::Error,
8858 "expose must be a sequence of string or number scalars",
8859 )
8860 .with_label(DiagnosticLabel::primary(
8861 effective_span(value),
8862 "unexpected service expose form",
8863 )),
8864 );
8865 return None;
8866 };
8867 let mut items = Vec::new();
8868 let mut seen = Vec::new();
8869 for item_value in values {
8870 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
8871 self.diagnostics.push(
8872 Diagnostic::new(
8873 EXPOSE_EXPECTED_SCALAR,
8874 Severity::Error,
8875 "expose entries must be string or number scalars",
8876 )
8877 .with_label(DiagnosticLabel::primary(
8878 effective_span(item_value),
8879 "unexpected exposed-port item",
8880 )),
8881 );
8882 continue;
8883 };
8884 let scalar_kind = match scalar.kind() {
8885 MergedScalarKind::String => ExposeScalarKind::String,
8886 MergedScalarKind::Number => ExposeScalarKind::Number,
8887 MergedScalarKind::Boolean => {
8888 self.diagnostics.push(
8889 Diagnostic::new(
8890 EXPOSE_EXPECTED_SCALAR,
8891 Severity::Error,
8892 "expose entries must be string or number scalars",
8893 )
8894 .with_label(DiagnosticLabel::primary(
8895 effective_span(item_value),
8896 "unexpected exposed-port item",
8897 )),
8898 );
8899 continue;
8900 }
8901 };
8902 if seen.contains(&(scalar_kind, scalar.value().to_owned())) {
8903 self.diagnostics.push(
8904 Diagnostic::new(
8905 EXPOSE_DUPLICATE_ITEM,
8906 Severity::Warning,
8907 "expose entries must be unique by exact scalar identity",
8908 )
8909 .with_label(DiagnosticLabel::primary(
8910 effective_span(item_value),
8911 "duplicate exposed-port item retained",
8912 )),
8913 );
8914 } else {
8915 seen.push((scalar_kind, scalar.value().to_owned()));
8916 }
8917 let kind = classify_expose_item(scalar.value(), scalar_kind);
8918 self.diagnose_expose_item(&kind, item_value);
8919 items.push(ProjectValue::new(
8920 ProjectExposeItem {
8921 authored: scalar.raw().to_owned(),
8922 value: scalar.value().to_owned(),
8923 scalar_kind,
8924 kind,
8925 },
8926 item_value,
8927 ));
8928 }
8929 Some(ProjectValue::new(items, value))
8930 }
8931
8932 fn diagnose_expose_item(&mut self, kind: &ExposeItemKind, value: &MergedValue) {
8933 let (code, severity, message, label) = match kind {
8934 ExposeItemKind::Documented { .. } | ExposeItemKind::Expression => return,
8935 ExposeItemKind::Sctp { .. } | ExposeItemKind::UnknownProtocol { .. } => (
8936 EXPOSE_PROVIDER_DEPENDENT,
8937 Severity::Warning,
8938 "expose protocol is outside the documented portable `tcp` and `udp` set",
8939 "provider-dependent exposed-port protocol retained",
8940 ),
8941 ExposeItemKind::Malformed => (
8942 EXPOSE_INVALID_ITEM,
8943 Severity::Error,
8944 "expose item must be a decimal port or range with an optional protocol",
8945 "malformed exposed-port item retained",
8946 ),
8947 };
8948 self.diagnostics.push(
8949 Diagnostic::new(code, severity, message).with_label(DiagnosticLabel::primary(effective_span(value), label)),
8950 );
8951 }
8952
8953 fn security_options(
8954 &mut self,
8955 value: &MergedValue,
8956 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectSecurityOptionItem>>>> {
8957 let MergedValueKind::Sequence(values) = value.kind() else {
8958 self.diagnostics.push(
8959 Diagnostic::new(
8960 SECURITY_OPT_EXPECTED_SEQUENCE,
8961 Severity::Error,
8962 "security_opt must be a sequence of string scalars",
8963 )
8964 .with_label(DiagnosticLabel::primary(
8965 effective_span(value),
8966 "unexpected service security-option form",
8967 )),
8968 );
8969 return None;
8970 };
8971 let mut items = Vec::new();
8972 let mut candidates = SecurityOptionCandidateCounts::default();
8973 for item_value in values {
8974 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
8975 self.diagnostics.push(
8976 Diagnostic::new(
8977 SECURITY_OPT_EXPECTED_STRING,
8978 Severity::Error,
8979 "security_opt entries must be string scalars",
8980 )
8981 .with_label(DiagnosticLabel::primary(
8982 effective_span(item_value),
8983 "unexpected security-option item retained in source evidence",
8984 )),
8985 );
8986 continue;
8987 };
8988 if scalar.kind() != MergedScalarKind::String {
8989 self.diagnostics.push(
8990 Diagnostic::new(
8991 SECURITY_OPT_EXPECTED_STRING,
8992 Severity::Error,
8993 "security_opt entries must be string scalars",
8994 )
8995 .with_label(DiagnosticLabel::primary(
8996 effective_span(item_value),
8997 "unexpected security-option scalar kind retained in source evidence",
8998 )),
8999 );
9000 continue;
9001 }
9002 let kind = classify_security_option(scalar.value());
9003 self.diagnose_security_option_item(&kind, effective_span(item_value), &mut candidates);
9004 items.push(ProjectValue::new(
9005 ProjectSecurityOptionItem {
9006 authored: scalar.raw().to_owned(),
9007 value: scalar.value().to_owned(),
9008 scalar_kind: scalar.kind(),
9009 kind,
9010 },
9011 item_value,
9012 ));
9013 }
9014 Some(ProjectValue::new(items, value))
9015 }
9016
9017 fn diagnose_security_option_item(
9018 &mut self,
9019 kind: &SecurityOptionKind,
9020 span: SourceSpan,
9021 candidates: &mut SecurityOptionCandidateCounts,
9022 ) {
9023 let diagnostic = match kind {
9024 SecurityOptionKind::AppArmor { .. } => {
9025 candidates.apparmor += 1;
9026 (candidates.apparmor > 1).then(|| {
9027 Diagnostic::new(
9028 SECURITY_OPT_APPARMOR_CONFLICT,
9029 Severity::Warning,
9030 "multiple AppArmor candidates are retained; a consumer must resolve the conflict explicitly",
9031 )
9032 .with_label(DiagnosticLabel::primary(
9033 span,
9034 "additional effective AppArmor candidate retained",
9035 ))
9036 })
9037 }
9038 SecurityOptionKind::AppArmorNearMiss => Some(
9039 Diagnostic::new(
9040 SECURITY_OPT_APPARMOR_NEAR_MISS,
9041 Severity::Warning,
9042 "AppArmor candidates require exact lowercase `apparmor=<profile>` spelling without whitespace",
9043 )
9044 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
9045 ),
9046 SecurityOptionKind::Seccomp { .. } => {
9047 candidates.seccomp += 1;
9048 (candidates.seccomp > 1).then(|| {
9049 Diagnostic::new(
9050 SECURITY_OPT_SECCOMP_CONFLICT,
9051 Severity::Warning,
9052 "multiple seccomp candidates are retained; a consumer must resolve the conflict explicitly",
9053 )
9054 .with_label(DiagnosticLabel::primary(
9055 span,
9056 "additional effective seccomp candidate retained",
9057 ))
9058 })
9059 }
9060 SecurityOptionKind::SeccompNearMiss => Some(
9061 Diagnostic::new(
9062 SECURITY_OPT_SECCOMP_NEAR_MISS,
9063 Severity::Warning,
9064 "seccomp candidates require exact lowercase `seccomp=<profile>` spelling without whitespace",
9065 )
9066 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
9067 ),
9068 SecurityOptionKind::NoNewPrivileges { .. } => {
9069 candidates.no_new_privileges += 1;
9070 (candidates.no_new_privileges > 1).then(|| {
9071 Diagnostic::new(
9072 SECURITY_OPT_NO_NEW_PRIVILEGES_CONFLICT,
9073 Severity::Warning,
9074 "multiple no-new-privileges candidates are retained; a consumer must resolve the conflict explicitly",
9075 )
9076 .with_label(DiagnosticLabel::primary(
9077 span,
9078 "additional effective no-new-privileges candidate retained",
9079 ))
9080 })
9081 }
9082 SecurityOptionKind::NoNewPrivilegesNearMiss => Some(
9083 Diagnostic::new(
9084 SECURITY_OPT_NO_NEW_PRIVILEGES_NEAR_MISS,
9085 Severity::Warning,
9086 "no-new-privileges candidates require exact lowercase `no-new-privileges:true` or `no-new-privileges:false` spelling without whitespace",
9087 )
9088 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
9089 ),
9090 SecurityOptionKind::Mask { .. }
9091 | SecurityOptionKind::MaskNearMiss
9092 | SecurityOptionKind::Unmask { .. }
9093 | SecurityOptionKind::UnmaskNearMiss => security_path_option_diagnostic(kind, span),
9094 SecurityOptionKind::SecurityLabelDisable { .. }
9095 | SecurityOptionKind::SecurityLabelDisableNearMiss
9096 | SecurityOptionKind::SecurityLabelFileType { .. }
9097 | SecurityOptionKind::SecurityLabelFileTypeNearMiss
9098 | SecurityOptionKind::SecurityLabelLevel { .. }
9099 | SecurityOptionKind::SecurityLabelLevelNearMiss
9100 | SecurityOptionKind::SecurityLabelNested { .. }
9101 | SecurityOptionKind::SecurityLabelNestedNearMiss
9102 | SecurityOptionKind::SecurityLabelType { .. }
9103 | SecurityOptionKind::SecurityLabelTypeNearMiss => {
9104 effective_security_label_diagnostic(kind, span, candidates)
9105 }
9106 SecurityOptionKind::Empty => Some(
9107 Diagnostic::new(
9108 SECURITY_OPT_EMPTY_ITEM,
9109 Severity::Error,
9110 "security_opt entries must not be empty strings",
9111 )
9112 .with_label(DiagnosticLabel::primary(span, "empty security option retained")),
9113 ),
9114 SecurityOptionKind::Expression | SecurityOptionKind::Other => None,
9115 };
9116 if let Some(diagnostic) = diagnostic {
9117 self.diagnostics.push(diagnostic);
9118 }
9119 }
9120
9121 fn tmpfs(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectTmpfs>> {
9122 let form = match value.kind() {
9123 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
9124 let item = self.tmpfs_item(value, scalar.value());
9125 ProjectTmpfs::Scalar(ProjectValue::new(item, value))
9126 }
9127 MergedValueKind::Sequence(values) => {
9128 let mut items = Vec::new();
9129 for item_value in values {
9130 let MergedValueKind::Scalar(scalar) = item_value.kind() else {
9131 self.diagnostics.push(
9132 Diagnostic::new(
9133 TMPFS_EXPECTED_STRING,
9134 Severity::Error,
9135 "tmpfs entries must be string scalars",
9136 )
9137 .with_label(DiagnosticLabel::primary(
9138 effective_span(item_value),
9139 "unexpected temporary-filesystem list item",
9140 )),
9141 );
9142 continue;
9143 };
9144 if scalar.kind() != MergedScalarKind::String {
9145 self.diagnostics.push(
9146 Diagnostic::new(
9147 TMPFS_EXPECTED_STRING,
9148 Severity::Error,
9149 "tmpfs entries must be string scalars",
9150 )
9151 .with_label(DiagnosticLabel::primary(
9152 effective_span(item_value),
9153 "unexpected temporary-filesystem list item",
9154 )),
9155 );
9156 continue;
9157 }
9158 let item = self.tmpfs_item(item_value, scalar.value());
9159 items.push(ProjectValue::new(item, item_value));
9160 }
9161 ProjectTmpfs::List(items)
9162 }
9163 _ => {
9164 self.diagnostics.push(
9165 Diagnostic::new(
9166 TMPFS_EXPECTED_FORM,
9167 Severity::Error,
9168 "tmpfs must be a string scalar or a sequence of string scalars",
9169 )
9170 .with_label(DiagnosticLabel::primary(
9171 effective_span(value),
9172 "unexpected service-level temporary-filesystem form",
9173 )),
9174 );
9175 return None;
9176 }
9177 };
9178 Some(ProjectValue::new(form, value))
9179 }
9180
9181 fn tmpfs_item(&mut self, source: &MergedValue, raw: &str) -> TmpfsItem {
9182 let item = TmpfsItem::parse(Located::new(raw.to_owned(), effective_span(source)));
9183 if item.kind() == TmpfsItemKind::ProviderDependent {
9184 self.diagnostics.push(
9185 Diagnostic::new(
9186 TMPFS_PROVIDER_DEPENDENT,
9187 Severity::Warning,
9188 "tmpfs item is malformed or uses provider- or target-specific options",
9189 )
9190 .with_label(DiagnosticLabel::primary(
9191 effective_span(source),
9192 "provider-dependent temporary-filesystem item",
9193 ))
9194 .with_note("use a non-empty path with only non-empty `mode`, `uid`, or `gid` assignments for documented portable syntax"),
9195 );
9196 }
9197 item
9198 }
9199
9200 fn sysctls(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectSysctls>> {
9201 let form = match value.kind() {
9202 MergedValueKind::Mapping(entries) => ProjectSysctls::Map(self.sysctls_map(entries)),
9203 MergedValueKind::Sequence(items) => ProjectSysctls::List(self.sysctls_list(items)),
9204 _ => {
9205 self.diagnostics.push(
9206 Diagnostic::new(
9207 SYSCTLS_EXPECTED_FORM,
9208 Severity::Error,
9209 "sysctls must be a mapping or a sequence of string scalars",
9210 )
9211 .with_label(DiagnosticLabel::primary(
9212 effective_span(value),
9213 "unexpected service sysctls form",
9214 )),
9215 );
9216 return None;
9217 }
9218 };
9219 Some(ProjectValue::new(form, value))
9220 }
9221
9222 fn credential_spec(
9223 &mut self,
9224 value: &MergedValue,
9225 service_path: &[String],
9226 ) -> Option<ProjectValue<ProjectCredentialSpec>> {
9227 let fields = self.mapping(value, "credential_spec must be a mapping")?;
9228 let mut path = service_path.to_vec();
9229 path.push("credential_spec".to_owned());
9230 let mut credential_spec = ProjectCredentialSpec {
9231 config: None,
9232 file: None,
9233 registry: None,
9234 unmodeled_fields: Vec::new(),
9235 };
9236 for field in fields {
9237 let value = match field.key() {
9238 "config" => self
9239 .strict_project_string(field.value(), "credential_spec config must be a YAML string scalar")
9240 .map(|value| credential_spec.config = Some(value)),
9241 "file" => self
9242 .strict_project_string(field.value(), "credential_spec file must be a YAML string scalar")
9243 .map(|value| credential_spec.file = Some(value)),
9244 "registry" => self
9245 .strict_project_string(field.value(), "credential_spec registry must be a YAML string scalar")
9246 .map(|value| credential_spec.registry = Some(value)),
9247 _ => {
9248 credential_spec.unmodeled_fields.push(field_reference(&path, field));
9249 Some(())
9250 }
9251 };
9252 if value.is_none() {
9253 credential_spec.unmodeled_fields.push(field_reference(&path, field));
9254 }
9255 }
9256 Some(ProjectValue::new(credential_spec, value))
9257 }
9258
9259 fn extends(&mut self, value: &MergedValue, service_path: &[String]) -> Option<ProjectValue<ProjectExtends>> {
9260 match value.kind() {
9261 MergedValueKind::Scalar(scalar)
9262 if scalar.kind() == MergedScalarKind::String && scalar.is_strict_yaml_string() =>
9263 {
9264 Some(ProjectValue::new(
9265 ProjectExtends::Short(ProjectValue::new(scalar.value().to_owned(), value)),
9266 value,
9267 ))
9268 }
9269 MergedValueKind::Mapping(fields) => {
9270 let mut path = service_path.to_vec();
9271 path.push("extends".to_owned());
9272 let mut reference = ProjectExtendsReference {
9273 service: None,
9274 file: None,
9275 unmodeled_fields: Vec::new(),
9276 };
9277 for field in fields {
9278 let typed = match field.key() {
9279 "service" => self
9280 .strict_project_string(field.value(), "extends service must be a YAML string scalar")
9281 .map(|value| reference.service = Some(value)),
9282 "file" => self
9283 .strict_project_string(field.value(), "extends file must be a YAML string scalar")
9284 .map(|value| reference.file = Some(value)),
9285 _ => {
9286 reference.unmodeled_fields.push(field_reference(&path, field));
9287 Some(())
9288 }
9289 };
9290 if typed.is_none() {
9291 reference.unmodeled_fields.push(field_reference(&path, field));
9292 }
9293 }
9294 if reference.service.is_none() {
9295 self.diagnostics.push(
9296 Diagnostic::new(
9297 EXTENDS_MISSING_SERVICE,
9298 Severity::Error,
9299 "long extends is missing required `service`",
9300 )
9301 .with_label(DiagnosticLabel::primary(
9302 effective_span(value),
9303 "long extends mapping retained without service",
9304 )),
9305 );
9306 }
9307 Some(ProjectValue::new(ProjectExtends::Long(reference), value))
9308 }
9309 _ => {
9310 self.expected(value, "extends must be a YAML string scalar or mapping");
9311 None
9312 }
9313 }
9314 }
9315
9316 fn provider(&mut self, value: &MergedValue, service_path: &[String]) -> Option<ProjectValue<ProjectProvider>> {
9317 let fields = self.mapping(value, "provider must be a mapping")?;
9318 let mut path = service_path.to_vec();
9319 path.push("provider".to_owned());
9320 let mut provider = ProjectProvider {
9321 type_: None,
9322 options: None,
9323 unmodeled_fields: Vec::new(),
9324 };
9325 for field in fields {
9326 let parsed = match field.key() {
9327 "type" => self
9328 .strict_project_string(field.value(), "provider type must be a YAML string scalar")
9329 .map(|value| provider.type_ = Some(value)),
9330 "options" => self
9331 .provider_options(field.value(), &path)
9332 .map(|value| provider.options = Some(value)),
9333 _ => {
9334 provider.unmodeled_fields.push(field_reference(&path, field));
9335 Some(())
9336 }
9337 };
9338 if parsed.is_none() {
9339 provider.unmodeled_fields.push(field_reference(&path, field));
9340 }
9341 }
9342 if provider.type_.is_none() {
9343 self.diagnostics.push(
9344 Diagnostic::new(
9345 PROVIDER_MISSING_TYPE,
9346 Severity::Error,
9347 "provider is missing required `type`",
9348 )
9349 .with_label(DiagnosticLabel::primary(
9350 effective_span(value),
9351 "provider mapping retained without type",
9352 )),
9353 );
9354 }
9355 Some(ProjectValue::new(provider, value))
9356 }
9357
9358 fn post_start(
9359 &mut self,
9360 value: &MergedValue,
9361 service_path: &[String],
9362 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectPostStartHook>>>> {
9363 self.lifecycle_hooks(
9364 value,
9365 service_path,
9366 "post_start",
9367 POST_START_MISSING_COMMAND,
9368 ProjectPostStartHook::Hook,
9369 || ProjectPostStartHook::Unmodeled,
9370 )
9371 }
9372
9373 fn pre_stop(
9374 &mut self,
9375 value: &MergedValue,
9376 service_path: &[String],
9377 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectPreStopHook>>>> {
9378 self.lifecycle_hooks(
9379 value,
9380 service_path,
9381 "pre_stop",
9382 PRE_STOP_MISSING_COMMAND,
9383 ProjectPreStopHook::Hook,
9384 || ProjectPreStopHook::Unmodeled,
9385 )
9386 }
9387
9388 fn pre_start(
9389 &mut self,
9390 value: &MergedValue,
9391 service_path: &[String],
9392 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectPreStartHook>>>> {
9393 let Some(items) = value.as_sequence() else {
9394 self.expected(value, "pre_start must be a sequence of hook mappings");
9395 return None;
9396 };
9397 let mut path = service_path.to_vec();
9398 path.push("pre_start".to_owned());
9399 let mut hooks = Vec::new();
9400 for item in items {
9401 let hook = if let Some(fields) = item.as_mapping() {
9402 ProjectPreStartHook::Hook(Box::new(self.pre_start_service_hook(fields, &path)))
9403 } else {
9404 self.expected(item, "pre_start items must be hook mappings");
9405 ProjectPreStartHook::Unmodeled
9406 };
9407 hooks.push(ProjectValue::new(hook, item));
9408 }
9409 Some(ProjectValue::new(hooks, value))
9410 }
9411
9412 fn lifecycle_hooks<T>(
9413 &mut self,
9414 value: &MergedValue,
9415 service_path: &[String],
9416 name: &str,
9417 missing_code: DiagnosticCode,
9418 hook_entry: fn(Box<ProjectServiceHook>) -> T,
9419 unmodeled_entry: fn() -> T,
9420 ) -> Option<ProjectValue<Vec<ProjectValue<T>>>> {
9421 let Some(items) = value.as_sequence() else {
9422 self.expected(value, &format!("{name} must be a sequence of hook mappings"));
9423 return None;
9424 };
9425 let mut path = service_path.to_vec();
9426 path.push(name.to_owned());
9427 let mut hooks = Vec::new();
9428 for item in items {
9429 let hook = if let Some(fields) = item.as_mapping() {
9430 hook_entry(Box::new(self.service_hook(item, fields, &path, name, missing_code)))
9431 } else {
9432 self.expected(item, &format!("{name} items must be hook mappings"));
9433 unmodeled_entry()
9434 };
9435 hooks.push(ProjectValue::new(hook, item));
9436 }
9437 Some(ProjectValue::new(hooks, value))
9438 }
9439
9440 fn service_hook(
9441 &mut self,
9442 value: &MergedValue,
9443 fields: &[MergedEntry],
9444 path: &[String],
9445 name: &str,
9446 missing_code: DiagnosticCode,
9447 ) -> ProjectServiceHook {
9448 let mut hook = ProjectServiceHook {
9449 command: None,
9450 environment: None,
9451 privileged: None,
9452 user: None,
9453 working_dir: None,
9454 unmodeled_fields: Vec::new(),
9455 };
9456 for field in fields {
9457 let parsed = match field.key() {
9458 "command" => self.command(field.value()).map(|value| hook.command = Some(value)),
9459 "environment" => self
9460 .environment(field.value())
9461 .map(|value| hook.environment = Some(value)),
9462 "privileged" => self
9463 .boolean(field.value(), &format!("{name} privileged must be a boolean"))
9464 .map(|value| hook.privileged = Some(value)),
9465 "user" => self
9466 .strict_project_string(field.value(), &format!("{name} user must be a YAML string scalar"))
9467 .map(|value| hook.user = Some(value)),
9468 "working_dir" => self
9469 .strict_project_string(
9470 field.value(),
9471 &format!("{name} working_dir must be a YAML string scalar"),
9472 )
9473 .map(|value| hook.working_dir = Some(value)),
9474 _ => {
9475 hook.unmodeled_fields.push(field_reference(path, field));
9476 Some(())
9477 }
9478 };
9479 if parsed.is_none() {
9480 hook.unmodeled_fields.push(field_reference(path, field));
9481 }
9482 }
9483 if hook.command.is_none() {
9484 self.diagnostics.push(
9485 Diagnostic::new(
9486 missing_code,
9487 Severity::Error,
9488 format!("{name} hook is missing required `command`"),
9489 )
9490 .with_label(DiagnosticLabel::primary(
9491 effective_span(value),
9492 format!("{name} hook retained without command"),
9493 )),
9494 );
9495 }
9496 hook
9497 }
9498
9499 fn pre_start_service_hook(&mut self, fields: &[MergedEntry], path: &[String]) -> ProjectPreStartServiceHook {
9500 let mut hook = ProjectPreStartServiceHook {
9501 command: None,
9502 image: None,
9503 environment: None,
9504 privileged: None,
9505 per_replica: None,
9506 user: None,
9507 working_dir: None,
9508 unmodeled_fields: Vec::new(),
9509 };
9510 for field in fields {
9511 let parsed = match field.key() {
9512 "command" => self.command(field.value()).map(|value| hook.command = Some(value)),
9513 "image" => self
9514 .strict_project_string(field.value(), "pre_start image must be a YAML string scalar")
9515 .map(|value| hook.image = Some(value)),
9516 "environment" => self
9517 .environment(field.value())
9518 .map(|value| hook.environment = Some(value)),
9519 "privileged" => self
9520 .boolean(field.value(), "pre_start privileged must be a boolean")
9521 .map(|value| hook.privileged = Some(value)),
9522 "per_replica" => self
9523 .boolean(field.value(), "pre_start per_replica must be a boolean")
9524 .map(|value| hook.per_replica = Some(value)),
9525 "user" => self
9526 .strict_project_string(field.value(), "pre_start user must be a YAML string scalar")
9527 .map(|value| hook.user = Some(value)),
9528 "working_dir" => self
9529 .strict_project_string(field.value(), "pre_start working_dir must be a YAML string scalar")
9530 .map(|value| hook.working_dir = Some(value)),
9531 _ => {
9532 hook.unmodeled_fields.push(field_reference(path, field));
9533 Some(())
9534 }
9535 };
9536 if parsed.is_none() {
9537 hook.unmodeled_fields.push(field_reference(path, field));
9538 }
9539 }
9540 hook
9541 }
9542
9543 fn provider_options(
9544 &mut self,
9545 value: &MergedValue,
9546 provider_path: &[String],
9547 ) -> Option<ProjectValue<ProjectProviderOptions>> {
9548 let Some(entries) = value.as_mapping() else {
9549 self.expected(value, "provider options must be a mapping");
9550 return None;
9551 };
9552 let mut path = provider_path.to_vec();
9553 path.push("options".to_owned());
9554 let mut options = Vec::new();
9555 let mut unmodeled_entries = Vec::new();
9556 for entry in entries {
9557 if entry.key().is_empty() {
9558 self.expected(entry.value(), "provider option keys must not be empty");
9559 unmodeled_entries.push(field_reference(&path, entry));
9560 continue;
9561 }
9562 let Some(value) = self.provider_option_value(entry.value()) else {
9563 unmodeled_entries.push(field_reference(&path, entry));
9564 continue;
9565 };
9566 let option = ProjectProviderOption {
9567 name: ProjectKey::from_entry(entry),
9568 value: ProjectValue::new(value, entry.value()),
9569 };
9570 options.push(ProjectValue::new(option, entry.value()));
9571 }
9572 Some(ProjectValue::new(
9573 ProjectProviderOptions {
9574 entries: options,
9575 unmodeled_entries,
9576 },
9577 value,
9578 ))
9579 }
9580
9581 fn provider_option_value(&mut self, value: &MergedValue) -> Option<ProjectProviderOptionValue> {
9582 if let Some(scalar) = Self::provider_option_scalar(value) {
9583 return Some(ProjectProviderOptionValue::Scalar(scalar));
9584 }
9585 let Some(items) = value.as_sequence() else {
9586 self.expected(
9587 value,
9588 "provider option values must be YAML string, number, or boolean scalars or sequences of them",
9589 );
9590 return None;
9591 };
9592 let mut parsed = Vec::new();
9593 for item in items {
9594 let value = if let Some(scalar) = Self::provider_option_scalar(item) {
9595 ProjectProviderOptionItem::Scalar(scalar)
9596 } else {
9597 self.expected(
9598 item,
9599 "provider option sequence items must be YAML string, number, or boolean scalars",
9600 );
9601 ProjectProviderOptionItem::Unmodeled
9602 };
9603 parsed.push(ProjectValue::new(value, item));
9604 }
9605 Some(ProjectProviderOptionValue::Sequence(parsed))
9606 }
9607
9608 fn provider_option_scalar(value: &MergedValue) -> Option<ComposeScalar> {
9609 let MergedValueKind::Scalar(scalar) = value.kind() else {
9610 return None;
9611 };
9612 match scalar.kind() {
9613 MergedScalarKind::String if scalar.is_strict_yaml_string() => {
9614 Some(ComposeScalar::String(scalar.value().to_owned()))
9615 }
9616 MergedScalarKind::Number => Some(ComposeScalar::Number(scalar.value().to_owned())),
9617 MergedScalarKind::Boolean => Some(ComposeScalar::Boolean(scalar.value().eq_ignore_ascii_case("true"))),
9618 MergedScalarKind::String => None,
9619 }
9620 }
9621
9622 fn logging(&mut self, value: &MergedValue, service_path: &[String]) -> Option<ProjectValue<ProjectLogging>> {
9623 let Some(fields) = value.as_mapping() else {
9624 self.diagnostics.push(
9625 Diagnostic::new(
9626 LOGGING_EXPECTED_MAPPING,
9627 Severity::Error,
9628 "logging must be a mapping with optional driver and options fields",
9629 )
9630 .with_label(DiagnosticLabel::primary(
9631 effective_span(value),
9632 "unexpected logging form",
9633 )),
9634 );
9635 return None;
9636 };
9637 let mut driver = None;
9638 let mut options = None;
9639 let mut unmodeled_fields = Vec::new();
9640 let mut path = service_path.to_vec();
9641 path.push("logging".to_owned());
9642 for field in fields {
9643 match field.key() {
9644 "driver" => {
9645 let Some(scalar) = field.value().as_scalar() else {
9646 self.logging_driver_finding(field.value());
9647 unmodeled_fields.push(field_reference(&path, field));
9648 continue;
9649 };
9650 if scalar.kind() != MergedScalarKind::String {
9651 self.logging_driver_finding(field.value());
9652 unmodeled_fields.push(field_reference(&path, field));
9653 continue;
9654 }
9655 driver = Some(ProjectValue::new(scalar.value().to_owned(), field.value()));
9656 }
9657 "options" => {
9658 let Some(entries) = field.value().as_mapping() else {
9659 self.diagnostics.push(
9660 Diagnostic::new(
9661 LOGGING_OPTIONS_EXPECTED_MAPPING,
9662 Severity::Error,
9663 "logging options must be a mapping",
9664 )
9665 .with_label(DiagnosticLabel::primary(
9666 effective_span(field.value()),
9667 "unexpected logging options form",
9668 )),
9669 );
9670 unmodeled_fields.push(field_reference(&path, field));
9671 continue;
9672 };
9673 options = Some(self.logging_options(field.value(), entries, &path));
9674 }
9675 _ => unmodeled_fields.push(field_reference(&path, field)),
9676 }
9677 }
9678 Some(ProjectValue::new(
9679 ProjectLogging {
9680 driver,
9681 options,
9682 unmodeled_fields,
9683 },
9684 value,
9685 ))
9686 }
9687
9688 fn logging_driver_finding(&mut self, value: &MergedValue) {
9689 self.diagnostics.push(
9690 Diagnostic::new(
9691 LOGGING_DRIVER_EXPECTED_STRING,
9692 Severity::Error,
9693 "logging driver must be a YAML string scalar",
9694 )
9695 .with_label(DiagnosticLabel::primary(
9696 effective_span(value),
9697 "non-string logging driver retained as unmodeled evidence",
9698 )),
9699 );
9700 }
9701
9702 fn logging_options(
9703 &mut self,
9704 source: &MergedValue,
9705 entries: &[MergedEntry],
9706 logging_path: &[String],
9707 ) -> ProjectValue<ProjectLoggingOptions> {
9708 let mut values = Vec::new();
9709 let mut unmodeled_entries = Vec::new();
9710 let mut path = logging_path.to_vec();
9711 path.push("options".to_owned());
9712 for entry in entries {
9713 if entry.key().is_empty() {
9714 self.diagnostics.push(
9715 Diagnostic::new(
9716 LOGGING_OPTION_EMPTY_KEY,
9717 Severity::Error,
9718 "logging option keys must not be empty",
9719 )
9720 .with_label(DiagnosticLabel::primary(entry_span(entry), "empty logging option key")),
9721 );
9722 unmodeled_entries.push(field_reference(&path, entry));
9723 continue;
9724 }
9725 let Some(value) = self.logging_option_value(entry.value()) else {
9726 unmodeled_entries.push(field_reference(&path, entry));
9727 continue;
9728 };
9729 let option = ProjectLoggingOption {
9730 name: ProjectKey::from_entry(entry),
9731 value: ProjectValue::new(value, entry.value()),
9732 };
9733 values.push(ProjectValue::new(option, entry.value()));
9734 }
9735 ProjectValue::new(
9736 ProjectLoggingOptions {
9737 entries: values,
9738 unmodeled_entries,
9739 },
9740 source,
9741 )
9742 }
9743
9744 fn logging_option_value(&mut self, value: &MergedValue) -> Option<ProjectLoggingOptionValue> {
9745 let parsed = match value.kind() {
9746 MergedValueKind::Null(_) => ProjectLoggingOptionValue::Null,
9747 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
9748 ProjectLoggingOptionValue::String {
9749 authored: scalar.raw().to_owned(),
9750 value: scalar.value().to_owned(),
9751 }
9752 }
9753 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::Number => {
9754 ProjectLoggingOptionValue::Number(scalar.value().to_owned())
9755 }
9756 _ => {
9757 self.diagnostics.push(
9758 Diagnostic::new(
9759 LOGGING_OPTION_EXPECTED_SCALAR,
9760 Severity::Error,
9761 "logging option values must be YAML string, number, or null scalars",
9762 )
9763 .with_label(DiagnosticLabel::primary(
9764 effective_span(value),
9765 "unsupported logging option value retained as unmodeled evidence",
9766 )),
9767 );
9768 return None;
9769 }
9770 };
9771 Some(parsed)
9772 }
9773
9774 fn sysctls_map(&mut self, entries: &[MergedEntry]) -> Vec<ProjectValue<ProjectSysctl>> {
9775 let mut sysctls = Vec::new();
9776 for entry in entries {
9777 if entry.key().is_empty() {
9778 self.diagnostics.push(
9779 Diagnostic::new(
9780 SYSCTLS_EMPTY_KEY,
9781 Severity::Error,
9782 "sysctls mapping keys must not be empty",
9783 )
9784 .with_label(DiagnosticLabel::primary(entry_span(entry), "empty sysctl name")),
9785 );
9786 continue;
9787 }
9788 let Some(scalar) = self.sysctl_scalar(entry.value()) else {
9789 continue;
9790 };
9791 let sysctl = ProjectSysctl {
9792 name: ProjectKey::from_entry(entry),
9793 value: ProjectValue::new(scalar, entry.value()),
9794 };
9795 sysctls.push(ProjectValue::new(sysctl, entry.value()));
9796 }
9797 sysctls
9798 }
9799
9800 fn sysctl_scalar(&mut self, value: &MergedValue) -> Option<ComposeScalar> {
9801 match value.kind() {
9802 MergedValueKind::Null(_) => Some(ComposeScalar::Null),
9803 MergedValueKind::Scalar(scalar) => Some(match scalar.kind() {
9804 MergedScalarKind::String => ComposeScalar::String(scalar.value().to_owned()),
9805 MergedScalarKind::Boolean => ComposeScalar::Boolean(scalar.value().eq_ignore_ascii_case("true")),
9806 MergedScalarKind::Number => ComposeScalar::Number(scalar.value().to_owned()),
9807 }),
9808 _ => {
9809 self.diagnostics.push(
9810 Diagnostic::new(
9811 SYSCTLS_EXPECTED_SCALAR,
9812 Severity::Error,
9813 "sysctls mapping values must be scalar strings, numbers, booleans, or null",
9814 )
9815 .with_label(DiagnosticLabel::primary(
9816 effective_span(value),
9817 "non-scalar sysctl value",
9818 )),
9819 );
9820 None
9821 }
9822 }
9823 }
9824
9825 fn sysctls_list(&mut self, items: &[MergedValue]) -> Vec<ProjectValue<String>> {
9826 let mut sysctls = Vec::new();
9827 let mut seen = BTreeMap::new();
9828 for item in items {
9829 let MergedValueKind::Scalar(scalar) = item.kind() else {
9830 self.invalid_sysctl_list_item(item);
9831 continue;
9832 };
9833 if scalar.kind() != MergedScalarKind::String {
9834 self.invalid_sysctl_list_item(item);
9835 continue;
9836 }
9837 let span = effective_span(item);
9838 if let Some(first) = seen.get(scalar.value()) {
9839 self.diagnostics.push(
9840 Diagnostic::new(
9841 SYSCTLS_DUPLICATE_ITEM,
9842 Severity::Error,
9843 "effective sysctls list entries must be unique exact strings",
9844 )
9845 .with_label(DiagnosticLabel::primary(span, "duplicate sysctl string"))
9846 .with_label(DiagnosticLabel::secondary(*first, "first identical string")),
9847 );
9848 } else {
9849 seen.insert(scalar.value().to_owned(), span);
9850 }
9851 sysctls.push(ProjectValue::new(scalar.value().to_owned(), item));
9852 }
9853 sysctls
9854 }
9855
9856 fn invalid_sysctl_list_item(&mut self, value: &MergedValue) {
9857 self.diagnostics.push(
9858 Diagnostic::new(
9859 SYSCTLS_EXPECTED_STRING,
9860 Severity::Error,
9861 "sysctls list entries must be YAML string scalars",
9862 )
9863 .with_label(DiagnosticLabel::primary(
9864 effective_span(value),
9865 "non-string sysctl list item",
9866 )),
9867 );
9868 }
9869
9870 fn ulimits(
9871 &mut self,
9872 value: &MergedValue,
9873 service_path: &[String],
9874 ) -> (Option<ProjectValue<ProjectUlimits>>, Vec<ProjectFieldReference>) {
9875 let Some(entries) = value.as_mapping() else {
9876 self.expected(value, "ulimits must be a mapping");
9877 return (None, Vec::new());
9878 };
9879 let mut limits = Vec::new();
9880 let mut unmodeled = Vec::new();
9881 let mut path = service_path.to_vec();
9882 path.push("ulimits".to_owned());
9883 for entry in entries {
9884 if !valid_ulimit_name(entry.key()) {
9885 self.diagnostics.push(
9886 Diagnostic::new(
9887 ULIMIT_INVALID_NAME,
9888 Severity::Error,
9889 "ulimit names must contain only lowercase ASCII letters",
9890 )
9891 .with_label(DiagnosticLabel::primary(entry_span(entry), "invalid ulimit name")),
9892 );
9893 unmodeled.push(field_reference(&path, entry));
9894 continue;
9895 }
9896 let Some(limit) = self.ulimit(entry, &path) else {
9897 unmodeled.push(field_reference(&path, entry));
9898 continue;
9899 };
9900 limits.push(ProjectValue::new(limit, entry.value()));
9901 }
9902 (
9903 Some(ProjectValue::new(ProjectUlimits { entries: limits }, value)),
9904 unmodeled,
9905 )
9906 }
9907
9908 fn ulimit(&mut self, entry: &MergedEntry, parent_path: &[String]) -> Option<ProjectUlimit> {
9909 let value = match entry.value().kind() {
9910 MergedValueKind::Scalar(_) => ProjectUlimitValue::Single(
9911 self.ulimit_scalar(entry.value())
9912 .map(|scalar| ProjectValue::new(scalar, entry.value()))?,
9913 ),
9914 MergedValueKind::Mapping(fields) => {
9915 let mut soft = None;
9916 let mut hard = None;
9917 let mut unmodeled_fields = Vec::new();
9918 let mut range_path = parent_path.to_vec();
9919 range_path.push(entry.key().to_owned());
9920 for field in fields {
9921 match field.key() {
9922 "soft" => {
9923 let parsed = self
9924 .ulimit_scalar(field.value())
9925 .map(|scalar| ProjectValue::new(scalar, field.value()));
9926 if parsed.is_none() {
9927 unmodeled_fields.push(field_reference(&range_path, field));
9928 }
9929 soft = parsed;
9930 }
9931 "hard" => {
9932 let parsed = self
9933 .ulimit_scalar(field.value())
9934 .map(|scalar| ProjectValue::new(scalar, field.value()));
9935 if parsed.is_none() {
9936 unmodeled_fields.push(field_reference(&range_path, field));
9937 }
9938 hard = parsed;
9939 }
9940 _ => unmodeled_fields.push(field_reference(&range_path, field)),
9941 }
9942 }
9943 if soft.is_none() {
9944 self.diagnostics.push(
9945 Diagnostic::new(
9946 ULIMIT_MISSING_RANGE_MEMBER,
9947 Severity::Error,
9948 "ulimit range is missing required `soft`",
9949 )
9950 .with_label(DiagnosticLabel::primary(
9951 effective_span(entry.value()),
9952 "missing soft limit",
9953 )),
9954 );
9955 }
9956 if hard.is_none() {
9957 self.diagnostics.push(
9958 Diagnostic::new(
9959 ULIMIT_MISSING_RANGE_MEMBER,
9960 Severity::Error,
9961 "ulimit range is missing required `hard`",
9962 )
9963 .with_label(DiagnosticLabel::primary(
9964 effective_span(entry.value()),
9965 "missing hard limit",
9966 )),
9967 );
9968 }
9969 ProjectUlimitValue::Range(ProjectUlimitRange {
9970 soft,
9971 hard,
9972 unmodeled_fields,
9973 })
9974 }
9975 _ => {
9976 self.expected(
9977 entry.value(),
9978 "ulimit must be a number/string scalar or a soft/hard mapping",
9979 );
9980 return None;
9981 }
9982 };
9983 Some(ProjectUlimit {
9984 name: ProjectKey::from_entry(entry),
9985 value,
9986 })
9987 }
9988
9989 fn ulimit_scalar(&mut self, value: &MergedValue) -> Option<ProjectUlimitScalar> {
9990 let Some(scalar) = value.as_scalar() else {
9991 self.expected(value, "ulimit values must be number or string scalars");
9992 return None;
9993 };
9994 if !matches!(scalar.kind(), MergedScalarKind::String | MergedScalarKind::Number) {
9995 self.diagnostics.push(
9996 Diagnostic::new(
9997 ULIMIT_INVALID_VALUE,
9998 Severity::Error,
9999 "ulimit values must be number or string scalars",
10000 )
10001 .with_label(DiagnosticLabel::primary(
10002 effective_span(value),
10003 "invalid ulimit scalar kind",
10004 )),
10005 );
10006 return None;
10007 }
10008 let parsed = LimitValue::parse(scalar.value().to_owned());
10009 if !parsed.is_valid() {
10010 self.diagnostics.push(
10011 Diagnostic::new(
10012 ULIMIT_INVALID_VALUE,
10013 Severity::Error,
10014 "ulimit must be -1, a non-negative integer, or an interpolation expression",
10015 )
10016 .with_label(DiagnosticLabel::primary(effective_span(value), "invalid ulimit value")),
10017 );
10018 }
10019 Some(ProjectUlimitScalar {
10020 authored: scalar.raw().to_owned(),
10021 value: parsed,
10022 kind: scalar.kind(),
10023 })
10024 }
10025
10026 fn pull_policy(&mut self, value: &MergedValue) -> Option<ProjectValue<PullPolicy>> {
10027 let policy = PullPolicy::parse(self.located_string(value, "pull_policy must be a non-null scalar")?);
10028 if !policy.is_recognized() {
10029 self.invalid(
10030 effective_span(value),
10031 "pull_policy must be a documented Compose policy, the retained `if_not_present` alias, schema-only `refresh`, an `every_` interval matching integer `w`, `d`, `h`, `m`, and `s` components, or interpolation",
10032 );
10033 }
10034 Some(ProjectValue::new(policy, value))
10035 }
10036
10037 fn stop_grace_period(&mut self, value: &MergedValue) -> Option<ProjectValue<StopGracePeriod>> {
10038 let scalar = self.scalar(value, "stop_grace_period must be a non-null scalar")?;
10039 let period = StopGracePeriod::parse(scalar.value().to_owned());
10040 if !period.is_valid() {
10041 self.invalid(
10042 effective_span(value),
10043 "stop_grace_period must match the ComposeLens duration policy using `us`, `ms`, `s`, `m`, or `h`, or contain an interpolation marker",
10044 );
10045 }
10046 Some(ProjectValue::new(period, value))
10047 }
10048
10049 fn command(&mut self, value: &MergedValue) -> Option<ProjectValue<Command>> {
10050 let span = effective_span(value);
10051 let command = match value.kind() {
10052 MergedValueKind::Null(_) => Command::Null(span),
10053 MergedValueKind::Scalar(scalar) => Command::String(Located::new(scalar.value().to_owned(), span)),
10054 MergedValueKind::Sequence(values) => {
10055 let mut arguments = Vec::new();
10056 for value in values {
10057 arguments.push(self.located_string(value, "command list item must be a scalar")?);
10058 }
10059 Command::List {
10060 span,
10061 values: arguments,
10062 }
10063 }
10064 _ => {
10065 self.expected(value, "command must be null, a scalar, or a sequence");
10066 return None;
10067 }
10068 };
10069 Some(ProjectValue::new(command, value))
10070 }
10071
10072 fn entrypoint(&mut self, value: &MergedValue) -> Option<ProjectValue<Entrypoint>> {
10073 let span = effective_span(value);
10074 let entrypoint = match value.kind() {
10075 MergedValueKind::Null(_) => Entrypoint::Null(span),
10076 MergedValueKind::Scalar(scalar) => Entrypoint::String(Located::new(scalar.value().to_owned(), span)),
10077 MergedValueKind::Sequence(values) => {
10078 let mut arguments = Vec::new();
10079 for value in values {
10080 arguments.push(self.located_string(value, "entrypoint list item must be a scalar")?);
10081 }
10082 Entrypoint::List {
10083 span,
10084 values: arguments,
10085 }
10086 }
10087 _ => {
10088 self.expected(value, "entrypoint must be null, a scalar, or a sequence");
10089 return None;
10090 }
10091 };
10092 Some(ProjectValue::new(entrypoint, value))
10093 }
10094
10095 fn user(&mut self, value: &MergedValue) -> Option<ProjectValue<UserSpec>> {
10096 let raw = self.project_string(value, "service user")?;
10097 Some(ProjectValue {
10098 value: UserSpec::parse(Located::new(raw.value, effective_span(value))),
10099 provenance: raw.provenance,
10100 sensitive: raw.sensitive,
10101 })
10102 }
10103
10104 fn userns_mode(&mut self, value: &MergedValue) -> Option<ProjectValue<UserNamespaceMode>> {
10105 let raw = self.project_string(value, "service user namespace mode")?;
10106 Some(ProjectValue {
10107 value: UserNamespaceMode::parse(Located::new(raw.value, effective_span(value))),
10108 provenance: raw.provenance,
10109 sensitive: raw.sensitive,
10110 })
10111 }
10112
10113 fn environment(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectEnvironment>> {
10114 let mut entries = Vec::new();
10115 match value.kind() {
10116 MergedValueKind::Mapping(values) => {
10117 for entry in values {
10118 let scalar = self.compose_scalar(entry.value(), "environment value must be a scalar or null")?;
10119 entries.push(ProjectEnvironmentEntry {
10120 name: ProjectKey::from_entry(entry),
10121 value: ProjectValue::new(scalar, entry.value()),
10122 syntax: entry.syntax(),
10123 });
10124 }
10125 }
10126 MergedValueKind::Sequence(values) => {
10127 for item in values {
10128 let raw = self.located_string(item, "environment list item must be a scalar")?;
10129 let (name, scalar, syntax) = raw.value().split_once('=').map_or_else(
10130 || (raw.value().clone(), ComposeScalar::Null, EntrySyntax::ListKeyOnly),
10131 |(name, value)| {
10132 (
10133 name.to_owned(),
10134 ComposeScalar::String(value.to_owned()),
10135 EntrySyntax::ListKeyValue,
10136 )
10137 },
10138 );
10139 entries.push(ProjectEnvironmentEntry {
10140 name: ProjectKey {
10141 value: name,
10142 sources: item.provenance().sources().to_vec(),
10143 sensitive: item.is_sensitive(),
10144 },
10145 value: ProjectValue::new(scalar, item),
10146 syntax,
10147 });
10148 }
10149 }
10150 _ => {
10151 self.expected(value, "environment must be a mapping or sequence");
10152 return None;
10153 }
10154 }
10155 Some(ProjectValue::new(ProjectEnvironment { entries }, value))
10156 }
10157
10158 fn environment_files(
10159 &mut self,
10160 value: &MergedValue,
10161 service_path: &[String],
10162 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectEnvironmentFile>>>> {
10163 let values = match value.kind() {
10164 MergedValueKind::Scalar(_) => std::slice::from_ref(value),
10165 MergedValueKind::Sequence(values) => values,
10166 _ => {
10167 self.expected(
10168 value,
10169 "env_file must be a scalar path or sequence of short/long entries",
10170 );
10171 return None;
10172 }
10173 };
10174 let mut environment_files = Vec::new();
10175 for (index, item) in values.iter().enumerate() {
10176 let mut path = service_path.to_vec();
10177 path.push("env_file".to_owned());
10178 path.push(index.to_string());
10179 let environment_file = match item.kind() {
10180 MergedValueKind::Scalar(scalar) => ProjectEnvironmentFile::Short(scalar.value().to_owned()),
10181 MergedValueKind::Mapping(fields) => {
10182 ProjectEnvironmentFile::Long(Box::new(self.long_environment_file(item, fields, &path)))
10183 }
10184 _ => {
10185 self.expected(
10186 item,
10187 "env_file item must use scalar short syntax or mapping long syntax",
10188 );
10189 continue;
10190 }
10191 };
10192 environment_files.push(ProjectValue::new(environment_file, item));
10193 }
10194 Some(ProjectValue::new(environment_files, value))
10195 }
10196
10197 fn long_environment_file(
10198 &mut self,
10199 value: &MergedValue,
10200 fields: &[MergedEntry],
10201 path: &[String],
10202 ) -> ProjectLongEnvironmentFile {
10203 let mut environment_file = ProjectLongEnvironmentFile {
10204 path: None,
10205 required: None,
10206 format: None,
10207 unmodeled_fields: Vec::new(),
10208 };
10209 for field in fields {
10210 match field.key() {
10211 "path" => {
10212 environment_file.path = self.project_string(field.value(), "environment-file path");
10213 }
10214 "required" => {
10215 environment_file.required = self
10216 .located_boolean(field.value(), "environment-file required option must be a boolean")
10217 .map(|value| ProjectValue::new(value.into_value(), field.value()));
10218 }
10219 "format" => {
10220 environment_file.format = self.environment_file_format(field.value());
10221 }
10222 _ => environment_file.unmodeled_fields.push(field_reference(path, field)),
10223 }
10224 }
10225 if environment_file.path.is_none() {
10226 self.missing(value, "long-syntax environment file is missing `path`");
10227 }
10228 environment_file
10229 }
10230
10231 fn environment_file_format(&mut self, value: &MergedValue) -> Option<ProjectValue<EnvironmentFileFormat>> {
10232 let raw = self.project_string(value, "environment-file format")?;
10233 let format = EnvironmentFileFormat::parse(Located::new(raw.value, effective_span(value)));
10234 if matches!(format.kind(), EnvironmentFileFormatKind::Other) {
10235 self.invalid(
10236 effective_span(value),
10237 "environment-file format must be `raw` or interpolation",
10238 );
10239 }
10240 Some(ProjectValue {
10241 value: format,
10242 provenance: raw.provenance,
10243 sensitive: raw.sensitive,
10244 })
10245 }
10246
10247 fn service_labels(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectLabels>> {
10248 let mut entries = Vec::new();
10249 let form = match value.kind() {
10250 MergedValueKind::Mapping(values) => {
10251 for entry in values {
10252 let scalar = if entry.syntax() == EntrySyntax::ListKeyOnly {
10253 ComposeScalar::String(String::new())
10254 } else {
10255 self.compose_scalar(entry.value(), "label value must be a scalar or null")?
10256 };
10257 entries.push(ProjectLabelEntry {
10258 name: ProjectKey::from_entry(entry),
10259 value: ProjectValue::new(scalar, entry.value()),
10260 syntax: entry.syntax(),
10261 });
10262 }
10263 ProjectLabelsForm::Map
10264 }
10265 MergedValueKind::Sequence(values) => {
10266 for item in values {
10267 let raw = self.located_string(item, "label list item must be a scalar")?;
10268 let (name, value, syntax) = raw.value().split_once('=').map_or_else(
10269 || (raw.value().clone(), String::new(), EntrySyntax::ListKeyOnly),
10270 |(name, value)| (name.to_owned(), value.to_owned(), EntrySyntax::ListKeyValue),
10271 );
10272 entries.push(ProjectLabelEntry {
10273 name: ProjectKey::from_value(name, item),
10274 value: ProjectValue::new(ComposeScalar::String(value), item),
10275 syntax,
10276 });
10277 }
10278 ProjectLabelsForm::List
10279 }
10280 _ => {
10281 self.expected(value, "labels must be a mapping or sequence");
10282 return None;
10283 }
10284 };
10285 Some(ProjectValue::new(ProjectLabels { form, entries }, value))
10286 }
10287
10288 fn service_annotations(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectAnnotations>> {
10289 let entries = match value.kind() {
10290 MergedValueKind::Mapping(values) => self.annotation_mapping(values),
10291 MergedValueKind::Sequence(values) => self.annotation_sequence(values),
10292 _ => {
10293 self.expected(value, "annotations must be a mapping or sequence");
10294 return None;
10295 }
10296 };
10297 Some(ProjectValue::new(ProjectAnnotations { entries }, value))
10298 }
10299
10300 fn annotation_mapping(&mut self, values: &[MergedEntry]) -> Vec<ProjectAnnotationEntry> {
10301 let mut entries = Vec::new();
10302 for entry in values {
10303 if entry.key().is_empty() {
10304 self.annotation_finding(
10305 ANNOTATIONS_EMPTY_NAME,
10306 Severity::Error,
10307 entry.value(),
10308 "service annotation name must not be empty",
10309 "empty annotation name",
10310 );
10311 }
10312 if entry
10313 .raw_list_item()
10314 .is_some_and(|raw| raw.kind() != MergedScalarKind::String)
10315 {
10316 self.annotation_finding(
10317 ANNOTATIONS_EXPECTED_STRING,
10318 Severity::Error,
10319 entry.value(),
10320 "annotation list entries must be string scalars",
10321 "non-string annotation item retained",
10322 );
10323 }
10324 let raw_list_item = entry.raw_list_item().map(|raw| ProjectValue {
10325 value: ProjectAnnotationScalar {
10326 authored: raw.raw().to_owned(),
10327 effective: compose_scalar_from_merged(raw),
10328 },
10329 provenance: entry.value().provenance().clone(),
10330 sensitive: raw.is_sensitive(),
10331 });
10332 let value = self.annotation_mapping_value(entry);
10333 let candidate = ProjectAnnotationEntry {
10334 name: ProjectKey::from_entry(entry),
10335 value,
10336 raw_list_item,
10337 syntax: entry.syntax(),
10338 contributors: vec![entry.value().provenance().clone()],
10339 };
10340 self.upsert_annotation(&mut entries, candidate, entry.value());
10341 }
10342 entries
10343 }
10344
10345 fn annotation_mapping_value(&mut self, entry: &MergedEntry) -> Option<ProjectValue<ProjectAnnotationScalar>> {
10346 if entry.syntax() == EntrySyntax::ListKeyOnly {
10347 self.annotation_finding(
10348 ANNOTATIONS_KEY_ONLY,
10349 Severity::Warning,
10350 entry.value(),
10351 "key-only service annotation has no explicit value",
10352 "ambiguous key-only annotation",
10353 );
10354 return None;
10355 }
10356 self.annotation_scalar(entry.value()).map(|scalar| ProjectValue {
10357 value: scalar,
10358 provenance: entry.value().provenance().clone(),
10359 sensitive: entry.value().is_sensitive(),
10360 })
10361 }
10362
10363 fn annotation_sequence(&mut self, values: &[MergedValue]) -> Vec<ProjectAnnotationEntry> {
10364 let mut entries = Vec::new();
10365 for item in values {
10366 let Some(candidate) = self.annotation_list_item(item) else {
10367 continue;
10368 };
10369 self.upsert_annotation(&mut entries, candidate, item);
10370 }
10371 entries
10372 }
10373
10374 fn annotation_list_item(&mut self, item: &MergedValue) -> Option<ProjectAnnotationEntry> {
10375 let Some(scalar) = item.as_scalar() else {
10376 self.invalid_annotation_list_item(item);
10377 return None;
10378 };
10379 if scalar.kind() != MergedScalarKind::String {
10380 self.invalid_annotation_list_item(item);
10381 return None;
10382 }
10383 let raw_list_item = Some(ProjectValue {
10384 value: ProjectAnnotationScalar {
10385 authored: scalar.raw().to_owned(),
10386 effective: ComposeScalar::String(scalar.value().to_owned()),
10387 },
10388 provenance: item.provenance().clone(),
10389 sensitive: scalar.is_sensitive(),
10390 });
10391 let (name, value, syntax) = if let Some((name, value)) = scalar.value().split_once('=') {
10392 (
10393 name.to_owned(),
10394 Some(ProjectValue {
10395 value: ProjectAnnotationScalar {
10396 authored: scalar.raw().to_owned(),
10397 effective: ComposeScalar::String(value.to_owned()),
10398 },
10399 provenance: item.provenance().clone(),
10400 sensitive: scalar.is_sensitive(),
10401 }),
10402 EntrySyntax::ListKeyValue,
10403 )
10404 } else {
10405 self.annotation_finding(
10406 ANNOTATIONS_KEY_ONLY,
10407 Severity::Warning,
10408 item,
10409 "key-only service annotation has no explicit value",
10410 "ambiguous key-only annotation",
10411 );
10412 (scalar.value().to_owned(), None, EntrySyntax::ListKeyOnly)
10413 };
10414 if name.is_empty() {
10415 self.annotation_finding(
10416 ANNOTATIONS_EMPTY_NAME,
10417 Severity::Error,
10418 item,
10419 "service annotation name must not be empty",
10420 "empty annotation name",
10421 );
10422 }
10423 Some(ProjectAnnotationEntry {
10424 name: ProjectKey::from_value(name, item),
10425 value,
10426 raw_list_item,
10427 syntax,
10428 contributors: vec![item.provenance().clone()],
10429 })
10430 }
10431
10432 fn invalid_annotation_list_item(&mut self, item: &MergedValue) {
10433 self.annotation_finding(
10434 ANNOTATIONS_EXPECTED_STRING,
10435 Severity::Error,
10436 item,
10437 "annotation list entries must be string scalars",
10438 "non-string annotation item retained in merged source",
10439 );
10440 }
10441
10442 fn annotation_scalar(&mut self, value: &MergedValue) -> Option<ProjectAnnotationScalar> {
10443 let effective = self.compose_scalar(
10444 value,
10445 "annotation mapping values must be scalar strings, numbers, booleans, or null",
10446 )?;
10447 let authored = match value.kind() {
10448 MergedValueKind::Null(crate::merge::NullStyle::Empty) => String::new(),
10449 MergedValueKind::Null(crate::merge::NullStyle::Explicit) => "null".to_owned(),
10450 MergedValueKind::Scalar(scalar) => scalar.raw().to_owned(),
10451 _ => unreachable!("compose_scalar accepted only scalar or null"),
10452 };
10453 Some(ProjectAnnotationScalar { authored, effective })
10454 }
10455
10456 fn upsert_annotation(
10457 &mut self,
10458 entries: &mut Vec<ProjectAnnotationEntry>,
10459 mut candidate: ProjectAnnotationEntry,
10460 source: &MergedValue,
10461 ) {
10462 if let Some(existing) = entries
10463 .iter_mut()
10464 .find(|entry| entry.name.value == candidate.name.value)
10465 {
10466 self.annotation_finding(
10467 ANNOTATIONS_DUPLICATE_NAME,
10468 Severity::Error,
10469 source,
10470 "service annotation names must be unique",
10471 "later annotation value replaces earlier effective value",
10472 );
10473 for span in candidate.name.sources.drain(..) {
10474 if !existing.name.sources.contains(&span) {
10475 existing.name.sources.push(span);
10476 }
10477 }
10478 existing.name.sensitive |= candidate.name.sensitive;
10479 existing.contributors.append(&mut candidate.contributors);
10480 existing.value = candidate.value;
10481 existing.raw_list_item = candidate.raw_list_item;
10482 existing.syntax = candidate.syntax;
10483 } else {
10484 entries.push(candidate);
10485 }
10486 }
10487
10488 fn annotation_finding(
10489 &mut self,
10490 code: DiagnosticCode,
10491 severity: Severity,
10492 value: &MergedValue,
10493 message: &'static str,
10494 label: &'static str,
10495 ) {
10496 self.diagnostics.push(
10497 Diagnostic::new(code, severity, message).with_label(DiagnosticLabel::primary(effective_span(value), label)),
10498 );
10499 }
10500
10501 fn healthcheck(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<ProjectValue<ProjectHealthcheck>> {
10502 let fields = self.mapping(value, "healthcheck must be a mapping")?;
10503 let mut healthcheck = ProjectHealthcheck {
10504 test: None,
10505 interval: None,
10506 timeout: None,
10507 retries: None,
10508 start_period: None,
10509 start_interval: None,
10510 disable: None,
10511 unmodeled_fields: Vec::new(),
10512 };
10513 let mut path = parent_path.to_vec();
10514 path.push("healthcheck".to_owned());
10515 for field in fields {
10516 match field.key() {
10517 "test" => healthcheck.test = self.healthcheck_test(field.value()),
10518 "interval" => {
10519 healthcheck.interval =
10520 self.healthcheck_duration(field.value(), "healthcheck interval must be a scalar");
10521 }
10522 "timeout" => {
10523 healthcheck.timeout =
10524 self.healthcheck_duration(field.value(), "healthcheck timeout must be a scalar");
10525 }
10526 "retries" => healthcheck.retries = self.healthcheck_retries(field.value()),
10527 "start_period" => {
10528 healthcheck.start_period =
10529 self.healthcheck_duration(field.value(), "healthcheck start_period must be a scalar");
10530 }
10531 "start_interval" => {
10532 healthcheck.start_interval =
10533 self.healthcheck_duration(field.value(), "healthcheck start_interval must be a scalar");
10534 }
10535 "disable" => {
10536 healthcheck.disable = self
10537 .located_boolean(field.value(), "healthcheck disable must be a boolean")
10538 .map(|value| ProjectValue::new(value.into_value(), field.value()));
10539 }
10540 _ => healthcheck.unmodeled_fields.push(field_reference(&path, field)),
10541 }
10542 }
10543 Some(ProjectValue::new(healthcheck, value))
10544 }
10545
10546 fn depends_on(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<ProjectValue<ProjectDependsOn>> {
10547 let dependencies = match value.kind() {
10548 MergedValueKind::Sequence(values) => {
10549 let mut dependencies = Vec::new();
10550 for value in values {
10551 let Some(service) = self.project_string(value, "dependency service name") else {
10552 continue;
10553 };
10554 let dependency = ProjectServiceDependency {
10555 service: ProjectKey::from_value(service.value, value),
10556 condition: None,
10557 restart: None,
10558 required: None,
10559 unmodeled_fields: Vec::new(),
10560 };
10561 dependencies.push(ProjectValue::new(dependency, value));
10562 }
10563 ProjectDependsOn::Short(dependencies)
10564 }
10565 MergedValueKind::Mapping(entries) => {
10566 let mut dependencies = Vec::new();
10567 let mut path = parent_path.to_vec();
10568 path.push("depends_on".to_owned());
10569 for entry in entries {
10570 let mut dependency = ProjectServiceDependency {
10571 service: ProjectKey::from_entry(entry),
10572 condition: None,
10573 restart: None,
10574 required: None,
10575 unmodeled_fields: Vec::new(),
10576 };
10577 let fields = match entry.value().kind() {
10578 MergedValueKind::Null(_) => &[][..],
10579 MergedValueKind::Mapping(fields) => fields.as_slice(),
10580 _ => {
10581 self.expected(entry.value(), "long dependency options must be a mapping or null");
10582 continue;
10583 }
10584 };
10585 let mut dependency_path = path.clone();
10586 dependency_path.push(entry.key().to_owned());
10587 for field in fields {
10588 match field.key() {
10589 "condition" => {
10590 let Some(condition) = self.project_string(field.value(), "dependency condition") else {
10591 continue;
10592 };
10593 let parsed = DependencyCondition::parse(condition.value);
10594 if !parsed.is_known() {
10595 self.invalid(
10596 effective_span(field.value()),
10597 "dependency condition is not defined by Compose",
10598 );
10599 }
10600 dependency.condition = Some(ProjectValue {
10601 value: parsed,
10602 provenance: condition.provenance,
10603 sensitive: condition.sensitive,
10604 });
10605 }
10606 "restart" => {
10607 dependency.restart = self
10608 .located_boolean(field.value(), "dependency restart must be a boolean")
10609 .map(|value| ProjectValue::new(value.into_value(), field.value()));
10610 }
10611 "required" => {
10612 dependency.required = self
10613 .located_boolean(field.value(), "dependency required must be a boolean")
10614 .map(|value| ProjectValue::new(value.into_value(), field.value()));
10615 }
10616 _ => dependency
10617 .unmodeled_fields
10618 .push(field_reference(&dependency_path, field)),
10619 }
10620 }
10621 dependencies.push(ProjectValue::new(dependency, entry.value()));
10622 }
10623 ProjectDependsOn::Long(dependencies)
10624 }
10625 _ => {
10626 self.expected(value, "depends_on must be a sequence or mapping");
10627 return None;
10628 }
10629 };
10630 Some(ProjectValue::new(dependencies, value))
10631 }
10632
10633 fn healthcheck_test(&mut self, value: &MergedValue) -> Option<ProjectValue<HealthcheckTest>> {
10634 let span = effective_span(value);
10635 let test = match value.kind() {
10636 MergedValueKind::Scalar(scalar) => HealthcheckTest::String(Located::new(scalar.value().to_owned(), span)),
10637 MergedValueKind::Sequence(values) => {
10638 let mut items = Vec::new();
10639 for value in values {
10640 items.push(self.located_string(value, "healthcheck test item must be a scalar")?);
10641 }
10642 let kind = items.first().map(|item| HealthcheckTestKind::parse(item.value()));
10643 HealthcheckTest::List {
10644 span,
10645 kind,
10646 values: items,
10647 }
10648 }
10649 _ => {
10650 self.expected(value, "healthcheck test must be a scalar or sequence");
10651 return None;
10652 }
10653 };
10654 Some(ProjectValue::new(test, value))
10655 }
10656
10657 fn healthcheck_duration(
10658 &mut self,
10659 value: &MergedValue,
10660 message: &str,
10661 ) -> Option<ProjectValue<HealthcheckDuration>> {
10662 let scalar = self.scalar(value, message)?;
10663 Some(ProjectValue::new(
10664 HealthcheckDuration::parse(scalar.value().to_owned()),
10665 value,
10666 ))
10667 }
10668
10669 fn healthcheck_retries(&mut self, value: &MergedValue) -> Option<ProjectValue<HealthcheckRetries>> {
10670 let scalar = self.scalar(value, "healthcheck retries must be a scalar")?;
10671 Some(ProjectValue::new(
10672 HealthcheckRetries::parse(scalar.value().to_owned()),
10673 value,
10674 ))
10675 }
10676
10677 fn extra_hosts(&mut self, value: &MergedValue) -> Option<ProjectValue<ProjectExtraHosts>> {
10678 let mut entries = Vec::new();
10679 match value.kind() {
10680 MergedValueKind::Mapping(values) => {
10681 for entry in values {
10682 let scalar = self.scalar(entry.value(), "extra_hosts address must be a scalar")?;
10683 entries.push(ProjectExtraHost {
10684 hostname: ProjectKey::from_entry(entry),
10685 address: ProjectValue::new(HostAddress::parse(scalar.value().to_owned()), entry.value()),
10686 syntax: EntrySyntax::Mapping,
10687 });
10688 }
10689 }
10690 MergedValueKind::Sequence(values) => {
10691 for item in values {
10692 let raw = self.located_string(item, "extra_hosts list item must be a scalar")?;
10693 let parsed = ShortExtraHost::parse(raw);
10694 let (Some(hostname), Some(address)) = (parsed.hostname(), parsed.address()) else {
10695 self.invalid(
10696 effective_span(item),
10697 "extra_hosts entry must contain a hostname and address",
10698 );
10699 continue;
10700 };
10701 entries.push(ProjectExtraHost {
10702 hostname: ProjectKey {
10703 value: hostname.to_owned(),
10704 sources: item.provenance().sources().to_vec(),
10705 sensitive: item.is_sensitive(),
10706 },
10707 address: ProjectValue::new(address.clone(), item),
10708 syntax: EntrySyntax::ListKeyValue,
10709 });
10710 }
10711 }
10712 _ => {
10713 self.expected(value, "extra_hosts must be a mapping or sequence");
10714 return None;
10715 }
10716 }
10717 Some(ProjectValue::new(ProjectExtraHosts { entries }, value))
10718 }
10719
10720 fn project_string(&mut self, value: &MergedValue, description: &str) -> Option<ProjectValue<String>> {
10721 let scalar = self.scalar(value, &format!("{description} must be a non-null scalar"))?;
10722 Some(ProjectValue::new(scalar.value().to_owned(), value))
10723 }
10724
10725 fn non_empty_project_string(&mut self, value: &MergedValue, description: &str) -> Option<ProjectValue<String>> {
10726 let string = self.project_string(value, description)?;
10727 if string.value().is_empty() {
10728 self.invalid(effective_span(value), "build dockerfile must be a non-empty scalar");
10729 return None;
10730 }
10731 Some(string)
10732 }
10733
10734 fn string_collection(
10735 &mut self,
10736 value: &MergedValue,
10737 message: &str,
10738 ) -> Option<ProjectValue<Vec<ProjectValue<String>>>> {
10739 let Some(values) = value.as_sequence() else {
10740 self.expected(value, message);
10741 return None;
10742 };
10743 let mut strings = Vec::new();
10744 for value in values {
10745 let scalar = self.scalar(value, "sequence item must be a non-null scalar")?;
10746 strings.push(ProjectValue::new(scalar.value().to_owned(), value));
10747 }
10748 Some(ProjectValue::new(strings, value))
10749 }
10750
10751 fn strict_string_collection(
10752 &mut self,
10753 value: &MergedValue,
10754 description: &str,
10755 ) -> (ProjectStringCollection, ProjectInvalidServiceStringItems) {
10756 let Some(values) = value.as_sequence() else {
10757 self.expected(value, &format!("{description} must be a sequence"));
10758 return (None, Vec::new());
10759 };
10760 let mut strings = Vec::new();
10761 let mut invalid = Vec::new();
10762 for item in values {
10763 match item.kind() {
10764 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => {
10765 strings.push(ProjectValue::new(scalar.value().to_owned(), item));
10766 }
10767 _ => {
10768 self.expected(item, &format!("{description} entries must be YAML string scalars"));
10769 invalid.push(ProjectInvalidServiceStringItem {
10770 span: effective_span(item),
10771 });
10772 }
10773 }
10774 }
10775 (Some(ProjectValue::new(strings, value)), invalid)
10776 }
10777
10778 fn capability_drop(&mut self, value: &MergedValue) -> Option<ProjectValue<Vec<ProjectValue<CapabilityDropItem>>>> {
10779 let Some(values) = value.as_sequence() else {
10780 self.expected(value, "cap_drop must be a sequence of string scalars");
10781 return None;
10782 };
10783 let mut items = Vec::new();
10784 let mut seen = BTreeMap::new();
10785 for item in values {
10786 let scalar = match item.kind() {
10787 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
10788 _ => {
10789 self.expected(item, "cap_drop entries must be string scalars");
10790 continue;
10791 }
10792 };
10793 let span = effective_span(item);
10794 if let Some(first) = seen.get(scalar.value()) {
10795 self.diagnostics.push(
10796 Diagnostic::new(
10797 CAP_DROP_DUPLICATE_ITEM,
10798 Severity::Error,
10799 "cap_drop entries must be unique exact strings",
10800 )
10801 .with_label(DiagnosticLabel::primary(span, "duplicate capability string"))
10802 .with_label(DiagnosticLabel::secondary(*first, "first identical string")),
10803 );
10804 } else {
10805 seen.insert(scalar.value().to_owned(), span);
10806 }
10807 let typed = CapabilityDropItem::new(Located::new(scalar.value().to_owned(), span));
10808 items.push(ProjectValue::new(typed, item));
10809 }
10810 Some(ProjectValue::new(items, value))
10811 }
10812
10813 fn capability_add(&mut self, value: &MergedValue) -> Option<ProjectValue<Vec<ProjectValue<CapabilityAddItem>>>> {
10814 let Some(values) = value.as_sequence() else {
10815 self.expected(value, "cap_add must be a sequence of string scalars");
10816 return None;
10817 };
10818 let mut items = Vec::new();
10819 let mut seen = BTreeMap::new();
10820 for item in values {
10821 let scalar = match item.kind() {
10822 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
10823 _ => {
10824 self.expected(item, "cap_add entries must be string scalars");
10825 continue;
10826 }
10827 };
10828 let span = effective_span(item);
10829 if let Some(first) = seen.get(scalar.value()) {
10830 self.diagnostics.push(
10831 Diagnostic::new(
10832 CAP_ADD_DUPLICATE_ITEM,
10833 Severity::Error,
10834 "cap_add entries must be unique exact strings",
10835 )
10836 .with_label(DiagnosticLabel::primary(span, "duplicate capability string"))
10837 .with_label(DiagnosticLabel::secondary(*first, "first identical string")),
10838 );
10839 } else {
10840 seen.insert(scalar.value().to_owned(), span);
10841 }
10842 let typed = CapabilityAddItem::new(Located::new(scalar.value().to_owned(), span));
10843 items.push(ProjectValue::new(typed, item));
10844 }
10845 Some(ProjectValue::new(items, value))
10846 }
10847
10848 fn devices(
10849 &mut self,
10850 value: &MergedValue,
10851 service_path: &[String],
10852 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectDevice>>>> {
10853 let Some(values) = value.as_sequence() else {
10854 self.expected(value, "service devices must be a sequence");
10855 return None;
10856 };
10857 let mut devices = Vec::new();
10858 for (index, item) in values.iter().enumerate() {
10859 let mut path = service_path.to_vec();
10860 path.push("devices".to_owned());
10861 path.push(index.to_string());
10862 let device = match item.kind() {
10863 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => ProjectDevice::Short(
10864 ShortDevice::new(Located::new(scalar.value().to_owned(), effective_span(item))),
10865 ),
10866 MergedValueKind::Mapping(fields) => ProjectDevice::Long(self.long_device(item, fields, &path)),
10867 _ => {
10868 self.diagnostics.push(
10869 Diagnostic::new(
10870 DEVICE_EXPECTED_FORM,
10871 Severity::Error,
10872 "service device must use string short syntax or mapping long syntax",
10873 )
10874 .with_label(DiagnosticLabel::primary(
10875 effective_span(item),
10876 "unsupported device form",
10877 )),
10878 );
10879 continue;
10880 }
10881 };
10882 devices.push(ProjectValue::new(device, item));
10883 }
10884 Some(ProjectValue::new(devices, value))
10885 }
10886
10887 fn long_device(&mut self, value: &MergedValue, fields: &[MergedEntry], path: &[String]) -> ProjectLongDevice {
10888 let mut device = ProjectLongDevice {
10889 source: None,
10890 target: None,
10891 permissions: None,
10892 extension_fields: Vec::new(),
10893 unknown_fields: Vec::new(),
10894 };
10895 for field in fields {
10896 let parsed = match field.key() {
10897 "source" | "target" | "permissions" => self.device_string(field.value(), field.key()),
10898 name if name.starts_with("x-") => {
10899 device.extension_fields.push(field_reference(path, field));
10900 continue;
10901 }
10902 _ => {
10903 device.unknown_fields.push(field_reference(path, field));
10904 continue;
10905 }
10906 };
10907 match field.key() {
10908 "source" => device.source = parsed,
10909 "target" => device.target = parsed,
10910 "permissions" => device.permissions = parsed,
10911 _ => unreachable!("unrecognized device fields continue before assignment"),
10912 }
10913 }
10914 if device.source.is_none() {
10915 self.missing(value, "long-syntax device is missing required string `source`");
10916 }
10917 device
10918 }
10919
10920 fn device_string(&mut self, value: &MergedValue, member: &str) -> Option<ProjectValue<String>> {
10921 let scalar = match value.kind() {
10922 MergedValueKind::Scalar(scalar) if scalar.kind() == MergedScalarKind::String => scalar,
10923 _ => {
10924 self.diagnostics.push(
10925 Diagnostic::new(
10926 DEVICE_EXPECTED_STRING,
10927 Severity::Error,
10928 format!("device {member} must be a string scalar"),
10929 )
10930 .with_label(DiagnosticLabel::primary(
10931 effective_span(value),
10932 "unexpected long-device member form",
10933 )),
10934 );
10935 return None;
10936 }
10937 };
10938 Some(ProjectValue::new(scalar.value().to_owned(), value))
10939 }
10940
10941 fn scalar<'value>(
10942 &mut self,
10943 value: &'value MergedValue,
10944 message: &str,
10945 ) -> Option<&'value crate::merge::MergedScalar> {
10946 let Some(scalar) = value.as_scalar() else {
10947 self.expected(value, message);
10948 return None;
10949 };
10950 Some(scalar)
10951 }
10952
10953 fn located_string(&mut self, value: &MergedValue, message: &str) -> Option<Located<String>> {
10954 let scalar = self.scalar(value, message)?;
10955 Some(Located::new(scalar.value().to_owned(), effective_span(value)))
10956 }
10957
10958 fn located_string_or_number(&mut self, value: &MergedValue, message: &str) -> Option<Located<ComposeScalar>> {
10959 let scalar = self.scalar(value, message)?;
10960 if !matches!(scalar.kind(), MergedScalarKind::String | MergedScalarKind::Number) {
10961 self.expected(value, message);
10962 return None;
10963 }
10964 Some(Located::new(compose_scalar_from_merged(scalar), effective_span(value)))
10965 }
10966
10967 fn strict_located_string(&mut self, value: &MergedValue, message: &str) -> Option<Located<String>> {
10968 let MergedValueKind::Scalar(scalar) = value.kind() else {
10969 self.expected(value, message);
10970 return None;
10971 };
10972 if scalar.kind() != MergedScalarKind::String || !scalar.is_strict_yaml_string() {
10973 self.expected(value, message);
10974 return None;
10975 }
10976 Some(Located::new(scalar.value().to_owned(), effective_span(value)))
10977 }
10978
10979 fn compose_scalar(&mut self, value: &MergedValue, message: &str) -> Option<ComposeScalar> {
10980 match value.kind() {
10981 MergedValueKind::Null(_) => Some(ComposeScalar::Null),
10982 MergedValueKind::Scalar(scalar) => Some(match scalar.kind() {
10983 MergedScalarKind::String => ComposeScalar::String(scalar.value().to_owned()),
10984 MergedScalarKind::Boolean => ComposeScalar::Boolean(scalar.value().eq_ignore_ascii_case("true")),
10985 MergedScalarKind::Number => ComposeScalar::Number(scalar.value().to_owned()),
10986 }),
10987 _ => {
10988 self.expected(value, message);
10989 None
10990 }
10991 }
10992 }
10993
10994 fn mapping<'value>(&mut self, value: &'value MergedValue, message: &str) -> Option<&'value [MergedEntry]> {
10995 let Some(entries) = value.as_mapping() else {
10996 self.expected(value, message);
10997 return None;
10998 };
10999 Some(entries)
11000 }
11001
11002 fn expected(&mut self, value: &MergedValue, message: &str) {
11003 self.diagnostics.push(
11004 Diagnostic::new(PROJECT_EXPECTED_FORM, Severity::Error, message).with_label(DiagnosticLabel::primary(
11005 effective_span(value),
11006 "unexpected merged value form",
11007 )),
11008 );
11009 }
11010
11011 fn missing(&mut self, value: &MergedValue, message: &str) {
11012 self.diagnostics.push(
11013 Diagnostic::new(PROJECT_MISSING_FIELD, Severity::Error, message).with_label(DiagnosticLabel::primary(
11014 effective_span(value),
11015 "required field is missing",
11016 )),
11017 );
11018 }
11019
11020 fn invalid(&mut self, span: SourceSpan, message: &str) {
11021 self.diagnostics.push(
11022 Diagnostic::new(PROJECT_INVALID_VALUE, Severity::Error, message)
11023 .with_label(DiagnosticLabel::primary(span, "invalid native value")),
11024 );
11025 }
11026
11027 fn record_root_unmodeled(&mut self, path: &[String], entry: &MergedEntry) {
11028 self.root_unmodeled.push(field_reference(path, entry));
11029 }
11030
11031 fn record_pending_unmodeled(&mut self, path: &[String], entry: &MergedEntry) {
11032 self.pending_unmodeled.push(field_reference(path, entry));
11033 }
11034}
11035
11036fn effective_security_label_diagnostic(
11037 kind: &SecurityOptionKind,
11038 span: SourceSpan,
11039 candidates: &mut SecurityOptionCandidateCounts,
11040) -> Option<Diagnostic> {
11041 match kind {
11042 SecurityOptionKind::SecurityLabelDisable { .. } | SecurityOptionKind::SecurityLabelDisableNearMiss => {
11043 security_label_disable_diagnostic(kind, span, &mut candidates.security_label_disable)
11044 }
11045 SecurityOptionKind::SecurityLabelFileType { .. } | SecurityOptionKind::SecurityLabelFileTypeNearMiss => {
11046 security_label_filetype_diagnostic(kind, span, &mut candidates.security_label_filetype)
11047 }
11048 SecurityOptionKind::SecurityLabelLevel { .. } | SecurityOptionKind::SecurityLabelLevelNearMiss => {
11049 security_label_level_diagnostic(kind, span, &mut candidates.security_label_level)
11050 }
11051 SecurityOptionKind::SecurityLabelNested { .. } | SecurityOptionKind::SecurityLabelNestedNearMiss => {
11052 security_label_nested_diagnostic(kind, span, &mut candidates.security_label_nested)
11053 }
11054 SecurityOptionKind::SecurityLabelType { .. } | SecurityOptionKind::SecurityLabelTypeNearMiss => {
11055 security_label_type_diagnostic(kind, span, &mut candidates.security_label_type)
11056 }
11057 _ => None,
11058 }
11059}
11060
11061fn security_label_disable_diagnostic(
11062 kind: &SecurityOptionKind,
11063 span: SourceSpan,
11064 candidates: &mut usize,
11065) -> Option<Diagnostic> {
11066 match kind {
11067 SecurityOptionKind::SecurityLabelDisable { .. } => {
11068 *candidates += 1;
11069 (*candidates > 1).then(|| {
11070 Diagnostic::new(
11071 SECURITY_OPT_SECURITY_LABEL_DISABLE_CONFLICT,
11072 Severity::Warning,
11073 "multiple SELinux label-disable candidates are retained; a consumer must resolve the conflict explicitly",
11074 )
11075 .with_label(DiagnosticLabel::primary(
11076 span,
11077 "additional effective SELinux label-disable candidate retained",
11078 ))
11079 })
11080 }
11081 SecurityOptionKind::SecurityLabelDisableNearMiss => Some(
11082 Diagnostic::new(
11083 SECURITY_OPT_SECURITY_LABEL_DISABLE_NEAR_MISS,
11084 Severity::Warning,
11085 "SELinux label-disable candidates require exact lowercase `label:disable` spelling without whitespace",
11086 )
11087 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
11088 ),
11089 _ => None,
11090 }
11091}
11092
11093fn security_label_filetype_diagnostic(
11094 kind: &SecurityOptionKind,
11095 span: SourceSpan,
11096 candidates: &mut usize,
11097) -> Option<Diagnostic> {
11098 match kind {
11099 SecurityOptionKind::SecurityLabelFileType { .. } => {
11100 *candidates += 1;
11101 (*candidates > 1).then(|| {
11102 Diagnostic::new(
11103 SECURITY_OPT_SECURITY_LABEL_FILETYPE_CONFLICT,
11104 Severity::Warning,
11105 "multiple SELinux label-filetype candidates are retained; a consumer must resolve the conflict explicitly",
11106 )
11107 .with_label(DiagnosticLabel::primary(
11108 span,
11109 "additional effective SELinux label-filetype candidate retained",
11110 ))
11111 })
11112 }
11113 SecurityOptionKind::SecurityLabelFileTypeNearMiss => Some(
11114 Diagnostic::new(
11115 SECURITY_OPT_SECURITY_LABEL_FILETYPE_NEAR_MISS,
11116 Severity::Warning,
11117 "SELinux label-filetype candidates require exact lowercase `label:filetype:<type>` spelling without whitespace",
11118 )
11119 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
11120 ),
11121 _ => None,
11122 }
11123}
11124
11125fn security_label_level_diagnostic(
11126 kind: &SecurityOptionKind,
11127 span: SourceSpan,
11128 candidates: &mut usize,
11129) -> Option<Diagnostic> {
11130 match kind {
11131 SecurityOptionKind::SecurityLabelLevel { .. } => {
11132 *candidates += 1;
11133 (*candidates > 1).then(|| {
11134 Diagnostic::new(
11135 SECURITY_OPT_SECURITY_LABEL_LEVEL_CONFLICT,
11136 Severity::Warning,
11137 "multiple SELinux label-level candidates are retained; a consumer must resolve the conflict explicitly",
11138 )
11139 .with_label(DiagnosticLabel::primary(
11140 span,
11141 "additional effective SELinux label-level candidate retained",
11142 ))
11143 })
11144 }
11145 SecurityOptionKind::SecurityLabelLevelNearMiss => Some(
11146 Diagnostic::new(
11147 SECURITY_OPT_SECURITY_LABEL_LEVEL_NEAR_MISS,
11148 Severity::Warning,
11149 "SELinux label-level candidates require exact lowercase `label:level:<level>` spelling without whitespace",
11150 )
11151 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
11152 ),
11153 _ => None,
11154 }
11155}
11156
11157fn security_label_nested_diagnostic(
11158 kind: &SecurityOptionKind,
11159 span: SourceSpan,
11160 candidates: &mut usize,
11161) -> Option<Diagnostic> {
11162 match kind {
11163 SecurityOptionKind::SecurityLabelNested { .. } => {
11164 *candidates += 1;
11165 (*candidates > 1).then(|| {
11166 Diagnostic::new(
11167 SECURITY_OPT_SECURITY_LABEL_NESTED_CONFLICT,
11168 Severity::Warning,
11169 "multiple SELinux label-nested candidates are retained; a consumer must resolve the conflict explicitly",
11170 )
11171 .with_label(DiagnosticLabel::primary(
11172 span,
11173 "additional effective SELinux label-nested candidate retained",
11174 ))
11175 })
11176 }
11177 SecurityOptionKind::SecurityLabelNestedNearMiss => Some(
11178 Diagnostic::new(
11179 SECURITY_OPT_SECURITY_LABEL_NESTED_NEAR_MISS,
11180 Severity::Warning,
11181 "SELinux label-nested candidates require exact lowercase `label:nested` spelling without whitespace",
11182 )
11183 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
11184 ),
11185 _ => None,
11186 }
11187}
11188
11189fn security_label_type_diagnostic(
11190 kind: &SecurityOptionKind,
11191 span: SourceSpan,
11192 candidates: &mut usize,
11193) -> Option<Diagnostic> {
11194 match kind {
11195 SecurityOptionKind::SecurityLabelType { .. } => {
11196 *candidates += 1;
11197 (*candidates > 1).then(|| {
11198 Diagnostic::new(
11199 SECURITY_OPT_SECURITY_LABEL_TYPE_CONFLICT,
11200 Severity::Warning,
11201 "multiple SELinux label-type candidates are retained; a consumer must resolve the conflict explicitly",
11202 )
11203 .with_label(DiagnosticLabel::primary(
11204 span,
11205 "additional effective SELinux label-type candidate retained",
11206 ))
11207 })
11208 }
11209 SecurityOptionKind::SecurityLabelTypeNearMiss => Some(
11210 Diagnostic::new(
11211 SECURITY_OPT_SECURITY_LABEL_TYPE_NEAR_MISS,
11212 Severity::Warning,
11213 "SELinux label-type candidates require exact lowercase `label:type:<type>` spelling with one non-empty whitespace-free type",
11214 )
11215 .with_label(DiagnosticLabel::primary(span, "raw near-miss security option retained")),
11216 ),
11217 _ => None,
11218 }
11219}
11220
11221impl Builder<'_> {
11222 fn ports(&mut self, value: &MergedValue, service_path: &[String]) -> Option<ProjectValue<Vec<ProjectValue<Port>>>> {
11223 let Some(values) = value.as_sequence() else {
11224 self.expected(value, "service ports must be a sequence");
11225 return None;
11226 };
11227 let mut ports = Vec::new();
11228 for (index, item) in values.iter().enumerate() {
11229 let mut path = service_path.to_vec();
11230 path.push("ports".to_owned());
11231 path.push(index.to_string());
11232 let port = match item.kind() {
11233 MergedValueKind::Scalar(scalar) => Port::Short(ShortPort::parse(Located::new(
11234 scalar.value().to_owned(),
11235 effective_span(item),
11236 ))),
11237 MergedValueKind::Mapping(fields) => Port::Long(Box::new(self.long_port(item, fields, &path))),
11238 _ => {
11239 self.expected(item, "service port must use scalar short syntax or mapping long syntax");
11240 continue;
11241 }
11242 };
11243 ports.push(ProjectValue::new(port, item));
11244 }
11245 Some(ProjectValue::new(ports, value))
11246 }
11247
11248 fn long_port(&mut self, value: &MergedValue, fields: &[MergedEntry], path: &[String]) -> LongPort {
11249 let mut port = LongPort::new(effective_span(value));
11250 let mut has_target = false;
11251 for field in fields {
11252 match field.key() {
11253 "target" => {
11254 if let Some(value) = self.located_string(field.value(), "port target must be a scalar") {
11255 port.set_target(value);
11256 has_target = true;
11257 }
11258 }
11259 "published" => self
11260 .located_string(field.value(), "published port must be a scalar")
11261 .into_iter()
11262 .for_each(|value| port.set_published(value)),
11263 "host_ip" => self
11264 .located_string(field.value(), "port host_ip must be a scalar")
11265 .into_iter()
11266 .for_each(|value| port.set_host_ip(value)),
11267 "protocol" => self
11268 .located_string(field.value(), "port protocol must be a scalar")
11269 .into_iter()
11270 .for_each(|value| port.set_protocol(value)),
11271 "app_protocol" => self
11272 .located_string(field.value(), "port app_protocol must be a scalar")
11273 .into_iter()
11274 .for_each(|value| port.set_app_protocol(value)),
11275 "mode" => self
11276 .located_string(field.value(), "port mode must be a scalar")
11277 .into_iter()
11278 .for_each(|value| port.set_mode(value)),
11279 "name" => self
11280 .located_string(field.value(), "port name must be a scalar")
11281 .into_iter()
11282 .for_each(|value| port.set_name(value)),
11283 _ => self.record_pending_unmodeled(path, field),
11284 }
11285 }
11286 if !has_target {
11287 self.missing(value, "long-syntax port is missing `target`");
11288 }
11289 port
11290 }
11291
11292 fn volumes(
11293 &mut self,
11294 value: &MergedValue,
11295 service_path: &[String],
11296 ) -> Option<ProjectValue<Vec<ProjectValue<VolumeMount>>>> {
11297 let Some(values) = value.as_sequence() else {
11298 self.expected(value, "service volumes must be a sequence");
11299 return None;
11300 };
11301 let mut mounts = Vec::new();
11302 for (index, item) in values.iter().enumerate() {
11303 let mut path = service_path.to_vec();
11304 path.push("volumes".to_owned());
11305 path.push(index.to_string());
11306 let mount = match item.kind() {
11307 MergedValueKind::Scalar(scalar) => VolumeMount::Short(ShortVolumeMount::new(Located::new(
11308 scalar.value().to_owned(),
11309 effective_span(item),
11310 ))),
11311 MergedValueKind::Mapping(fields) => VolumeMount::Long(Box::new(self.long_volume(item, fields, &path))),
11312 _ => {
11313 self.expected(
11314 item,
11315 "service volume must use scalar short syntax or mapping long syntax",
11316 );
11317 continue;
11318 }
11319 };
11320 mounts.push(ProjectValue::new(mount, item));
11321 }
11322 Some(ProjectValue::new(mounts, value))
11323 }
11324
11325 fn project_volume_mount_options_collection(
11328 value: &MergedValue,
11329 service_path: &[String],
11330 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectVolumeMountOptions>>>> {
11331 let values = value.as_sequence()?;
11332 let mut mounts = Vec::new();
11333 for (index, item) in values.iter().enumerate() {
11334 let mut path = service_path.to_vec();
11335 path.push("volumes".to_owned());
11336 path.push(index.to_string());
11337 let options = match item.kind() {
11338 MergedValueKind::Scalar(_) => ProjectVolumeMountOptions::new(),
11339 MergedValueKind::Mapping(fields) => Self::project_volume_mount_options(fields, &path),
11340 _ => continue,
11341 };
11342 mounts.push(ProjectValue::new(options, item));
11343 }
11344 Some(ProjectValue::new(mounts, value))
11345 }
11346
11347 fn project_volume_mount_options(fields: &[MergedEntry], path: &[String]) -> ProjectVolumeMountOptions {
11348 let mut options = ProjectVolumeMountOptions::new();
11349 for field in fields {
11350 match field.key() {
11351 "type" | "source" | "target" | "read_only" => {}
11352 "consistency" => match Self::volume_option_string(field.value()) {
11353 Some(value) => options.consistency = Some(value),
11354 None => options.unmodeled_fields.push(field_reference(path, field)),
11355 },
11356 "bind" => match Self::project_bind_mount_options(field.value(), path) {
11357 Some(value) => options.bind = Some(value),
11358 None => options.unmodeled_fields.push(field_reference(path, field)),
11359 },
11360 "image" => match Self::project_image_mount_options(field.value(), path) {
11361 Some(value) => options.image = Some(value),
11362 None => options.unmodeled_fields.push(field_reference(path, field)),
11363 },
11364 "tmpfs" => match Self::project_tmpfs_mount_options(field.value(), path) {
11365 Some(value) => options.tmpfs = Some(value),
11366 None => options.unmodeled_fields.push(field_reference(path, field)),
11367 },
11368 "volume" => match Self::project_named_volume_mount_options(field.value(), path) {
11369 Some(value) => options.volume = Some(value),
11370 None => options.unmodeled_fields.push(field_reference(path, field)),
11371 },
11372 _ => options.unmodeled_fields.push(field_reference(path, field)),
11373 }
11374 }
11375 options
11376 }
11377
11378 fn project_bind_mount_options(value: &MergedValue, parent_path: &[String]) -> Option<ProjectBindMountOptions> {
11379 let fields = value.as_mapping()?;
11380 let mut options = ProjectBindMountOptions {
11381 recursive: None,
11382 extension_fields: Vec::new(),
11383 unknown_fields: Vec::new(),
11384 };
11385 let mut path = parent_path.to_vec();
11386 path.push("bind".to_owned());
11387 for field in fields {
11388 match field.key() {
11389 "recursive" => match Self::volume_option_string(field.value()) {
11390 Some(value) => options.recursive = Some(value),
11391 None => options.unknown_fields.push(field_reference(&path, field)),
11392 },
11393 "propagation" | "create_host_path" | "selinux" => {}
11394 key if key.starts_with("x-") => options.extension_fields.push(field_reference(&path, field)),
11395 _ => options.unknown_fields.push(field_reference(&path, field)),
11396 }
11397 }
11398 Some(options)
11399 }
11400
11401 fn project_image_mount_options(value: &MergedValue, parent_path: &[String]) -> Option<ProjectImageMountOptions> {
11402 let fields = value.as_mapping()?;
11403 let mut options = ProjectImageMountOptions {
11404 subpath: None,
11405 extension_fields: Vec::new(),
11406 unknown_fields: Vec::new(),
11407 };
11408 let mut path = parent_path.to_vec();
11409 path.push("image".to_owned());
11410 for field in fields {
11411 match field.key() {
11412 "subpath" => match Self::volume_option_string(field.value()) {
11413 Some(value) => options.subpath = Some(value),
11414 None => options.unknown_fields.push(field_reference(&path, field)),
11415 },
11416 key if key.starts_with("x-") => options.extension_fields.push(field_reference(&path, field)),
11417 _ => options.unknown_fields.push(field_reference(&path, field)),
11418 }
11419 }
11420 Some(options)
11421 }
11422
11423 fn project_tmpfs_mount_options(value: &MergedValue, parent_path: &[String]) -> Option<ProjectTmpfsMountOptions> {
11424 let fields = value.as_mapping()?;
11425 let mut options = ProjectTmpfsMountOptions {
11426 size: None,
11427 mode: None,
11428 extension_fields: Vec::new(),
11429 unknown_fields: Vec::new(),
11430 };
11431 let mut path = parent_path.to_vec();
11432 path.push("tmpfs".to_owned());
11433 for field in fields {
11434 match field.key() {
11435 "size" => match Self::volume_option_string_or_number(field.value()) {
11436 Some(value) => options.size = Some(value),
11437 None => options.unknown_fields.push(field_reference(&path, field)),
11438 },
11439 "mode" => match Self::volume_option_string_or_number(field.value()) {
11440 Some(value) => options.mode = Some(value),
11441 None => options.unknown_fields.push(field_reference(&path, field)),
11442 },
11443 key if key.starts_with("x-") => options.extension_fields.push(field_reference(&path, field)),
11444 _ => options.unknown_fields.push(field_reference(&path, field)),
11445 }
11446 }
11447 Some(options)
11448 }
11449
11450 fn project_named_volume_mount_options(
11451 value: &MergedValue,
11452 parent_path: &[String],
11453 ) -> Option<ProjectNamedVolumeMountOptions> {
11454 let fields = value.as_mapping()?;
11455 let mut options = ProjectNamedVolumeMountOptions {
11456 nocopy: None,
11457 subpath: None,
11458 labels: None,
11459 extension_fields: Vec::new(),
11460 unknown_fields: Vec::new(),
11461 };
11462 let mut path = parent_path.to_vec();
11463 path.push("volume".to_owned());
11464 for field in fields {
11465 match field.key() {
11466 "nocopy" => match Self::volume_option_boolean(field.value()) {
11467 Some(value) => options.nocopy = Some(value),
11468 None => options.unknown_fields.push(field_reference(&path, field)),
11469 },
11470 "subpath" => match Self::volume_option_string(field.value()) {
11471 Some(value) => options.subpath = Some(value),
11472 None => options.unknown_fields.push(field_reference(&path, field)),
11473 },
11474 "labels" => match Self::volume_option_labels(field.value()) {
11475 Some(value) => options.labels = Some(value),
11476 None => options.unknown_fields.push(field_reference(&path, field)),
11477 },
11478 key if key.starts_with("x-") => options.extension_fields.push(field_reference(&path, field)),
11479 _ => options.unknown_fields.push(field_reference(&path, field)),
11480 }
11481 }
11482 Some(options)
11483 }
11484
11485 fn volume_option_string(value: &MergedValue) -> Option<ProjectValue<String>> {
11486 let scalar = value.as_scalar()?;
11487 Some(ProjectValue::new(scalar.value().to_owned(), value))
11488 }
11489
11490 fn volume_option_string_or_number(value: &MergedValue) -> Option<ProjectValue<ComposeScalar>> {
11491 let scalar = value.as_scalar()?;
11492 matches!(scalar.kind(), MergedScalarKind::String | MergedScalarKind::Number)
11493 .then(|| ProjectValue::new(compose_scalar_from_merged(scalar), value))
11494 }
11495
11496 fn volume_option_boolean(value: &MergedValue) -> Option<ProjectValue<BooleanValue>> {
11497 let scalar = value.as_scalar()?;
11498 let boolean = if scalar.kind() == MergedScalarKind::Boolean {
11499 BooleanValue::Literal(scalar.value().eq_ignore_ascii_case("true"))
11500 } else if value.is_sensitive() && matches!(scalar.value(), "true" | "false") {
11501 BooleanValue::Literal(scalar.value() == "true")
11502 } else if scalar.value().contains('$') {
11503 BooleanValue::Expression(scalar.value().to_owned())
11504 } else {
11505 return None;
11506 };
11507 Some(ProjectValue::new(boolean, value))
11508 }
11509
11510 fn volume_option_labels(value: &MergedValue) -> Option<ProjectValue<Labels>> {
11511 let labels = match value.kind() {
11512 MergedValueKind::Sequence(values) => {
11513 let mut labels = Vec::new();
11514 for value in values {
11515 let scalar = value.as_scalar()?;
11516 labels.push(Located::new(scalar.value().to_owned(), effective_span(value)));
11517 }
11518 Labels::List {
11519 span: effective_span(value),
11520 values: labels,
11521 }
11522 }
11523 MergedValueKind::Mapping(entries) => {
11524 let mut labels = Vec::new();
11525 for entry in entries {
11526 let scalar = Self::volume_option_compose_scalar(entry.value())?;
11527 let span = effective_span(entry.value());
11528 labels.push(KeyValueEntry::new(
11529 Located::new(entry.key().to_owned(), entry_span(entry)),
11530 Located::new(scalar, span),
11531 span,
11532 ));
11533 }
11534 Labels::Map {
11535 span: effective_span(value),
11536 entries: labels,
11537 }
11538 }
11539 _ => return None,
11540 };
11541 Some(ProjectValue::new(labels, value))
11542 }
11543
11544 fn volume_option_compose_scalar(value: &MergedValue) -> Option<ComposeScalar> {
11545 match value.kind() {
11546 MergedValueKind::Null(_) => Some(ComposeScalar::Null),
11547 MergedValueKind::Scalar(scalar) => Some(compose_scalar_from_merged(scalar)),
11548 _ => None,
11549 }
11550 }
11551
11552 fn long_volume(&mut self, value: &MergedValue, fields: &[MergedEntry], path: &[String]) -> LongVolumeMount {
11553 let mut mount = LongVolumeMount::new(effective_span(value));
11554 let mut has_type = false;
11555 let mut has_target = false;
11556 for field in fields {
11557 match field.key() {
11558 "type" => {
11559 if let Some(value) = self.located_string(field.value(), "volume type must be a scalar") {
11560 mount.set_mount_type(Located::new(MountType::from_text(value.value().clone()), value.span()));
11561 has_type = true;
11562 }
11563 }
11564 "source" => self
11565 .located_string(field.value(), "volume source must be a scalar")
11566 .into_iter()
11567 .for_each(|value| mount.set_source(value)),
11568 "target" => {
11569 if let Some(value) = self.located_string(field.value(), "volume target must be a scalar") {
11570 mount.set_target(value);
11571 has_target = true;
11572 }
11573 }
11574 "read_only" => self
11575 .located_boolean(field.value(), "volume read_only must be a boolean")
11576 .into_iter()
11577 .for_each(|value| mount.set_read_only(value)),
11578 "consistency" => {
11579 if let Some(value) = self.located_string(field.value(), "volume consistency must be a scalar") {
11580 mount.set_consistency(value);
11581 } else {
11582 self.record_pending_unmodeled(path, field);
11583 }
11584 }
11585 "bind" => self
11586 .bind_options(field.value(), path)
11587 .into_iter()
11588 .for_each(|value| mount.set_bind(value)),
11589 "image" => {
11590 if let Some(value) = self.image_mount_options(field.value(), path) {
11591 mount.set_image(value);
11592 } else {
11593 self.record_pending_unmodeled(path, field);
11594 }
11595 }
11596 "tmpfs" => {
11597 if let Some(value) = self.tmpfs_mount_options(field.value(), path) {
11598 mount.set_tmpfs(value);
11599 } else {
11600 self.record_pending_unmodeled(path, field);
11601 }
11602 }
11603 "volume" => {
11604 if let Some(value) = self.volume_mount_options(field.value(), path) {
11605 mount.set_volume(value);
11606 } else {
11607 self.record_pending_unmodeled(path, field);
11608 }
11609 }
11610 _ => self.record_pending_unmodeled(path, field),
11611 }
11612 }
11613 if !has_type {
11614 self.missing(value, "long-syntax volume is missing `type`");
11615 }
11616 if !has_target {
11617 self.missing(value, "long-syntax volume is missing `target`");
11618 }
11619 mount
11620 }
11621
11622 fn grants(
11623 &mut self,
11624 value: &MergedValue,
11625 service_path: &[String],
11626 kind: &str,
11627 ) -> Option<ProjectValue<Vec<ProjectValue<ProjectGrant>>>> {
11628 self.grants_at(value, service_path, &format!("{kind}s"), kind).0
11629 }
11630
11631 fn build_secret_grants(
11632 &mut self,
11633 value: &MergedValue,
11634 build_path: &[String],
11635 ) -> (Option<ProjectValue<Vec<ProjectValue<ProjectGrant>>>>, bool) {
11636 self.grants_at(value, build_path, "secrets", "build secret")
11637 }
11638
11639 fn grants_at(
11640 &mut self,
11641 value: &MergedValue,
11642 parent_path: &[String],
11643 field_name: &str,
11644 kind: &str,
11645 ) -> (Option<ProjectValue<Vec<ProjectValue<ProjectGrant>>>>, bool) {
11646 let Some(values) = value.as_sequence() else {
11647 self.expected(value, &format!("{kind}s must be a sequence"));
11648 return (None, true);
11649 };
11650 let mut grants = Vec::new();
11651 let mut malformed = false;
11652 for (index, item) in values.iter().enumerate() {
11653 let mut path = parent_path.to_vec();
11654 path.push(field_name.to_owned());
11655 path.push(index.to_string());
11656 let grant = match item.kind() {
11657 MergedValueKind::Scalar(scalar) => ProjectGrant::Short(scalar.value().to_owned()),
11658 MergedValueKind::Mapping(fields) => {
11659 let grant = self.long_grant(item, fields, &path, kind);
11660 malformed |= grant.source().is_none();
11661 ProjectGrant::Long(Box::new(grant))
11662 }
11663 _ => {
11664 self.expected(
11665 item,
11666 &format!("{kind} must use scalar short syntax or mapping long syntax"),
11667 );
11668 malformed = true;
11669 continue;
11670 }
11671 };
11672 grants.push(ProjectValue::new(grant, item));
11673 }
11674 (Some(ProjectValue::new(grants, value)), malformed)
11675 }
11676
11677 fn long_grant(
11678 &mut self,
11679 value: &MergedValue,
11680 fields: &[MergedEntry],
11681 path: &[String],
11682 kind: &str,
11683 ) -> ProjectLongGrant {
11684 let mut grant = ProjectLongGrant {
11685 source: None,
11686 target: None,
11687 uid: None,
11688 gid: None,
11689 mode: None,
11690 unmodeled_fields: Vec::new(),
11691 };
11692 for field in fields {
11693 let parsed = match field.key() {
11694 "source" => self.project_string(field.value(), &format!("{kind} source")),
11695 "target" => self.project_string(field.value(), &format!("{kind} target")),
11696 "uid" => self.project_string(field.value(), &format!("{kind} uid")),
11697 "gid" => self.project_string(field.value(), &format!("{kind} gid")),
11698 "mode" => self.project_string(field.value(), &format!("{kind} mode")),
11699 _ => {
11700 grant.unmodeled_fields.push(field_reference(path, field));
11701 continue;
11702 }
11703 };
11704 if parsed.is_none() {
11705 grant.unmodeled_fields.push(field_reference(path, field));
11706 }
11707 match field.key() {
11708 "source" => grant.source = parsed,
11709 "target" => grant.target = parsed,
11710 "uid" => grant.uid = parsed,
11711 "gid" => grant.gid = parsed,
11712 "mode" => grant.mode = parsed,
11713 _ => unreachable!("unrecognized grant fields continue before assignment"),
11714 }
11715 }
11716 if grant.source.is_none() {
11717 self.missing(value, &format!("long-syntax {kind} is missing `source`"));
11718 }
11719 grant
11720 }
11721
11722 fn bind_options(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<BindOptions> {
11723 let fields = self.mapping(value, "volume bind options must be a mapping")?;
11724 let mut bind = BindOptions::new(effective_span(value));
11725 let mut path = parent_path.to_vec();
11726 path.push("bind".to_owned());
11727 for field in fields {
11728 match field.key() {
11729 "propagation" => self
11730 .located_string(field.value(), "bind propagation must be a scalar")
11731 .into_iter()
11732 .for_each(|value| bind.set_propagation(value)),
11733 "create_host_path" => self
11734 .located_boolean(field.value(), "bind create_host_path must be a boolean")
11735 .into_iter()
11736 .for_each(|value| bind.set_create_host_path(value)),
11737 "selinux" => {
11738 if let Some(value) = self.located_string(field.value(), "bind SELinux mode must be a scalar") {
11739 let mode = match value.value().as_str() {
11740 "z" => Some(SelinuxRelabel::Shared),
11741 "Z" => Some(SelinuxRelabel::Private),
11742 _ => None,
11743 };
11744 if let Some(mode) = mode {
11745 bind.set_selinux(Located::new(mode, value.span()));
11746 } else {
11747 self.invalid(value.span(), "bind SELinux mode must be `z` or `Z`");
11748 }
11749 }
11750 }
11751 "recursive" => {
11752 if let Some(value) = self.located_string(field.value(), "bind recursive mode must be a scalar") {
11753 bind.set_recursive(value);
11754 } else {
11755 self.record_pending_unmodeled(&path, field);
11756 }
11757 }
11758 _ => self.record_pending_unmodeled(&path, field),
11759 }
11760 }
11761 Some(bind)
11762 }
11763
11764 fn image_mount_options(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<ImageMountOptions> {
11765 let fields = self.mapping(value, "volume image options must be a mapping")?;
11766 let mut options = ImageMountOptions::new(effective_span(value));
11767 let mut path = parent_path.to_vec();
11768 path.push("image".to_owned());
11769 for field in fields {
11770 match field.key() {
11771 "subpath" => {
11772 if let Some(value) = self.located_string(field.value(), "image mount subpath must be a scalar") {
11773 options.set_subpath(value);
11774 } else {
11775 self.record_pending_unmodeled(&path, field);
11776 }
11777 }
11778 _ => self.record_pending_unmodeled(&path, field),
11779 }
11780 }
11781 Some(options)
11782 }
11783
11784 fn tmpfs_mount_options(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<TmpfsMountOptions> {
11785 let fields = self.mapping(value, "volume tmpfs options must be a mapping")?;
11786 let mut options = TmpfsMountOptions::new(effective_span(value));
11787 let mut path = parent_path.to_vec();
11788 path.push("tmpfs".to_owned());
11789 for field in fields {
11790 match field.key() {
11791 "size" => {
11792 if let Some(value) =
11793 self.located_string_or_number(field.value(), "tmpfs size must be a number or string")
11794 {
11795 options.set_size(value);
11796 } else {
11797 self.record_pending_unmodeled(&path, field);
11798 }
11799 }
11800 "mode" => {
11801 if let Some(value) =
11802 self.located_string_or_number(field.value(), "tmpfs mode must be a number or string")
11803 {
11804 options.set_mode(value);
11805 } else {
11806 self.record_pending_unmodeled(&path, field);
11807 }
11808 }
11809 _ => self.record_pending_unmodeled(&path, field),
11810 }
11811 }
11812 Some(options)
11813 }
11814
11815 fn volume_mount_options(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<VolumeMountOptions> {
11816 let fields = self.mapping(value, "named-volume options must be a mapping")?;
11817 let mut options = VolumeMountOptions::new(effective_span(value));
11818 let mut path = parent_path.to_vec();
11819 path.push("volume".to_owned());
11820 for field in fields {
11821 match field.key() {
11822 "nocopy" => {
11823 if let Some(value) = self.located_boolean(field.value(), "volume nocopy must be a boolean") {
11824 options.set_nocopy(value);
11825 } else {
11826 self.record_pending_unmodeled(&path, field);
11827 }
11828 }
11829 "subpath" => {
11830 if let Some(value) = self.located_string(field.value(), "volume subpath must be a scalar") {
11831 options.set_subpath(value);
11832 } else {
11833 self.record_pending_unmodeled(&path, field);
11834 }
11835 }
11836 "labels" => {
11837 if let Some(value) = self.labels(field.value()) {
11838 options.set_labels(value);
11839 } else {
11840 self.record_pending_unmodeled(&path, field);
11841 }
11842 }
11843 _ => self.record_pending_unmodeled(&path, field),
11844 }
11845 }
11846 Some(options)
11847 }
11848
11849 fn service_networks(
11850 &mut self,
11851 value: &MergedValue,
11852 service_path: &[String],
11853 ) -> Option<ProjectValue<ServiceNetworks>> {
11854 let span = effective_span(value);
11855 let networks = match value.kind() {
11856 MergedValueKind::Sequence(values) => {
11857 let mut names = Vec::new();
11858 for value in values {
11859 names.push(self.located_string(value, "service network name must be a scalar")?);
11860 }
11861 ServiceNetworks::Short { span, names }
11862 }
11863 MergedValueKind::Mapping(entries) => {
11864 let mut networks = Vec::new();
11865 for entry in entries {
11866 let mut path = service_path.to_vec();
11867 path.push("networks".to_owned());
11868 path.push(entry.key().to_owned());
11869 networks.push(self.service_network(entry, &path)?);
11870 }
11871 ServiceNetworks::Long { span, networks }
11872 }
11873 _ => {
11874 self.expected(value, "service networks must be a sequence or mapping");
11875 return None;
11876 }
11877 };
11878 Some(ProjectValue::new(networks, value))
11879 }
11880
11881 fn service_network(&mut self, entry: &MergedEntry, path: &[String]) -> Option<ServiceNetwork> {
11882 let value = entry.value();
11883 let span = effective_span(value);
11884 let mut network = ServiceNetwork::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
11885 let fields = match value.kind() {
11886 MergedValueKind::Null(_) => return Some(network),
11887 MergedValueKind::Mapping(fields) => fields,
11888 _ => {
11889 self.expected(value, "service network attachment must be a mapping or null");
11890 return None;
11891 }
11892 };
11893 for field in fields {
11894 match field.key() {
11895 "aliases" => self
11896 .located_string_sequence(field.value(), "network aliases must be a sequence")
11897 .into_iter()
11898 .for_each(|value| network.set_aliases(value)),
11899 "interface_name" => self
11900 .located_string(field.value(), "network interface_name must be a scalar")
11901 .into_iter()
11902 .for_each(|value| network.set_interface_name(value)),
11903 "ipv4_address" => self
11904 .located_string(field.value(), "network ipv4_address must be a scalar")
11905 .into_iter()
11906 .for_each(|value| network.set_ipv4_address(value)),
11907 "ipv6_address" => self
11908 .located_string(field.value(), "network ipv6_address must be a scalar")
11909 .into_iter()
11910 .for_each(|value| network.set_ipv6_address(value)),
11911 "link_local_ips" => self
11912 .located_string_sequence(field.value(), "link_local_ips must be a sequence")
11913 .into_iter()
11914 .for_each(|value| network.set_link_local_ips(value)),
11915 "mac_address" => self
11916 .located_string(field.value(), "network mac_address must be a scalar")
11917 .into_iter()
11918 .for_each(|value| network.set_mac_address(value)),
11919 "driver_opts" => self
11920 .key_value_mapping(field.value(), "network driver_opts must be a mapping")
11921 .into_iter()
11922 .for_each(|value| network.set_driver_opts(value)),
11923 "gw_priority" => self
11924 .located_string(field.value(), "network gw_priority must be a scalar")
11925 .into_iter()
11926 .for_each(|value| network.set_gw_priority(value)),
11927 "priority" => self
11928 .located_string(field.value(), "network priority must be a scalar")
11929 .into_iter()
11930 .for_each(|value| network.set_priority(value)),
11931 _ => self.record_pending_unmodeled(path, field),
11932 }
11933 }
11934 Some(network)
11935 }
11936
11937 fn located_boolean(&mut self, value: &MergedValue, message: &str) -> Option<Located<BooleanValue>> {
11938 let scalar = self.scalar(value, message)?;
11939 let boolean = if scalar.kind() == MergedScalarKind::Boolean {
11940 BooleanValue::Literal(scalar.value().eq_ignore_ascii_case("true"))
11941 } else if value.is_sensitive() && matches!(scalar.value(), "true" | "false") {
11942 BooleanValue::Literal(scalar.value() == "true")
11943 } else if scalar.value().contains('$') {
11944 BooleanValue::Expression(scalar.value().to_owned())
11945 } else {
11946 self.invalid(effective_span(value), message);
11947 return None;
11948 };
11949 Some(Located::new(boolean, effective_span(value)))
11950 }
11951
11952 fn resource_external(&mut self, value: &MergedValue, kind: &str) -> Option<ResourceExternal> {
11953 let Some(entries) = value.as_mapping() else {
11954 return self
11955 .located_boolean(value, &format!("{kind} external must be a boolean"))
11956 .map(ResourceExternal::Boolean);
11957 };
11958 let mut name_mapping = ExternalNameMapping::new(effective_span(value));
11959 for entry in entries {
11960 match entry.key() {
11961 "name" => match self.strict_located_string(
11962 entry.value(),
11963 "deprecated external name must be a non-empty YAML string",
11964 ) {
11965 Some(name) if !name.value().is_empty() => name_mapping.set_name(name),
11966 Some(_) | None => name_mapping.push_unknown(FieldReference::generated(
11967 Located::new(entry.key().to_owned(), entry_span(entry)),
11968 entry_span(entry),
11969 Some(effective_span(entry.value())),
11970 )),
11971 },
11972 key if key.starts_with("x-") => name_mapping.push_extension(FieldReference::generated(
11973 Located::new(key.to_owned(), entry_span(entry)),
11974 entry_span(entry),
11975 Some(effective_span(entry.value())),
11976 )),
11977 _ => name_mapping.push_unknown(FieldReference::generated(
11978 Located::new(entry.key().to_owned(), entry_span(entry)),
11979 entry_span(entry),
11980 Some(effective_span(entry.value())),
11981 )),
11982 }
11983 }
11984 if name_mapping.name().is_none() {
11985 self.missing(value, "deprecated external mapping requires a non-empty `name` string");
11986 }
11987 self.diagnostics.push(
11988 Diagnostic::new(
11989 crate::model::RESOURCE_EXTERNAL_NAME_MAPPING_DEPRECATED,
11990 Severity::Warning,
11991 format!("{kind} `external.name` mapping syntax is deprecated"),
11992 )
11993 .with_label(DiagnosticLabel::primary(
11994 name_mapping.span(),
11995 "use `name: VALUE` with `external: true` instead",
11996 )),
11997 );
11998 Some(ResourceExternal::NameMapping(Box::new(name_mapping)))
11999 }
12000
12001 fn validate_resource_external_name(
12002 &mut self,
12003 external: Option<&ResourceExternal>,
12004 modern: Option<&Located<String>>,
12005 fallback: SourceSpan,
12006 ) {
12007 let (Some(ResourceExternal::NameMapping(name_mapping)), Some(modern)) = (external, modern) else {
12008 return;
12009 };
12010 self.diagnostics.push(
12011 Diagnostic::new(
12012 crate::model::RESOURCE_EXTERNAL_NAME_CONFLICT,
12013 Severity::Error,
12014 "resource cannot use both `name` and deprecated `external.name` mapping syntax",
12015 )
12016 .with_label(DiagnosticLabel::primary(modern.span(), "resource name"))
12017 .with_label(DiagnosticLabel::secondary(
12018 name_mapping.name().map_or_else(|| name_mapping.span(), Located::span),
12019 "deprecated external name",
12020 ))
12021 .with_label(DiagnosticLabel::secondary(fallback, "both values remain retained")),
12022 );
12023 }
12024
12025 fn validate_external_creation_configuration(
12026 &mut self,
12027 external: Option<&ResourceExternal>,
12028 fields: &[Option<SourceSpan>],
12029 kind: &str,
12030 ) {
12031 if !external.is_some_and(ResourceExternal::is_explicitly_external) {
12032 return;
12033 }
12034 let Some(span) = fields.iter().flatten().next().copied() else {
12035 return;
12036 };
12037 self.diagnostics.push(
12038 Diagnostic::new(
12039 crate::model::RESOURCE_EXTERNAL_CREATION_CONFIGURATION,
12040 Severity::Error,
12041 format!("external {kind} cannot also configure creation-time metadata"),
12042 )
12043 .with_label(DiagnosticLabel::primary(span, "metadata remains retained for review")),
12044 );
12045 }
12046
12047 fn located_string_sequence(&mut self, value: &MergedValue, message: &str) -> Option<Vec<Located<String>>> {
12048 let Some(values) = value.as_sequence() else {
12049 self.expected(value, message);
12050 return None;
12051 };
12052 let mut strings = Vec::new();
12053 for value in values {
12054 strings.push(self.located_string(value, "sequence item must be a scalar")?);
12055 }
12056 Some(strings)
12057 }
12058
12059 fn key_value_mapping(&mut self, value: &MergedValue, message: &str) -> Option<Vec<KeyValueEntry>> {
12060 let Some(entries) = value.as_mapping() else {
12061 self.expected(value, message);
12062 return None;
12063 };
12064 let mut values = Vec::new();
12065 for entry in entries {
12066 let scalar = self.compose_scalar(entry.value(), "mapping value must be a scalar or null")?;
12067 let value_span = effective_span(entry.value());
12068 values.push(KeyValueEntry::new(
12069 Located::new(entry.key().to_owned(), entry_span(entry)),
12070 Located::new(scalar, value_span),
12071 value_span,
12072 ));
12073 }
12074 Some(values)
12075 }
12076
12077 fn network_definitions(&mut self, value: &MergedValue) -> Vec<ProjectResource<NetworkDefinition>> {
12078 let Some(entries) = self.mapping(value, "top-level networks must be a mapping") else {
12079 return Vec::new();
12080 };
12081 entries
12082 .iter()
12083 .filter_map(|entry| {
12084 let definition = self.network_definition(entry)?;
12085 Some(ProjectResource {
12086 name: ProjectKey::from_entry(entry),
12087 definition: ProjectValue::new(definition, entry.value()),
12088 })
12089 })
12090 .collect()
12091 }
12092
12093 fn network_definition(&mut self, entry: &MergedEntry) -> Option<NetworkDefinition> {
12094 let value = entry.value();
12095 let span = effective_span(value);
12096 let mut network = NetworkDefinition::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
12097 let fields = match value.kind() {
12098 MergedValueKind::Null(_) => return Some(network),
12099 MergedValueKind::Mapping(fields) => fields,
12100 _ => {
12101 self.expected(value, "network definition must be a mapping or null");
12102 return None;
12103 }
12104 };
12105 let path = ["networks".to_owned(), entry.key().to_owned()];
12106 for field in fields {
12107 match field.key() {
12108 "driver" => self
12109 .located_string(field.value(), "network driver must be a scalar")
12110 .into_iter()
12111 .for_each(|value| network.set_driver(value)),
12112 "driver_opts" => self
12113 .key_value_mapping(field.value(), "network driver_opts must be a mapping")
12114 .into_iter()
12115 .for_each(|value| network.set_driver_opts(value)),
12116 "attachable" => self
12117 .located_boolean(field.value(), "network attachable must be a boolean")
12118 .into_iter()
12119 .for_each(|value| network.set_attachable(value)),
12120 "enable_ipv4" => self
12121 .located_boolean(field.value(), "network enable_ipv4 must be a boolean")
12122 .into_iter()
12123 .for_each(|value| network.set_enable_ipv4(value)),
12124 "enable_ipv6" => self
12125 .located_boolean(field.value(), "network enable_ipv6 must be a boolean")
12126 .into_iter()
12127 .for_each(|value| network.set_enable_ipv6(value)),
12128 "external" => self
12129 .resource_external(field.value(), "network")
12130 .into_iter()
12131 .for_each(|value| network.set_external(value)),
12132 "internal" => self
12133 .located_boolean(field.value(), "network internal must be a boolean")
12134 .into_iter()
12135 .for_each(|value| network.set_internal(value)),
12136 "ipam" => self
12137 .ipam(field.value(), &path)
12138 .into_iter()
12139 .for_each(|value| network.set_ipam(value)),
12140 "labels" => self
12141 .labels(field.value())
12142 .into_iter()
12143 .for_each(|value| network.set_labels(value)),
12144 "name" => self
12145 .located_string(field.value(), "network custom name must be a scalar")
12146 .into_iter()
12147 .for_each(|value| network.set_custom_name(value)),
12148 _ => self.record_root_unmodeled(&path, field),
12149 }
12150 }
12151 self.validate_resource_external_name(network.external(), network.custom_name(), network.span());
12152 Some(network)
12153 }
12154
12155 fn ipam(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<Ipam> {
12156 let fields = self.mapping(value, "network IPAM must be a mapping")?;
12157 let mut ipam = Ipam::new(effective_span(value));
12158 let mut path = parent_path.to_vec();
12159 path.push("ipam".to_owned());
12160 for field in fields {
12161 match field.key() {
12162 "driver" => self
12163 .located_string(field.value(), "IPAM driver must be a scalar")
12164 .into_iter()
12165 .for_each(|value| ipam.set_driver(value)),
12166 "config" => self
12167 .ipam_configs(field.value(), &path)
12168 .into_iter()
12169 .for_each(|value| ipam.set_config(value)),
12170 "options" => self
12171 .key_value_mapping(field.value(), "IPAM options must be a mapping")
12172 .into_iter()
12173 .for_each(|value| ipam.set_options(value)),
12174 _ => self.record_root_unmodeled(&path, field),
12175 }
12176 }
12177 Some(ipam)
12178 }
12179
12180 fn ipam_configs(&mut self, value: &MergedValue, parent_path: &[String]) -> Option<Vec<IpamConfig>> {
12181 let Some(values) = value.as_sequence() else {
12182 self.expected(value, "IPAM config must be a sequence");
12183 return None;
12184 };
12185 let mut configs = Vec::new();
12186 for (index, value) in values.iter().enumerate() {
12187 let Some(fields) = value.as_mapping() else {
12188 self.expected(value, "IPAM config entry must be a mapping");
12189 continue;
12190 };
12191 let mut config = IpamConfig::new(effective_span(value));
12192 let mut path = parent_path.to_vec();
12193 path.push("config".to_owned());
12194 path.push(index.to_string());
12195 for field in fields {
12196 match field.key() {
12197 "subnet" => self
12198 .located_string(field.value(), "IPAM subnet must be a scalar")
12199 .into_iter()
12200 .for_each(|value| config.set_subnet(value)),
12201 "ip_range" => self
12202 .located_string(field.value(), "IPAM ip_range must be a scalar")
12203 .into_iter()
12204 .for_each(|value| config.set_ip_range(value)),
12205 "gateway" => self
12206 .located_string(field.value(), "IPAM gateway must be a scalar")
12207 .into_iter()
12208 .for_each(|value| config.set_gateway(value)),
12209 "aux_addresses" => self
12210 .key_value_mapping(field.value(), "IPAM aux_addresses must be a mapping")
12211 .into_iter()
12212 .for_each(|value| config.set_aux_addresses(value)),
12213 _ => self.record_root_unmodeled(&path, field),
12214 }
12215 }
12216 configs.push(config);
12217 }
12218 Some(configs)
12219 }
12220
12221 fn volume_definitions(&mut self, value: &MergedValue) -> Vec<ProjectResource<VolumeDefinition>> {
12222 let Some(entries) = self.mapping(value, "top-level volumes must be a mapping") else {
12223 return Vec::new();
12224 };
12225 entries
12226 .iter()
12227 .filter_map(|entry| {
12228 let definition = self.volume_definition(entry)?;
12229 Some(ProjectResource {
12230 name: ProjectKey::from_entry(entry),
12231 definition: ProjectValue::new(definition, entry.value()),
12232 })
12233 })
12234 .collect()
12235 }
12236
12237 fn volume_definition(&mut self, entry: &MergedEntry) -> Option<VolumeDefinition> {
12238 let value = entry.value();
12239 let span = effective_span(value);
12240 let mut volume = VolumeDefinition::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
12241 let fields = match value.kind() {
12242 MergedValueKind::Null(_) => return Some(volume),
12243 MergedValueKind::Mapping(fields) => fields,
12244 _ => {
12245 self.expected(value, "volume definition must be a mapping or null");
12246 return None;
12247 }
12248 };
12249 let path = ["volumes".to_owned(), entry.key().to_owned()];
12250 for field in fields {
12251 match field.key() {
12252 "driver" => self
12253 .located_string(field.value(), "volume driver must be a scalar")
12254 .into_iter()
12255 .for_each(|value| volume.set_driver(value)),
12256 "driver_opts" => self
12257 .key_value_mapping(field.value(), "volume driver_opts must be a mapping")
12258 .into_iter()
12259 .for_each(|value| volume.set_driver_opts(value)),
12260 "external" => self
12261 .resource_external(field.value(), "volume")
12262 .into_iter()
12263 .for_each(|value| volume.set_external(value)),
12264 "labels" => self
12265 .labels(field.value())
12266 .into_iter()
12267 .for_each(|value| volume.set_labels(value)),
12268 "name" => self
12269 .located_string(field.value(), "volume custom name must be a scalar")
12270 .into_iter()
12271 .for_each(|value| volume.set_custom_name(value)),
12272 _ => self.record_root_unmodeled(&path, field),
12273 }
12274 }
12275 self.validate_external_volume_driver_configuration(&volume);
12276 self.validate_external_volume_labels_configuration(&volume);
12277 self.validate_resource_external_name(volume.external(), volume.custom_name(), volume.span());
12278 Some(volume)
12279 }
12280
12281 fn validate_external_volume_driver_configuration(&mut self, volume: &VolumeDefinition) {
12282 if !volume.external().is_some_and(ResourceExternal::is_explicitly_external)
12283 || (volume.driver().is_none() && volume.driver_opts().is_empty())
12284 {
12285 return;
12286 }
12287 let span = volume
12288 .driver()
12289 .map(Located::span)
12290 .or_else(|| volume.driver_opts().first().map(KeyValueEntry::span))
12291 .unwrap_or_else(|| volume.span());
12292 self.diagnostics.push(
12293 Diagnostic::new(
12294 VOLUME_EXTERNAL_DRIVER_CONFIGURATION,
12295 Severity::Error,
12296 "external volume cannot also configure `driver` or `driver_opts`",
12297 )
12298 .with_label(DiagnosticLabel::primary(
12299 span,
12300 "driver configuration remains retained for review",
12301 )),
12302 );
12303 }
12304
12305 fn validate_external_volume_labels_configuration(&mut self, volume: &VolumeDefinition) {
12306 if !volume.external().is_some_and(ResourceExternal::is_explicitly_external) || volume.labels().is_none() {
12307 return;
12308 }
12309 let span = volume.labels().map_or_else(|| volume.span(), Labels::span);
12310 self.diagnostics.push(
12311 Diagnostic::new(
12312 VOLUME_EXTERNAL_LABELS_CONFIGURATION,
12313 Severity::Error,
12314 "external volume cannot also configure `labels`",
12315 )
12316 .with_label(DiagnosticLabel::primary(span, "labels remain retained for review")),
12317 );
12318 }
12319
12320 fn config_definitions(&mut self, value: &MergedValue) -> Vec<ProjectResource<ConfigDefinition>> {
12321 let Some(entries) = self.mapping(value, "top-level configs must be a mapping") else {
12322 return Vec::new();
12323 };
12324 entries
12325 .iter()
12326 .filter_map(|entry| {
12327 let definition = self.config_definition(entry)?;
12328 Some(ProjectResource {
12329 name: ProjectKey::from_entry(entry),
12330 definition: ProjectValue::new(definition, entry.value()),
12331 })
12332 })
12333 .collect()
12334 }
12335
12336 fn config_definition(&mut self, entry: &MergedEntry) -> Option<ConfigDefinition> {
12337 let value = entry.value();
12338 let span = effective_span(value);
12339 let mut config = ConfigDefinition::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
12340 let fields = match value.kind() {
12341 MergedValueKind::Null(_) => return Some(config),
12342 MergedValueKind::Mapping(fields) => fields,
12343 _ => {
12344 self.expected(value, "config definition must be a mapping or null");
12345 return None;
12346 }
12347 };
12348 let path = ["configs".to_owned(), entry.key().to_owned()];
12349 for field in fields {
12350 match field.key() {
12351 "file" => self
12352 .located_string(field.value(), "config file must be a scalar")
12353 .into_iter()
12354 .for_each(|value| config.set_file(value)),
12355 "environment" => self
12356 .located_string(field.value(), "config environment must be a scalar")
12357 .into_iter()
12358 .for_each(|value| config.set_environment(value)),
12359 "content" => self
12360 .located_string(field.value(), "config content must be a scalar")
12361 .into_iter()
12362 .for_each(|value| config.set_content(value)),
12363 "external" => self
12364 .resource_external(field.value(), "config")
12365 .into_iter()
12366 .for_each(|value| config.set_external(value)),
12367 "labels" => self
12368 .labels(field.value())
12369 .into_iter()
12370 .for_each(|value| config.set_labels(value)),
12371 "template_driver" => self
12372 .strict_located_string(field.value(), "config template_driver must be a YAML string")
12373 .into_iter()
12374 .for_each(|value| config.set_template_driver(value)),
12375 "name" => self
12376 .located_string(field.value(), "config custom name must be a scalar")
12377 .into_iter()
12378 .for_each(|value| config.set_custom_name(value)),
12379 _ => self.record_root_unmodeled(&path, field),
12380 }
12381 }
12382 self.validate_resource_external_name(config.external(), config.custom_name(), config.span());
12383 self.validate_external_creation_configuration(
12384 config.external(),
12385 &[
12386 config.file().map(Located::span),
12387 config.environment().map(Located::span),
12388 config.content().map(Located::span),
12389 config.labels().map(Labels::span),
12390 config.template_driver().map(Located::span),
12391 ],
12392 "config",
12393 );
12394 Some(config)
12395 }
12396
12397 fn secret_definitions(&mut self, value: &MergedValue) -> Vec<ProjectResource<SecretDefinition>> {
12398 let Some(entries) = self.mapping(value, "top-level secrets must be a mapping") else {
12399 return Vec::new();
12400 };
12401 entries
12402 .iter()
12403 .filter_map(|entry| {
12404 let definition = self.secret_definition(entry)?;
12405 Some(ProjectResource {
12406 name: ProjectKey::from_entry(entry),
12407 definition: ProjectValue::new(definition, entry.value()),
12408 })
12409 })
12410 .collect()
12411 }
12412
12413 fn secret_definition(&mut self, entry: &MergedEntry) -> Option<SecretDefinition> {
12414 let value = entry.value();
12415 let span = effective_span(value);
12416 let mut secret = SecretDefinition::new(Located::new(entry.key().to_owned(), entry_span(entry)), span);
12417 let fields = match value.kind() {
12418 MergedValueKind::Null(_) => return Some(secret),
12419 MergedValueKind::Mapping(fields) => fields,
12420 _ => {
12421 self.expected(value, "secret definition must be a mapping or null");
12422 return None;
12423 }
12424 };
12425 let path = ["secrets".to_owned(), entry.key().to_owned()];
12426 for field in fields {
12427 match field.key() {
12428 "file" => self
12429 .located_string(field.value(), "secret file must be a scalar")
12430 .into_iter()
12431 .for_each(|value| secret.set_file(value)),
12432 "environment" => self
12433 .located_string(field.value(), "secret environment must be a scalar")
12434 .into_iter()
12435 .for_each(|value| secret.set_environment(value)),
12436 "external" => self
12437 .resource_external(field.value(), "secret")
12438 .into_iter()
12439 .for_each(|value| secret.set_external(value)),
12440 "driver" => self
12441 .strict_located_string(field.value(), "secret driver must be a YAML string")
12442 .into_iter()
12443 .for_each(|value| secret.set_driver(value)),
12444 "driver_opts" => self
12445 .key_value_mapping(field.value(), "secret driver_opts must be a mapping")
12446 .into_iter()
12447 .for_each(|value| secret.set_driver_opts(value)),
12448 "labels" => self
12449 .labels(field.value())
12450 .into_iter()
12451 .for_each(|value| secret.set_labels(value)),
12452 "template_driver" => self
12453 .strict_located_string(field.value(), "secret template_driver must be a YAML string")
12454 .into_iter()
12455 .for_each(|value| secret.set_template_driver(value)),
12456 "name" => self
12457 .located_string(field.value(), "secret custom name must be a scalar")
12458 .into_iter()
12459 .for_each(|value| secret.set_custom_name(value)),
12460 _ => self.record_root_unmodeled(&path, field),
12461 }
12462 }
12463 self.validate_resource_external_name(secret.external(), secret.custom_name(), secret.span());
12464 self.validate_external_creation_configuration(
12465 secret.external(),
12466 &[
12467 secret.file().map(Located::span),
12468 secret.environment().map(Located::span),
12469 secret.driver().map(Located::span),
12470 secret.driver_opts().first().map(KeyValueEntry::span),
12471 secret.labels().map(Labels::span),
12472 secret.template_driver().map(Located::span),
12473 ],
12474 "secret",
12475 );
12476 Some(secret)
12477 }
12478
12479 fn labels(&mut self, value: &MergedValue) -> Option<Labels> {
12480 let span = effective_span(value);
12481 match value.kind() {
12482 MergedValueKind::Sequence(_) => self
12483 .located_string_sequence(value, "labels must be a scalar sequence")
12484 .map(|values| Labels::List { span, values }),
12485 MergedValueKind::Mapping(_) => self
12486 .key_value_mapping(value, "labels must be a scalar mapping")
12487 .map(|entries| Labels::Map { span, entries }),
12488 _ => {
12489 self.expected(value, "labels must be a sequence or mapping");
12490 None
12491 }
12492 }
12493 }
12494}
12495
12496fn compose_scalar_from_merged(scalar: &crate::merge::MergedScalar) -> ComposeScalar {
12497 match scalar.kind() {
12498 MergedScalarKind::String => ComposeScalar::String(scalar.value().to_owned()),
12499 MergedScalarKind::Boolean => ComposeScalar::Boolean(scalar.value().eq_ignore_ascii_case("true")),
12500 MergedScalarKind::Number => ComposeScalar::Number(scalar.value().to_owned()),
12501 }
12502}
12503
12504fn project_command_is_non_empty(command: &Command) -> bool {
12505 match command {
12506 Command::Null(_) => false,
12507 Command::String(value) => !value.value().is_empty(),
12508 Command::List { values, .. } => !values.is_empty(),
12509 }
12510}
12511
12512fn project_value_span<T>(value: &ProjectValue<T>) -> SourceSpan {
12513 let Some(span) = value.provenance().effective_source() else {
12514 unreachable!("merged project values retain source provenance");
12515 };
12516 span
12517}
12518
12519fn normalize_integer(value: &str) -> Option<String> {
12524 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
12525 return None;
12526 }
12527 Some(value.trim_start_matches('0').to_owned().if_empty_then("0"))
12528}
12529
12530trait EmptyStringExt {
12531 fn if_empty_then(self, fallback: &str) -> Self;
12532}
12533impl EmptyStringExt for String {
12534 fn if_empty_then(self, fallback: &str) -> Self {
12535 if self.is_empty() { fallback.to_owned() } else { self }
12536 }
12537}
12538
12539fn normalize_decimal(value: &str) -> Option<String> {
12541 let (whole, fraction) = value.split_once('.').map_or((value, ""), |parts| parts);
12542 if whole.is_empty()
12543 || (value.contains('.') && fraction.is_empty())
12544 || !whole.bytes().all(|byte| byte.is_ascii_digit())
12545 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
12546 {
12547 return None;
12548 }
12549 let whole = whole.trim_start_matches('0');
12550 let fraction = fraction.trim_end_matches('0');
12551 let whole = if whole.is_empty() { "0" } else { whole };
12552 Some(if fraction.is_empty() {
12553 whole.to_owned()
12554 } else {
12555 format!("{whole}.{fraction}")
12556 })
12557}
12558
12559fn same_memory_value(service: &MemLimit, deploy: &DeployResourceMemory) -> Option<bool> {
12562 fn parts(value: &str) -> Option<(&str, &str)> {
12563 ["kb", "mb", "gb", "b", "k", "m", "g"]
12564 .into_iter()
12565 .find_map(|suffix| value.strip_suffix(suffix).map(|amount| (amount, suffix)))
12566 }
12567 let (service_amount, service_unit) = parts(service.raw().value())?;
12568 let (deploy_amount, deploy_unit) = parts(deploy.raw())?;
12569 let (Some(service_amount), Some(deploy_amount)) =
12570 (normalize_integer(service_amount), normalize_integer(deploy_amount))
12571 else {
12572 return None;
12573 };
12574 Some(service_unit == deploy_unit && service_amount == deploy_amount)
12575}
12576
12577fn memory_compare_same_unit(left: &str, right: &str) -> Option<std::cmp::Ordering> {
12578 fn parts(value: &str) -> Option<(&str, &str)> {
12579 ["kb", "mb", "gb", "b", "k", "m", "g"]
12580 .into_iter()
12581 .find_map(|unit| value.strip_suffix(unit).map(|amount| (amount, unit)))
12582 }
12583 let (left_amount, left_unit) = parts(left)?;
12584 let (right_amount, right_unit) = parts(right)?;
12585 if left_unit != right_unit {
12586 return None;
12587 }
12588 let left_amount = normalize_integer(left_amount)?;
12589 let right_amount = normalize_integer(right_amount)?;
12590 Some(
12591 left_amount
12592 .len()
12593 .cmp(&right_amount.len())
12594 .then_with(|| left_amount.cmp(&right_amount)),
12595 )
12596}
12597
12598fn field_reference(path: &[String], entry: &MergedEntry) -> ProjectFieldReference {
12599 let mut complete_path = path.to_vec();
12600 complete_path.push(entry.key().to_owned());
12601 ProjectFieldReference {
12602 path: complete_path,
12603 key: ProjectKey::from_entry(entry),
12604 provenance: entry.value().provenance().clone(),
12605 extension: entry.key().starts_with("x-"),
12606 sensitive: entry.value().is_sensitive(),
12607 }
12608}
12609
12610fn value_reference(path: &[String], value: &MergedValue) -> ProjectFieldReference {
12611 let key = path.last().cloned().unwrap_or_default();
12612 ProjectFieldReference {
12613 path: path.to_vec(),
12614 key: ProjectKey::from_value(key, value),
12615 provenance: value.provenance().clone(),
12616 extension: false,
12617 sensitive: value.is_sensitive(),
12618 }
12619}
12620
12621fn effective_span(value: &MergedValue) -> SourceSpan {
12622 value
12623 .provenance()
12624 .effective_source()
12625 .or_else(|| value.provenance().sources().first().copied())
12626 .unwrap_or_else(|| SourceSpan::from_valid_offsets(SourceId::new(0), 0, 0))
12627}
12628
12629fn entry_span(entry: &MergedEntry) -> SourceSpan {
12630 entry
12631 .key_sources()
12632 .last()
12633 .copied()
12634 .or_else(|| entry.key_sources().first().copied())
12635 .unwrap_or_else(|| effective_span(entry.value()))
12636}