use std::collections::{BTreeMap, BTreeSet};
use boxferry_engine::{
ConversionKind, ConversionOutcome, Diagnostic, DiagnosticCode, DiagnosticField, DiagnosticValue, ImportAdapter,
ImportResult, InvalidDiagnosticCode, NativeFinding, Severity,
};
use boxferry_model::{
Application, Command, Entrypoint, Identifier, ImageReference, MetadataLabel, ModelError, Mount, MountSource,
Network, NetworkAttachment as NeutralNetworkAttachment, ProtectedString, Provenance, ResourceOwnership, Secret,
Service, ServiceDependency, ServiceGroup, SourceId, Sourced, Volume,
};
use podman_lens::{
ContainerMountKind, ContainerMountObservation, ContainerMountSource, ContainerObservation,
ContainerSecretGrantObservation, NativeNamespaceMode, NativeNetworkingObservation, ObservationField,
ObservationOrigin, ResourceDetails, ResourceIdentity, ResourceKind, ResourceObservation, ResourceObservationState,
};
use crate::PodmanSource;
#[derive(Clone, Debug)]
pub struct PodmanImporter {
unsupported: DiagnosticCode,
invalid: DiagnosticCode,
policy: DiagnosticCode,
identity: DiagnosticCode,
secret: DiagnosticCode,
}
impl PodmanImporter {
pub fn new() -> Result<Self, InvalidDiagnosticCode> {
Ok(Self {
unsupported: DiagnosticCode::new("BFP0002")?,
invalid: DiagnosticCode::new("BFP0001")?,
policy: DiagnosticCode::new("BFP0003")?,
identity: DiagnosticCode::new("BFP0004")?,
secret: DiagnosticCode::new("BFP0005")?,
})
}
}
impl ImportAdapter for PodmanImporter {
type Source = PodmanSource;
fn import(&self, source: &Self::Source) -> ImportResult {
let Ok(source_id) = SourceId::new("podman") else {
return ImportResult::new(
None,
Vec::new(),
vec![Diagnostic::new(
self.invalid.clone(),
Severity::Error,
"Podman provenance identity is invalid",
)],
);
};
Mapping::new(self, source, source_id).map()
}
}
struct Mapping<'a> {
importer: &'a PodmanImporter,
source: &'a PodmanSource,
origin: Provenance,
selected: BTreeSet<ResourceIdentity>,
outcomes: Vec<ConversionOutcome>,
diagnostics: Vec<Diagnostic>,
service_names: BTreeMap<String, Identifier>,
pod_memberships: BTreeMap<String, Identifier>,
}
impl<'a> Mapping<'a> {
fn new(importer: &'a PodmanImporter, source: &'a PodmanSource, source_id: SourceId) -> Self {
Self {
importer,
source,
origin: Provenance::runtime_observation(source_id),
selected: selected_identities(source),
outcomes: Vec::new(),
diagnostics: Vec::new(),
service_names: BTreeMap::new(),
pod_memberships: BTreeMap::new(),
}
}
fn map(mut self) -> ImportResult {
let mut application = Application::new(self.source.application_name().clone());
self.report_missing_selected_observations();
self.report_native_findings();
self.exact("application");
for observation in observations(self.source, &self.selected, ResourceKind::Network) {
self.map_network(&mut application, observation);
}
for observation in observations(self.source, &self.selected, ResourceKind::Volume) {
self.map_volume(&mut application, observation);
}
for observation in observations(self.source, &self.selected, ResourceKind::Secret) {
self.map_secret(&mut application, observation);
}
for observation in observations(self.source, &self.selected, ResourceKind::Container) {
self.map_container(&mut application, observation);
}
for observation in observations(self.source, &self.selected, ResourceKind::Pod) {
self.map_pod(&mut application, observation);
}
for observation in observations(self.source, &self.selected, ResourceKind::Image) {
self.map_image(observation);
}
ImportResult::new(Some(application), self.outcomes, self.diagnostics)
}
fn map_network(&mut self, application: &mut Application, observation: &ResourceObservation) {
let subject = format!("networks.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Network(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
let Some(name) = self.identifier(&subject, observation.header().identity()) else {
return;
};
let mut network = Network::new(name, ResourceOwnership::Uncertain);
self.map_network_labels(&subject, details.labels(), &mut network);
self.observation_only(
&format!("{subject}.internal"),
details.internal(),
"effective network-internal state needs explicit promotion policy",
);
self.observation_only(
&format!("{subject}.ipam"),
details.subnets(),
"effective network IPAM needs explicit promotion policy",
);
self.observation_only(
&format!("{subject}.routes"),
details.routes(),
"native network routes have no neutral desired-state field",
);
self.observation_only(
&format!("{subject}.options"),
details.options(),
"network option values are intentionally not exposed by PodmanLens",
);
self.exact(&subject);
if let Err(error) = application.add_network(self.sourced(network)) {
self.model_error(subject, &error);
}
}
fn map_volume(&mut self, application: &mut Application, observation: &ResourceObservation) {
let subject = format!("volumes.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Volume(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
let Some(name) = self.identifier(&subject, observation.header().identity()) else {
return;
};
let mut volume = Volume::new(name, ResourceOwnership::Uncertain);
self.map_volume_labels(&subject, details.labels(), &mut volume);
self.observation_only(
&format!("{subject}.driver"),
details.driver(),
"effective volume driver is observation-only",
);
self.observation_only(
&format!("{subject}.uid"),
details.uid(),
"effective volume ownership is observation-only",
);
self.observation_only(
&format!("{subject}.gid"),
details.gid(),
"effective volume ownership is observation-only",
);
self.observation_only(
&format!("{subject}.created_at"),
details.created_at(),
"volume creation time is runtime evidence only",
);
self.observation_only(
&format!("{subject}.anonymous"),
details.anonymous(),
"anonymous-volume classification is runtime evidence only",
);
self.exact(&subject);
if let Err(error) = application.add_volume(self.sourced(volume)) {
self.model_error(subject, &error);
}
}
fn map_secret(&mut self, application: &mut Application, observation: &ResourceObservation) {
let subject = format!("secrets.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Secret(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
let Some(name) = self.identifier(&subject, observation.header().identity()) else {
return;
};
let secret = Secret::new(name, ResourceOwnership::Uncertain);
self.observation_only(
&format!("{subject}.driver"),
details.driver(),
"secret metadata does not contain deployable secret material",
);
self.observation_only(
&format!("{subject}.labels"),
details.labels(),
"neutral secret metadata has no label field",
);
self.observation_only(
&format!("{subject}.created_at"),
details.created_at(),
"secret creation time is runtime evidence only",
);
self.observation_only(
&format!("{subject}.updated_at"),
details.updated_at(),
"secret update time is runtime evidence only",
);
self.exact(&subject);
if let Err(error) = application.add_secret(self.sourced(secret)) {
self.model_error(subject, &error);
}
}
fn map_container(&mut self, application: &mut Application, observation: &ResourceObservation) {
let subject = format!("services.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Container(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
let is_infra = details.infra().observed().is_some_and(|value| *value.value());
self.observation_only(
&format!("{subject}.infra"),
details.infra(),
"infra-container classification is runtime topology evidence",
);
if is_infra {
self.exact(format!("{subject}.pod_infra"));
self.report_infra_container_fields(&subject, details);
return;
}
let Some(name) = self.identifier(&subject, observation.header().identity()) else {
return;
};
let mut service = Service::new(name.clone());
self.service_names
.insert(observation.header().identity().id().to_owned(), name.clone());
if let Some(native_name) = observation.header().identity().name() {
self.service_names.insert(native_name.to_owned(), name);
}
self.map_pod_membership(&subject, observation.header().identity(), details);
self.map_configured_image(&subject, details, &mut service);
self.map_configured_settings(&subject, observation.header().identity().id(), details, &mut service);
self.map_container_labels(&subject, details.labels(), &mut service);
self.map_mounts(&subject, details, &mut service);
self.map_container_networks(&subject, details, &mut service);
self.map_dependencies(&subject, details, &mut service);
self.observation_only(
&format!("{subject}.environment"),
details.environment(),
"protected environment values require caller authorization",
);
self.report_secret_grants(&format!("{subject}.secret_grants"), details.secret_grants());
self.observation_only(
&format!("{subject}.memory_swappiness"),
details.memory_swappiness(),
"configured memory swappiness has no neutral field",
);
self.observation_only(
&format!("{subject}.restart_policy"),
details.restart_policy(),
"effective restart policy needs explicit promotion policy",
);
self.observation_only(
&format!("{subject}.healthcheck"),
details.health_check(),
"effective health configuration needs explicit promotion policy",
);
self.observation_only(
&format!("{subject}.health_failure_action"),
details.health_failure_action(),
"effective health failure action has no complete neutral equivalent",
);
self.observation_only(
&format!("{subject}.startup_healthcheck"),
details.startup_health_check(),
"startup health has no complete neutral equivalent",
);
self.observation_only(
&format!("{subject}.logging"),
details.logging(),
"effective logging needs explicit promotion policy",
);
self.observation_only(
&format!("{subject}.security"),
details.security(),
"effective security settings need explicit promotion policy",
);
self.observation_only(
&format!("{subject}.namespaces"),
details.namespaces(),
"effective namespace settings need explicit promotion policy",
);
self.observation_only(
&format!("{subject}.resource_controls"),
details.resource_controls(),
"effective resource controls need explicit promotion policy",
);
self.exact(&subject);
if let Err(error) = application.add_service(self.sourced(service)) {
self.model_error(subject, &error);
}
}
fn map_pod(&mut self, application: &mut Application, observation: &ResourceObservation) {
let subject = format!("service_groups.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Pod(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
let Some(name) = self.identifier(&subject, observation.header().identity()) else {
return;
};
let mut group = ServiceGroup::new(name.clone(), ResourceOwnership::Uncertain);
for container in observations(self.source, &self.selected, ResourceKind::Container) {
if self.pod_memberships.get(container.header().identity().id()) != Some(&name) {
continue;
}
if let Some(member) = self.service_names.get(container.header().identity().id()).cloned() {
if let Err(error) = group.add_member(self.sourced(member)) {
self.model_error(format!("{subject}.members"), &error);
} else {
self.exact(format!("{subject}.members"));
}
}
}
self.report_networking_only(&format!("{subject}.runtime"), details.networking());
self.observation_only(
&format!("{subject}.create_infra"),
details.create_infra(),
"Podman infra-container creation is native runtime evidence",
);
self.observation_only(
&format!("{subject}.labels"),
details.labels(),
"neutral service groups have no metadata-label field",
);
self.exact(&subject);
if let Err(error) = application.add_service_group(self.sourced(group)) {
self.model_error(subject, &error);
}
}
fn map_image(&mut self, observation: &ResourceObservation) {
let subject = format!("images.{}", identity_name(observation.header().identity()));
if !self.require_complete(observation, &subject) {
return;
}
let ResourceDetails::Image(details) = observation.details() else {
self.invalid(subject, "native resource kind and details disagree");
return;
};
self.observation_only(
&format!("{subject}.labels"),
details.labels(),
"runtime image labels are evidence, not authored acquisition",
);
self.observation_only(
&format!("{subject}.repo_tags"),
details.repo_tags(),
"local repository tags must not replace configured image intent",
);
self.observation_only(
&format!("{subject}.repo_digests"),
details.repo_digests(),
"local repository digests must not replace configured image intent",
);
self.observation_only(
&format!("{subject}.environment"),
details.environment(),
"protected image defaults are observation-only",
);
self.observation_only(
&format!("{subject}.digest"),
details.digest(),
"local image digest is resolution evidence only",
);
self.observation_only(
&format!("{subject}.created"),
details.created(),
"image creation time is runtime evidence only",
);
self.observation_only(
&format!("{subject}.author"),
details.author(),
"image author is metadata evidence only",
);
self.observation_only(
&format!("{subject}.architecture"),
details.architecture(),
"image architecture is metadata evidence only",
);
self.observation_only(
&format!("{subject}.operating_system"),
details.operating_system(),
"image operating system is metadata evidence only",
);
self.observation_only(
&format!("{subject}.manifest_type"),
details.manifest_type(),
"image manifest type is metadata evidence only",
);
}
fn map_configured_image(&mut self, subject: &str, details: &ContainerObservation, service: &mut Service) {
let field_subject = format!("{subject}.image");
if let Some(image) = self.configured(details.configured_image(), &field_subject) {
match ImageReference::parse(image) {
Ok(image) => service.set_image(self.sourced(image)),
Err(error) => self.model_error(field_subject, &error),
}
}
self.observation_only(
&format!("{subject}.local_image_id"),
details.local_image_id(),
"local image resolution must not replace configured image intent",
);
}
fn map_configured_settings(
&mut self,
subject: &str,
native_id: &str,
details: &ContainerObservation,
service: &mut Service,
) {
let command_subject = format!("{subject}.command");
if let Some(command) = self.configured(details.command(), &command_subject) {
let value = if command.arguments().is_empty() {
Command::Empty
} else {
Command::Exec(command.arguments().iter().map(ProtectedString::plain).collect())
};
service.set_command(self.sourced(value));
}
let entrypoint_subject = format!("{subject}.entrypoint");
if let Some(entrypoint) = self.configured(details.entrypoint(), &entrypoint_subject) {
let value = if entrypoint.arguments().is_empty() {
Entrypoint::Empty
} else {
Entrypoint::Exec(entrypoint.arguments().iter().map(ProtectedString::plain).collect())
};
service.set_entrypoint(self.sourced(value));
}
let user_subject = format!("{subject}.user");
if let Some(user) = self.configured(details.user(), &user_subject) {
let spelling = user.value();
if !spelling.is_empty() {
if let Some((user, group)) = spelling.split_once(':') {
if !user.is_empty() && !group.is_empty() && !group.contains(':') {
service.set_user(self.sourced(ProtectedString::plain(user)));
service.set_group(self.sourced(ProtectedString::plain(group)));
} else {
service.set_user(self.sourced(ProtectedString::plain(spelling)));
}
} else {
service.set_user(self.sourced(ProtectedString::plain(spelling)));
}
}
}
let workdir_subject = format!("{subject}.working_directory");
if let Some(workdir) = self.configured(details.working_directory(), &workdir_subject) {
service.set_working_directory(self.sourced(ProtectedString::plain(workdir.value())));
}
let hostname_subject = format!("{subject}.hostname");
let runtime_hostname = native_id.get(..12).unwrap_or(native_id);
let host_uts_namespace = details
.namespaces()
.observed()
.and_then(|namespaces| namespaces.value().uts().observed())
.is_some_and(|uts| matches!(uts.value(), NativeNamespaceMode::Host));
let hostname_reason = if host_uts_namespace {
"a hostname observed with the host UTS namespace is not portable authored intent"
} else {
"Podman's ID-derived hostname is runtime-assigned evidence"
};
if let Some(hostname) = self.configured_unless(
details.hostname(),
&hostname_subject,
|hostname| hostname.value() == runtime_hostname || host_uts_namespace,
hostname_reason,
) {
service.set_hostname(self.sourced(ProtectedString::plain(hostname.value())));
}
}
fn map_container_labels(
&mut self,
subject: &str,
field: &ObservationField<podman_lens::observation::Labels>,
service: &mut Service,
) {
let labels_subject = format!("{subject}.labels");
let Some(labels) = self.configured(field, &labels_subject) else {
return;
};
for (name, value) in labels {
match Identifier::new(name) {
Ok(name) => service.add_label(self.sourced(MetadataLabel::new(name, ProtectedString::plain(value)))),
Err(error) => self.model_error(&labels_subject, &error),
}
}
}
fn map_network_labels(
&mut self,
subject: &str,
field: &ObservationField<podman_lens::observation::Labels>,
network: &mut Network,
) {
let labels_subject = format!("{subject}.labels");
let Some(labels) = self.configured(field, &labels_subject) else {
return;
};
for (name, value) in labels {
match Identifier::new(name) {
Ok(name) => network.add_label(self.sourced(MetadataLabel::new(name, ProtectedString::plain(value)))),
Err(error) => self.model_error(&labels_subject, &error),
}
}
}
fn map_volume_labels(
&mut self,
subject: &str,
field: &ObservationField<podman_lens::observation::Labels>,
volume: &mut Volume,
) {
let labels_subject = format!("{subject}.labels");
let Some(labels) = self.configured(field, &labels_subject) else {
return;
};
for (name, value) in labels {
match Identifier::new(name) {
Ok(name) => volume.add_label(self.sourced(MetadataLabel::new(name, ProtectedString::plain(value)))),
Err(error) => self.model_error(&labels_subject, &error),
}
}
}
fn report_infra_container_fields(&mut self, subject: &str, details: &ContainerObservation) {
macro_rules! report {
($name:literal, $field:expr) => {
self.observation_only(
&format!("{subject}.{}", $name),
$field,
"infra-container field is runtime topology evidence only",
);
};
}
report!("configured_image", details.configured_image());
report!("labels", details.labels());
report!("local_image_id", details.local_image_id());
report!("environment", details.environment());
report!("command", details.command());
report!("entrypoint", details.entrypoint());
report!("user", details.user());
report!("working_directory", details.working_directory());
report!("hostname", details.hostname());
report!("pod_membership", details.pod_membership());
report!("native_dependencies", details.native_dependencies());
report!("mounts", details.mounts());
if let Some(mounts) = details.mounts().observed() {
for (index, mount) in mounts.value().iter().enumerate() {
self.report_unpromoted_mount(&format!("{subject}.mounts[{index}]"), mount);
}
}
self.report_secret_grants(&format!("{subject}.secret_grants"), details.secret_grants());
report!("memory_swappiness", details.memory_swappiness());
report!("restart_policy", details.restart_policy());
report!("health_check", details.health_check());
report!("health_failure_action", details.health_failure_action());
report!("startup_health_check", details.startup_health_check());
report!("logging", details.logging());
report!("security", details.security());
report!("namespaces", details.namespaces());
report!("resource_controls", details.resource_controls());
self.report_networking_only(&format!("{subject}.networking"), details.networking());
}
fn map_pod_membership(&mut self, subject: &str, identity: &ResourceIdentity, details: &ContainerObservation) {
let field_subject = format!("{subject}.pod_membership");
let Some(reference) = self.configured(details.pod_membership(), &field_subject) else {
return;
};
let Some(group) = self.resolve_name(ResourceKind::Pod, reference.reference()) else {
self.identity_conflict(
field_subject,
"pod reference does not resolve to exactly one selected pod",
);
return;
};
self.pod_memberships.insert(identity.id().to_owned(), group);
}
fn report_unpromoted_mount(&mut self, subject: &str, mount: &ContainerMountObservation) {
self.report_mount_core(subject, mount);
self.report_mount_remainders(subject, mount);
}
fn report_mount_core(&mut self, subject: &str, mount: &ContainerMountObservation) {
self.observation_only(
&format!("{subject}.source"),
mount.source(),
"mount source is not promoted without an exact portable mapping",
);
self.observation_only(
&format!("{subject}.destination"),
mount.destination(),
"mount destination is not promoted without an exact portable mapping",
);
self.observation_only(
&format!("{subject}.writable"),
mount.writable(),
"mount access is not promoted without an exact portable mapping",
);
}
fn report_mount_remainders(&mut self, subject: &str, mount: &ContainerMountObservation) {
self.observation_only(
&format!("{subject}.local_backing_path"),
mount.local_backing_path(),
"target-local mount backing paths are observation-only",
);
self.observation_only(
&format!("{subject}.options"),
mount.options(),
"native mount options have no complete neutral mapping",
);
self.observation_only(
&format!("{subject}.propagation"),
mount.propagation(),
"native mount propagation has no reviewed neutral mapping",
);
self.observation_only(
&format!("{subject}.subpath"),
mount.subpath(),
"native volume subpath has no reviewed neutral mapping",
);
}
fn map_mounts(&mut self, subject: &str, details: &ContainerObservation, service: &mut Service) {
let field_subject = format!("{subject}.mounts");
let Some(observed) = details.mounts().observed() else {
self.report_state(details.mounts(), &field_subject);
return;
};
if observed.origin() != ObservationOrigin::Effective {
for (index, mount) in observed.value().iter().enumerate() {
self.report_unpromoted_mount(&format!("{field_subject}[{index}]"), mount);
}
self.unsupported(
&field_subject,
"runtime-assigned or local mount evidence is not desired intent",
);
return;
}
if !self.source.promotion_policy().promotes_effective_named_volume_mounts() {
for (index, mount) in observed.value().iter().enumerate() {
self.report_unpromoted_mount(&format!("{field_subject}[{index}]"), mount);
}
self.promotion_required_with_flag(
&field_subject,
"effective named-volume mounts require explicit promotion authorization",
"--promote-podman-effective-named-volumes",
);
return;
}
for (index, mount) in observed.value().iter().enumerate() {
let mount_subject = format!("{field_subject}[{index}]");
self.report_mount_remainders(&mount_subject, mount);
if mount.kind() != ContainerMountKind::NamedVolume {
self.report_mount_core(&mount_subject, mount);
self.unsupported(
mount_subject,
"local bind paths never become portable intent automatically",
);
continue;
}
let (Some(source), Some(destination), Some(writable)) = (
mount.source().observed(),
mount.destination().observed(),
mount.writable().observed(),
) else {
self.report_state(mount.source(), &format!("{mount_subject}.source"));
self.report_state(mount.destination(), &format!("{mount_subject}.destination"));
self.report_state(mount.writable(), &format!("{mount_subject}.writable"));
self.invalid(mount_subject, "mount source, destination, or access is incomplete");
continue;
};
if !matches!(
source.origin(),
ObservationOrigin::Configured | ObservationOrigin::Effective
) || !matches!(
destination.origin(),
ObservationOrigin::Configured | ObservationOrigin::Effective
) || !matches!(
writable.origin(),
ObservationOrigin::Configured | ObservationOrigin::Effective
) {
self.unsupported(
mount_subject,
"runtime-assigned or local mount fields are never promoted",
);
continue;
}
let ContainerMountSource::NamedVolume(source_name) = source.value() else {
self.unsupported(mount_subject, "mount source is target-local");
continue;
};
let Some(volume_name) = self.resolve_name(ResourceKind::Volume, source_name) else {
self.identity_conflict(mount_subject, "named-volume reference does not resolve uniquely");
continue;
};
match Mount::new(MountSource::Volume(volume_name), destination.value(), !writable.value()) {
Ok(mount) => {
service.add_mount(self.decision_sourced(mount));
self.approximate_with_flag(
mount_subject,
"portable named-volume mount promoted by explicit BoxFerry policy",
"--promote-podman-effective-named-volumes",
);
}
Err(error) => self.model_error(mount_subject, &error),
}
}
}
fn report_networking_only(&mut self, subject: &str, field: &ObservationField<NativeNetworkingObservation>) {
self.observation_only(
subject,
field,
"native networking evidence is not promoted at this topology boundary",
);
if let Some(networking) = field.observed() {
self.report_networking_remainders(subject, networking.value());
self.observation_only(
&format!("{subject}.references"),
networking.value().networks(),
"native network references are not promoted at this topology boundary",
);
}
}
fn report_networking_remainders(&mut self, subject: &str, networking: &NativeNetworkingObservation) {
macro_rules! report {
($name:literal, $field:expr, $reason:literal) => {
self.observation_only(&format!("{subject}.{}", $name), $field, $reason);
};
}
report!(
"port_bindings",
networking.port_bindings(),
"runtime port bindings require explicit authored publication intent"
);
if let Some(bindings) = networking.port_bindings().observed() {
for (index, binding) in bindings.value().iter().enumerate() {
self.observation_only(
&format!("{subject}.port_bindings[{index}].host_ip"),
binding.host_ip(),
"runtime-assigned host IP is observation-only",
);
self.observation_only(
&format!("{subject}.port_bindings[{index}].host_port"),
binding.host_port(),
"runtime-assigned host port is observation-only",
);
}
}
report!(
"create_net_ns",
networking.create_net_ns(),
"effective network namespace creation requires explicit promotion"
);
report!(
"host_network",
networking.host_network(),
"effective host networking requires explicit promotion"
);
report!(
"dns_servers",
networking.dns_servers(),
"effective DNS servers require explicit promotion"
);
report!(
"dns_search",
networking.dns_search(),
"effective DNS search domains require explicit promotion"
);
report!(
"dns_options",
networking.dns_options(),
"effective DNS options require explicit promotion"
);
report!(
"host_entries",
networking.host_entries(),
"opaque host-entry evidence cannot reconstruct authored mappings"
);
report!(
"network_options",
networking.network_options(),
"opaque network options cannot reconstruct authored mappings"
);
report!(
"no_manage_resolv_conf",
networking.no_manage_resolv_conf(),
"effective resolver-management policy requires explicit promotion"
);
report!(
"no_manage_hosts",
networking.no_manage_hosts(),
"effective hosts-management policy requires explicit promotion"
);
report!(
"static_ip",
networking.static_ip(),
"runtime-assigned static IP evidence is not promoted automatically"
);
report!(
"static_mac",
networking.static_mac(),
"runtime-assigned static MAC evidence is not promoted automatically"
);
}
fn map_container_networks(&mut self, subject: &str, details: &ContainerObservation, service: &mut Service) {
if details.pod_membership().is_observed() {
self.report_networking_only(&format!("{subject}.pod_member_networking"), details.networking());
return;
}
let field_subject = format!("{subject}.networks");
let Some(networking) = details.networking().observed() else {
self.report_state(details.networking(), &field_subject);
return;
};
self.report_networking_remainders(&field_subject, networking.value());
let Some(networks) = networking.value().networks().observed() else {
self.report_state(networking.value().networks(), &field_subject);
return;
};
if networks.origin() != ObservationOrigin::Effective {
self.unsupported(
&field_subject,
"runtime-assigned or local network references are never promoted",
);
return;
}
if !self.source.promotion_policy().promotes_effective_named_networks() {
self.promotion_required_with_flag(
&field_subject,
"effective named networks require explicit promotion authorization",
"--promote-podman-effective-named-networks",
);
return;
}
for reference in networks.value() {
let Some(name) = self.resolve_name(ResourceKind::Network, reference.reference()) else {
self.identity_conflict(&field_subject, "network reference does not resolve uniquely");
continue;
};
service.add_network(self.decision_sourced(NeutralNetworkAttachment::new(name, Vec::new())));
self.approximate_with_flag(
&field_subject,
"effective named network promoted without runtime-assigned addresses",
"--promote-podman-effective-named-networks",
);
}
}
fn map_dependencies(&mut self, subject: &str, details: &ContainerObservation, service: &mut Service) {
let field_subject = format!("{subject}.dependencies");
let Some(dependencies) = self.configured(details.native_dependencies(), &field_subject) else {
return;
};
for dependency in dependencies {
let Some(name) = self.resolve_name(ResourceKind::Container, dependency.reference()) else {
self.identity_conflict(&field_subject, "container dependency does not resolve uniquely");
continue;
};
service.add_dependency(self.sourced(ServiceDependency::new(name)));
}
}
fn report_missing_selected_observations(&mut self) {
let missing = self
.selected
.iter()
.filter(|identity| self.source.inventory().observation(identity).is_none())
.cloned()
.collect::<Vec<_>>();
for identity in missing {
self.identity_conflict(
format!(
"podman.selected[{}].{}",
identity.kind().canonical_rank(),
identity_name(&identity)
),
"selected discovery identity is absent from the supplied Podman inventory",
);
}
}
fn report_secret_grants(&mut self, subject: &str, field: &ObservationField<Vec<ContainerSecretGrantObservation>>) {
let Some(grants) = field.observed() else {
self.report_state(field, subject);
return;
};
self.secret_incomplete(subject, "inspect does not preserve secret delivery form and target");
for (index, grant) in grants.value().iter().enumerate() {
let grant_subject = format!("{subject}[{index}]");
self.report_secret_grant_field(&format!("{grant_subject}.reference"), grant.reference());
self.report_secret_grant_field(&format!("{grant_subject}.uid"), grant.uid());
self.report_secret_grant_field(&format!("{grant_subject}.gid"), grant.gid());
self.report_secret_grant_field(&format!("{grant_subject}.mode"), grant.mode());
}
}
fn report_secret_grant_field<T>(&mut self, subject: &str, field: &ObservationField<T>) {
if field.is_observed() {
self.secret_incomplete(
subject,
"secret grant metadata cannot reconstruct authored delivery form or target",
);
} else {
self.report_state(field, subject);
}
}
fn configured<'b, T>(&mut self, field: &'b ObservationField<T>, subject: &str) -> Option<&'b T> {
let Some(observed) = field.observed() else {
self.report_state(field, subject);
return None;
};
match observed.origin() {
ObservationOrigin::Configured => {
self.exact(subject);
Some(observed.value())
}
ObservationOrigin::Effective => {
self.promotion_required(
subject,
"effective observation requires explicit field-specific promotion",
);
None
}
ObservationOrigin::RuntimeAssigned | ObservationOrigin::LocalResolution => {
self.unsupported_with_origin(
subject,
"runtime-assigned and local-resolution observations are never authored intent",
observed.origin(),
);
None
}
_ => {
self.unsupported(subject, "future observation origin is not reviewed for promotion");
None
}
}
}
fn configured_unless<'b, T>(
&mut self,
field: &'b ObservationField<T>,
subject: &str,
reject: impl FnOnce(&T) -> bool,
reason: &'static str,
) -> Option<&'b T> {
let Some(observed) = field.observed() else {
self.report_state(field, subject);
return None;
};
match observed.origin() {
ObservationOrigin::Configured if reject(observed.value()) => {
self.unsupported_with_origin(subject, reason, observed.origin());
None
}
ObservationOrigin::Configured => {
self.exact(subject);
Some(observed.value())
}
ObservationOrigin::Effective => {
self.promotion_required(
subject,
"effective observation requires explicit field-specific promotion",
);
None
}
ObservationOrigin::RuntimeAssigned | ObservationOrigin::LocalResolution => {
self.unsupported_with_origin(
subject,
"runtime-assigned and local-resolution observations are never authored intent",
observed.origin(),
);
None
}
_ => {
self.unsupported(subject, "future observation origin is not reviewed for promotion");
None
}
}
}
fn observation_only<T>(&mut self, subject: &str, field: &ObservationField<T>, reason: &'static str) {
match field {
ObservationField::Observed(observed) => match observed.origin() {
ObservationOrigin::Configured => self.unsupported_with_origin(subject, reason, observed.origin()),
ObservationOrigin::Effective => self.promotion_required(subject, reason),
ObservationOrigin::RuntimeAssigned | ObservationOrigin::LocalResolution => self
.unsupported_with_origin(
subject,
"runtime-assigned and local-resolution observations are never authored intent",
observed.origin(),
),
_ => self.unsupported(subject, "future observation origin is not reviewed"),
},
_ => self.report_state(field, subject),
}
}
fn report_state<T>(&mut self, field: &ObservationField<T>, subject: &str) {
match field {
ObservationField::Absent | ObservationField::NotApplicable => {
self.exact(subject);
}
ObservationField::VersionInapplicable => self.unsupported(
subject,
"native field is version-inapplicable for the inspected Podman version",
),
ObservationField::Observed(observed) => match observed.origin() {
ObservationOrigin::Configured => self.exact(subject),
ObservationOrigin::Effective => self.promotion_required(
subject,
"effective observation requires explicit field-specific promotion",
),
ObservationOrigin::RuntimeAssigned | ObservationOrigin::LocalResolution => self
.unsupported_with_origin(
subject,
"runtime-assigned and local-resolution observations are never authored intent",
observed.origin(),
),
_ => self.unsupported(subject, "future observation origin is not reviewed for promotion"),
},
ObservationField::Unavailable => {
self.invalid(subject, "native observation is unavailable");
}
ObservationField::Malformed => {
self.invalid(subject, "native observation is malformed");
}
ObservationField::Unmodelled(_) => {
self.unsupported(subject, "native field is retained only as bounded metadata");
}
_ => self.unsupported(subject, "future native field state is not reviewed"),
}
}
fn require_complete(&mut self, observation: &ResourceObservation, subject: &str) -> bool {
if observation.header().state() == ResourceObservationState::Complete {
true
} else {
let has_causal_finding = observation
.header()
.findings()
.iter()
.any(|finding| invalid_inventory_finding(finding.code()));
if !has_causal_finding {
self.invalid_observation(
subject,
"selected native resource is unavailable or malformed",
observation.header().identity(),
);
}
false
}
}
fn identifier(&mut self, subject: &str, identity: &ResourceIdentity) -> Option<Identifier> {
match Identifier::new(identity_name(identity)) {
Ok(name) => Some(name),
Err(error) => {
self.model_error(subject, &error);
None
}
}
}
fn resolve_name(&self, kind: ResourceKind, reference: &str) -> Option<Identifier> {
let mut matches = self
.selected
.iter()
.filter(|identity| {
identity.kind() == kind && (identity.id() == reference || identity.name() == Some(reference))
})
.map(identity_name);
let first = matches.next()?;
if matches.next().is_some() {
return None;
}
Identifier::new(first).ok()
}
fn report_native_findings(&mut self) {
let mut inventory_findings = self
.source
.inventory()
.sections()
.iter()
.filter(|section| self.inventory_section_is_relevant(section.kind()))
.flat_map(|section| {
section
.findings()
.iter()
.filter(|finding| finding.resource().is_none())
.cloned()
.map(move |finding| (section.kind(), None, finding))
})
.collect::<Vec<_>>();
inventory_findings.extend(
self.selected
.iter()
.filter_map(|identity| self.source.inventory().observation(identity))
.flat_map(|observation| {
observation.header().findings().iter().cloned().map(move |finding| {
(
observation.header().identity().kind(),
Some(observation_state_name(observation.header().state())),
finding,
)
})
})
.collect::<Vec<_>>(),
);
let mut ordinary = BTreeMap::<
(String, &'static str, Option<String>, Option<&'static str>),
Vec<(ResourceKind, Option<&'static str>, podman_lens::InventoryFinding)>,
>::new();
for (kind, state, finding) in inventory_findings {
if invalid_inventory_finding(finding.code()) {
self.inventory_finding(&finding, kind, state);
continue;
}
let key = (
finding.code().as_str().to_owned(),
resource_kind_name(kind),
finding.resource().map(resource_locator),
state,
);
ordinary.entry(key).or_default().push((kind, state, finding));
}
for findings in ordinary.values() {
self.native_inventory_findings(findings);
}
let graph_findings = self.source.graph().findings().to_vec();
for finding in graph_findings {
self.discovery_finding(&finding);
}
}
fn inventory_section_is_relevant(&self, kind: ResourceKind) -> bool {
let graph = self.source.graph();
graph.all_requested()
|| !graph.requested_label_roots().is_empty()
|| graph.requested_roots().iter().any(|selector| selector.kind() == kind)
|| self.selected.iter().any(|identity| identity.kind() == kind)
}
fn inventory_finding(
&mut self,
finding: &podman_lens::InventoryFinding,
section_kind: ResourceKind,
observation_state: Option<&'static str>,
) {
let code = finding.code();
let resource = finding.resource().map(resource_locator);
let subject = resource
.clone()
.unwrap_or_else(|| format!("podman.inventory.{}", resource_kind_name(section_kind)));
let reason = podman_native_summary(code);
let context = self.inventory_finding_context(finding, section_kind, observation_state);
let mut native = NativeFinding::new(
"podman",
"podman-lens",
code.as_str(),
"acquisition",
Severity::Warning,
reason,
)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject.clone())))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)));
let adapter_code = self.importer.invalid.clone();
let mut diagnostic = diagnostic_with_context(
adapter_code.clone(),
Severity::Error,
"Podman source evidence is incomplete or malformed",
&subject,
reason,
"rejected",
None,
);
for field in context {
native = native.with_field(field.clone());
diagnostic = diagnostic.with_field(field);
}
self.diagnostics.push(diagnostic.with_native_finding(native));
self.push_loss(
format!("podman.acquisition.{}", code.as_str()),
ConversionKind::Invalid,
adapter_code,
);
}
fn inventory_finding_context(
&self,
finding: &podman_lens::InventoryFinding,
section_kind: ResourceKind,
observation_state: Option<&'static str>,
) -> Vec<DiagnosticField> {
let mut fields = vec![
DiagnosticField::new("observation_origin", DiagnosticValue::plain("acquisition")),
DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(self.source.observed_engine_version()),
),
DiagnosticField::new("source_api", DiagnosticValue::plain(self.source.observed_api_version())),
DiagnosticField::new(
"resource_kind",
DiagnosticValue::plain(resource_kind_name(section_kind)),
),
];
if let Some(resource) = finding.resource() {
fields.push(DiagnosticField::new(
"resource",
DiagnosticValue::plain(resource_locator(resource)),
));
}
if let Some(state) = observation_state {
fields.push(DiagnosticField::new("observation_state", DiagnosticValue::plain(state)));
}
if let Some(path) = finding.field_path() {
fields.push(DiagnosticField::new("native_path", DiagnosticValue::plain(path)));
}
if let Some(occurrence) = finding.occurrence() {
fields.push(DiagnosticField::new(
"occurrence",
DiagnosticValue::plain(occurrence.to_string()),
));
}
fields
}
fn discovery_finding(&mut self, finding: &podman_lens::DiscoveryFinding) {
let code = finding.code();
if !invalid_discovery_finding(code) {
self.native_discovery_finding(finding);
return;
}
let source_version = self.source.observed_engine_version();
let source_api = self.source.observed_api_version();
let resource = discovery_resource_locator(finding);
let subject = resource.clone().unwrap_or_else(|| "podman.discovery".to_owned());
let reason = podman_native_summary(code);
let native = NativeFinding::new(
"podman",
"podman-lens",
code.as_str(),
"discovery",
Severity::Warning,
reason,
)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject.clone())))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain("discovery"),
))
.with_field(DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(source_version),
))
.with_field(DiagnosticField::new("source_api", DiagnosticValue::plain(source_api)));
let native = resource.as_deref().map_or(native.clone(), |resource| {
native.with_field(DiagnosticField::new("resource", DiagnosticValue::plain(resource)))
});
let native = finding.field_path().map_or(native.clone(), |field_path| {
native.with_field(DiagnosticField::new("native_path", DiagnosticValue::plain(field_path)))
});
let diagnostic = Diagnostic::new(
self.importer.invalid.clone(),
Severity::Error,
"Podman resource discovery is incomplete or ambiguous",
)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject)))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)))
.with_field(DiagnosticField::new("decision", DiagnosticValue::plain("rejected")))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain("discovery"),
))
.with_field(DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(source_version),
))
.with_field(DiagnosticField::new("source_api", DiagnosticValue::plain(source_api)));
let diagnostic = resource.as_deref().map_or(diagnostic.clone(), |resource| {
diagnostic.with_field(DiagnosticField::new("resource", DiagnosticValue::plain(resource)))
});
let diagnostic = finding.field_path().map_or(diagnostic.clone(), |field_path| {
diagnostic.with_field(DiagnosticField::new("native_path", DiagnosticValue::plain(field_path)))
});
self.diagnostics.push(diagnostic.with_native_finding(native));
self.push_loss(
format!("podman.discovery.{}", code.as_str()),
ConversionKind::Invalid,
self.importer.invalid.clone(),
);
}
fn native_discovery_finding(&mut self, finding: &podman_lens::DiscoveryFinding) {
let code = finding.code();
let stage = "discovery";
let reason = podman_native_summary(code);
let resource = discovery_resource_locator(finding);
let subject = resource.clone().unwrap_or_else(|| "podman.discovery".to_owned());
let native = NativeFinding::new("podman", "podman-lens", code.as_str(), stage, Severity::Warning, reason)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject.clone())))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain(stage),
))
.with_field(DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(self.source.observed_engine_version()),
))
.with_field(DiagnosticField::new(
"source_api",
DiagnosticValue::plain(self.source.observed_api_version()),
));
let native = resource.as_deref().map_or(native.clone(), |resource| {
native.with_field(DiagnosticField::new("resource", DiagnosticValue::plain(resource)))
});
let native = finding.field_path().map_or(native.clone(), |path| {
native.with_field(DiagnosticField::new("native_path", DiagnosticValue::plain(path)))
});
let mut diagnostic = diagnostic_with_context(
self.importer.unsupported.clone(),
Severity::Warning,
"PodmanLens retained a native discovery finding",
&subject,
reason,
"omitted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain(stage),
))
.with_field(DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(self.source.observed_engine_version()),
))
.with_field(DiagnosticField::new(
"source_api",
DiagnosticValue::plain(self.source.observed_api_version()),
));
if let Some(resource) = resource {
diagnostic = diagnostic.with_field(DiagnosticField::new("resource", DiagnosticValue::plain(resource)));
}
if let Some(path) = finding.field_path() {
diagnostic = diagnostic.with_field(DiagnosticField::new("native_path", DiagnosticValue::plain(path)));
}
self.diagnostics.push(diagnostic.with_native_finding(native));
self.push_loss(
format!("podman.{stage}.{}", code.as_str()),
ConversionKind::Unsupported,
self.importer.unsupported.clone(),
);
}
fn native_inventory_findings(
&mut self,
findings: &[(ResourceKind, Option<&'static str>, podman_lens::InventoryFinding)],
) {
let Some((section_kind, observation_state, finding)) = findings.first() else {
return;
};
let code = finding.code();
let stage = "acquisition";
let reason = podman_native_summary(code);
let resource = finding.resource().map(resource_locator);
let subject = resource
.clone()
.unwrap_or_else(|| format!("podman.inventory.{}", resource_kind_name(*section_kind)));
let native = NativeFinding::new("podman", "podman-lens", code.as_str(), stage, Severity::Warning, reason)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject.clone())))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)));
let mut diagnostic = diagnostic_with_context(
self.importer.unsupported.clone(),
Severity::Warning,
"PodmanLens retained a native acquisition finding",
&subject,
reason,
"omitted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
));
let mut context = vec![
DiagnosticField::new(
"resource_kind",
DiagnosticValue::plain(resource_kind_name(*section_kind)),
),
DiagnosticField::new("observation_origin", DiagnosticValue::plain(stage)),
DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(self.source.observed_engine_version()),
),
DiagnosticField::new("source_api", DiagnosticValue::plain(self.source.observed_api_version())),
DiagnosticField::new("occurrence_count", DiagnosticValue::plain(findings.len().to_string())),
];
if let Some(resource) = resource {
context.push(DiagnosticField::new("resource", DiagnosticValue::plain(resource)));
}
if let Some(state) = observation_state {
context.push(DiagnosticField::new(
"observation_state",
DiagnosticValue::plain(*state),
));
}
let paths = findings
.iter()
.filter_map(|(_, _, finding)| finding.field_path())
.collect::<BTreeSet<_>>();
if paths.len() == 1 {
if let Some(path) = paths.first() {
context.push(DiagnosticField::new("native_path", DiagnosticValue::plain(*path)));
}
} else if !paths.is_empty() {
context.push(DiagnosticField::new(
"native_path_count",
DiagnosticValue::plain(paths.len().to_string()),
));
context.push(DiagnosticField::new(
"native_path_samples",
DiagnosticValue::plain(paths.iter().take(8).copied().collect::<Vec<_>>().join(", ")),
));
}
let mut native = native;
for field in context {
native = native.with_field(field.clone());
diagnostic = diagnostic.with_field(field);
}
self.diagnostics.push(diagnostic.with_native_finding(native));
for _ in findings {
self.push_loss(
format!("podman.{stage}.{}", code.as_str()),
ConversionKind::Unsupported,
self.importer.unsupported.clone(),
);
}
}
fn invalid_observation(&mut self, subject: impl Into<String>, summary: &'static str, identity: &ResourceIdentity) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.invalid.clone(),
Severity::Error,
summary,
&subject,
summary,
"rejected",
None,
)
.with_field(DiagnosticField::new(
"source_engine",
DiagnosticValue::plain(self.source.observed_engine_version()),
))
.with_field(DiagnosticField::new(
"source_api",
DiagnosticValue::plain(self.source.observed_api_version()),
))
.with_field(DiagnosticField::new(
"resource",
DiagnosticValue::plain(resource_locator(identity)),
)),
);
self.push_loss(subject, ConversionKind::Invalid, self.importer.invalid.clone());
}
fn exact(&mut self, subject: impl Into<String>) {
self.outcomes
.push(ConversionOutcome::exact(subject).with_origin(self.origin.clone()));
}
fn approximate_with_flag(&mut self, subject: impl Into<String>, summary: &'static str, flag: &'static str) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.policy.clone(),
Severity::Warning,
summary,
&subject,
summary,
"approximated",
Some("approximate"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain(flag),
))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain("effective"),
)),
);
self.push_loss(subject, ConversionKind::Approximate, self.importer.policy.clone());
}
fn promotion_required(&mut self, subject: impl Into<String>, summary: &'static str) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.policy.clone(),
Severity::Warning,
summary,
&subject,
summary,
"not-promoted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain("effective"),
)),
);
self.push_loss(subject, ConversionKind::Unsupported, self.importer.policy.clone());
}
fn promotion_required_with_flag(&mut self, subject: impl Into<String>, summary: &'static str, flag: &'static str) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.policy.clone(),
Severity::Warning,
summary,
&subject,
summary,
"not-promoted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain(flag),
))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain("effective"),
)),
);
self.push_loss(subject, ConversionKind::Unsupported, self.importer.policy.clone());
}
fn unsupported(&mut self, subject: impl Into<String>, summary: &'static str) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.unsupported.clone(),
Severity::Warning,
summary,
&subject,
summary,
"omitted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
)),
);
self.push_loss(subject, ConversionKind::Unsupported, self.importer.unsupported.clone());
}
fn unsupported_with_origin(
&mut self,
subject: impl Into<String>,
summary: &'static str,
origin: ObservationOrigin,
) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.unsupported.clone(),
Severity::Warning,
summary,
&subject,
summary,
"omitted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
))
.with_field(DiagnosticField::new(
"observation_origin",
DiagnosticValue::plain(observation_origin_name(origin)),
)),
);
self.push_loss(subject, ConversionKind::Unsupported, self.importer.unsupported.clone());
}
fn identity_conflict(&mut self, subject: impl Into<String>, summary: &'static str) {
let subject = subject.into();
self.diagnostics.push(diagnostic_with_context(
self.importer.identity.clone(),
Severity::Error,
summary,
&subject,
summary,
"rejected",
None,
));
self.push_loss(subject, ConversionKind::Invalid, self.importer.identity.clone());
}
fn secret_incomplete(&mut self, subject: impl Into<String>, summary: &'static str) {
let subject = subject.into();
self.diagnostics.push(
diagnostic_with_context(
self.importer.secret.clone(),
Severity::Warning,
summary,
&subject,
summary,
"omitted",
Some("partial"),
)
.with_field(DiagnosticField::new(
"available_promotion",
DiagnosticValue::plain("none"),
)),
);
self.push_loss(subject, ConversionKind::Unsupported, self.importer.secret.clone());
}
fn invalid(&mut self, subject: impl Into<String>, summary: &'static str) {
let subject = subject.into();
self.diagnostics.push(diagnostic_with_context(
self.importer.invalid.clone(),
Severity::Error,
summary,
&subject,
summary,
"rejected",
None,
));
self.push_loss(subject, ConversionKind::Invalid, self.importer.invalid.clone());
}
fn model_error(&mut self, subject: impl Into<String>, error: &ModelError) {
match error {
ModelError::DuplicateResource { .. }
| ModelError::DuplicateServiceGroupMember { .. }
| ModelError::UnknownServiceGroupMember { .. }
| ModelError::ServiceInMultipleGroups { .. } => self.identity_conflict(
subject,
"native identities or references conflict in the neutral namespace",
),
_ => {
self.invalid(subject, "native value cannot be represented safely in neutral model");
}
}
}
fn push_loss(&mut self, subject: impl Into<String>, kind: ConversionKind, code: DiagnosticCode) {
if let Ok(outcome) = ConversionOutcome::loss(subject, kind, code) {
self.outcomes.push(outcome.with_origin(self.origin.clone()));
}
}
fn sourced<T>(&self, value: T) -> Sourced<T> {
Sourced::from_source(value, self.origin.clone())
}
fn decision_sourced<T>(&self, value: T) -> Sourced<T> {
Sourced::from_source(value, Provenance::conversion_decision(self.origin.source_id().clone()))
}
}
fn diagnostic_with_context(
code: DiagnosticCode,
severity: Severity,
summary: impl Into<String>,
subject: &str,
reason: &str,
decision: &'static str,
required_loss_policy: Option<&'static str>,
) -> Diagnostic {
let diagnostic = Diagnostic::new(code, severity, summary)
.with_field(DiagnosticField::new("subject", DiagnosticValue::plain(subject)))
.with_field(DiagnosticField::new("reason", DiagnosticValue::plain(reason)))
.with_field(DiagnosticField::new("decision", DiagnosticValue::plain(decision)));
required_loss_policy.map_or(diagnostic.clone(), |policy| {
diagnostic.with_field(DiagnosticField::new(
"required_loss_policy",
DiagnosticValue::plain(policy),
))
})
}
fn podman_native_summary(code: podman_lens::DiagnosticCode) -> &'static str {
match code {
podman_lens::DiagnosticCode::AdvisoryLabelIncomplete => {
"optional Podman application-grouping labels are incomplete; affected resources remain ungrouped"
}
podman_lens::DiagnosticCode::AdvisoryLabelConflict => {
"optional Podman application-grouping labels disagree; affected resources remain ungrouped"
}
_ => podman_lens::Diagnostic::new(code).message(),
}
}
const fn observation_state_name(state: ResourceObservationState) -> &'static str {
match state {
ResourceObservationState::Complete => "complete",
ResourceObservationState::Unavailable => "unavailable",
ResourceObservationState::Malformed => "malformed",
_ => "unknown",
}
}
const fn observation_origin_name(origin: ObservationOrigin) -> &'static str {
match origin {
ObservationOrigin::Configured => "configured",
ObservationOrigin::Effective => "effective",
ObservationOrigin::RuntimeAssigned => "runtime-assigned",
ObservationOrigin::LocalResolution => "local-resolution",
_ => "unknown",
}
}
fn resource_locator(identity: &ResourceIdentity) -> String {
let kind = resource_kind_name(identity.kind());
let reference = identity.name().unwrap_or(identity.id());
format!("{kind}:{reference}")
}
fn resource_kind_name(kind: ResourceKind) -> &'static str {
match kind {
ResourceKind::Container => "container",
ResourceKind::Image => "image",
ResourceKind::Network => "network",
ResourceKind::Pod => "pod",
ResourceKind::Secret => "secret",
ResourceKind::Volume => "volume",
_ => "unknown",
}
}
fn discovery_resource_locator(finding: &podman_lens::DiscoveryFinding) -> Option<String> {
if let Some(resource) = finding.resource_identity() {
return Some(resource_locator(resource));
}
if let Some(selector) = finding.selector() {
return Some(format!(
"{}:{}",
resource_kind_name(selector.kind()),
selector.reference()
));
}
finding
.label_selector()
.map(|selector| format!("label:{}", selector.name()))
}
fn invalid_inventory_finding(code: podman_lens::DiagnosticCode) -> bool {
matches!(
code,
podman_lens::DiagnosticCode::InventoryHttpStatus
| podman_lens::DiagnosticCode::InventoryJson
| podman_lens::DiagnosticCode::InventoryShape
| podman_lens::DiagnosticCode::ResourceUnavailable
| podman_lens::DiagnosticCode::ResourceMalformed
| podman_lens::DiagnosticCode::RelationshipConflict
| podman_lens::DiagnosticCode::UnresolvedRelationship
| podman_lens::DiagnosticCode::PodMembershipConflict
)
}
fn invalid_discovery_finding(code: podman_lens::DiagnosticCode) -> bool {
matches!(
code,
podman_lens::DiagnosticCode::SelectorUnresolved
| podman_lens::DiagnosticCode::SelectorAmbiguous
| podman_lens::DiagnosticCode::RelationshipAmbiguous
)
}
fn observations<'a>(
source: &'a PodmanSource,
selected: &BTreeSet<ResourceIdentity>,
kind: ResourceKind,
) -> Vec<&'a ResourceObservation> {
selected
.iter()
.filter(|identity| identity.kind() == kind)
.filter_map(|identity| source.inventory().observation(identity))
.collect()
}
fn selected_identities(source: &PodmanSource) -> BTreeSet<ResourceIdentity> {
let mut selected = BTreeSet::new();
selected.extend(source.graph().resolved_roots().iter().cloned());
for group in source.graph().groups() {
selected.insert(group.id().clone());
selected.extend(group.members().iter().cloned());
selected.extend(group.prerequisites().iter().cloned());
}
selected.extend(source.graph().shared_prerequisites().iter().cloned());
for dependency in source.graph().dependencies() {
selected.insert(dependency.dependent().clone());
selected.insert(dependency.prerequisite().clone());
}
selected
}
fn identity_name(identity: &ResourceIdentity) -> &str {
identity.name().unwrap_or(identity.id())
}