use super::{ImplementationVersion, VersionRange};
use crate::diagnostic::Severity;
const SPEC_URL: &str = "https://github.com/compose-spec/compose-spec/blob/main/spec.md";
const DOCKER_MERGE_URL: &str = "https://docs.docker.com/reference/compose-file/merge/";
const DOCKER_SELINUX_ISSUE_URL: &str = "https://github.com/docker/compose/issues/13396";
const DOCKER_HOST_GATEWAY_URL: &str = "https://docs.docker.com/compose/how-tos/networking/";
const PODMAN_5_4_RUN_URL: &str = "https://docs.podman.io/en/v5.4.0/markdown/podman-run.1.html";
const PROVIDER_CONFORMANCE_URL: &str =
"https://github.com/Strukturpiloten/compose-lens/blob/main/docs/research/provider-config-conformance-2026-07-31.md";
const SPEC_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
EvidenceKind::Specification,
SPEC_URL,
"current Compose Specification syntax",
None,
None,
)];
const DOCKER_OVERRIDE_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
EvidenceKind::OfficialDocumentation,
DOCKER_MERGE_URL,
"Docker documents !override as requiring Compose 2.24.4 or newer",
Some(VersionRange::from_minimum(ImplementationVersion::new(2, 24, 4))),
None,
)];
const DOCKER_RESET_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
EvidenceKind::OfficialDocumentation,
DOCKER_MERGE_URL,
"current Docker documentation describes !reset but does not identify its first supported version",
None,
None,
)];
const DOCKER_PODMAN_SELINUX_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
EvidenceKind::IssueReproduction,
DOCKER_SELINUX_ISSUE_URL,
"Docker Compose 2.40.3 with Podman 5.6.2 applied short-form relabeling but long-form relabeling was ineffective",
Some(VersionRange::exact(ImplementationVersion::new(2, 40, 3))),
Some(VersionRange::exact(ImplementationVersion::new(5, 6, 2))),
)];
const HOST_GATEWAY_EVIDENCE: &[CompatibilityEvidence] = &[
CompatibilityEvidence::new(
EvidenceKind::OfficialDocumentation,
DOCKER_HOST_GATEWAY_URL,
"Docker documents host-gateway as an implementation-provided host address",
None,
None,
),
CompatibilityEvidence::new(
EvidenceKind::OfficialDocumentation,
PODMAN_5_4_RUN_URL,
"Podman 5.4 documents host-gateway for --add-host",
None,
Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
),
];
const PODMAN_USERNS_EVIDENCE: &[CompatibilityEvidence] = &[CompatibilityEvidence::new(
EvidenceKind::OfficialDocumentation,
PODMAN_5_4_RUN_URL,
"Podman 5.4 documents keep-id, auto, and nomap user namespace modes",
None,
Some(VersionRange::from_minimum(ImplementationVersion::new(5, 4, 0))),
)];
const DOCKER_2_24_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific Docker Compose 2.24.3 config observations",
ImplementationVersion::new(2, 24, 3),
)];
const DOCKER_2_24_4_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific Docker Compose 2.24.4 config observations",
ImplementationVersion::new(2, 24, 4),
)];
const DOCKER_2_40_3_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific Docker Compose 2.40.3 config observations",
ImplementationVersion::new(2, 40, 3),
)];
const DOCKER_5_3_1_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific Docker Compose 5.3.1 config observations",
ImplementationVersion::new(5, 3, 1),
)];
const PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific podman-compose 1.3.0 config observations",
ImplementationVersion::new(1, 3, 0),
)];
const PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE: &[CompatibilityEvidence] = &[provider_evidence(
"reviewed feature-specific podman-compose 1.5.0 config observations",
ImplementationVersion::new(1, 5, 0),
)];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompatibilityFeature {
ImageTagAndDigest,
ShortBindSelinuxRelabel,
LongBindSelinuxRelabel,
ResetTag,
OverrideTag,
HostGatewayToken,
PodmanUserNamespaceMode,
ExtensionField,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CompatibilityClassification {
Supported,
Extension,
ImplementationSpecific,
Deprecated,
Unsupported,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ComposeProvider {
Specification,
DockerCompose(ImplementationVersion),
PodmanCompose(ImplementationVersion),
Tolerant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContainerRuntime {
DockerEngine(ImplementationVersion),
Podman(ImplementationVersion),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompatibilityProfile {
provider: ComposeProvider,
runtime: Option<ContainerRuntime>,
}
impl CompatibilityProfile {
#[must_use]
pub const fn specification() -> Self {
Self {
provider: ComposeProvider::Specification,
runtime: None,
}
}
#[must_use]
pub const fn docker_compose(version: ImplementationVersion) -> Self {
Self {
provider: ComposeProvider::DockerCompose(version),
runtime: None,
}
}
#[must_use]
pub const fn podman_compose(version: ImplementationVersion) -> Self {
Self {
provider: ComposeProvider::PodmanCompose(version),
runtime: None,
}
}
#[must_use]
pub const fn tolerant() -> Self {
Self {
provider: ComposeProvider::Tolerant,
runtime: None,
}
}
#[must_use]
pub const fn with_runtime(mut self, runtime: ContainerRuntime) -> Self {
self.runtime = Some(runtime);
self
}
#[must_use]
pub const fn provider(self) -> ComposeProvider {
self.provider
}
#[must_use]
pub const fn runtime(self) -> Option<ContainerRuntime> {
self.runtime
}
#[must_use]
pub fn classify(self, feature: CompatibilityFeature) -> CompatibilityRule {
match self.provider {
ComposeProvider::Specification => specification_rule(feature),
ComposeProvider::DockerCompose(version) => docker_rule(self, version, feature),
ComposeProvider::PodmanCompose(version) => podman_compose_rule(version, feature),
ComposeProvider::Tolerant => tolerant_rule(feature),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EvidenceKind {
Specification,
OfficialDocumentation,
IssueReproduction,
ProviderConformance,
RuntimeConformance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompatibilityEvidence {
kind: EvidenceKind,
source: &'static str,
summary: &'static str,
provider_versions: Option<VersionRange>,
runtime_versions: Option<VersionRange>,
}
impl CompatibilityEvidence {
const fn new(
kind: EvidenceKind,
source: &'static str,
summary: &'static str,
provider_versions: Option<VersionRange>,
runtime_versions: Option<VersionRange>,
) -> Self {
Self {
kind,
source,
summary,
provider_versions,
runtime_versions,
}
}
#[must_use]
pub const fn kind(self) -> EvidenceKind {
self.kind
}
#[must_use]
pub const fn source(self) -> &'static str {
self.source
}
#[must_use]
pub const fn summary(self) -> &'static str {
self.summary
}
#[must_use]
pub const fn provider_versions(self) -> Option<VersionRange> {
self.provider_versions
}
#[must_use]
pub const fn runtime_versions(self) -> Option<VersionRange> {
self.runtime_versions
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompatibilityRule {
feature: CompatibilityFeature,
classification: CompatibilityClassification,
diagnostic_severity: Option<Severity>,
explanation: &'static str,
evidence: &'static [CompatibilityEvidence],
}
impl CompatibilityRule {
#[must_use]
pub const fn feature(&self) -> CompatibilityFeature {
self.feature
}
#[must_use]
pub const fn classification(&self) -> CompatibilityClassification {
self.classification
}
#[must_use]
pub const fn diagnostic_severity(&self) -> Option<Severity> {
self.diagnostic_severity
}
#[must_use]
pub const fn explanation(&self) -> &'static str {
self.explanation
}
#[must_use]
pub const fn evidence(&self) -> &'static [CompatibilityEvidence] {
self.evidence
}
}
fn specification_rule(feature: CompatibilityFeature) -> CompatibilityRule {
match feature {
CompatibilityFeature::ImageTagAndDigest => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"the documented image grammar selects a tag or a digest, while real implementations may accept both",
SPEC_EVIDENCE,
),
CompatibilityFeature::ExtensionField => rule(
feature,
CompatibilityClassification::Extension,
None,
"x- fields use the Compose extension namespace",
SPEC_EVIDENCE,
),
CompatibilityFeature::HostGatewayToken => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"host-gateway is resolved by container implementations rather than by Compose syntax alone",
HOST_GATEWAY_EVIDENCE,
),
CompatibilityFeature::PodmanUserNamespaceMode => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"the Compose field is portable but keep-id, auto, and nomap values are Podman-specific",
PODMAN_USERNS_EVIDENCE,
),
CompatibilityFeature::ShortBindSelinuxRelabel
| CompatibilityFeature::LongBindSelinuxRelabel
| CompatibilityFeature::ResetTag
| CompatibilityFeature::OverrideTag => rule(
feature,
CompatibilityClassification::Supported,
None,
"the construct is defined by the current Compose Specification",
SPEC_EVIDENCE,
),
}
}
fn docker_rule(
profile: CompatibilityProfile,
version: ImplementationVersion,
feature: CompatibilityFeature,
) -> CompatibilityRule {
match feature {
CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(2, 24, 3) => rule(
feature,
CompatibilityClassification::Unsupported,
Some(Severity::Error),
"Docker Compose 2.24.3 accepted !override syntax but did not apply replacement semantics",
DOCKER_2_24_3_PROVIDER_EVIDENCE,
),
CompatibilityFeature::OverrideTag if version < ImplementationVersion::new(2, 24, 4) => rule(
feature,
CompatibilityClassification::Unsupported,
Some(Severity::Error),
"the selected Docker Compose version predates documented !override support",
DOCKER_OVERRIDE_EVIDENCE,
),
CompatibilityFeature::OverrideTag => rule(
feature,
CompatibilityClassification::Supported,
None,
"the selected Docker Compose version meets the documented !override minimum",
DOCKER_OVERRIDE_EVIDENCE,
),
CompatibilityFeature::ResetTag if !docker_provider_evidence(version).is_empty() => rule(
feature,
CompatibilityClassification::Supported,
None,
"the selected exact Docker Compose version applied !reset in reviewed provider conformance",
docker_provider_evidence(version),
),
CompatibilityFeature::ResetTag => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"Docker documents !reset, but the available evidence does not establish its first supported release",
DOCKER_RESET_EVIDENCE,
),
CompatibilityFeature::ShortBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
feature,
CompatibilityClassification::Supported,
None,
"the exact reported provider/runtime pair applied short-form SELinux relabeling",
DOCKER_PODMAN_SELINUX_EVIDENCE,
),
CompatibilityFeature::LongBindSelinuxRelabel if is_reported_selinux_context(profile) => rule(
feature,
CompatibilityClassification::Unsupported,
Some(Severity::Error),
"the exact reported provider/runtime pair accepted this form but did not relabel the host path",
DOCKER_PODMAN_SELINUX_EVIDENCE,
),
CompatibilityFeature::ShortBindSelinuxRelabel => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"SELinux relabeling depends on the backend runtime, host platform, and authored mount form",
docker_provider_evidence(version),
),
CompatibilityFeature::LongBindSelinuxRelabel => rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"no versioned evidence covers long-form SELinux behavior for the selected provider/runtime pair",
docker_provider_evidence(version),
),
CompatibilityFeature::ImageTagAndDigest if !docker_provider_evidence(version).is_empty() => rule(
feature,
CompatibilityClassification::Supported,
None,
"the selected exact Docker Compose version retained the combined tag and digest",
docker_provider_evidence(version),
),
CompatibilityFeature::ImageTagAndDigest => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"combined image tags and digests require implementation evidence beyond the documented Compose grammar",
SPEC_EVIDENCE,
),
CompatibilityFeature::ExtensionField => rule(
feature,
CompatibilityClassification::Extension,
None,
"x- fields use the Compose extension namespace",
SPEC_EVIDENCE,
),
CompatibilityFeature::HostGatewayToken => rule(
feature,
CompatibilityClassification::ImplementationSpecific,
Some(Severity::Warning),
"host-gateway depends on provider pass-through and runtime network configuration",
HOST_GATEWAY_EVIDENCE,
),
CompatibilityFeature::PodmanUserNamespaceMode => rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"Podman documents this runtime value, but no versioned Docker Compose pass-through observation is recorded",
PODMAN_USERNS_EVIDENCE,
),
}
}
fn podman_compose_rule(version: ImplementationVersion, feature: CompatibilityFeature) -> CompatibilityRule {
if feature == CompatibilityFeature::ExtensionField {
return rule(
feature,
CompatibilityClassification::Extension,
None,
"x- fields use the Compose extension namespace",
SPEC_EVIDENCE,
);
}
let evidence = podman_compose_provider_evidence(version);
if !evidence.is_empty() {
return match feature {
CompatibilityFeature::ImageTagAndDigest => rule(
feature,
CompatibilityClassification::Supported,
None,
"the selected exact podman-compose version retained the combined tag and digest",
evidence,
),
CompatibilityFeature::ResetTag => rule(
feature,
CompatibilityClassification::Unsupported,
Some(Severity::Error),
"the selected exact podman-compose version failed while processing !reset",
evidence,
),
CompatibilityFeature::OverrideTag if version == ImplementationVersion::new(1, 3, 0) => rule(
feature,
CompatibilityClassification::Unsupported,
Some(Severity::Error),
"podman-compose 1.3.0 rejected !override",
evidence,
),
CompatibilityFeature::OverrideTag => rule(
feature,
CompatibilityClassification::Supported,
None,
"podman-compose 1.5.0 applied !override replacement semantics",
evidence,
),
CompatibilityFeature::ShortBindSelinuxRelabel | CompatibilityFeature::LongBindSelinuxRelabel => rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"provider config accepted the SELinux form, but no reviewed runtime-effect record establishes relabeling",
evidence,
),
CompatibilityFeature::HostGatewayToken => rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"Podman 5.4 documents the runtime token, but provider pass-through has not been recorded",
HOST_GATEWAY_EVIDENCE,
),
CompatibilityFeature::PodmanUserNamespaceMode => rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"Podman 5.4 documents the runtime mode, but provider pass-through has not been recorded",
PODMAN_USERNS_EVIDENCE,
),
CompatibilityFeature::ExtensionField => unreachable!("extension fields returned above"),
};
}
rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Warning),
"no versioned podman-compose conformance evidence covers this construct yet",
&[],
)
}
const fn provider_evidence(summary: &'static str, version: ImplementationVersion) -> CompatibilityEvidence {
CompatibilityEvidence::new(
EvidenceKind::ProviderConformance,
PROVIDER_CONFORMANCE_URL,
summary,
Some(VersionRange::exact(version)),
None,
)
}
fn docker_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
match version {
value if value == ImplementationVersion::new(2, 24, 3) => DOCKER_2_24_3_PROVIDER_EVIDENCE,
value if value == ImplementationVersion::new(2, 24, 4) => DOCKER_2_24_4_PROVIDER_EVIDENCE,
value if value == ImplementationVersion::new(2, 40, 3) => DOCKER_2_40_3_PROVIDER_EVIDENCE,
value if value == ImplementationVersion::new(5, 3, 1) => DOCKER_5_3_1_PROVIDER_EVIDENCE,
_ => &[],
}
}
fn podman_compose_provider_evidence(version: ImplementationVersion) -> &'static [CompatibilityEvidence] {
match version {
value if value == ImplementationVersion::new(1, 3, 0) => PODMAN_COMPOSE_1_3_0_PROVIDER_EVIDENCE,
value if value == ImplementationVersion::new(1, 5, 0) => PODMAN_COMPOSE_1_5_0_PROVIDER_EVIDENCE,
_ => &[],
}
}
fn tolerant_rule(feature: CompatibilityFeature) -> CompatibilityRule {
if feature == CompatibilityFeature::ExtensionField {
return rule(
feature,
CompatibilityClassification::Extension,
None,
"x- fields are preserved as Compose extensions",
SPEC_EVIDENCE,
);
}
rule(
feature,
CompatibilityClassification::Unknown,
Some(Severity::Note),
"tolerant preservation deliberately makes no runtime-support claim",
&[],
)
}
fn is_reported_selinux_context(profile: CompatibilityProfile) -> bool {
profile.provider == ComposeProvider::DockerCompose(ImplementationVersion::new(2, 40, 3))
&& profile.runtime == Some(ContainerRuntime::Podman(ImplementationVersion::new(5, 6, 2)))
}
fn rule(
feature: CompatibilityFeature,
classification: CompatibilityClassification,
diagnostic_severity: Option<Severity>,
explanation: &'static str,
evidence: &'static [CompatibilityEvidence],
) -> CompatibilityRule {
CompatibilityRule {
feature,
classification,
diagnostic_severity,
explanation,
evidence,
}
}