use std::fmt;
use semver::Version;
use crate::{
Diagnostic, DiagnosticCode, PodmanLensResult,
evidence::{CapabilityCatalogueEntry, capability_catalogue},
};
fn parse_semantic_version(value: &str) -> PodmanLensResult<Version> {
let normalized = Version::parse(value).map_err(|_| Diagnostic::new(DiagnosticCode::InvalidVersion))?;
if !normalized.pre.is_empty() {
return Err(Diagnostic::new(DiagnosticCode::InvalidVersion));
}
Ok(normalized)
}
fn parse_reported_version(value: &str) -> PodmanLensResult<Version> {
let normalized = crate::evidence::normalized_reported_version(value)?;
parse_semantic_version(normalized.as_deref().unwrap_or(value))
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ObservedPodmanVersion {
original: String,
normalized: Version,
}
impl ObservedPodmanVersion {
pub fn parse(value: &str) -> PodmanLensResult<Self> {
let normalized = parse_semantic_version(value)?;
Ok(Self {
original: value.to_owned(),
normalized,
})
}
pub(crate) fn parse_reported(value: &str) -> PodmanLensResult<Self> {
Ok(Self {
original: value.to_owned(),
normalized: parse_reported_version(value)?,
})
}
#[must_use]
pub const fn as_semver(&self) -> &Version {
&self.normalized
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
}
impl fmt::Display for ObservedPodmanVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.original.fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ObservedApiVersion {
original: String,
normalized: Version,
}
impl ObservedApiVersion {
pub fn parse(value: &str) -> PodmanLensResult<Self> {
let normalized = parse_semantic_version(value)?;
Ok(Self {
original: value.to_owned(),
normalized,
})
}
pub(crate) fn parse_reported(value: &str) -> PodmanLensResult<Self> {
Ok(Self {
original: value.to_owned(),
normalized: parse_reported_version(value)?,
})
}
#[must_use]
pub const fn as_semver(&self) -> &Version {
&self.normalized
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
}
impl fmt::Display for ObservedApiVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.original.fmt(formatter)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SupportedPodmanRange;
impl SupportedPodmanRange {
pub const MINIMUM: &'static str = "5.4.0";
pub const MAXIMUM_EXCLUSIVE: &'static str = "6.2.0";
#[must_use]
pub fn contains(self, version: &ObservedPodmanVersion) -> bool {
let version = version.as_semver();
version >= &Version::new(5, 4, 0) && version < &Version::new(6, 2, 0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetProfile {
podman_version: ObservedPodmanVersion,
api_version: ObservedApiVersion,
execution_context: TargetExecutionContext,
cgroup_capabilities: Option<CgroupCapabilityEvidence>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum TargetExecutionContext {
#[default]
Unknown,
Rootless,
Rootful,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CgroupVersion {
V1,
V2,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum CgroupController {
Cpu,
Memory,
Pids,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CgroupCapabilityEvidence {
version: CgroupVersion,
controllers: std::collections::BTreeSet<CgroupController>,
}
impl CgroupCapabilityEvidence {
#[must_use]
pub fn new<I>(version: CgroupVersion, controllers: I) -> Self
where
I: IntoIterator<Item = CgroupController>,
{
Self {
version,
controllers: controllers.into_iter().collect(),
}
}
#[must_use]
pub const fn version(&self) -> CgroupVersion {
self.version
}
#[must_use]
pub fn supports(&self, controller: CgroupController) -> bool {
self.controllers.contains(&controller)
}
}
impl TargetProfile {
pub fn new(podman_version: ObservedPodmanVersion, api_version: ObservedApiVersion) -> PodmanLensResult<Self> {
if !SupportedPodmanRange.contains(&podman_version) {
return Err(Diagnostic::new(DiagnosticCode::IncompatibleTargetProfile));
}
let entry = matching_catalogue_entry(&podman_version)?;
if !api_version_is_supported(&entry, &podman_version, &api_version) {
return Err(Diagnostic::new(DiagnosticCode::IncompatibleTargetProfile));
}
Ok(Self {
podman_version,
api_version,
execution_context: TargetExecutionContext::Unknown,
cgroup_capabilities: None,
})
}
pub fn set_execution_context(&mut self, context: TargetExecutionContext) {
self.execution_context = context;
}
pub fn set_cgroup_capabilities(&mut self, capabilities: CgroupCapabilityEvidence) {
self.cgroup_capabilities = Some(capabilities);
}
#[must_use]
pub fn podman_version(&self) -> &ObservedPodmanVersion {
&self.podman_version
}
#[must_use]
pub fn api_version(&self) -> &ObservedApiVersion {
&self.api_version
}
#[must_use]
pub const fn execution_context(&self) -> TargetExecutionContext {
self.execution_context
}
#[must_use]
pub fn cgroup_capabilities(&self) -> Option<&CgroupCapabilityEvidence> {
self.cgroup_capabilities.as_ref()
}
}
fn matching_catalogue_entry(version: &ObservedPodmanVersion) -> PodmanLensResult<CapabilityCatalogueEntry> {
capability_catalogue()?
.into_iter()
.find(|entry| entry.output_supported() && version_in_entry(version.as_semver(), entry))
.ok_or_else(|| Diagnostic::new(DiagnosticCode::IncompatibleTargetProfile))
}
pub(crate) fn matching_input_catalogue_entry(
version: &ObservedPodmanVersion,
) -> PodmanLensResult<CapabilityCatalogueEntry> {
capability_catalogue()?
.into_iter()
.find(|entry| {
if entry.output_supported() {
version_in_entry(version.as_semver(), entry)
} else {
entry.matches_reported_podman_version(version.original())
}
})
.ok_or_else(|| Diagnostic::new(DiagnosticCode::ObservedCompatibility))
}
pub(crate) fn observed_input_is_supported(
podman_version: &ObservedPodmanVersion,
api_version: &ObservedApiVersion,
) -> PodmanLensResult<CapabilityCatalogueEntry> {
let entry = matching_input_catalogue_entry(podman_version)?;
if observed_api_version_is_supported(&entry, podman_version, api_version) {
Ok(entry)
} else {
Err(Diagnostic::new(DiagnosticCode::ObservedCompatibility))
}
}
fn observed_api_version_is_supported(
entry: &CapabilityCatalogueEntry,
podman_version: &ObservedPodmanVersion,
api_version: &ObservedApiVersion,
) -> bool {
if !entry.output_supported() {
return entry.matches_reported_version_pair(podman_version.original(), api_version.original());
}
api_version_is_supported(entry, podman_version, api_version)
}
fn version_in_entry(version: &Version, entry: &CapabilityCatalogueEntry) -> bool {
let Ok(minimum) = Version::parse(entry.minimum_podman_version()) else {
return false;
};
let Ok(maximum_exclusive) = Version::parse(entry.maximum_exclusive_podman_version()) else {
return false;
};
version >= &minimum && version < &maximum_exclusive
}
fn api_version_is_supported(
entry: &CapabilityCatalogueEntry,
podman_version: &ObservedPodmanVersion,
api_version: &ObservedApiVersion,
) -> bool {
let Ok(minimum_api) = Version::parse(entry.minimum_libpod_api_version()) else {
return false;
};
semantic_core_inclusive(api_version.as_semver(), &minimum_api, podman_version.as_semver())
}
fn semantic_core_inclusive(version: &Version, minimum: &Version, maximum: &Version) -> bool {
version >= minimum && version <= maximum
}
#[cfg(test)]
mod tests {
use super::{ObservedApiVersion, ObservedPodmanVersion, TargetProfile, observed_input_is_supported};
#[test]
fn legacy_distro_anchors_are_input_only_and_not_implicit_targets() -> Result<(), Box<dyn std::error::Error>> {
for (engine, api) in [
("3.0.1", "3.0.0"),
("3.4.4", "3.4.4"),
("4.3.1", "4.3.1"),
("4.9.3", "4.9.3"),
("4.9.4", "4.9.4"),
] {
let engine = ObservedPodmanVersion::parse(engine)?;
let api = ObservedApiVersion::parse(api)?;
let evidence = observed_input_is_supported(&engine, &api)?;
assert!(!evidence.output_supported());
assert!(TargetProfile::new(engine, api).is_err());
}
Ok(())
}
#[test]
fn unreviewed_legacy_patches_fail_closed() -> Result<(), Box<dyn std::error::Error>> {
let engine = ObservedPodmanVersion::parse("3.4.5")?;
let api = ObservedApiVersion::parse("3.4.5")?;
assert!(observed_input_is_supported(&engine, &api).is_err());
Ok(())
}
#[test]
fn legacy_anchors_require_the_exact_observed_api_version() -> Result<(), Box<dyn std::error::Error>> {
for (engine, wrong_api) in [("3.0.1", "3.0.1"), ("3.4.4", "3.1.0"), ("4.9.4", "4.0.0")] {
let engine = ObservedPodmanVersion::parse(engine)?;
let wrong_api = ObservedApiVersion::parse(wrong_api)?;
assert!(observed_input_is_supported(&engine, &wrong_api).is_err());
}
Ok(())
}
}