use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use crate::paths::state::{ResolvedCoordinationScope, StateLayout};
#[derive(Debug, Clone)]
pub(crate) struct LoadedPodIdentity {
pub name: String,
pub display_name: Option<String>,
pub manifest_path: PathBuf,
pub policy_path: PathBuf,
pub memory_path: PathBuf,
pub owned_profiles: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedPodMemoryBinding {
pub name: String,
pub identity: Option<LoadedPodIdentity>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct PodIdentityView {
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_path: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub owned_profiles: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct CoordinationScopeView {
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shared_root: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct LoadedMachineIdentity {
pub pod_name: String,
pub id: String,
pub display_name: Option<String>,
pub manifest_path: PathBuf,
pub trust_class: MachineTrustClass,
pub available_profiles: Vec<String>,
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MachineTrustClass {
#[default]
Owned,
Limited,
Observer,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct MachineIdentityView {
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub pod_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_class: Option<MachineTrustClass>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub available_profiles: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct PodManifestFile {
pod: PodSection,
#[serde(default)]
profiles: BTreeMap<String, PodProfileSection>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct PodSection {
name: String,
#[serde(default)]
display_name: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct PodProfileSection {
description: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct MachineManifestFile {
machine: MachineSection,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct MachineSection {
id: String,
display_name: Option<String>,
trust_class: MachineTrustClass,
profiles: Vec<String>,
capabilities: Vec<String>,
}
pub(crate) fn resolve_active_identity(layout: &StateLayout) -> Result<Option<LoadedPodIdentity>> {
let profile_name = layout.profile().as_str();
let mut matches = Vec::new();
for manifest in load_all_manifests(layout)? {
if manifest
.owned_profiles
.iter()
.any(|owned| owned == profile_name)
{
matches.push(manifest);
}
}
if matches.len() > 1 {
let owners = matches
.iter()
.map(|identity| identity.name.clone())
.collect::<Vec<_>>()
.join(", ");
bail!(
"profile `{profile_name}` is claimed by multiple pod identities ({owners}); edit the conflicting `pod.toml` files so one profile belongs to only one pod"
);
}
Ok(matches.into_iter().next())
}
pub(crate) fn resolve_coordination_scope_view(
layout: &StateLayout,
locality_id: &str,
) -> Result<CoordinationScopeView> {
let resolved = layout.resolved_coordination_scope(locality_id)?;
Ok(coordination_scope_view_from(resolved.as_ref()))
}
pub(crate) fn resolve_pod_identity_view(layout: &StateLayout) -> Result<PodIdentityView> {
let resolved = resolve_active_identity(layout)?;
Ok(pod_identity_view_from(resolved.as_ref()))
}
pub(crate) fn resolve_active_machine_identity(
layout: &StateLayout,
) -> Result<Option<LoadedMachineIdentity>> {
let Some(identity) = resolve_active_identity(layout)? else {
return Ok(None);
};
let manifest_path = layout.pod_machine_manifest_path(&identity.name)?;
if !manifest_path.is_file() {
return Ok(None);
}
Ok(Some(load_machine_manifest(&identity, &manifest_path)?))
}
pub(crate) fn resolve_machine_identity_view(layout: &StateLayout) -> Result<MachineIdentityView> {
let active_pod = resolve_active_identity(layout)?;
let active_machine = active_pod
.as_ref()
.map(|_| resolve_active_machine_identity(layout))
.transpose()?
.flatten();
machine_identity_view_from(active_pod.as_ref(), active_machine.as_ref(), layout)
}
pub(crate) fn resolve_pod_memory_binding(
layout: &StateLayout,
locality_id: &str,
) -> Result<Option<ResolvedPodMemoryBinding>> {
if let Some(identity) = resolve_active_identity(layout)? {
return Ok(Some(ResolvedPodMemoryBinding {
name: identity.name.clone(),
identity: Some(identity),
}));
}
let Some(scope) = layout.resolved_coordination_scope(locality_id)? else {
return Ok(None);
};
Ok(Some(ResolvedPodMemoryBinding {
name: scope.name,
identity: None,
}))
}
fn load_all_manifests(layout: &StateLayout) -> Result<Vec<LoadedPodIdentity>> {
let pods_root = layout.ccd_root().join("pods");
let Ok(entries) = fs::read_dir(&pods_root) else {
return Ok(Vec::new());
};
let mut manifests = Vec::new();
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let pod_dir = entry.path();
let manifest_path = pod_dir.join("pod.toml");
if !manifest_path.is_file() {
continue;
}
manifests.push(load_manifest(&pod_dir, &manifest_path)?);
}
Ok(manifests)
}
fn load_manifest(pod_dir: &Path, manifest_path: &Path) -> Result<LoadedPodIdentity> {
let raw = fs::read_to_string(manifest_path)
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
let manifest: PodManifestFile = toml::from_str(&raw)
.with_context(|| format!("failed to parse {}", manifest_path.display()))?;
if manifest.profiles.is_empty() {
bail!(
"{} must declare at least one owned profile under [profiles]",
manifest_path.display()
);
}
let dir_name = pod_dir
.file_name()
.and_then(|segment| segment.to_str())
.ok_or_else(|| anyhow::anyhow!("invalid pod directory name for {}", pod_dir.display()))?;
crate::paths::state::validate_pod_name(&manifest.pod.name)?;
if manifest.pod.name != dir_name {
bail!(
"{} declares pod name `{}` but lives under `pods/{dir_name}`; keep the manifest name aligned with its directory",
manifest_path.display(),
manifest.pod.name
);
}
let owned_profiles = manifest
.profiles
.into_keys()
.map(|name| {
if name.trim().is_empty() {
bail!(
"{} contains an empty profile key under [profiles]",
manifest_path.display()
);
}
Ok(name)
})
.collect::<Result<Vec<_>>>()?;
let memory_path = pod_dir.join("memory.md");
let policy_path = pod_dir.join("policy.md");
Ok(LoadedPodIdentity {
name: manifest.pod.name,
display_name: manifest.pod.display_name,
manifest_path: manifest_path.to_path_buf(),
policy_path,
memory_path,
owned_profiles,
})
}
fn load_machine_manifest(
pod_identity: &LoadedPodIdentity,
manifest_path: &Path,
) -> Result<LoadedMachineIdentity> {
let raw = fs::read_to_string(manifest_path)
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
let manifest: MachineManifestFile = toml::from_str(&raw)
.with_context(|| format!("failed to parse {}", manifest_path.display()))?;
crate::paths::state::validate_machine_id(&manifest.machine.id)?;
if manifest.machine.id.trim().is_empty() {
bail!(
"{} must declare a non-empty machine.id",
manifest_path.display()
);
}
for profile in &manifest.machine.profiles {
if profile.trim().is_empty() {
bail!(
"{} contains an empty profile entry in machine.profiles",
manifest_path.display()
);
}
if !pod_identity
.owned_profiles
.iter()
.any(|owned| owned == profile)
{
bail!(
"{} declares machine profile `{profile}` that is not owned by pod `{}`",
manifest_path.display(),
pod_identity.name
);
}
}
Ok(LoadedMachineIdentity {
pod_name: pod_identity.name.clone(),
id: manifest.machine.id,
display_name: manifest.machine.display_name,
manifest_path: manifest_path.to_path_buf(),
trust_class: manifest.machine.trust_class,
available_profiles: manifest.machine.profiles,
capabilities: manifest.machine.capabilities,
})
}
fn pod_identity_view_from(identity: Option<&LoadedPodIdentity>) -> PodIdentityView {
match identity {
Some(identity) => PodIdentityView {
status: "declared",
name: Some(identity.name.clone()),
display_name: identity.display_name.clone(),
manifest_path: Some(identity.manifest_path.display().to_string()),
policy_path: Some(identity.policy_path.display().to_string()),
memory_path: Some(identity.memory_path.display().to_string()),
owned_profiles: identity.owned_profiles.clone(),
reason: None,
},
None => PodIdentityView {
status: "missing",
name: None,
display_name: None,
manifest_path: None,
policy_path: None,
memory_path: None,
owned_profiles: Vec::new(),
reason: None,
},
}
}
fn machine_identity_view_from(
pod_identity: Option<&LoadedPodIdentity>,
machine_identity: Option<&LoadedMachineIdentity>,
layout: &StateLayout,
) -> Result<MachineIdentityView> {
match machine_identity {
Some(identity) => Ok(MachineIdentityView {
status: "declared",
pod_name: Some(identity.pod_name.clone()),
id: Some(identity.id.clone()),
display_name: identity.display_name.clone(),
manifest_path: Some(identity.manifest_path.display().to_string()),
trust_class: Some(identity.trust_class),
available_profiles: identity.available_profiles.clone(),
capabilities: identity.capabilities.clone(),
reason: None,
}),
None => Ok(MachineIdentityView {
status: "missing",
pod_name: pod_identity.map(|identity| identity.name.clone()),
id: None,
display_name: None,
manifest_path: pod_identity
.map(|identity| layout.pod_machine_manifest_path(&identity.name))
.transpose()?
.map(|path| path.display().to_string()),
trust_class: None,
available_profiles: Vec::new(),
capabilities: Vec::new(),
reason: pod_identity.map(|identity| {
format!(
"no machine identity is declared yet for pod `{}`",
identity.name
)
}),
}),
}
}
fn coordination_scope_view_from(
scope: Option<&ResolvedCoordinationScope>,
) -> CoordinationScopeView {
match scope {
Some(scope) => CoordinationScopeView {
status: "configured",
name: Some(scope.name.clone()),
source: Some(scope.source.as_str()),
config_path: Some(scope.config_path.display().to_string()),
shared_root: Some(scope.shared_root.display().to_string()),
},
None => CoordinationScopeView {
status: "missing",
name: None,
source: None,
config_path: None,
shared_root: None,
},
}
}