use serde::{Deserialize, Serialize};
use super::super::PlacementTypeError;
use super::super::capability::{CapabilityId, Provenance};
use super::super::memory::MemoryDemand;
use super::super::requirement::Requirement;
use super::facts::TrustTier;
pub const MAX_KIND_LEN: usize = 64;
pub const MAX_NAME_LEN: usize = 256;
pub const MAX_REQUIREMENTS: usize = 64;
pub const MAX_VARIANTS: usize = 16;
pub const MAX_LIST: usize = 64;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Execution {
#[default]
Native,
Emulated,
}
impl Execution {
pub fn as_str(self) -> &'static str {
match self {
Execution::Native => "native",
Execution::Emulated => "emulated",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecutionVariant {
pub name: String,
#[serde(default)]
pub execution: Execution,
#[serde(default)]
pub requirements: Vec<Requirement>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkloadRequirements {
#[serde(default)]
pub common: Vec<Requirement>,
#[serde(default)]
pub variants: Vec<ExecutionVariant>,
#[serde(default)]
pub memory: MemoryDemand,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LatencyClass {
#[default]
Batch,
Interactive,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Better {
#[default]
Lower,
Higher,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerfTarget {
pub id: CapabilityId,
pub param: String,
pub param_value: String,
#[serde(default)]
pub better: Better,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Preference {
pub name: String,
pub requirement: Requirement,
pub weight: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlacementPolicy {
#[serde(default = "default_tier")]
pub min_trust: TrustTier,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_provenance: Option<Provenance>,
#[serde(default)]
pub labels: Vec<String>,
#[serde(default)]
pub excludes: Vec<String>,
#[serde(default)]
pub latency_class: LatencyClass,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub perf: Option<PerfTarget>,
#[serde(default)]
pub preferences: Vec<Preference>,
#[serde(default)]
pub sticky: bool,
#[serde(default)]
pub revocable_refs: Vec<String>,
}
fn default_tier() -> TrustTier {
TrustTier::Paired
}
impl Default for PlacementPolicy {
fn default() -> Self {
Self {
min_trust: default_tier(),
min_provenance: None,
labels: vec![],
excludes: vec![],
latency_class: LatencyClass::Batch,
perf: None,
preferences: vec![],
sticky: false,
revocable_refs: vec![],
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "WorkloadSpecRaw")]
pub struct WorkloadSpec {
pub kind: String,
pub name: String,
#[serde(default)]
pub requirements: WorkloadRequirements,
#[serde(default)]
pub policy: PlacementPolicy,
}
#[derive(Deserialize)]
struct WorkloadSpecRaw {
kind: String,
name: String,
#[serde(default)]
requirements: WorkloadRequirements,
#[serde(default)]
policy: PlacementPolicy,
}
impl TryFrom<WorkloadSpecRaw> for WorkloadSpec {
type Error = PlacementTypeError;
fn try_from(r: WorkloadSpecRaw) -> Result<Self, Self::Error> {
let s = WorkloadSpec {
kind: r.kind,
name: r.name,
requirements: r.requirements,
policy: r.policy,
};
s.validate()?;
Ok(s)
}
}
fn bad(why: impl Into<String>) -> PlacementTypeError {
PlacementTypeError::InvalidRequirement(why.into())
}
fn check_text(what: &str, s: &str) -> Result<(), PlacementTypeError> {
if s.is_empty() || s.len() > MAX_NAME_LEN || s.chars().any(char::is_control) {
return Err(bad(format!(
"{what} must be 1..={MAX_NAME_LEN} printable bytes"
)));
}
Ok(())
}
fn check_list(what: &str, v: &[String]) -> Result<(), PlacementTypeError> {
if v.len() > MAX_LIST {
return Err(bad(format!("too many {what} (max {MAX_LIST})")));
}
v.iter().try_for_each(|s| check_text(what, s))
}
fn check_reqs(v: &[Requirement]) -> Result<(), PlacementTypeError> {
if v.len() > MAX_REQUIREMENTS {
return Err(bad(format!(
"too many requirements (max {MAX_REQUIREMENTS})"
)));
}
v.iter().try_for_each(Requirement::validate)
}
impl WorkloadSpec {
pub fn new(kind: &str, name: &str) -> Self {
Self {
kind: kind.to_string(),
name: name.to_string(),
requirements: WorkloadRequirements::default(),
policy: PlacementPolicy::default(),
}
}
pub fn validate(&self) -> Result<(), PlacementTypeError> {
let k = &self.kind;
if k.is_empty()
|| k.len() > MAX_KIND_LEN
|| !k
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
{
return Err(bad("kind must be 1..=64 of [a-z0-9_-]"));
}
check_text("name", &self.name)?;
let r = &self.requirements;
check_reqs(&r.common)?;
if r.variants.len() > MAX_VARIANTS {
return Err(bad(format!("too many variants (max {MAX_VARIANTS})")));
}
for v in &r.variants {
check_text("variant name", &v.name)?;
check_reqs(&v.requirements)?;
}
let p = &self.policy;
check_list("labels", &p.labels)?;
check_list("excludes", &p.excludes)?;
check_list("revocable refs", &p.revocable_refs)?;
if p.preferences.len() > MAX_LIST {
return Err(bad(format!("too many preferences (max {MAX_LIST})")));
}
for pref in &p.preferences {
check_text("preference name", &pref.name)?;
pref.requirement.validate()?;
if !pref.weight.is_finite() || pref.weight < 0.0 {
return Err(bad("preference weight must be finite and non-negative"));
}
}
if let Some(t) = &p.perf {
check_text("perf param", &t.param)?;
check_text("perf param value", &t.param_value)?;
if t.budget.is_some_and(|b| !b.is_finite() || b < 0.0) {
return Err(bad("perf budget must be finite and non-negative"));
}
}
Ok(())
}
pub fn effective_variants(&self) -> Vec<ExecutionVariant> {
if self.requirements.variants.is_empty() {
vec![ExecutionVariant {
name: "default".to_string(),
execution: Execution::Native,
requirements: vec![],
}]
} else {
self.requirements.variants.clone()
}
}
}