use super::{verify_runtime_base, RuntimeBaseConformanceCase, RuntimeBaseConformanceReport};
use crate::contract::{
HealthCheckKind, MountKind, NetworkMode, ResourceControl, RuntimeCapabilities, RuntimeFeature,
};
use crate::{RuntimeClient, RuntimeError, RuntimeResult};
use async_trait::async_trait;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RuntimeConformanceProfile {
Base,
Recovery,
Networking,
Mounts,
Health,
Resources,
Logs,
Exec,
Security,
Outputs,
Evidence,
}
impl RuntimeConformanceProfile {
pub const fn as_str(self) -> &'static str {
match self {
Self::Base => "base",
Self::Recovery => "recovery",
Self::Networking => "networking",
Self::Mounts => "mounts",
Self::Health => "health",
Self::Resources => "resources",
Self::Logs => "logs",
Self::Exec => "exec",
Self::Security => "security",
Self::Outputs => "outputs",
Self::Evidence => "evidence",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConformanceProfileRequirements {
pub profile: RuntimeConformanceProfile,
pub case_ids: BTreeSet<String>,
pub capability_claims: BTreeSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConformanceProfileEvidence {
pub profile: RuntimeConformanceProfile,
pub case_ids: BTreeSet<String>,
pub capability_claims: BTreeSet<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RuntimeConformanceInventory {
pub entries: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeConformanceSuiteReport {
pub base: RuntimeBaseConformanceReport,
pub profiles: Vec<RuntimeConformanceProfileEvidence>,
pub inventory_before: RuntimeConformanceInventory,
pub inventory_after: RuntimeConformanceInventory,
}
#[async_trait]
pub trait RuntimeConformanceFixture: Send + Sync {
fn base_case(&self) -> &RuntimeBaseConformanceCase;
fn available_profiles(&self) -> BTreeSet<RuntimeConformanceProfile>;
async fn inventory(&self) -> RuntimeResult<RuntimeConformanceInventory>;
async fn run_profile(
&self,
client: &dyn RuntimeClient,
capabilities: &RuntimeCapabilities,
profile: RuntimeConformanceProfile,
) -> RuntimeResult<RuntimeConformanceProfileEvidence>;
async fn cleanup(&self) -> RuntimeResult<()>;
}
pub fn required_runtime_profiles(
capabilities: &RuntimeCapabilities,
) -> RuntimeResult<BTreeSet<RuntimeConformanceProfile>> {
capabilities.validate().map_err(RuntimeError::Protocol)?;
let mut profiles = BTreeSet::from([
RuntimeConformanceProfile::Base,
RuntimeConformanceProfile::Recovery,
]);
if !capabilities.network_modes.is_empty() {
profiles.insert(RuntimeConformanceProfile::Networking);
}
if !capabilities.mount_kinds.is_empty() {
profiles.insert(RuntimeConformanceProfile::Mounts);
}
if !capabilities.health_check_kinds.is_empty() {
profiles.insert(RuntimeConformanceProfile::Health);
}
if !capabilities.resource_controls.is_empty() {
profiles.insert(RuntimeConformanceProfile::Resources);
}
if capabilities.supports_feature(RuntimeFeature::Logs) {
profiles.insert(RuntimeConformanceProfile::Logs);
}
if capabilities.supports_feature(RuntimeFeature::Exec) {
profiles.insert(RuntimeConformanceProfile::Exec);
}
if !capabilities.isolation_levels.is_empty()
|| capabilities.supports_feature(RuntimeFeature::SecretReferences)
{
profiles.insert(RuntimeConformanceProfile::Security);
}
if capabilities.supports_feature(RuntimeFeature::OutputArtifacts) {
profiles.insert(RuntimeConformanceProfile::Outputs);
}
if capabilities.supports_feature(RuntimeFeature::Usage)
|| capabilities.supports_feature(RuntimeFeature::Attestation)
{
profiles.insert(RuntimeConformanceProfile::Evidence);
}
Ok(profiles)
}
pub fn runtime_profile_requirements(
capabilities: &RuntimeCapabilities,
profile: RuntimeConformanceProfile,
) -> RuntimeResult<RuntimeConformanceProfileRequirements> {
capabilities.validate().map_err(RuntimeError::Protocol)?;
let mut case_ids = base_case_ids(profile)
.iter()
.copied()
.map(str::to_owned)
.collect::<BTreeSet<_>>();
extend_capability_case_ids(capabilities, profile, &mut case_ids);
if profile == RuntimeConformanceProfile::Security
&& capabilities.supports_feature(RuntimeFeature::SecretReferences)
{
case_ids.insert("SECURITY-SECRET-NONDISCLOSURE".into());
}
if profile == RuntimeConformanceProfile::Evidence
&& capabilities.supports_feature(RuntimeFeature::Attestation)
{
case_ids.insert("EVIDENCE-ATTESTATION-VALIDITY".into());
}
if profile == RuntimeConformanceProfile::Evidence
&& capabilities.supports_feature(RuntimeFeature::Usage)
{
case_ids.insert("EVIDENCE-USAGE-VALIDITY".into());
}
let capability_claims = capability_claims(capabilities, profile);
Ok(RuntimeConformanceProfileRequirements {
profile,
case_ids,
capability_claims,
})
}
pub async fn verify_runtime_profiles(
client: &dyn RuntimeClient,
fixture: &dyn RuntimeConformanceFixture,
) -> RuntimeResult<RuntimeConformanceSuiteReport> {
let capabilities = client.capabilities().await?;
capabilities.validate().map_err(RuntimeError::Protocol)?;
let missing_lifecycle = [
RuntimeFeature::DurableIdentity,
RuntimeFeature::Stop,
RuntimeFeature::Remove,
]
.into_iter()
.filter(|feature| !capabilities.supports_feature(*feature))
.map(|feature| format!("feature:{feature:?}"))
.collect::<Vec<_>>();
if !missing_lifecycle.is_empty() {
return Err(RuntimeError::UnsupportedCapabilities(missing_lifecycle));
}
fixture
.base_case()
.validate()
.map_err(RuntimeError::InvalidRequest)?;
for spec in fixture.base_case().specifications() {
let missing = capabilities
.missing_for(spec)
.map_err(RuntimeError::InvalidRequest)?;
if !missing.is_empty() {
return Err(RuntimeError::UnsupportedCapabilities(missing));
}
}
let required = required_runtime_profiles(&capabilities)?;
let mut selected = fixture.available_profiles();
selected.insert(RuntimeConformanceProfile::Base);
let missing_profiles = required.difference(&selected).copied().collect::<Vec<_>>();
if !missing_profiles.is_empty() {
return Err(RuntimeError::Protocol(format!(
"conformance fixture omits required profiles: {}",
missing_profiles
.iter()
.map(|profile| profile.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
}
selected.extend(required);
let inventory_before = fixture.inventory().await?;
let execution: RuntimeResult<(
RuntimeBaseConformanceReport,
Vec<RuntimeConformanceProfileEvidence>,
)> = async {
let base = verify_runtime_base(client, fixture.base_case()).await?;
let mut evidence = vec![base_evidence(&capabilities)?];
for profile in selected
.iter()
.copied()
.filter(|profile| *profile != RuntimeConformanceProfile::Base)
{
let actual = fixture.run_profile(client, &capabilities, profile).await?;
validate_evidence(&capabilities, profile, &actual)?;
evidence.push(actual);
}
Ok((base, evidence))
}
.await;
let cleanup = fixture.cleanup().await;
let inventory_after = fixture.inventory().await;
cleanup?;
let inventory_after = inventory_after?;
if inventory_after != inventory_before {
return Err(RuntimeError::Protocol(format!(
"conformance cleanup changed provider inventory: before={:?}, after={:?}",
inventory_before.entries, inventory_after.entries
)));
}
let (base, profiles) = execution?;
Ok(RuntimeConformanceSuiteReport {
base,
profiles,
inventory_before,
inventory_after,
})
}
fn validate_evidence(
capabilities: &RuntimeCapabilities,
profile: RuntimeConformanceProfile,
evidence: &RuntimeConformanceProfileEvidence,
) -> RuntimeResult<()> {
if evidence.profile != profile {
return Err(RuntimeError::Protocol(format!(
"conformance {} fixture returned {} evidence",
profile.as_str(),
evidence.profile.as_str()
)));
}
let required = runtime_profile_requirements(capabilities, profile)?;
let missing_cases = required
.case_ids
.difference(&evidence.case_ids)
.cloned()
.collect::<Vec<_>>();
let missing_claims = required
.capability_claims
.difference(&evidence.capability_claims)
.cloned()
.collect::<Vec<_>>();
if !missing_cases.is_empty() || !missing_claims.is_empty() {
return Err(RuntimeError::Protocol(format!(
"conformance {} evidence is incomplete: missing cases {:?}, missing claims {:?}",
profile.as_str(),
missing_cases,
missing_claims
)));
}
Ok(())
}
fn base_evidence(
capabilities: &RuntimeCapabilities,
) -> RuntimeResult<RuntimeConformanceProfileEvidence> {
let required = runtime_profile_requirements(capabilities, RuntimeConformanceProfile::Base)?;
Ok(RuntimeConformanceProfileEvidence {
profile: required.profile,
case_ids: required.case_ids,
capability_claims: required.capability_claims,
})
}
fn base_case_ids(profile: RuntimeConformanceProfile) -> &'static [&'static str] {
match profile {
RuntimeConformanceProfile::Base => &[
"BASE-TASK-SUCCESS",
"BASE-TASK-FAILURE",
"BASE-TASK-TIMEOUT",
"BASE-SERVICE-LIFECYCLE",
"BASE-EXACT-REPLAY",
"BASE-GENERATION-CONFLICT",
"BASE-TOMBSTONE",
],
RuntimeConformanceProfile::Recovery => &[
"RECOVERY-CREATE-BEFORE-ACK",
"RECOVERY-CLIENT-RESTART",
"RECOVERY-PROVIDER-RESTART",
"RECOVERY-EXTERNAL-DELETION",
"RECOVERY-SAME-GENERATION-REPLACEMENT",
"RECOVERY-DUPLICATE-DETECTION",
],
RuntimeConformanceProfile::Networking => &[],
RuntimeConformanceProfile::Mounts => &["MOUNT-READ-ONLY", "MOUNT-CLEANUP"],
RuntimeConformanceProfile::Health => &[
"HEALTH-THRESHOLD-TRANSITION",
"HEALTH-PROBE-TIMEOUT",
"HEALTH-START-PERIOD",
"HEALTH-UNHEALTHY-EXIT",
],
RuntimeConformanceProfile::Resources => &[],
RuntimeConformanceProfile::Logs => &[
"LOG-STREAM-FILTER",
"LOG-TOTAL-ORDER",
"LOG-CURSOR-RESUME",
"LOG-SAME-TIMESTAMP",
"LOG-LIMIT",
"LOG-ROTATION-GAP",
"LOG-RETENTION",
"LOG-LARGE-RECORD",
],
RuntimeConformanceProfile::Exec => &[
"EXEC-STATE-POLICY",
"EXEC-TIMEOUT-REPLAY",
"EXEC-EXIT-CODE",
"EXEC-OUTPUT-BOUNDS",
"EXEC-OUTPUT-TRUNCATION",
"EXEC-IDENTITY-GENERATION-BINDING",
],
RuntimeConformanceProfile::Security => &[
"SECURITY-DIGEST-PINNING",
"SECURITY-METADATA-TAMPER",
"SECURITY-NAMESPACE-SEPARATION",
"SECURITY-LEAST-PRIVILEGE",
"SECURITY-HOSTILE-INPUT",
],
RuntimeConformanceProfile::Outputs => &["OUTPUT-EXACT-BOUNDED", "OUTPUT-DIGEST-BINDING"],
RuntimeConformanceProfile::Evidence => &[
"EVIDENCE-SPEC-BINDING",
"EVIDENCE-SEMANTICS-PROFILE-BINDING",
],
}
}
fn extend_capability_case_ids(
capabilities: &RuntimeCapabilities,
profile: RuntimeConformanceProfile,
case_ids: &mut BTreeSet<String>,
) {
match profile {
RuntimeConformanceProfile::Networking => {
for mode in &capabilities.network_modes {
match mode {
NetworkMode::None => {
case_ids.insert("NETWORK-MODE-NONE".into());
case_ids.insert("NETWORK-OUTBOUND-DENIED".into());
}
NetworkMode::Outbound => {
case_ids.insert("NETWORK-MODE-OUTBOUND".into());
case_ids.insert("NETWORK-OUTBOUND-ALLOWED".into());
}
NetworkMode::Service => {
case_ids.insert("NETWORK-MODE-SERVICE".into());
case_ids.insert("NETWORK-PROTOCOL-TCP".into());
case_ids.insert("NETWORK-PROTOCOL-UDP".into());
case_ids.insert("NETWORK-LOOPBACK-PUBLICATION".into());
case_ids.insert("NETWORK-PORT-COLLISION".into());
}
}
}
}
RuntimeConformanceProfile::Mounts => {
for kind in &capabilities.mount_kinds {
case_ids.insert(
match kind {
MountKind::Artifact => "MOUNT-ARTIFACT-BEHAVIOR",
MountKind::Volume => "MOUNT-VOLUME-PERSISTENCE",
MountKind::Tmpfs => "MOUNT-TMPFS-ISOLATION",
}
.into(),
);
}
}
RuntimeConformanceProfile::Health => {
for kind in &capabilities.health_check_kinds {
case_ids.insert(
match kind {
HealthCheckKind::Http => "HEALTH-PROBE-HTTP",
HealthCheckKind::Tcp => "HEALTH-PROBE-TCP",
HealthCheckKind::Command => "HEALTH-PROBE-COMMAND",
}
.into(),
);
}
}
RuntimeConformanceProfile::Resources => {
for control in &capabilities.resource_controls {
let (configuration, behavior) = match control {
ResourceControl::Cpu => ("RESOURCE-CPU-CONFIG", "RESOURCE-CPU-BEHAVIOR"),
ResourceControl::Memory => {
("RESOURCE-MEMORY-CONFIG", "RESOURCE-MEMORY-BEHAVIOR")
}
ResourceControl::Pids => ("RESOURCE-PIDS-CONFIG", "RESOURCE-PIDS-BEHAVIOR"),
ResourceControl::EphemeralStorage => (
"RESOURCE-EPHEMERAL-STORAGE-CONFIG",
"RESOURCE-EPHEMERAL-STORAGE-BEHAVIOR",
),
ResourceControl::ExecutionTimeout => (
"RESOURCE-EXECUTION-TIMEOUT-CONFIG",
"RESOURCE-EXECUTION-TIMEOUT-BEHAVIOR",
),
};
case_ids.insert(configuration.into());
case_ids.insert(behavior.into());
}
}
RuntimeConformanceProfile::Base
| RuntimeConformanceProfile::Recovery
| RuntimeConformanceProfile::Logs
| RuntimeConformanceProfile::Exec
| RuntimeConformanceProfile::Security
| RuntimeConformanceProfile::Outputs
| RuntimeConformanceProfile::Evidence => {}
}
}
fn capability_claims(
capabilities: &RuntimeCapabilities,
profile: RuntimeConformanceProfile,
) -> BTreeSet<String> {
match profile {
RuntimeConformanceProfile::Base => capabilities
.unit_classes
.iter()
.map(|class| format!("unit_class:{class:?}"))
.collect(),
RuntimeConformanceProfile::Recovery => BTreeSet::from(["feature:DurableIdentity".into()]),
RuntimeConformanceProfile::Networking => capabilities
.network_modes
.iter()
.map(|mode| format!("network_mode:{mode:?}"))
.collect(),
RuntimeConformanceProfile::Mounts => capabilities
.mount_kinds
.iter()
.map(|kind| format!("mount_kind:{kind:?}"))
.collect(),
RuntimeConformanceProfile::Health => capabilities
.health_check_kinds
.iter()
.map(|kind| format!("health_check:{kind:?}"))
.collect(),
RuntimeConformanceProfile::Resources => capabilities
.resource_controls
.iter()
.map(|control| format!("resource_control:{control:?}"))
.collect(),
RuntimeConformanceProfile::Logs => BTreeSet::from(["feature:Logs".into()]),
RuntimeConformanceProfile::Exec => BTreeSet::from(["feature:Exec".into()]),
RuntimeConformanceProfile::Security => {
let mut claims = capabilities
.isolation_levels
.iter()
.map(|level| format!("isolation:{level:?}"))
.collect::<BTreeSet<_>>();
claims.insert("feature:DurableIdentity".into());
if capabilities.supports_feature(RuntimeFeature::SecretReferences) {
claims.insert("feature:SecretReferences".into());
}
claims
}
RuntimeConformanceProfile::Outputs => BTreeSet::from(["feature:OutputArtifacts".into()]),
RuntimeConformanceProfile::Evidence => [RuntimeFeature::Usage, RuntimeFeature::Attestation]
.into_iter()
.filter(|feature| capabilities.supports_feature(*feature))
.map(|feature| format!("feature:{feature:?}"))
.collect(),
}
}