1mod command;
4mod dependency;
5mod environment;
6mod host;
7mod identity;
8mod image;
9mod network;
10mod port;
11mod resource;
12mod restart;
13mod sections;
14mod ulimit;
15mod value;
16mod volume;
17
18pub use command::Command;
19pub use dependency::{
20 DependencyCondition, DependsOn, Healthcheck, HealthcheckDuration, HealthcheckRetries, HealthcheckTest,
21 HealthcheckTestKind, ServiceDependency,
22};
23pub use environment::{
24 Environment, EnvironmentFile, EnvironmentFileFormat, EnvironmentFileFormatKind, EnvironmentListEntry,
25 EnvironmentMapEntry, LongEnvironmentFile,
26};
27pub use host::{ExtraHostSeparator, ExtraHosts, HostAddress, HostAddressKind, LongExtraHost, ShortExtraHost};
28pub use identity::{IdentityComponent, UserNamespaceMode, UserNamespaceModeKind, UserSpec};
29pub use image::{ImageDigest, ImageReference};
30pub use network::{Ipam, IpamConfig, NetworkDefinition, ServiceNetwork, ServiceNetworks};
31pub use port::{LongPort, Port, ShortPort};
32pub use resource::{ConfigDefinition, ConfigGrant, LongGrant, SecretDefinition, SecretGrant, VolumeDefinition};
33pub use restart::{RestartPolicy, RestartPolicyKind};
34pub use sections::{
35 Build, BuildDefinition, BuildField, BuildFieldKind, DeployDefinition, DeployField, DeployFieldKind,
36};
37pub use ulimit::{LimitValue, Ulimit, UlimitRange, UlimitValue, Ulimits};
38pub use value::{BooleanValue, ComposeScalar, KeyValueEntry, Labels};
39pub use volume::{
40 BindOptions, ContainerPath, ContainerPathKind, LongVolumeMount, MountType, SelinuxRelabel, ShortVolumeMount,
41 VolumeMount, VolumeSyntax,
42};
43
44use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
45use crate::source::{SourceId, SourceSpan};
46use crate::syntax::{SyntaxDocument, scalar_string_from_source};
47use std::collections::{BTreeMap, BTreeSet};
48use yaml_edit::{AnchorRegistry, AsYaml, Mapping, ScalarType, ScalarValue, YamlNode};
49
50pub const DOCUMENT_ROOT_TYPE: DiagnosticCode = DiagnosticCode::new("compose.document.expected-mapping");
52
53pub const MULTIPLE_DOCUMENTS: DiagnosticCode = DiagnosticCode::new("compose.document.multiple-documents");
55
56pub const DUPLICATE_FIELD: DiagnosticCode = DiagnosticCode::new("compose.model.duplicate-field");
58
59pub const EXPECTED_MAPPING: DiagnosticCode = DiagnosticCode::new("compose.model.expected-mapping");
61
62pub const EXPECTED_SEQUENCE: DiagnosticCode = DiagnosticCode::new("compose.model.expected-sequence");
64
65pub const EXPECTED_SCALAR: DiagnosticCode = DiagnosticCode::new("compose.model.expected-scalar");
67
68pub const EXPECTED_BOOLEAN: DiagnosticCode = DiagnosticCode::new("compose.model.expected-boolean");
70
71pub const EXPECTED_FIELD_FORM: DiagnosticCode = DiagnosticCode::new("compose.model.expected-field-form");
73
74pub const PORT_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.port.expected-short-or-long");
76
77pub const PORT_MISSING_TARGET: DiagnosticCode = DiagnosticCode::new("compose.port.long.missing-target");
79
80pub const GRANT_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.grant.expected-short-or-long");
82
83pub const GRANT_MISSING_SOURCE: DiagnosticCode = DiagnosticCode::new("compose.grant.long.missing-source");
85
86pub const RESOURCE_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.resource.expected-mapping-or-null");
88
89pub const VOLUME_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.volume.expected-short-or-long");
91
92pub const VOLUME_MISSING_TYPE: DiagnosticCode = DiagnosticCode::new("compose.volume.long.missing-type");
94
95pub const VOLUME_MISSING_TARGET: DiagnosticCode = DiagnosticCode::new("compose.volume.long.missing-target");
97
98pub const VOLUME_INVALID_SELINUX: DiagnosticCode = DiagnosticCode::new("compose.volume.bind.invalid-selinux");
100
101pub const EXTRA_HOST_INVALID_ENTRY: DiagnosticCode = DiagnosticCode::new("compose.extra-hosts.invalid-entry");
103
104pub const ULIMIT_INVALID_VALUE: DiagnosticCode = DiagnosticCode::new("compose.ulimits.invalid-value");
106
107pub const HEALTHCHECK_INVALID_TEST: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-test");
109
110pub const HEALTHCHECK_INVALID_DURATION: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-duration");
112
113pub const HEALTHCHECK_INVALID_RETRIES: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-retries");
115
116pub const RESTART_INVALID_POLICY: DiagnosticCode = DiagnosticCode::new("compose.restart.invalid-policy");
118
119pub const ENVIRONMENT_FILE_EXPECTED_FORM: DiagnosticCode =
121 DiagnosticCode::new("compose.environment-file.expected-short-or-long");
122
123pub const ENVIRONMENT_FILE_MISSING_PATH: DiagnosticCode =
125 DiagnosticCode::new("compose.environment-file.long.missing-path");
126
127pub const ENVIRONMENT_FILE_INVALID_FORMAT: DiagnosticCode =
129 DiagnosticCode::new("compose.environment-file.invalid-format");
130
131pub const DEPENDENCY_INVALID_CONDITION: DiagnosticCode = DiagnosticCode::new("compose.dependencies.invalid-condition");
133
134pub const DEPENDENCY_MISSING_SERVICE: DiagnosticCode = DiagnosticCode::new("compose.dependencies.missing-service");
136
137pub const DEPENDENCY_MISSING_HEALTHCHECK: DiagnosticCode =
139 DiagnosticCode::new("compose.dependencies.missing-healthcheck");
140
141pub const DEPENDENCY_HEALTHCHECK_UNVERIFIED: DiagnosticCode =
143 DiagnosticCode::new("compose.dependencies.healthcheck-unverified");
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Located<T> {
148 value: T,
149 span: SourceSpan,
150}
151
152impl<T> Located<T> {
153 pub(crate) const fn new(value: T, span: SourceSpan) -> Self {
154 Self { value, span }
155 }
156
157 #[must_use]
159 pub const fn value(&self) -> &T {
160 &self.value
161 }
162
163 #[must_use]
165 pub const fn span(&self) -> SourceSpan {
166 self.span
167 }
168
169 #[must_use]
171 pub fn into_value(self) -> T {
172 self.value
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct FieldReference {
182 name: Located<String>,
183 span: SourceSpan,
184 value_span: Option<SourceSpan>,
185}
186
187impl FieldReference {
188 #[must_use]
190 pub const fn name(&self) -> &Located<String> {
191 &self.name
192 }
193
194 #[must_use]
196 pub const fn span(&self) -> SourceSpan {
197 self.span
198 }
199
200 #[must_use]
202 pub const fn value_span(&self) -> Option<SourceSpan> {
203 self.value_span
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct Service {
210 name: Located<String>,
211 span: SourceSpan,
212 container_name: Option<Located<String>>,
213 image: Option<Located<ImageReference>>,
214 command: Option<Command>,
215 environment: Option<Environment>,
216 environment_files: Vec<EnvironmentFile>,
217 labels: Option<Labels>,
218 extra_hosts: Option<ExtraHosts>,
219 user: Option<UserSpec>,
220 userns_mode: Option<UserNamespaceMode>,
221 group_add: Vec<Located<String>>,
222 working_dir: Option<Located<String>>,
223 read_only: Option<Located<BooleanValue>>,
224 restart: Option<RestartPolicy>,
225 ulimits: Option<Ulimits>,
226 depends_on: Option<DependsOn>,
227 healthcheck: Option<Healthcheck>,
228 build: Option<Build>,
229 deploy: Option<DeployDefinition>,
230 ports: Vec<Port>,
231 volumes: Vec<VolumeMount>,
232 networks: Option<ServiceNetworks>,
233 profiles: Vec<Located<String>>,
234 configs: Vec<ConfigGrant>,
235 secrets: Vec<SecretGrant>,
236 extension_fields: Vec<FieldReference>,
237 unknown_fields: Vec<FieldReference>,
238}
239
240impl Service {
241 fn new(name: Located<String>, span: SourceSpan) -> Self {
242 Self {
243 name,
244 span,
245 container_name: None,
246 image: None,
247 command: None,
248 environment: None,
249 environment_files: Vec::new(),
250 labels: None,
251 extra_hosts: None,
252 user: None,
253 userns_mode: None,
254 group_add: Vec::new(),
255 working_dir: None,
256 read_only: None,
257 restart: None,
258 ulimits: None,
259 depends_on: None,
260 healthcheck: None,
261 build: None,
262 deploy: None,
263 ports: Vec::new(),
264 volumes: Vec::new(),
265 networks: None,
266 profiles: Vec::new(),
267 configs: Vec::new(),
268 secrets: Vec::new(),
269 extension_fields: Vec::new(),
270 unknown_fields: Vec::new(),
271 }
272 }
273
274 #[must_use]
276 pub const fn name(&self) -> &Located<String> {
277 &self.name
278 }
279
280 #[must_use]
282 pub const fn span(&self) -> SourceSpan {
283 self.span
284 }
285
286 #[must_use]
288 pub const fn container_name(&self) -> Option<&Located<String>> {
289 self.container_name.as_ref()
290 }
291
292 #[must_use]
294 pub const fn image(&self) -> Option<&Located<ImageReference>> {
295 self.image.as_ref()
296 }
297
298 #[must_use]
300 pub const fn command(&self) -> Option<&Command> {
301 self.command.as_ref()
302 }
303
304 #[must_use]
306 pub const fn environment(&self) -> Option<&Environment> {
307 self.environment.as_ref()
308 }
309
310 #[must_use]
312 pub fn environment_files(&self) -> &[EnvironmentFile] {
313 &self.environment_files
314 }
315
316 #[must_use]
318 pub const fn labels(&self) -> Option<&Labels> {
319 self.labels.as_ref()
320 }
321
322 #[must_use]
324 pub const fn extra_hosts(&self) -> Option<&ExtraHosts> {
325 self.extra_hosts.as_ref()
326 }
327
328 #[must_use]
330 pub const fn user(&self) -> Option<&UserSpec> {
331 self.user.as_ref()
332 }
333
334 #[must_use]
336 pub const fn userns_mode(&self) -> Option<&UserNamespaceMode> {
337 self.userns_mode.as_ref()
338 }
339
340 #[must_use]
342 pub fn group_add(&self) -> &[Located<String>] {
343 &self.group_add
344 }
345
346 #[must_use]
348 pub const fn working_dir(&self) -> Option<&Located<String>> {
349 self.working_dir.as_ref()
350 }
351
352 #[must_use]
354 pub const fn read_only(&self) -> Option<&Located<BooleanValue>> {
355 self.read_only.as_ref()
356 }
357
358 #[must_use]
360 pub const fn restart(&self) -> Option<&RestartPolicy> {
361 self.restart.as_ref()
362 }
363
364 #[must_use]
366 pub const fn ulimits(&self) -> Option<&Ulimits> {
367 self.ulimits.as_ref()
368 }
369
370 #[must_use]
372 pub const fn depends_on(&self) -> Option<&DependsOn> {
373 self.depends_on.as_ref()
374 }
375
376 #[must_use]
378 pub const fn healthcheck(&self) -> Option<&Healthcheck> {
379 self.healthcheck.as_ref()
380 }
381
382 #[must_use]
384 pub const fn build(&self) -> Option<&Build> {
385 self.build.as_ref()
386 }
387
388 #[must_use]
390 pub const fn deploy(&self) -> Option<&DeployDefinition> {
391 self.deploy.as_ref()
392 }
393
394 #[must_use]
396 pub fn ports(&self) -> &[Port] {
397 &self.ports
398 }
399
400 #[must_use]
402 pub fn volumes(&self) -> &[VolumeMount] {
403 &self.volumes
404 }
405
406 #[must_use]
408 pub const fn networks(&self) -> Option<&ServiceNetworks> {
409 self.networks.as_ref()
410 }
411
412 #[must_use]
414 pub fn profiles(&self) -> &[Located<String>] {
415 &self.profiles
416 }
417
418 #[must_use]
420 pub fn configs(&self) -> &[ConfigGrant] {
421 &self.configs
422 }
423
424 #[must_use]
426 pub fn secrets(&self) -> &[SecretGrant] {
427 &self.secrets
428 }
429
430 #[must_use]
432 pub fn extension_fields(&self) -> &[FieldReference] {
433 &self.extension_fields
434 }
435
436 #[must_use]
438 pub fn unknown_fields(&self) -> &[FieldReference] {
439 &self.unknown_fields
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct ComposeDocument {
446 source_id: SourceId,
447 span: SourceSpan,
448 name: Option<Located<String>>,
449 services: Vec<Service>,
450 networks: Vec<NetworkDefinition>,
451 volumes: Vec<VolumeDefinition>,
452 configs: Vec<ConfigDefinition>,
453 secrets: Vec<SecretDefinition>,
454 extension_fields: Vec<FieldReference>,
455 unknown_fields: Vec<FieldReference>,
456}
457
458impl ComposeDocument {
459 #[must_use]
465 pub fn parse(syntax: &SyntaxDocument) -> ModelParse {
466 Parser::new(syntax).parse()
467 }
468
469 #[must_use]
471 pub const fn source_id(&self) -> SourceId {
472 self.source_id
473 }
474
475 #[must_use]
477 pub const fn span(&self) -> SourceSpan {
478 self.span
479 }
480
481 #[must_use]
483 pub const fn name(&self) -> Option<&Located<String>> {
484 self.name.as_ref()
485 }
486
487 #[must_use]
489 pub fn services(&self) -> &[Service] {
490 &self.services
491 }
492
493 #[must_use]
495 pub fn service(&self, name: &str) -> Option<&Service> {
496 self.services.iter().find(|service| service.name.value == name)
497 }
498
499 #[must_use]
504 pub fn validate_dependencies(&self) -> Vec<Diagnostic> {
505 let mut diagnostics = Vec::new();
506 for service in &self.services {
507 let Some(depends_on) = service.depends_on() else {
508 continue;
509 };
510 match depends_on {
511 DependsOn::Short { services, .. } => {
512 for target in services {
513 if self.service(target.value()).is_none() {
514 diagnostics.push(missing_dependency_diagnostic(target.span(), false, true));
515 }
516 }
517 }
518 DependsOn::Long { services, .. } => {
519 for dependency in services {
520 let required = !matches!(
521 dependency.required().map(Located::value),
522 Some(BooleanValue::Literal(false))
523 );
524 let Some(target) = self.service(dependency.service().value()) else {
525 diagnostics.push(missing_dependency_diagnostic(
526 dependency.service().span(),
527 false,
528 required,
529 ));
530 continue;
531 };
532 let needs_healthcheck = matches!(
533 dependency.condition().map(Located::value),
534 Some(DependencyCondition::ServiceHealthy)
535 );
536 if needs_healthcheck && target.healthcheck().is_none() {
537 let span = dependency
538 .condition()
539 .map_or_else(|| dependency.service().span(), Located::span);
540 diagnostics.push(unverified_healthcheck_diagnostic(span));
541 } else if needs_healthcheck && target.healthcheck().is_some_and(Healthcheck::is_disabled) {
542 let span = dependency
543 .condition()
544 .map_or_else(|| dependency.service().span(), Located::span);
545 diagnostics.push(missing_dependency_diagnostic(span, true, required));
546 }
547 }
548 }
549 }
550 }
551 diagnostics
552 }
553
554 #[must_use]
556 pub fn networks(&self) -> &[NetworkDefinition] {
557 &self.networks
558 }
559
560 #[must_use]
562 pub fn volumes(&self) -> &[VolumeDefinition] {
563 &self.volumes
564 }
565
566 #[must_use]
568 pub fn configs(&self) -> &[ConfigDefinition] {
569 &self.configs
570 }
571
572 #[must_use]
574 pub fn secrets(&self) -> &[SecretDefinition] {
575 &self.secrets
576 }
577
578 #[must_use]
580 pub fn extension_fields(&self) -> &[FieldReference] {
581 &self.extension_fields
582 }
583
584 #[must_use]
586 pub fn unknown_fields(&self) -> &[FieldReference] {
587 &self.unknown_fields
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq)]
593pub struct ModelParse {
594 document: Option<ComposeDocument>,
595 diagnostics: Vec<Diagnostic>,
596}
597
598impl ModelParse {
599 #[must_use]
601 pub const fn document(&self) -> Option<&ComposeDocument> {
602 self.document.as_ref()
603 }
604
605 #[must_use]
607 pub fn diagnostics(&self) -> &[Diagnostic] {
608 &self.diagnostics
609 }
610
611 #[must_use]
613 pub fn is_valid(&self) -> bool {
614 !self
615 .diagnostics
616 .iter()
617 .any(|diagnostic| diagnostic.severity() == Severity::Error)
618 }
619
620 #[must_use]
622 pub fn into_parts(self) -> (Option<ComposeDocument>, Vec<Diagnostic>) {
623 (self.document, self.diagnostics)
624 }
625}
626
627fn missing_dependency_diagnostic(span: SourceSpan, healthcheck: bool, required: bool) -> Diagnostic {
628 let severity = if required { Severity::Error } else { Severity::Warning };
629 if healthcheck {
630 Diagnostic::new(
631 DEPENDENCY_MISSING_HEALTHCHECK,
632 severity,
633 if required {
634 "service_healthy dependency requires an enabled health check"
635 } else {
636 "optional service_healthy dependency has no enabled health check"
637 },
638 )
639 .with_label(DiagnosticLabel::primary(span, "dependency cannot become healthy"))
640 } else {
641 Diagnostic::new(
642 DEPENDENCY_MISSING_SERVICE,
643 severity,
644 if required {
645 "service dependency is not declared in this Compose document"
646 } else {
647 "optional service dependency is not declared in this Compose document"
648 },
649 )
650 .with_label(DiagnosticLabel::primary(span, "missing dependency service"))
651 }
652}
653
654fn unverified_healthcheck_diagnostic(span: SourceSpan) -> Diagnostic {
655 Diagnostic::new(
656 DEPENDENCY_HEALTHCHECK_UNVERIFIED,
657 Severity::Warning,
658 "service_healthy dependency has no Compose healthcheck to validate",
659 )
660 .with_label(DiagnosticLabel::primary(span, "image health metadata is not available"))
661 .with_note("the dependency image may still define a health check; verify it at build or runtime")
662}
663
664#[derive(Debug)]
665struct Parser {
666 source_id: SourceId,
667 source_span: SourceSpan,
668 source: String,
669 tree: yaml_edit::YamlFile,
670 anchors: AnchorRegistry,
671 diagnostics: Vec<Diagnostic>,
672}
673
674impl Parser {
675 fn new(syntax: &SyntaxDocument) -> Self {
676 let tree = syntax.yaml_file();
677 let anchors = tree
678 .document()
679 .map_or_else(AnchorRegistry::new, |document| AnchorRegistry::from_document(&document));
680 Self {
681 source_id: syntax.source_id(),
682 source_span: syntax.source_span(),
683 source: syntax.source_text().to_owned(),
684 tree,
685 anchors,
686 diagnostics: Vec::new(),
687 }
688 }
689
690 fn parse(mut self) -> ModelParse {
691 if self.tree.documents().count() > 1 {
692 self.diagnostics.push(
693 Diagnostic::new(
694 MULTIPLE_DOCUMENTS,
695 Severity::Error,
696 "Compose input must contain one YAML document",
697 )
698 .with_label(DiagnosticLabel::primary(self.source_span, "multiple YAML documents")),
699 );
700 }
701
702 let Some(root) = self.tree.document() else {
703 self.diagnostics.push(
704 Diagnostic::new(
705 DOCUMENT_ROOT_TYPE,
706 Severity::Error,
707 "Compose document root must be a mapping",
708 )
709 .with_label(DiagnosticLabel::primary(self.source_span, "empty document")),
710 );
711 return ModelParse {
712 document: None,
713 diagnostics: self.diagnostics,
714 };
715 };
716 let root_span = span_from_position(self.source_id, root.byte_range());
717 let Some(mapping) = root.as_mapping() else {
718 self.diagnostics.push(
719 Diagnostic::new(
720 DOCUMENT_ROOT_TYPE,
721 Severity::Error,
722 "Compose document root must be a mapping",
723 )
724 .with_label(DiagnosticLabel::primary(root_span, "not a mapping")),
725 );
726 return ModelParse {
727 document: None,
728 diagnostics: self.diagnostics,
729 };
730 };
731
732 let document = self.parse_root(&mapping, root_span);
733 ModelParse {
734 document: Some(document),
735 diagnostics: self.diagnostics,
736 }
737 }
738
739 fn parse_root(&mut self, mapping: &Mapping, span: SourceSpan) -> ComposeDocument {
740 let mut document = ComposeDocument {
741 source_id: self.source_id,
742 span,
743 name: None,
744 services: Vec::new(),
745 networks: Vec::new(),
746 volumes: Vec::new(),
747 configs: Vec::new(),
748 secrets: Vec::new(),
749 extension_fields: Vec::new(),
750 unknown_fields: Vec::new(),
751 };
752 let mut seen = BTreeMap::new();
753
754 for field in self.fields(mapping) {
755 let duplicate = self.record_duplicate(&mut seen, &field);
756 match field.name.value.as_str() {
757 "name" if !duplicate => {
758 document.name = self.parse_string(&field, "project name");
759 }
760 "services" if !duplicate => {
761 document.services = self.parse_services(&field);
762 }
763 "networks" if !duplicate => {
764 document.networks = self.parse_network_definitions(&field);
765 }
766 "volumes" if !duplicate => {
767 document.volumes = self.parse_volume_definitions(&field);
768 }
769 "configs" if !duplicate => {
770 document.configs = self.parse_config_definitions(&field);
771 }
772 "secrets" if !duplicate => {
773 document.secrets = self.parse_secret_definitions(&field);
774 }
775 name if name.starts_with("x-") => {
776 document.extension_fields.push(field.reference());
777 }
778 _ if duplicate => {}
779 _ => document.unknown_fields.push(field.reference()),
780 }
781 }
782 document
783 }
784
785 fn parse_services(&mut self, field: &ParsedField) -> Vec<Service> {
786 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
787 self.expected(EXPECTED_MAPPING, field, "services must be a mapping");
788 return Vec::new();
789 };
790 let mut services = Vec::new();
791 let mut seen = BTreeMap::new();
792 for service_field in self.fields(mapping) {
793 self.record_duplicate(&mut seen, &service_field);
794 let Some(service_mapping) = service_field.value.as_ref().and_then(YamlNode::as_mapping) else {
795 self.expected(EXPECTED_MAPPING, &service_field, "service definition must be a mapping");
796 continue;
797 };
798 services.push(self.parse_service(&service_field, service_mapping));
799 }
800 services
801 }
802
803 fn parse_service(&mut self, field: &ParsedField, mapping: &Mapping) -> Service {
804 let mut service = Service::new(field.name.clone(), field.span);
805 let mut seen = BTreeMap::new();
806 for service_field in self.fields(mapping) {
807 let duplicate = self.record_duplicate(&mut seen, &service_field);
808 match service_field.name.value.as_str() {
809 "container_name" if !duplicate => {
810 service.container_name = self.parse_string(&service_field, "service container name");
811 }
812 "image" if !duplicate => {
813 service.image = self
814 .parse_string(&service_field, "service image")
815 .map(|value| Located::new(ImageReference::parse(value.value), value.span));
816 }
817 "command" if !duplicate => {
818 service.command = self.parse_command(&service_field);
819 }
820 "environment" if !duplicate => {
821 service.environment = self.parse_environment(&service_field);
822 }
823 "env_file" if !duplicate => {
824 service.environment_files = self.parse_environment_files(&service_field);
825 }
826 "labels" if !duplicate => {
827 service.labels = self.parse_labels(&service_field);
828 }
829 "extra_hosts" if !duplicate => {
830 service.extra_hosts = self.parse_extra_hosts(&service_field);
831 }
832 "user" if !duplicate => {
833 service.user = self.parse_string(&service_field, "service user").map(UserSpec::parse);
834 }
835 "userns_mode" if !duplicate => {
836 service.userns_mode = self
837 .parse_string(&service_field, "service user namespace mode")
838 .map(UserNamespaceMode::parse);
839 }
840 "group_add" if !duplicate => {
841 service.group_add = self.parse_string_sequence(&service_field, "service supplementary groups");
842 }
843 "working_dir" if !duplicate => {
844 service.working_dir = self.parse_string(&service_field, "service working directory");
845 }
846 "read_only" if !duplicate => {
847 service.read_only = self.parse_boolean(&service_field, "service read_only");
848 }
849 "restart" if !duplicate => {
850 service.restart = self.parse_restart_policy(&service_field);
851 }
852 "ulimits" if !duplicate => {
853 service.ulimits = self.parse_ulimits(&service_field);
854 }
855 "depends_on" if !duplicate => {
856 service.depends_on = self.parse_depends_on(&service_field);
857 }
858 "healthcheck" if !duplicate => {
859 service.healthcheck = self.parse_healthcheck(&service_field);
860 }
861 "build" if !duplicate => {
862 service.build = self.parse_build(&service_field);
863 }
864 "deploy" if !duplicate => {
865 service.deploy = self.parse_deploy(&service_field);
866 }
867 "ports" if !duplicate => {
868 service.ports = self.parse_service_ports(&service_field);
869 }
870 "volumes" if !duplicate => {
871 service.volumes = self.parse_service_volumes(&service_field);
872 }
873 "networks" if !duplicate => {
874 service.networks = self.parse_service_networks(&service_field);
875 }
876 "profiles" if !duplicate => {
877 service.profiles = self.parse_string_sequence(&service_field, "service profiles");
878 }
879 "configs" if !duplicate => {
880 service.configs = self.parse_config_grants(&service_field);
881 }
882 "secrets" if !duplicate => {
883 service.secrets = self.parse_secret_grants(&service_field);
884 }
885 name if name.starts_with("x-") => {
886 service.extension_fields.push(service_field.reference());
887 }
888 _ if duplicate => {}
889 _ => service.unknown_fields.push(service_field.reference()),
890 }
891 }
892 service
893 }
894
895 fn parse_restart_policy(&mut self, field: &ParsedField) -> Option<RestartPolicy> {
896 let value = self.parse_string(field, "service restart policy")?;
897 let policy = RestartPolicy::parse(value);
898 if !policy.is_valid() {
899 self.diagnostics.push(
900 Diagnostic::new(
901 RESTART_INVALID_POLICY,
902 Severity::Error,
903 "restart must be `no`, `always`, `on-failure[:max-retries]`, `unless-stopped`, or interpolation",
904 )
905 .with_label(DiagnosticLabel::primary(
906 policy.raw().span(),
907 "invalid service restart policy",
908 )),
909 );
910 }
911 Some(policy)
912 }
913
914 fn parse_command(&mut self, field: &ParsedField) -> Option<Command> {
915 match field.value.as_ref() {
916 Some(YamlNode::Scalar(scalar)) => {
917 let span = span_from_position(self.source_id, scalar.byte_range());
918 if ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null {
919 Some(Command::Null(span))
920 } else {
921 Some(Command::String(Located::new(
922 scalar_string_from_source(&self.source, scalar),
923 span,
924 )))
925 }
926 }
927 Some(YamlNode::Sequence(sequence)) => {
928 let span = span_from_position(self.source_id, sequence.byte_range());
929 let values =
930 self.parse_scalar_nodes(sequence.values(), field.span, "command list items must be scalars");
931 Some(Command::List { span, values })
932 }
933 _ => {
934 self.expected(
935 EXPECTED_FIELD_FORM,
936 field,
937 "command must be null, a scalar, or a sequence",
938 );
939 None
940 }
941 }
942 }
943
944 fn parse_environment(&mut self, field: &ParsedField) -> Option<Environment> {
945 match field.value.as_ref() {
946 Some(YamlNode::Sequence(sequence)) => {
947 let span = span_from_position(self.source_id, sequence.byte_range());
948 let entries = self
949 .parse_scalar_nodes(sequence.values(), field.span, "environment list items must be scalars")
950 .into_iter()
951 .map(EnvironmentListEntry::parse)
952 .collect();
953 Some(Environment::List { span, entries })
954 }
955 Some(YamlNode::Mapping(mapping)) => {
956 let span = span_from_position(self.source_id, mapping.byte_range());
957 let entries = self.parse_environment_map(mapping);
958 Some(Environment::Map { span, entries })
959 }
960 _ => {
961 self.expected(EXPECTED_FIELD_FORM, field, "environment must be a sequence or mapping");
962 None
963 }
964 }
965 }
966
967 fn parse_environment_map(&mut self, mapping: &Mapping) -> Vec<EnvironmentMapEntry> {
968 let mut entries = Vec::new();
969 let mut seen = BTreeMap::new();
970 for field in self.fields(mapping) {
971 if self.record_duplicate(&mut seen, &field) {
972 continue;
973 }
974 let value = self.parse_compose_scalar(&field, "environment values must be scalars");
975 if let Some(value) = value {
976 entries.push(EnvironmentMapEntry::new(field.name, value, field.span));
977 }
978 }
979 entries
980 }
981
982 fn parse_environment_files(&mut self, field: &ParsedField) -> Vec<EnvironmentFile> {
983 match field.value.as_ref() {
984 Some(YamlNode::Scalar(_)) => self
985 .parse_string(field, "service environment-file path")
986 .map(EnvironmentFile::Short)
987 .into_iter()
988 .collect(),
989 Some(YamlNode::Sequence(sequence)) => sequence
990 .values()
991 .filter_map(|value| match value {
992 YamlNode::Scalar(scalar) => {
993 let span = span_from_position(self.source_id, scalar.byte_range());
994 Some(EnvironmentFile::Short(Located::new(
995 scalar_string_from_source(&self.source, &scalar),
996 span,
997 )))
998 }
999 YamlNode::Mapping(mapping) => Some(EnvironmentFile::Long(Box::new(
1000 self.parse_long_environment_file(&mapping),
1001 ))),
1002 _ => {
1003 self.diagnostics.push(
1004 Diagnostic::new(
1005 ENVIRONMENT_FILE_EXPECTED_FORM,
1006 Severity::Error,
1007 "env_file item must use scalar short syntax or mapping long syntax",
1008 )
1009 .with_label(DiagnosticLabel::primary(
1010 node_span(self.source_id, &value).unwrap_or(field.span),
1011 "invalid environment-file item",
1012 )),
1013 );
1014 None
1015 }
1016 })
1017 .collect(),
1018 _ => {
1019 self.expected(
1020 EXPECTED_FIELD_FORM,
1021 field,
1022 "env_file must be a scalar path or a sequence of short/long entries",
1023 );
1024 Vec::new()
1025 }
1026 }
1027 }
1028
1029 fn parse_long_environment_file(&mut self, mapping: &Mapping) -> LongEnvironmentFile {
1030 let span = span_from_position(self.source_id, mapping.byte_range());
1031 let mut environment_file = LongEnvironmentFile::new(span);
1032 let mut seen = BTreeMap::new();
1033 for field in self.fields(mapping) {
1034 let duplicate = self.record_duplicate(&mut seen, &field);
1035 match field.name.value.as_str() {
1036 "path" if !duplicate => self
1037 .parse_string(&field, "environment-file path")
1038 .into_iter()
1039 .for_each(|value| environment_file.set_path(value)),
1040 "required" if !duplicate => self
1041 .parse_boolean(&field, "environment-file required option")
1042 .into_iter()
1043 .for_each(|value| environment_file.set_required(value)),
1044 "format" if !duplicate => {
1045 if let Some(raw) = self.parse_string(&field, "environment-file format") {
1046 let format = EnvironmentFileFormat::parse(raw);
1047 if !format.is_valid() {
1048 self.diagnostics.push(
1049 Diagnostic::new(
1050 ENVIRONMENT_FILE_INVALID_FORMAT,
1051 Severity::Error,
1052 "environment-file format must be `raw` or interpolation",
1053 )
1054 .with_label(DiagnosticLabel::primary(format.raw().span(), "invalid format")),
1055 );
1056 }
1057 environment_file.set_format(format);
1058 }
1059 }
1060 name if name.starts_with("x-") => environment_file.push_extension(field.reference()),
1061 _ if duplicate => {}
1062 _ => environment_file.push_unknown(field.reference()),
1063 }
1064 }
1065 if environment_file.path().is_none() {
1066 self.missing(
1067 ENVIRONMENT_FILE_MISSING_PATH,
1068 span,
1069 "long environment-file entry is missing `path`",
1070 );
1071 }
1072 environment_file
1073 }
1074
1075 fn parse_extra_hosts(&mut self, field: &ParsedField) -> Option<ExtraHosts> {
1076 match field.value.as_ref() {
1077 Some(YamlNode::Sequence(sequence)) => {
1078 let span = span_from_position(self.source_id, sequence.byte_range());
1079 let entries = self
1080 .parse_scalar_nodes(sequence.values(), field.span, "extra_hosts entries must be scalars")
1081 .into_iter()
1082 .map(|raw| {
1083 let entry = ShortExtraHost::parse(raw);
1084 if !entry.is_complete() {
1085 self.diagnostics.push(
1086 Diagnostic::new(
1087 EXTRA_HOST_INVALID_ENTRY,
1088 Severity::Error,
1089 "short extra_hosts entry must contain a hostname and address",
1090 )
1091 .with_label(DiagnosticLabel::primary(
1092 entry.raw().span(),
1093 "missing separator or value",
1094 )),
1095 );
1096 }
1097 entry
1098 })
1099 .collect();
1100 Some(ExtraHosts::Short { span, entries })
1101 }
1102 Some(YamlNode::Mapping(mapping)) => {
1103 let span = span_from_position(self.source_id, mapping.byte_range());
1104 let mut entries = Vec::new();
1105 let mut seen = BTreeMap::new();
1106 for host in self.fields(mapping) {
1107 if self.record_duplicate(&mut seen, &host) {
1108 continue;
1109 }
1110 if let Some(address) = self.parse_string(&host, "extra host address") {
1111 let address = Located::new(HostAddress::parse(address.value), address.span);
1112 entries.push(LongExtraHost::new(host.name, address, host.span));
1113 }
1114 }
1115 Some(ExtraHosts::Long { span, entries })
1116 }
1117 _ => {
1118 self.expected(EXPECTED_FIELD_FORM, field, "extra_hosts must be a sequence or mapping");
1119 None
1120 }
1121 }
1122 }
1123
1124 fn parse_ulimits(&mut self, field: &ParsedField) -> Option<Ulimits> {
1125 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1126 self.expected(EXPECTED_MAPPING, field, "ulimits must be a mapping");
1127 return None;
1128 };
1129 let span = span_from_position(self.source_id, mapping.byte_range());
1130 let mut entries = Vec::new();
1131 let mut seen = BTreeMap::new();
1132 for limit in self.fields(mapping) {
1133 if self.record_duplicate(&mut seen, &limit) {
1134 continue;
1135 }
1136 let value = match limit.value.as_ref() {
1137 Some(YamlNode::Scalar(_)) => self.parse_limit_value(&limit, "ulimit value").map(UlimitValue::Single),
1138 Some(YamlNode::Mapping(range)) => Some(UlimitValue::Range(self.parse_ulimit_range(range))),
1139 _ => {
1140 self.expected(
1141 EXPECTED_FIELD_FORM,
1142 &limit,
1143 "ulimit must be a scalar or soft/hard mapping",
1144 );
1145 None
1146 }
1147 };
1148 if let Some(value) = value {
1149 entries.push(Ulimit::new(limit.name, limit.span, value));
1150 }
1151 }
1152 Some(Ulimits::new(span, entries))
1153 }
1154
1155 fn parse_ulimit_range(&mut self, mapping: &Mapping) -> UlimitRange {
1156 let span = span_from_position(self.source_id, mapping.byte_range());
1157 let mut range = UlimitRange::new(span);
1158 let mut seen = BTreeMap::new();
1159 for field in self.fields(mapping) {
1160 let duplicate = self.record_duplicate(&mut seen, &field);
1161 match field.name.value.as_str() {
1162 "soft" if !duplicate => self
1163 .parse_limit_value(&field, "ulimit soft value")
1164 .into_iter()
1165 .for_each(|value| range.set_soft(value)),
1166 "hard" if !duplicate => self
1167 .parse_limit_value(&field, "ulimit hard value")
1168 .into_iter()
1169 .for_each(|value| range.set_hard(value)),
1170 name if name.starts_with("x-") => range.push_extension(field.reference()),
1171 _ if duplicate => {}
1172 _ => range.push_unknown(field.reference()),
1173 }
1174 }
1175 range
1176 }
1177
1178 fn parse_limit_value(&mut self, field: &ParsedField, description: &str) -> Option<Located<LimitValue>> {
1179 let value = self.parse_string(field, description)?;
1180 let parsed = LimitValue::parse(value.value);
1181 if !parsed.is_valid() {
1182 self.diagnostics.push(
1183 Diagnostic::new(
1184 ULIMIT_INVALID_VALUE,
1185 Severity::Error,
1186 "ulimit must be -1, a non-negative integer, or an interpolation expression",
1187 )
1188 .with_label(DiagnosticLabel::primary(value.span, "invalid ulimit value")),
1189 );
1190 }
1191 Some(Located::new(parsed, value.span))
1192 }
1193
1194 fn parse_depends_on(&mut self, field: &ParsedField) -> Option<DependsOn> {
1195 match field.value.as_ref() {
1196 Some(YamlNode::Sequence(sequence)) => {
1197 let span = span_from_position(self.source_id, sequence.byte_range());
1198 let services = self.parse_scalar_nodes(
1199 sequence.values(),
1200 field.span,
1201 "dependency service names must be scalars",
1202 );
1203 Some(DependsOn::Short { span, services })
1204 }
1205 Some(YamlNode::Mapping(mapping)) => {
1206 let span = span_from_position(self.source_id, mapping.byte_range());
1207 let mut services = Vec::new();
1208 let mut seen = BTreeMap::new();
1209 for dependency in self.fields(mapping) {
1210 if self.record_duplicate(&mut seen, &dependency) {
1211 continue;
1212 }
1213 let mut parsed = ServiceDependency::new(dependency.name.clone(), dependency.span);
1214 if Self::field_is_null(&dependency) {
1215 services.push(parsed);
1216 continue;
1217 }
1218 let Some(options) = dependency.value.as_ref().and_then(YamlNode::as_mapping) else {
1219 self.expected(
1220 EXPECTED_MAPPING,
1221 &dependency,
1222 "long dependency options must be a mapping or null",
1223 );
1224 continue;
1225 };
1226 let mut option_seen = BTreeMap::new();
1227 for option in self.fields(options) {
1228 let duplicate = self.record_duplicate(&mut option_seen, &option);
1229 match option.name.value.as_str() {
1230 "condition" if !duplicate => {
1231 if let Some(value) = self.parse_string(&option, "dependency condition") {
1232 let condition = DependencyCondition::parse(value.value);
1233 if !condition.is_known() {
1234 self.diagnostics.push(
1235 Diagnostic::new(
1236 DEPENDENCY_INVALID_CONDITION,
1237 Severity::Error,
1238 "dependency condition is not defined by Compose",
1239 )
1240 .with_label(
1241 DiagnosticLabel::primary(value.span, "unknown dependency condition"),
1242 ),
1243 );
1244 }
1245 parsed.set_condition(Located::new(condition, value.span));
1246 }
1247 }
1248 "restart" if !duplicate => self
1249 .parse_boolean(&option, "dependency restart")
1250 .into_iter()
1251 .for_each(|value| parsed.set_restart(value)),
1252 "required" if !duplicate => self
1253 .parse_boolean(&option, "dependency required")
1254 .into_iter()
1255 .for_each(|value| parsed.set_required(value)),
1256 name if name.starts_with("x-") => parsed.push_extension(option.reference()),
1257 _ if duplicate => {}
1258 _ => parsed.push_unknown(option.reference()),
1259 }
1260 }
1261 services.push(parsed);
1262 }
1263 Some(DependsOn::Long { span, services })
1264 }
1265 _ => {
1266 self.expected(EXPECTED_FIELD_FORM, field, "depends_on must be a sequence or mapping");
1267 None
1268 }
1269 }
1270 }
1271
1272 fn parse_healthcheck(&mut self, field: &ParsedField) -> Option<Healthcheck> {
1273 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1274 self.expected(EXPECTED_MAPPING, field, "healthcheck must be a mapping");
1275 return None;
1276 };
1277 let span = span_from_position(self.source_id, mapping.byte_range());
1278 let mut healthcheck = Healthcheck::new(span);
1279 let mut seen = BTreeMap::new();
1280 for option in self.fields(mapping) {
1281 let duplicate = self.record_duplicate(&mut seen, &option);
1282 match option.name.value.as_str() {
1283 "test" if !duplicate => self
1284 .parse_healthcheck_test(&option)
1285 .into_iter()
1286 .for_each(|value| healthcheck.set_test(value)),
1287 "interval" if !duplicate => self
1288 .parse_healthcheck_duration(&option, "healthcheck interval")
1289 .into_iter()
1290 .for_each(|value| healthcheck.set_interval(value)),
1291 "timeout" if !duplicate => self
1292 .parse_healthcheck_duration(&option, "healthcheck timeout")
1293 .into_iter()
1294 .for_each(|value| healthcheck.set_timeout(value)),
1295 "retries" if !duplicate => self
1296 .parse_healthcheck_retries(&option)
1297 .into_iter()
1298 .for_each(|value| healthcheck.set_retries(value)),
1299 "start_period" if !duplicate => self
1300 .parse_healthcheck_duration(&option, "healthcheck start period")
1301 .into_iter()
1302 .for_each(|value| healthcheck.set_start_period(value)),
1303 "start_interval" if !duplicate => self
1304 .parse_healthcheck_duration(&option, "healthcheck start interval")
1305 .into_iter()
1306 .for_each(|value| healthcheck.set_start_interval(value)),
1307 "disable" if !duplicate => self
1308 .parse_boolean(&option, "healthcheck disable")
1309 .into_iter()
1310 .for_each(|value| healthcheck.set_disable(value)),
1311 name if name.starts_with("x-") => healthcheck.push_extension(option.reference()),
1312 _ if duplicate => {}
1313 _ => healthcheck.push_unknown(option.reference()),
1314 }
1315 }
1316 Some(healthcheck)
1317 }
1318
1319 fn parse_healthcheck_duration(
1320 &mut self,
1321 field: &ParsedField,
1322 description: &str,
1323 ) -> Option<Located<HealthcheckDuration>> {
1324 let value = self.parse_string(field, description)?;
1325 let duration = HealthcheckDuration::parse(value.value);
1326 if !duration.is_valid() {
1327 self.diagnostics.push(
1328 Diagnostic::new(
1329 HEALTHCHECK_INVALID_DURATION,
1330 Severity::Error,
1331 "healthcheck duration must use Compose duration syntax or interpolation",
1332 )
1333 .with_label(DiagnosticLabel::primary(value.span, "invalid healthcheck duration")),
1334 );
1335 }
1336 Some(Located::new(duration, value.span))
1337 }
1338
1339 fn parse_healthcheck_retries(&mut self, field: &ParsedField) -> Option<Located<HealthcheckRetries>> {
1340 let value = self.parse_string(field, "healthcheck retries")?;
1341 let retries = HealthcheckRetries::parse(value.value);
1342 if !retries.is_valid() {
1343 self.diagnostics.push(
1344 Diagnostic::new(
1345 HEALTHCHECK_INVALID_RETRIES,
1346 Severity::Error,
1347 "healthcheck retries must be a non-negative integer or interpolation expression",
1348 )
1349 .with_label(DiagnosticLabel::primary(value.span, "invalid healthcheck retry count")),
1350 );
1351 }
1352 Some(Located::new(retries, value.span))
1353 }
1354
1355 fn parse_healthcheck_test(&mut self, field: &ParsedField) -> Option<HealthcheckTest> {
1356 match field.value.as_ref() {
1357 Some(YamlNode::Scalar(_)) => self
1358 .parse_string(field, "healthcheck test")
1359 .map(HealthcheckTest::String),
1360 Some(YamlNode::Sequence(sequence)) => {
1361 let span = span_from_position(self.source_id, sequence.byte_range());
1362 let values =
1363 self.parse_scalar_nodes(sequence.values(), field.span, "healthcheck test items must be scalars");
1364 let kind = values.first().map(|value| HealthcheckTestKind::parse(value.value()));
1365 if kind.is_none()
1366 || kind == Some(HealthcheckTestKind::Other)
1367 || (kind == Some(HealthcheckTestKind::None) && values.len() != 1)
1368 {
1369 self.diagnostics.push(
1370 Diagnostic::new(
1371 HEALTHCHECK_INVALID_TEST,
1372 Severity::Error,
1373 "healthcheck list must begin with NONE, CMD, or CMD-SHELL",
1374 )
1375 .with_label(DiagnosticLabel::primary(span, "invalid healthcheck command mode")),
1376 );
1377 }
1378 Some(HealthcheckTest::List { span, kind, values })
1379 }
1380 _ => {
1381 self.expected(
1382 EXPECTED_FIELD_FORM,
1383 field,
1384 "healthcheck test must be a scalar or sequence",
1385 );
1386 None
1387 }
1388 }
1389 }
1390
1391 fn parse_build(&mut self, field: &ParsedField) -> Option<Build> {
1392 match field.value.as_ref() {
1393 Some(YamlNode::Scalar(_)) => self.parse_string(field, "build context").map(Build::Context),
1394 Some(YamlNode::Mapping(mapping)) => {
1395 let span = span_from_position(self.source_id, mapping.byte_range());
1396 let mut definition = BuildDefinition::new(span);
1397 let mut seen = BTreeMap::new();
1398 for option in self.fields(mapping) {
1399 let duplicate = self.record_duplicate(&mut seen, &option);
1400 if duplicate {
1401 continue;
1402 }
1403 if let Some(kind) = BuildFieldKind::from_name(option.name.value()) {
1404 definition.push_field(BuildField::new(kind, option.reference()));
1405 } else if option.name.value().starts_with("x-") {
1406 definition.push_extension(option.reference());
1407 } else {
1408 definition.push_unknown(option.reference());
1409 }
1410 }
1411 Some(Build::Definition(definition))
1412 }
1413 _ => {
1414 self.expected(EXPECTED_FIELD_FORM, field, "build must be a scalar context or mapping");
1415 None
1416 }
1417 }
1418 }
1419
1420 fn parse_deploy(&mut self, field: &ParsedField) -> Option<DeployDefinition> {
1421 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1422 self.expected(EXPECTED_MAPPING, field, "deploy must be a mapping");
1423 return None;
1424 };
1425 let span = span_from_position(self.source_id, mapping.byte_range());
1426 let mut definition = DeployDefinition::new(span);
1427 let mut seen = BTreeMap::new();
1428 for option in self.fields(mapping) {
1429 let duplicate = self.record_duplicate(&mut seen, &option);
1430 if duplicate {
1431 continue;
1432 }
1433 if let Some(kind) = DeployFieldKind::from_name(option.name.value()) {
1434 definition.push_field(DeployField::new(kind, option.reference()));
1435 } else if option.name.value().starts_with("x-") {
1436 definition.push_extension(option.reference());
1437 } else {
1438 definition.push_unknown(option.reference());
1439 }
1440 }
1441 Some(definition)
1442 }
1443
1444 fn source_column(&self, offset: usize) -> usize {
1445 let prefix = self.source.get(..offset).unwrap_or_default();
1446 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
1447 self.source[line_start..offset].chars().count()
1448 }
1449
1450 fn parse_service_ports(&mut self, field: &ParsedField) -> Vec<Port> {
1451 let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1452 self.expected(EXPECTED_SEQUENCE, field, "service ports must be a sequence");
1453 return Vec::new();
1454 };
1455
1456 let mut ports = Vec::new();
1457 for value in sequence.values() {
1458 match value {
1459 YamlNode::Scalar(scalar) => {
1460 let span = span_from_position(self.source_id, scalar.byte_range());
1461 ports.push(Port::Short(ShortPort::parse(Located::new(
1462 scalar_string_from_source(&self.source, &scalar),
1463 span,
1464 ))));
1465 }
1466 YamlNode::Mapping(mapping) => {
1467 ports.push(Port::Long(Box::new(self.parse_long_port(&mapping))));
1468 }
1469 other => self.unsupported_sequence_item(
1470 PORT_EXPECTED_FORM,
1471 &other,
1472 field.span,
1473 "service port must use scalar short syntax or mapping long syntax",
1474 ),
1475 }
1476 }
1477 ports
1478 }
1479
1480 fn parse_long_port(&mut self, mapping: &Mapping) -> LongPort {
1481 let span = span_from_position(self.source_id, mapping.byte_range());
1482 let mut port = LongPort::new(span);
1483 let mut seen = BTreeMap::new();
1484 for field in self.fields(mapping) {
1485 let duplicate = self.record_duplicate(&mut seen, &field);
1486 match field.name.value.as_str() {
1487 "target" if !duplicate => self
1488 .parse_string(&field, "port target")
1489 .into_iter()
1490 .for_each(|value| port.set_target(value)),
1491 "published" if !duplicate => self
1492 .parse_string(&field, "published port")
1493 .into_iter()
1494 .for_each(|value| port.set_published(value)),
1495 "host_ip" if !duplicate => self
1496 .parse_string(&field, "port host IP")
1497 .into_iter()
1498 .for_each(|value| port.set_host_ip(value)),
1499 "protocol" if !duplicate => self
1500 .parse_string(&field, "port protocol")
1501 .into_iter()
1502 .for_each(|value| port.set_protocol(value)),
1503 "app_protocol" if !duplicate => self
1504 .parse_string(&field, "port application protocol")
1505 .into_iter()
1506 .for_each(|value| port.set_app_protocol(value)),
1507 "mode" if !duplicate => self
1508 .parse_string(&field, "port mode")
1509 .into_iter()
1510 .for_each(|value| port.set_mode(value)),
1511 "name" if !duplicate => self
1512 .parse_string(&field, "port name")
1513 .into_iter()
1514 .for_each(|value| port.set_name(value)),
1515 name if name.starts_with("x-") => port.push_extension(field.reference()),
1516 _ if duplicate => {}
1517 _ => port.push_unknown(field.reference()),
1518 }
1519 }
1520 if port.target().is_none() {
1521 self.missing(PORT_MISSING_TARGET, span, "long port is missing `target`");
1522 }
1523 port
1524 }
1525
1526 fn parse_service_networks(&mut self, field: &ParsedField) -> Option<ServiceNetworks> {
1527 match field.value.as_ref() {
1528 Some(YamlNode::Sequence(sequence)) => {
1529 let span = span_from_position(self.source_id, sequence.byte_range());
1530 let names =
1531 self.parse_scalar_nodes(sequence.values(), field.span, "service network names must be scalars");
1532 Some(ServiceNetworks::Short { span, names })
1533 }
1534 Some(YamlNode::Mapping(mapping)) => {
1535 let span = span_from_position(self.source_id, mapping.byte_range());
1536 let networks = self.parse_service_network_map(mapping);
1537 Some(ServiceNetworks::Long { span, networks })
1538 }
1539 _ => {
1540 self.expected(
1541 EXPECTED_FIELD_FORM,
1542 field,
1543 "service networks must be a sequence or mapping",
1544 );
1545 None
1546 }
1547 }
1548 }
1549
1550 fn parse_service_network_map(&mut self, mapping: &Mapping) -> Vec<ServiceNetwork> {
1551 let mut networks = Vec::new();
1552 let mut seen = BTreeMap::new();
1553 for field in self.fields(mapping) {
1554 if self.record_duplicate(&mut seen, &field) {
1555 continue;
1556 }
1557 if Self::field_is_null(&field) {
1558 networks.push(ServiceNetwork::new(field.name, field.span));
1559 continue;
1560 }
1561 let Some(options) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1562 self.expected(
1563 EXPECTED_MAPPING,
1564 &field,
1565 "service network options must be a mapping or null",
1566 );
1567 continue;
1568 };
1569 networks.push(self.parse_service_network(&field, options));
1570 }
1571 networks
1572 }
1573
1574 fn parse_service_network(&mut self, field: &ParsedField, mapping: &Mapping) -> ServiceNetwork {
1575 let mut network = ServiceNetwork::new(field.name.clone(), field.span);
1576 let mut seen = BTreeMap::new();
1577 for option in self.fields(mapping) {
1578 let duplicate = self.record_duplicate(&mut seen, &option);
1579 match option.name.value.as_str() {
1580 "aliases" if !duplicate => network.set_aliases(self.parse_string_sequence(&option, "network aliases")),
1581 "interface_name" if !duplicate => self
1582 .parse_string(&option, "network interface name")
1583 .into_iter()
1584 .for_each(|value| network.set_interface_name(value)),
1585 "ipv4_address" if !duplicate => self
1586 .parse_string(&option, "network IPv4 address")
1587 .into_iter()
1588 .for_each(|value| network.set_ipv4_address(value)),
1589 "ipv6_address" if !duplicate => self
1590 .parse_string(&option, "network IPv6 address")
1591 .into_iter()
1592 .for_each(|value| network.set_ipv6_address(value)),
1593 "link_local_ips" if !duplicate => {
1594 network.set_link_local_ips(self.parse_string_sequence(&option, "link-local IP addresses"));
1595 }
1596 "mac_address" if !duplicate => self
1597 .parse_string(&option, "network MAC address")
1598 .into_iter()
1599 .for_each(|value| network.set_mac_address(value)),
1600 "driver_opts" if !duplicate => {
1601 network.set_driver_opts(self.parse_scalar_mapping(&option, "network driver options"));
1602 }
1603 "gw_priority" if !duplicate => self
1604 .parse_string(&option, "network gateway priority")
1605 .into_iter()
1606 .for_each(|value| network.set_gw_priority(value)),
1607 "priority" if !duplicate => self
1608 .parse_string(&option, "network priority")
1609 .into_iter()
1610 .for_each(|value| network.set_priority(value)),
1611 name if name.starts_with("x-") => network.push_extension(option.reference()),
1612 _ if duplicate => {}
1613 _ => network.push_unknown(option.reference()),
1614 }
1615 }
1616 network
1617 }
1618
1619 fn parse_config_grants(&mut self, field: &ParsedField) -> Vec<ConfigGrant> {
1620 self.parse_grants(field)
1621 .into_iter()
1622 .map(|grant| match grant {
1623 ParsedGrant::Short(value) => ConfigGrant::Short(value),
1624 ParsedGrant::Long(value) => ConfigGrant::Long(value),
1625 })
1626 .collect()
1627 }
1628
1629 fn parse_secret_grants(&mut self, field: &ParsedField) -> Vec<SecretGrant> {
1630 self.parse_grants(field)
1631 .into_iter()
1632 .map(|grant| match grant {
1633 ParsedGrant::Short(value) => SecretGrant::Short(value),
1634 ParsedGrant::Long(value) => SecretGrant::Long(value),
1635 })
1636 .collect()
1637 }
1638
1639 fn parse_grants(&mut self, field: &ParsedField) -> Vec<ParsedGrant> {
1640 let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1641 self.expected(EXPECTED_SEQUENCE, field, "service grants must be a sequence");
1642 return Vec::new();
1643 };
1644 let mut grants = Vec::new();
1645 for value in sequence.values() {
1646 match value {
1647 YamlNode::Scalar(scalar) => {
1648 let span = span_from_position(self.source_id, scalar.byte_range());
1649 grants.push(ParsedGrant::Short(Located::new(
1650 scalar_string_from_source(&self.source, &scalar),
1651 span,
1652 )));
1653 }
1654 YamlNode::Mapping(mapping) => {
1655 grants.push(ParsedGrant::Long(Box::new(self.parse_long_grant(&mapping))));
1656 }
1657 other => self.unsupported_sequence_item(
1658 GRANT_EXPECTED_FORM,
1659 &other,
1660 field.span,
1661 "grant must use scalar short syntax or mapping long syntax",
1662 ),
1663 }
1664 }
1665 grants
1666 }
1667
1668 fn parse_long_grant(&mut self, mapping: &Mapping) -> LongGrant {
1669 let span = span_from_position(self.source_id, mapping.byte_range());
1670 let mut grant = LongGrant::new(span);
1671 let mut seen = BTreeMap::new();
1672 for field in self.fields(mapping) {
1673 let duplicate = self.record_duplicate(&mut seen, &field);
1674 match field.name.value.as_str() {
1675 "source" if !duplicate => self
1676 .parse_string(&field, "grant source")
1677 .into_iter()
1678 .for_each(|value| grant.set_source(value)),
1679 "target" if !duplicate => self
1680 .parse_string(&field, "grant target")
1681 .into_iter()
1682 .for_each(|value| grant.set_target(value)),
1683 "uid" if !duplicate => self
1684 .parse_string(&field, "grant user ID")
1685 .into_iter()
1686 .for_each(|value| grant.set_uid(value)),
1687 "gid" if !duplicate => self
1688 .parse_string(&field, "grant group ID")
1689 .into_iter()
1690 .for_each(|value| grant.set_gid(value)),
1691 "mode" if !duplicate => self
1692 .parse_string(&field, "grant mode")
1693 .into_iter()
1694 .for_each(|value| grant.set_mode(value)),
1695 name if name.starts_with("x-") => grant.push_extension(field.reference()),
1696 _ if duplicate => {}
1697 _ => grant.push_unknown(field.reference()),
1698 }
1699 }
1700 if grant.source().is_none() {
1701 self.missing(GRANT_MISSING_SOURCE, span, "long grant is missing `source`");
1702 }
1703 grant
1704 }
1705
1706 fn parse_service_volumes(&mut self, field: &ParsedField) -> Vec<VolumeMount> {
1707 let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1708 self.expected(EXPECTED_SEQUENCE, field, "service volumes must be a sequence");
1709 return Vec::new();
1710 };
1711
1712 sequence
1713 .values()
1714 .filter_map(|value| match value {
1715 YamlNode::Scalar(scalar) => {
1716 let span = span_from_position(self.source_id, scalar.byte_range());
1717 let raw = Located::new(scalar_string_from_source(&self.source, &scalar), span);
1718 Some(VolumeMount::Short(ShortVolumeMount::new(raw)))
1719 }
1720 YamlNode::Mapping(mapping) => Some(VolumeMount::Long(Box::new(self.parse_long_volume(&mapping)))),
1721 other => {
1722 let span = node_span(self.source_id, &other).unwrap_or(field.span);
1723 self.diagnostics.push(
1724 Diagnostic::new(
1725 VOLUME_EXPECTED_FORM,
1726 Severity::Error,
1727 "service volume must use scalar short syntax or mapping long syntax",
1728 )
1729 .with_label(DiagnosticLabel::primary(span, "unsupported volume form")),
1730 );
1731 None
1732 }
1733 })
1734 .collect()
1735 }
1736
1737 fn parse_long_volume(&mut self, mapping: &Mapping) -> LongVolumeMount {
1738 let span = span_from_position(self.source_id, mapping.byte_range());
1739 let mut mount = LongVolumeMount::new(span);
1740 let mut seen = BTreeMap::new();
1741 for field in self.fields(mapping) {
1742 let duplicate = self.record_duplicate(&mut seen, &field);
1743 match field.name.value.as_str() {
1744 "type" if !duplicate => {
1745 if let Some(value) = self.parse_string(&field, "volume type") {
1746 mount.set_mount_type(Located::new(MountType::from_text(value.value), value.span));
1747 }
1748 }
1749 "source" if !duplicate => {
1750 if let Some(value) = self.parse_string(&field, "volume source") {
1751 mount.set_source(value);
1752 }
1753 }
1754 "target" if !duplicate => {
1755 if let Some(value) = self.parse_string(&field, "volume target") {
1756 mount.set_target(value);
1757 }
1758 }
1759 "read_only" if !duplicate => {
1760 if let Some(value) = self.parse_boolean(&field, "read_only") {
1761 mount.set_read_only(value);
1762 }
1763 }
1764 "bind" if !duplicate => {
1765 if let Some(value) = self.parse_bind_options(&field) {
1766 mount.set_bind(value);
1767 }
1768 }
1769 name if name.starts_with("x-") => mount.push_extension(field.reference()),
1770 _ if duplicate => {}
1771 _ => mount.push_unknown(field.reference()),
1772 }
1773 }
1774
1775 if mount.mount_type().is_none() {
1776 self.missing(VOLUME_MISSING_TYPE, span, "long volume is missing `type`");
1777 }
1778 if mount.target().is_none() {
1779 self.missing(VOLUME_MISSING_TARGET, span, "long volume is missing `target`");
1780 }
1781 mount
1782 }
1783
1784 fn parse_bind_options(&mut self, field: &ParsedField) -> Option<BindOptions> {
1785 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1786 self.expected(EXPECTED_MAPPING, field, "bind options must be a mapping");
1787 return None;
1788 };
1789 let span = span_from_position(self.source_id, mapping.byte_range());
1790 let mut bind = BindOptions::new(span);
1791 let mut seen = BTreeMap::new();
1792 for bind_field in self.fields(mapping) {
1793 let duplicate = self.record_duplicate(&mut seen, &bind_field);
1794 match bind_field.name.value.as_str() {
1795 "propagation" if !duplicate => {
1796 if let Some(value) = self.parse_string(&bind_field, "bind propagation") {
1797 bind.set_propagation(value);
1798 }
1799 }
1800 "create_host_path" if !duplicate => {
1801 if let Some(value) = self.parse_boolean(&bind_field, "create_host_path") {
1802 bind.set_create_host_path(value);
1803 }
1804 }
1805 "selinux" if !duplicate => {
1806 if let Some(value) = self.parse_string(&bind_field, "SELinux relabel mode") {
1807 let mode = match value.value.as_str() {
1808 "z" => Some(SelinuxRelabel::Shared),
1809 "Z" => Some(SelinuxRelabel::Private),
1810 _ => None,
1811 };
1812 if let Some(mode) = mode {
1813 bind.set_selinux(Located::new(mode, value.span));
1814 } else {
1815 self.diagnostics.push(
1816 Diagnostic::new(
1817 VOLUME_INVALID_SELINUX,
1818 Severity::Error,
1819 "SELinux relabel mode must be `z` or `Z`",
1820 )
1821 .with_label(DiagnosticLabel::primary(value.span, "invalid SELinux mode")),
1822 );
1823 }
1824 }
1825 }
1826 name if name.starts_with("x-") => bind.push_extension(bind_field.reference()),
1827 _ if duplicate => {}
1828 _ => bind.push_unknown(bind_field.reference()),
1829 }
1830 }
1831 Some(bind)
1832 }
1833
1834 fn parse_network_definitions(&mut self, field: &ParsedField) -> Vec<NetworkDefinition> {
1835 let Some(mapping) = self.resource_collection(field, "networks") else {
1836 return Vec::new();
1837 };
1838 let mut definitions = Vec::new();
1839 let mut seen = BTreeMap::new();
1840 for resource in self.fields(&mapping) {
1841 if self.record_duplicate(&mut seen, &resource) {
1842 continue;
1843 }
1844 if Self::field_is_null(&resource) {
1845 definitions.push(NetworkDefinition::new(resource.name, resource.span));
1846 continue;
1847 }
1848 let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
1849 self.expected(
1850 RESOURCE_EXPECTED_FORM,
1851 &resource,
1852 "network definition must be a mapping or null",
1853 );
1854 continue;
1855 };
1856 definitions.push(self.parse_network_definition(&resource, definition));
1857 }
1858 definitions
1859 }
1860
1861 fn parse_network_definition(&mut self, field: &ParsedField, mapping: &Mapping) -> NetworkDefinition {
1862 let mut network = NetworkDefinition::new(field.name.clone(), field.span);
1863 let mut seen = BTreeMap::new();
1864 for option in self.fields(mapping) {
1865 let duplicate = self.record_duplicate(&mut seen, &option);
1866 match option.name.value.as_str() {
1867 "driver" if !duplicate => self
1868 .parse_string(&option, "network driver")
1869 .into_iter()
1870 .for_each(|value| network.set_driver(value)),
1871 "driver_opts" if !duplicate => {
1872 network.set_driver_opts(self.parse_scalar_mapping(&option, "network driver options"));
1873 }
1874 "attachable" if !duplicate => self
1875 .parse_boolean(&option, "network attachable")
1876 .into_iter()
1877 .for_each(|value| network.set_attachable(value)),
1878 "enable_ipv4" if !duplicate => self
1879 .parse_boolean(&option, "network enable_ipv4")
1880 .into_iter()
1881 .for_each(|value| network.set_enable_ipv4(value)),
1882 "enable_ipv6" if !duplicate => self
1883 .parse_boolean(&option, "network enable_ipv6")
1884 .into_iter()
1885 .for_each(|value| network.set_enable_ipv6(value)),
1886 "external" if !duplicate => self
1887 .parse_boolean(&option, "network external")
1888 .into_iter()
1889 .for_each(|value| network.set_external(value)),
1890 "internal" if !duplicate => self
1891 .parse_boolean(&option, "network internal")
1892 .into_iter()
1893 .for_each(|value| network.set_internal(value)),
1894 "ipam" if !duplicate => self
1895 .parse_ipam(&option)
1896 .into_iter()
1897 .for_each(|value| network.set_ipam(value)),
1898 "labels" if !duplicate => self
1899 .parse_labels(&option)
1900 .into_iter()
1901 .for_each(|value| network.set_labels(value)),
1902 "name" if !duplicate => self
1903 .parse_string(&option, "network custom name")
1904 .into_iter()
1905 .for_each(|value| network.set_custom_name(value)),
1906 name if name.starts_with("x-") => network.push_extension(option.reference()),
1907 _ if duplicate => {}
1908 _ => network.push_unknown(option.reference()),
1909 }
1910 }
1911 network
1912 }
1913
1914 fn parse_ipam(&mut self, field: &ParsedField) -> Option<Ipam> {
1915 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1916 self.expected(EXPECTED_MAPPING, field, "network IPAM must be a mapping");
1917 return None;
1918 };
1919 let span = span_from_position(self.source_id, mapping.byte_range());
1920 let mut ipam = Ipam::new(span);
1921 let mut seen = BTreeMap::new();
1922 for option in self.fields(mapping) {
1923 let duplicate = self.record_duplicate(&mut seen, &option);
1924 match option.name.value.as_str() {
1925 "driver" if !duplicate => self
1926 .parse_string(&option, "IPAM driver")
1927 .into_iter()
1928 .for_each(|value| ipam.set_driver(value)),
1929 "config" if !duplicate => ipam.set_config(self.parse_ipam_configs(&option)),
1930 "options" if !duplicate => {
1931 ipam.set_options(self.parse_scalar_mapping(&option, "IPAM options"));
1932 }
1933 name if name.starts_with("x-") => ipam.push_extension(option.reference()),
1934 _ if duplicate => {}
1935 _ => ipam.push_unknown(option.reference()),
1936 }
1937 }
1938 Some(ipam)
1939 }
1940
1941 fn parse_ipam_configs(&mut self, field: &ParsedField) -> Vec<IpamConfig> {
1942 let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1943 self.expected(EXPECTED_SEQUENCE, field, "IPAM config must be a sequence");
1944 return Vec::new();
1945 };
1946 let mut configs = Vec::new();
1947 for value in sequence.values() {
1948 let YamlNode::Mapping(mapping) = value else {
1949 self.unsupported_sequence_item(
1950 EXPECTED_MAPPING,
1951 &value,
1952 field.span,
1953 "IPAM config entries must be mappings",
1954 );
1955 continue;
1956 };
1957 configs.push(self.parse_ipam_config(&mapping));
1958 }
1959 configs
1960 }
1961
1962 fn parse_ipam_config(&mut self, mapping: &Mapping) -> IpamConfig {
1963 let span = span_from_position(self.source_id, mapping.byte_range());
1964 let mut config = IpamConfig::new(span);
1965 let mut seen = BTreeMap::new();
1966 for field in self.fields(mapping) {
1967 let duplicate = self.record_duplicate(&mut seen, &field);
1968 match field.name.value.as_str() {
1969 "subnet" if !duplicate => self
1970 .parse_string(&field, "IPAM subnet")
1971 .into_iter()
1972 .for_each(|value| config.set_subnet(value)),
1973 "ip_range" if !duplicate => self
1974 .parse_string(&field, "IPAM allocation range")
1975 .into_iter()
1976 .for_each(|value| config.set_ip_range(value)),
1977 "gateway" if !duplicate => self
1978 .parse_string(&field, "IPAM gateway")
1979 .into_iter()
1980 .for_each(|value| config.set_gateway(value)),
1981 "aux_addresses" if !duplicate => {
1982 config.set_aux_addresses(self.parse_scalar_mapping(&field, "IPAM auxiliary addresses"));
1983 }
1984 name if name.starts_with("x-") => config.push_extension(field.reference()),
1985 _ if duplicate => {}
1986 _ => config.push_unknown(field.reference()),
1987 }
1988 }
1989 config
1990 }
1991
1992 fn parse_volume_definitions(&mut self, field: &ParsedField) -> Vec<VolumeDefinition> {
1993 let Some(mapping) = self.resource_collection(field, "volumes") else {
1994 return Vec::new();
1995 };
1996 let mut definitions = Vec::new();
1997 let mut seen = BTreeMap::new();
1998 for resource in self.fields(&mapping) {
1999 if self.record_duplicate(&mut seen, &resource) {
2000 continue;
2001 }
2002 let mut volume = VolumeDefinition::new(resource.name.clone(), resource.span);
2003 if Self::field_is_null(&resource) {
2004 definitions.push(volume);
2005 continue;
2006 }
2007 let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
2008 self.expected(
2009 RESOURCE_EXPECTED_FORM,
2010 &resource,
2011 "volume definition must be a mapping or null",
2012 );
2013 continue;
2014 };
2015 let mut nested_seen = BTreeMap::new();
2016 for option in self.fields(definition) {
2017 let duplicate = self.record_duplicate(&mut nested_seen, &option);
2018 match option.name.value.as_str() {
2019 "driver" if !duplicate => self
2020 .parse_string(&option, "volume driver")
2021 .into_iter()
2022 .for_each(|value| volume.set_driver(value)),
2023 "driver_opts" if !duplicate => {
2024 volume.set_driver_opts(self.parse_scalar_mapping(&option, "volume driver options"));
2025 }
2026 "external" if !duplicate => self
2027 .parse_boolean(&option, "volume external")
2028 .into_iter()
2029 .for_each(|value| volume.set_external(value)),
2030 "labels" if !duplicate => self
2031 .parse_labels(&option)
2032 .into_iter()
2033 .for_each(|value| volume.set_labels(value)),
2034 "name" if !duplicate => self
2035 .parse_string(&option, "volume custom name")
2036 .into_iter()
2037 .for_each(|value| volume.set_custom_name(value)),
2038 name if name.starts_with("x-") => volume.push_extension(option.reference()),
2039 _ if duplicate => {}
2040 _ => volume.push_unknown(option.reference()),
2041 }
2042 }
2043 definitions.push(volume);
2044 }
2045 definitions
2046 }
2047
2048 fn parse_config_definitions(&mut self, field: &ParsedField) -> Vec<ConfigDefinition> {
2049 let Some(mapping) = self.resource_collection(field, "configs") else {
2050 return Vec::new();
2051 };
2052 let mut definitions = Vec::new();
2053 let mut seen = BTreeMap::new();
2054 for resource in self.fields(&mapping) {
2055 if self.record_duplicate(&mut seen, &resource) {
2056 continue;
2057 }
2058 let mut config = ConfigDefinition::new(resource.name.clone(), resource.span);
2059 if Self::field_is_null(&resource) {
2060 definitions.push(config);
2061 continue;
2062 }
2063 let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
2064 self.expected(
2065 RESOURCE_EXPECTED_FORM,
2066 &resource,
2067 "config definition must be a mapping or null",
2068 );
2069 continue;
2070 };
2071 let mut nested_seen = BTreeMap::new();
2072 for option in self.fields(definition) {
2073 let duplicate = self.record_duplicate(&mut nested_seen, &option);
2074 match option.name.value.as_str() {
2075 "file" if !duplicate => self
2076 .parse_string(&option, "config file")
2077 .into_iter()
2078 .for_each(|value| config.set_file(value)),
2079 "environment" if !duplicate => self
2080 .parse_string(&option, "config environment source")
2081 .into_iter()
2082 .for_each(|value| config.set_environment(value)),
2083 "content" if !duplicate => self
2084 .parse_string(&option, "config content")
2085 .into_iter()
2086 .for_each(|value| config.set_content(value)),
2087 "external" if !duplicate => self
2088 .parse_boolean(&option, "config external")
2089 .into_iter()
2090 .for_each(|value| config.set_external(value)),
2091 "name" if !duplicate => self
2092 .parse_string(&option, "config custom name")
2093 .into_iter()
2094 .for_each(|value| config.set_custom_name(value)),
2095 name if name.starts_with("x-") => config.push_extension(option.reference()),
2096 _ if duplicate => {}
2097 _ => config.push_unknown(option.reference()),
2098 }
2099 }
2100 definitions.push(config);
2101 }
2102 definitions
2103 }
2104
2105 fn parse_secret_definitions(&mut self, field: &ParsedField) -> Vec<SecretDefinition> {
2106 let Some(mapping) = self.resource_collection(field, "secrets") else {
2107 return Vec::new();
2108 };
2109 let mut definitions = Vec::new();
2110 let mut seen = BTreeMap::new();
2111 for resource in self.fields(&mapping) {
2112 if self.record_duplicate(&mut seen, &resource) {
2113 continue;
2114 }
2115 let mut secret = SecretDefinition::new(resource.name.clone(), resource.span);
2116 if Self::field_is_null(&resource) {
2117 definitions.push(secret);
2118 continue;
2119 }
2120 let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
2121 self.expected(
2122 RESOURCE_EXPECTED_FORM,
2123 &resource,
2124 "secret definition must be a mapping or null",
2125 );
2126 continue;
2127 };
2128 let mut nested_seen = BTreeMap::new();
2129 for option in self.fields(definition) {
2130 let duplicate = self.record_duplicate(&mut nested_seen, &option);
2131 match option.name.value.as_str() {
2132 "file" if !duplicate => self
2133 .parse_string(&option, "secret file")
2134 .into_iter()
2135 .for_each(|value| secret.set_file(value)),
2136 "environment" if !duplicate => self
2137 .parse_string(&option, "secret environment source")
2138 .into_iter()
2139 .for_each(|value| secret.set_environment(value)),
2140 "external" if !duplicate => self
2141 .parse_boolean(&option, "secret external")
2142 .into_iter()
2143 .for_each(|value| secret.set_external(value)),
2144 "name" if !duplicate => self
2145 .parse_string(&option, "secret custom name")
2146 .into_iter()
2147 .for_each(|value| secret.set_custom_name(value)),
2148 name if name.starts_with("x-") => secret.push_extension(option.reference()),
2149 _ if duplicate => {}
2150 _ => secret.push_unknown(option.reference()),
2151 }
2152 }
2153 definitions.push(secret);
2154 }
2155 definitions
2156 }
2157
2158 fn resource_collection(&mut self, field: &ParsedField, kind: &str) -> Option<Mapping> {
2159 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
2160 self.expected(EXPECTED_MAPPING, field, format!("top-level {kind} must be a mapping"));
2161 return None;
2162 };
2163 Some(mapping.clone())
2164 }
2165
2166 fn parse_string(&mut self, field: &ParsedField, description: &str) -> Option<Located<String>> {
2167 let Some(scalar) = field.value.as_ref().and_then(YamlNode::as_scalar) else {
2168 self.expected(EXPECTED_SCALAR, field, format!("{description} must be a scalar"));
2169 return None;
2170 };
2171 if ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null {
2172 self.expected(
2173 EXPECTED_SCALAR,
2174 field,
2175 format!("{description} must be a non-null scalar"),
2176 );
2177 return None;
2178 }
2179 Some(Located::new(
2180 scalar_string_from_source(&self.source, scalar),
2181 span_from_position(self.source_id, scalar.byte_range()),
2182 ))
2183 }
2184
2185 fn parse_boolean(&mut self, field: &ParsedField, description: &str) -> Option<Located<BooleanValue>> {
2186 let Some(scalar) = field.value.as_ref().and_then(YamlNode::as_scalar) else {
2187 self.expected(EXPECTED_BOOLEAN, field, format!("{description} must be a boolean"));
2188 return None;
2189 };
2190 let span = span_from_position(self.source_id, scalar.byte_range());
2191 let scalar_value = ScalarValue::from_scalar(scalar);
2192 if let Some(value) = scalar_value.to_bool() {
2193 return Some(Located::new(BooleanValue::Literal(value), span));
2194 }
2195 let value = scalar_string_from_source(&self.source, scalar);
2196 if value.contains('$') {
2197 return Some(Located::new(BooleanValue::Expression(value), span));
2198 }
2199 self.diagnostics.push(
2200 Diagnostic::new(
2201 EXPECTED_BOOLEAN,
2202 Severity::Error,
2203 format!("{description} must be a boolean or interpolation expression"),
2204 )
2205 .with_label(DiagnosticLabel::primary(span, "not a boolean expression")),
2206 );
2207 None
2208 }
2209
2210 fn parse_string_sequence(&mut self, field: &ParsedField, description: &str) -> Vec<Located<String>> {
2211 let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
2212 self.expected(EXPECTED_SEQUENCE, field, format!("{description} must be a sequence"));
2213 return Vec::new();
2214 };
2215 self.parse_scalar_nodes(
2216 sequence.values(),
2217 field.span,
2218 format!("{description} entries must be scalars"),
2219 )
2220 }
2221
2222 fn parse_scalar_nodes(
2223 &mut self,
2224 nodes: impl Iterator<Item = YamlNode>,
2225 fallback_span: SourceSpan,
2226 message: impl Into<String>,
2227 ) -> Vec<Located<String>> {
2228 let message = message.into();
2229 let mut values = Vec::new();
2230 for node in nodes {
2231 let YamlNode::Scalar(scalar) = node else {
2232 self.unsupported_sequence_item(EXPECTED_SCALAR, &node, fallback_span, &message);
2233 continue;
2234 };
2235 let scalar_value = ScalarValue::from_scalar(&scalar);
2236 if scalar_value.scalar_type() == ScalarType::Null {
2237 self.unsupported_sequence_item(EXPECTED_SCALAR, &YamlNode::Scalar(scalar), fallback_span, &message);
2238 continue;
2239 }
2240 let span = span_from_position(self.source_id, scalar.byte_range());
2241 values.push(Located::new(scalar_string_from_source(&self.source, &scalar), span));
2242 }
2243 values
2244 }
2245
2246 fn parse_scalar_mapping(&mut self, field: &ParsedField, description: &str) -> Vec<KeyValueEntry> {
2247 let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
2248 self.expected(EXPECTED_MAPPING, field, format!("{description} must be a mapping"));
2249 return Vec::new();
2250 };
2251 let mut entries = Vec::new();
2252 let mut seen = BTreeMap::new();
2253 for entry in self.fields(mapping) {
2254 if self.record_duplicate(&mut seen, &entry) {
2255 continue;
2256 }
2257 if let Some(value) = self.parse_compose_scalar(&entry, format!("{description} values must be scalars")) {
2258 entries.push(KeyValueEntry::new(entry.name, value, entry.span));
2259 }
2260 }
2261 entries
2262 }
2263
2264 fn parse_compose_scalar(
2265 &mut self,
2266 field: &ParsedField,
2267 message: impl Into<String>,
2268 ) -> Option<Located<ComposeScalar>> {
2269 let Some(node) = field.value.as_ref() else {
2270 return Some(Located::new(ComposeScalar::Null, field.name.span));
2271 };
2272 let Some(scalar) = node.as_scalar() else {
2273 self.expected(EXPECTED_SCALAR, field, message);
2274 return None;
2275 };
2276 let span = span_from_position(self.source_id, scalar.byte_range());
2277 let value = ScalarValue::from_scalar(scalar);
2278 let typed = match value.scalar_type() {
2279 ScalarType::Null => ComposeScalar::Null,
2280 ScalarType::Boolean => ComposeScalar::Boolean(value.to_bool().unwrap_or(false)),
2281 ScalarType::Integer | ScalarType::Float => {
2282 ComposeScalar::Number(scalar_string_from_source(&self.source, scalar))
2283 }
2284 ScalarType::String | ScalarType::Timestamp | ScalarType::Regex => {
2285 ComposeScalar::String(scalar_string_from_source(&self.source, scalar))
2286 }
2287 };
2288 Some(Located::new(typed, span))
2289 }
2290
2291 fn parse_labels(&mut self, field: &ParsedField) -> Option<Labels> {
2292 match field.value.as_ref() {
2293 Some(YamlNode::Sequence(sequence)) => {
2294 let span = span_from_position(self.source_id, sequence.byte_range());
2295 let values =
2296 self.parse_scalar_nodes(sequence.values(), field.span, "label list entries must be scalars");
2297 Some(Labels::List { span, values })
2298 }
2299 Some(YamlNode::Mapping(mapping)) => {
2300 let span = span_from_position(self.source_id, mapping.byte_range());
2301 let entries = self.parse_scalar_mapping(field, "labels");
2302 Some(Labels::Map { span, entries })
2303 }
2304 _ => {
2305 self.expected(EXPECTED_FIELD_FORM, field, "labels must be a sequence or mapping");
2306 None
2307 }
2308 }
2309 }
2310
2311 fn field_is_null(field: &ParsedField) -> bool {
2312 field.value.as_ref().is_none_or(|node| {
2313 node.as_scalar()
2314 .is_some_and(|scalar| ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null)
2315 })
2316 }
2317
2318 fn unsupported_sequence_item(
2319 &mut self,
2320 code: DiagnosticCode,
2321 node: &YamlNode,
2322 fallback_span: SourceSpan,
2323 message: impl Into<String>,
2324 ) {
2325 let span = node_span(self.source_id, node).unwrap_or(fallback_span);
2326 self.diagnostics.push(
2327 Diagnostic::new(code, Severity::Error, message)
2328 .with_label(DiagnosticLabel::primary(span, "unsupported value form")),
2329 );
2330 }
2331
2332 fn fields(&mut self, mapping: &Mapping) -> Vec<ParsedField> {
2333 let fields = self.raw_fields(mapping);
2334 let mut fields = self.flatten_empty_value_continuations(fields);
2335 for field in &mut fields {
2336 field.value = field.value.take().map(|value| self.resolve_alias(value));
2337 }
2338 fields
2339 }
2340
2341 fn raw_fields(&mut self, mapping: &Mapping) -> Vec<ParsedField> {
2342 mapping
2343 .entries()
2344 .filter_map(|entry| {
2345 let key = entry.key_node()?;
2346 let Some(scalar) = key.as_scalar() else {
2347 let span = node_span(self.source_id, &key)
2348 .unwrap_or_else(|| span_from_position(self.source_id, mapping.byte_range()));
2349 self.diagnostics.push(
2350 Diagnostic::new(EXPECTED_SCALAR, Severity::Error, "Compose mapping keys must be scalars")
2351 .with_label(DiagnosticLabel::primary(span, "non-scalar key")),
2352 );
2353 return None;
2354 };
2355 let name_span = span_from_position(self.source_id, scalar.byte_range());
2356 let authored_value = entry.value_node();
2357 let value_span = authored_value
2358 .as_ref()
2359 .and_then(|value| node_span(self.source_id, value));
2360 let value = authored_value.map(unwrap_processing_tag);
2361 let span = value_span.map_or(name_span, |value_span| union(name_span, value_span));
2362 Some(ParsedField {
2363 name: Located::new(scalar_string_from_source(&self.source, scalar), name_span),
2364 value,
2365 value_span,
2366 span,
2367 })
2368 })
2369 .collect()
2370 }
2371
2372 fn resolve_alias(&self, node: YamlNode) -> YamlNode {
2373 let mut node = node;
2374 let mut visited = BTreeSet::new();
2375 for _ in 0..64 {
2376 let YamlNode::Alias(alias) = &node else {
2377 return node;
2378 };
2379 if !visited.insert(alias.name()) {
2380 return node;
2381 }
2382 let Some(target) = self.anchors.resolve(&alias.name()).and_then(|target| {
2383 YamlNode::from_syntax(target.clone()).or_else(|| target.children().find_map(YamlNode::from_syntax))
2384 }) else {
2385 return node;
2386 };
2387 node = target;
2388 }
2389 node
2390 }
2391
2392 fn flatten_empty_value_continuations(&mut self, fields: Vec<ParsedField>) -> Vec<ParsedField> {
2393 let Some(target_column) = fields.first().map(|field| self.source_column(field.name.span.start())) else {
2394 return fields;
2395 };
2396 self.recover_fields(fields, target_column)
2397 }
2398
2399 fn recover_fields(&mut self, fields: Vec<ParsedField>, target_column: usize) -> Vec<ParsedField> {
2400 let mut flattened = Vec::new();
2401 for mut field in fields {
2402 let field_column = self.source_column(field.name.span.start());
2403 let nested_mapping = field.value.as_ref().and_then(YamlNode::as_mapping).cloned();
2404 let continuation = nested_mapping.as_ref().is_some_and(|mapping| {
2405 !self.is_flow_mapping(mapping)
2406 && mapping
2407 .entries()
2408 .find_map(|entry| {
2409 let key = entry.key_node()?;
2410 let scalar = key.as_scalar()?;
2411 Some(scalar.byte_range().start as usize)
2412 })
2413 .is_some_and(|key_start| self.source_column(key_start) <= field_column)
2414 });
2415
2416 if continuation {
2417 field.value = None;
2418 field.value_span = None;
2419 field.span = field.name.span;
2420 }
2421 if field_column == target_column {
2422 flattened.push(field);
2423 }
2424 if let Some(mapping) = nested_mapping.filter(|mapping| !self.is_flow_mapping(mapping)) {
2425 let nested = self.raw_fields(&mapping);
2426 flattened.extend(self.recover_fields(nested, target_column));
2427 }
2428 }
2429 flattened
2430 }
2431
2432 fn is_flow_mapping(&self, mapping: &Mapping) -> bool {
2433 let position = mapping.byte_range();
2434 self.source
2435 .get(position.start as usize..position.end as usize)
2436 .is_some_and(|text| text.trim_start().starts_with('{'))
2437 }
2438
2439 fn record_duplicate(&mut self, seen: &mut BTreeMap<String, SourceSpan>, field: &ParsedField) -> bool {
2440 if let Some(first) = seen.get(field.name.value()) {
2441 self.diagnostics.push(
2442 Diagnostic::new(
2443 DUPLICATE_FIELD,
2444 Severity::Error,
2445 "Compose mapping fields must be unique",
2446 )
2447 .with_label(DiagnosticLabel::primary(field.name.span, "duplicate field"))
2448 .with_label(DiagnosticLabel::secondary(*first, "first field")),
2449 );
2450 true
2451 } else {
2452 seen.insert(field.name.value.clone(), field.name.span);
2453 false
2454 }
2455 }
2456
2457 fn expected(&mut self, code: DiagnosticCode, field: &ParsedField, message: impl Into<String>) {
2458 self.diagnostics.push(
2459 Diagnostic::new(code, Severity::Error, message)
2460 .with_label(DiagnosticLabel::primary(field.span, "unexpected value form")),
2461 );
2462 }
2463
2464 fn missing(&mut self, code: DiagnosticCode, span: SourceSpan, message: &'static str) {
2465 self.diagnostics.push(
2466 Diagnostic::new(code, Severity::Error, message)
2467 .with_label(DiagnosticLabel::primary(span, "incomplete long syntax")),
2468 );
2469 }
2470}
2471
2472fn unwrap_processing_tag(node: YamlNode) -> YamlNode {
2473 let YamlNode::TaggedNode(tagged) = &node else {
2474 return node;
2475 };
2476 if !matches!(tagged.tag().as_deref(), Some("!reset" | "!override")) {
2477 return node;
2478 }
2479 tagged
2480 .as_node()
2481 .and_then(|syntax| syntax.children().find_map(YamlNode::from_syntax))
2482 .unwrap_or(node)
2483}
2484
2485#[derive(Debug, Clone)]
2486enum ParsedGrant {
2487 Short(Located<String>),
2488 Long(Box<LongGrant>),
2489}
2490
2491#[derive(Debug, Clone)]
2492struct ParsedField {
2493 name: Located<String>,
2494 value: Option<YamlNode>,
2495 value_span: Option<SourceSpan>,
2496 span: SourceSpan,
2497}
2498
2499impl ParsedField {
2500 fn reference(&self) -> FieldReference {
2501 FieldReference {
2502 name: self.name.clone(),
2503 span: self.span,
2504 value_span: self.value_span,
2505 }
2506 }
2507}
2508
2509fn node_span(source_id: SourceId, node: &YamlNode) -> Option<SourceSpan> {
2510 let position = match node {
2511 YamlNode::Scalar(value) => value.byte_range(),
2512 YamlNode::Mapping(value) => value.byte_range(),
2513 YamlNode::Sequence(value) => value.byte_range(),
2514 YamlNode::Alias(_) | YamlNode::TaggedNode(_) => {
2515 let range = node.as_node()?.text_range();
2516 return Some(SourceSpan::from_valid_offsets(
2517 source_id,
2518 u32::from(range.start()) as usize,
2519 u32::from(range.end()) as usize,
2520 ));
2521 }
2522 };
2523 Some(span_from_position(source_id, position))
2524}
2525
2526fn span_from_position(source_id: SourceId, position: yaml_edit::TextPosition) -> SourceSpan {
2527 SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize)
2528}
2529
2530fn union(left: SourceSpan, right: SourceSpan) -> SourceSpan {
2531 SourceSpan::from_valid_offsets(
2532 left.source_id(),
2533 left.start().min(right.start()),
2534 left.end().max(right.end()),
2535 )
2536}