1use super::{ImplementationVersion, VersionRange};
4use crate::diagnostic::Severity;
5
6const SPEC_URL: &str = "https://github.com/compose-spec/compose-spec/blob/main/spec.md";
7const DOCKER_MERGE_URL: &str = "https://docs.docker.com/reference/compose-file/merge/";
8const DOCKER_SELINUX_ISSUE_URL: &str = "https://github.com/docker/compose/issues/13396";
9const DOCKER_HOST_GATEWAY_URL: &str = "https://docs.docker.com/compose/how-tos/networking/";
10const PODMAN_5_4_RUN_URL: &str = "https://docs.podman.io/en/v5.4.0/markdown/podman-run.1.html";
11const PROVIDER_CONFORMANCE_URL: &str =
12 "https://github.com/Strukturpiloten/compose-lens/blob/main/docs/research/provider-config-conformance-2026-07-31.md";
13
14const SPEC_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
15 EvidenceKind::Specification,
16 SPEC_URL,
17 "current Compose Specification syntax",
18 None,
19 None,
20)];
21const DOCKER_OVERRIDE_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
22 EvidenceKind::OfficialDocumentation,
23 DOCKER_MERGE_URL,
24 "Docker documents !override as requiring Compose 2.24.4 or newer",
25 Some(VersionRange::from_minimum(ImplementationVersion::new(2, 24, 4))),
26 None,
27)];
28const DOCKER_RESET_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
29 EvidenceKind::OfficialDocumentation,
30 DOCKER_MERGE_URL,
31 "current Docker documentation describes !reset but does not identify its first supported version",
32 None,
33 None,
34)];
35const DOCKER_PODMAN_SELINUX_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
36 EvidenceKind::IssueReproduction,
37 DOCKER_SELINUX_ISSUE_URL,
38 "Docker Compose 2.40.3 with Podman 5.6.2 applied short-form relabeling but long-form relabeling was ineffective",
39 Some(VersionRange::exact(ImplementationVersion::new(2, 40, 3))),
40 Some(VersionRange::exact(ImplementationVersion::new(5, 6, 2))),
41)];
42const HOST_GATEWAY_EVIDENCE: &[CompatibilityEvidence] = &[
43 CompatibilityEvidence::new(
44 EvidenceKind::OfficialDocumentation,
45 DOCKER_HOST_GATEWAY_URL,
46 "Docker documents host-gateway as an implementation-provided host address",
47 None,
48 None,
49 ),
50 CompatibilityEvidence::new(
51 EvidenceKind::OfficialDocumentation,
52 PODMAN_5_4_RUN_URL,
53 "Podman 5.4 documents host-gateway for --add-host",
54 None,
55 Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
56 ),
57];
58const PODMAN_USERNS_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
59 EvidenceKind::OfficialDocumentation,
60 PODMAN_5_4_RUN_URL,
61 "Podman 5.4 documents keep-id, auto, and nomap user namespace modes",
62 None,
63 Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
64)];
65const DOCKER_2_24_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
66 "reviewed feature-specific Docker Compose 2.24.3 config observations",
67 ImplementationVersion::new(2, 24, 3),
68)];
69const DOCKER_2_24_4_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
70 "reviewed feature-specific Docker Compose 2.24.4 config observations",
71 ImplementationVersion::new(2, 24, 4),
72)];
73const DOCKER_2_40_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
74 "reviewed feature-specific Docker Compose 2.40.3 config observations",
75 ImplementationVersion::new(2, 40, 3),
76)];
77const DOCKER_5_3_1_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
78 "reviewed feature-specific Docker Compose 5.3.1 config observations",
79 ImplementationVersion::new(5, 3, 1),
80)];
81const PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
82 "reviewed feature-specific podman-compose 1.3.0 config observations",
83 ImplementationVersion::new(1, 3, 0),
84)];
85const PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
86 "reviewed feature-specific podman-compose 1.5.0 config observations",
87 ImplementationVersion::new(1, 5, 0),
88)];
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum CompatibilityFeature {
94 ImageTagAndDigest,
96 ShortBindSelinuxRelabel,
98 LongBindSelinuxRelabel,
100 ResetTag,
102 OverrideTag,
104 HostGatewayToken,
106 PodmanUserNamespaceMode,
108 ExtensionField,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114#[non_exhaustive]
115pub enum CompatibilityClassification {
116 Supported,
118 Extension,
120 ImplementationSpecific,
122 Deprecated,
124 Unsupported,
126 Unknown,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132#[non_exhaustive]
133pub enum ComposeProvider {
134 Specification,
136 DockerCompose(ImplementationVersion),
138 PodmanCompose(ImplementationVersion),
140 Tolerant,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146#[non_exhaustive]
147pub enum ContainerRuntime {
148 DockerEngine(ImplementationVersion),
150 Podman(ImplementationVersion),
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub struct CompatibilityProfile {
160 provider: ComposeProvider,
161 runtime: Option<ContainerRuntime>,
162}
163
164impl CompatibilityProfile {
165 #[must_use]
167 pub const fn specification() -> Self {
168 Self {
169 provider: ComposeProvider::Specification,
170 runtime: None,
171 }
172 }
173
174 #[must_use]
176 pub const fn docker_compose(version: ImplementationVersion) -> Self {
177 Self {
178 provider: ComposeProvider::DockerCompose(version),
179 runtime: None,
180 }
181 }
182
183 #[must_use]
185 pub const fn podman_compose(version: ImplementationVersion) -> Self {
186 Self {
187 provider: ComposeProvider::PodmanCompose(version),
188 runtime: None,
189 }
190 }
191
192 #[must_use]
194 pub const fn tolerant() -> Self {
195 Self {
196 provider: ComposeProvider::Tolerant,
197 runtime: None,
198 }
199 }
200
201 #[must_use]
203 pub const fn with_runtime(mut self, runtime: ContainerRuntime) -> Self {
204 self.runtime = Some(runtime);
205 self
206 }
207
208 #[must_use]
210 pub const fn provider(self) -> ComposeProvider {
211 self.provider
212 }
213
214 #[must_use]
216 pub const fn runtime(self) -> Option<ContainerRuntime> {
217 self.runtime
218 }
219
220 #[must_use]
222 pub fn classify(self, feature: CompatibilityFeature) -> CompatibilityRule {
223 match self.provider {
224 ComposeProvider::Specification => specification_rule(feature),
225 ComposeProvider::DockerCompose(version) => docker_rule(self, version, feature),
226 ComposeProvider::PodmanCompose(version) => podman_compose_rule(version, feature),
227 ComposeProvider::Tolerant => tolerant_rule(feature),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
234#[non_exhaustive]
235pub enum EvidenceKind {
236 Specification,
238 OfficialDocumentation,
240 IssueReproduction,
242 ProviderConformance,
244 RuntimeConformance,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250pub struct CompatibilityEvidence {
251 kind: EvidenceKind,
252 source: &'static str,
253 summary: &'static str,
254 provider_versions: Option<VersionRange>,
255 runtime_versions: Option<VersionRange>,
256}
257
258impl CompatibilityEvidence {
259 const fn new(
260 kind: EvidenceKind,
261 source: &'static str,
262 summary: &'static str,
263 provider_versions: Option<VersionRange>,
264 runtime_versions: Option<VersionRange>,
265 ) -> Self {
266 Self {
267 kind,
268 source,
269 summary,
270 provider_versions,
271 runtime_versions,
272 }
273 }
274
275 #[must_use]
277 pub const fn kind(self) -> EvidenceKind {
278 self.kind
279 }
280
281 #[must_use]
283 pub const fn source(self) -> &'static str {
284 self.source
285 }
286
287 #[must_use]
289 pub const fn summary(self) -> &'static str {
290 self.summary
291 }
292
293 #[must_use]
295 pub const fn provider_versions(self) -> Option<VersionRange> {
296 self.provider_versions
297 }
298
299 #[must_use]
301 pub const fn runtime_versions(self) -> Option<VersionRange> {
302 self.runtime_versions
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct CompatibilityRule {
309 feature: CompatibilityFeature,
310 classification: CompatibilityClassification,
311 diagnostic_severity: Option<Severity>,
312 explanation: &'static str,
313 evidence: &'static [CompatibilityEvidence],
314}
315
316impl CompatibilityRule {
317 #[must_use]
319 pub const fn feature(&self) -> CompatibilityFeature {
320 self.feature
321 }
322
323 #[must_use]
325 pub const fn classification(&self) -> CompatibilityClassification {
326 self.classification
327 }
328
329 #[must_use]
331 pub const fn diagnostic_severity(&self) -> Option<Severity> {
332 self.diagnostic_severity
333 }
334
335 #[must_use]
337 pub const fn explanation(&self) -> &'static str {
338 self.explanation
339 }
340
341 #[must_use]
343 pub const fn evidence(&self) -> &'static [CompatibilityEvidence] {
344 self.evidence
345 }
346}
347
348fn specification_rule(feature: CompatibilityFeature) -> CompatibilityRule {
349 match feature {
350 CompatibilityFeature::ImageTagAndDigest => rule(
351 feature,
352 CompatibilityClassification::ImplementationSpecific,
353 Some(Severity::Warning),
354 "the documented image grammar selects a tag or a digest, while real implementations may accept both",
355 SPEC_EVIDENCE,
356 ),
357 CompatibilityFeature::ExtensionField => rule(
358 feature,
359 CompatibilityClassification::Extension,
360 None,
361 "x- fields use the Compose extension namespace",
362 SPEC_EVIDENCE,
363 ),
364 CompatibilityFeature::HostGatewayToken => rule(
365 feature,
366 CompatibilityClassification::ImplementationSpecific,
367 Some(Severity::Warning),
368 "host-gateway is resolved by container implementations rather than by Compose syntax alone",
369 HOST_GATEWAY_EVIDENCE,
370 ),
371 CompatibilityFeature::PodmanUserNamespaceMode => rule(
372 feature,
373 CompatibilityClassification::ImplementationSpecific,
374 Some(Severity::Warning),
375 "the Compose field is portable but keep-id, auto, and nomap values are Podman-specific",
376 PODMAN_USERNS_EVIDENCE,
377 ),
378 CompatibilityFeature::ShortBindSelinuxRelabel
379 | CompatibilityFeature::LongBindSelinuxRelabel
380 | CompatibilityFeature::ResetTag
381 | CompatibilityFeature::OverrideTag => rule(
382 feature,
383 CompatibilityClassification::Supported,
384 None,
385 "the construct is defined by the current Compose Specification",
386 SPEC_EVIDENCE,
387 ),
388 }
389}
390
391fn docker_rule(
392 profile: CompatibilityProfile,
393 version: ImplementationVersion,
394 feature: CompatibilityFeature,
395) -> CompatibilityRule {
396 match feature {
397 CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(2, 24, 3) => rule(
398 feature,
399 CompatibilityClassification::Unsupported,
400 Some(Severity::Error),
401 "Docker Compose 2.24.3 accepted !override syntax but did not apply replacement semantics",
402 DOCKER_2_24_3_PROVIDER_EVIDENCE,
403 ),
404 CompatibilityFeature::OverrideTag if version < ImplementationVersion::new(2, 24, 4) => rule(
405 feature,
406 CompatibilityClassification::Unsupported,
407 Some(Severity::Error),
408 "the selected Docker Compose version predates documented !override support",
409 DOCKER_OVERRIDE_EVIDENCE,
410 ),
411 CompatibilityFeature::OverrideTag => rule(
412 feature,
413 CompatibilityClassification::Supported,
414 None,
415 "the selected Docker Compose version meets the documented !override minimum",
416 DOCKER_OVERRIDE_EVIDENCE,
417 ),
418 CompatibilityFeature::ResetTag if !docker_provider_evidence(version).is_empty() => rule(
419 feature,
420 CompatibilityClassification::Supported,
421 None,
422 "the selected exact Docker Compose version applied !reset in reviewed provider conformance",
423 docker_provider_evidence(version),
424 ),
425 CompatibilityFeature::ResetTag => rule(
426 feature,
427 CompatibilityClassification::ImplementationSpecific,
428 Some(Severity::Warning),
429 "Docker documents !reset, but the available evidence does not establish its first supported release",
430 DOCKER_RESET_EVIDENCE,
431 ),
432 CompatibilityFeature::ShortBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
433 feature,
434 CompatibilityClassification::Supported,
435 None,
436 "the exact reported provider/runtime pair applied short-form SELinux relabeling",
437 DOCKER_PODMAN_SELINUX_EVIDENCE,
438 ),
439 CompatibilityFeature::LongBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
440 feature,
441 CompatibilityClassification::Unsupported,
442 Some(Severity::Error),
443 "the exact reported provider/runtime pair accepted this form but did not relabel the host path",
444 DOCKER_PODMAN_SELINUX_EVIDENCE,
445 ),
446 CompatibilityFeature::ShortBindSelinuxRelabel => rule(
447 feature,
448 CompatibilityClassification::ImplementationSpecific,
449 Some(Severity::Warning),
450 "SELinux relabeling depends on the backend runtime, host platform, and authored mount form",
451 docker_provider_evidence(version),
452 ),
453 CompatibilityFeature::LongBindSelinuxRelabel => rule(
454 feature,
455 CompatibilityClassification::Unknown,
456 Some(Severity::Warning),
457 "no versioned evidence covers long-form SELinux behavior for the selected provider/runtime pair",
458 docker_provider_evidence(version),
459 ),
460 CompatibilityFeature::ImageTagAndDigest if !docker_provider_evidence(version).is_empty() => rule(
461 feature,
462 CompatibilityClassification::Supported,
463 None,
464 "the selected exact Docker Compose version retained the combined tag and digest",
465 docker_provider_evidence(version),
466 ),
467 CompatibilityFeature::ImageTagAndDigest => rule(
468 feature,
469 CompatibilityClassification::ImplementationSpecific,
470 Some(Severity::Warning),
471 "combined image tags and digests require implementation evidence beyond the documented Compose grammar",
472 SPEC_EVIDENCE,
473 ),
474 CompatibilityFeature::ExtensionField => rule(
475 feature,
476 CompatibilityClassification::Extension,
477 None,
478 "x- fields use the Compose extension namespace",
479 SPEC_EVIDENCE,
480 ),
481 CompatibilityFeature::HostGatewayToken => rule(
482 feature,
483 CompatibilityClassification::ImplementationSpecific,
484 Some(Severity::Warning),
485 "host-gateway depends on provider pass-through and runtime network configuration",
486 HOST_GATEWAY_EVIDENCE,
487 ),
488 CompatibilityFeature::PodmanUserNamespaceMode => rule(
489 feature,
490 CompatibilityClassification::Unknown,
491 Some(Severity::Warning),
492 "Podman documents this runtime value, but no versioned Docker Compose pass-through observation is recorded",
493 PODMAN_USERNS_EVIDENCE,
494 ),
495 }
496}
497
498fn podman_compose_rule(version: ImplementationVersion, feature: CompatibilityFeature) -> CompatibilityRule {
499 if feature == CompatibilityFeature::ExtensionField {
500 return rule(
501 feature,
502 CompatibilityClassification::Extension,
503 None,
504 "x- fields use the Compose extension namespace",
505 SPEC_EVIDENCE,
506 );
507 }
508 let evidence = podman_compose_provider_evidence(version);
509 if !evidence.is_empty() {
510 return match feature {
511 CompatibilityFeature::ImageTagAndDigest => rule(
512 feature,
513 CompatibilityClassification::Supported,
514 None,
515 "the selected exact podman-compose version retained the combined tag and digest",
516 evidence,
517 ),
518 CompatibilityFeature::ResetTag => rule(
519 feature,
520 CompatibilityClassification::Unsupported,
521 Some(Severity::Error),
522 "the selected exact podman-compose version failed while processing !reset",
523 evidence,
524 ),
525 CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(1, 3, 0) => rule(
526 feature,
527 CompatibilityClassification::Unsupported,
528 Some(Severity::Error),
529 "podman-compose 1.3.0 rejected !override",
530 evidence,
531 ),
532 CompatibilityFeature::OverrideTag => rule(
533 feature,
534 CompatibilityClassification::Supported,
535 None,
536 "podman-compose 1.5.0 applied !override replacement semantics",
537 evidence,
538 ),
539 CompatibilityFeature::ShortBindSelinuxRelabel | CompatibilityFeature::LongBindSelinuxRelabel => rule(
540 feature,
541 CompatibilityClassification::Unknown,
542 Some(Severity::Warning),
543 "provider config accepted the SELinux form, but no reviewed runtime-effect record establishes relabeling",
544 evidence,
545 ),
546 CompatibilityFeature::HostGatewayToken => rule(
547 feature,
548 CompatibilityClassification::Unknown,
549 Some(Severity::Warning),
550 "Podman 5.4 documents the runtime token, but provider pass-through has not been recorded",
551 HOST_GATEWAY_EVIDENCE,
552 ),
553 CompatibilityFeature::PodmanUserNamespaceMode => rule(
554 feature,
555 CompatibilityClassification::Unknown,
556 Some(Severity::Warning),
557 "Podman 5.4 documents the runtime mode, but provider pass-through has not been recorded",
558 PODMAN_USERNS_EVIDENCE,
559 ),
560 CompatibilityFeature::ExtensionField => unreachable!("extension fields returned above"),
561 };
562 }
563 rule(
564 feature,
565 CompatibilityClassification::Unknown,
566 Some(Severity::Warning),
567 "no versioned podman-compose conformance evidence covers this construct yet",
568 &[],
569 )
570}
571
572const fn provider_evidence(summary: &'static str, version: ImplementationVersion) -> CompatibilityEvidence {
573 CompatibilityEvidence::new(
574 EvidenceKind::ProviderConformance,
575 PROVIDER_CONFORMANCE_URL,
576 summary,
577 Some(VersionRange::exact(version)),
578 None,
579 )
580}
581
582fn docker_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
583 match version {
584 value if value == ImplementationVersion::new(2, 24, 3) => DOCKER_2_24_3_PROVIDER_EVIDENCE,
585 value if value == ImplementationVersion::new(2, 24, 4) => DOCKER_2_24_4_PROVIDER_EVIDENCE,
586 value if value == ImplementationVersion::new(2, 40, 3) => DOCKER_2_40_3_PROVIDER_EVIDENCE,
587 value if value == ImplementationVersion::new(5, 3, 1) => DOCKER_5_3_1_PROVIDER_EVIDENCE,
588 _ => &[],
589 }
590}
591
592fn podman_compose_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
593 match version {
594 value if value == ImplementationVersion::new(1, 3, 0) => PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE,
595 value if value == ImplementationVersion::new(1, 5, 0) => PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE,
596 _ => &[],
597 }
598}
599
600fn tolerant_rule(feature: CompatibilityFeature) -> CompatibilityRule {
601 if feature == CompatibilityFeature::ExtensionField {
602 return rule(
603 feature,
604 CompatibilityClassification::Extension,
605 None,
606 "x- fields are preserved as Compose extensions",
607 SPEC_EVIDENCE,
608 );
609 }
610 rule(
611 feature,
612 CompatibilityClassification::Unknown,
613 Some(Severity::Note),
614 "tolerant preservation deliberately makes no runtime-support claim",
615 &[],
616 )
617}
618
619fn is_reported_selinux_context(profile: CompatibilityProfile) -> bool {
620 profile.provider == ComposeProvider::DockerCompose(ImplementationVersion::new(2, 40, 3))
621 && profile.runtime == Some(ContainerRuntime::Podman(ImplementationVersion::new(5, 6, 2)))
622}
623
624fn rule(
625 feature: CompatibilityFeature,
626 classification: CompatibilityClassification,
627 diagnostic_severity: Option<Severity>,
628 explanation: &'static str,
629 evidence: &'static [CompatibilityEvidence],
630) -> CompatibilityRule {
631 CompatibilityRule {
632 feature,
633 classification,
634 diagnostic_severity,
635 explanation,
636 evidence,
637 }
638}