use super::NodeResources;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlacementConstraint {
NodeSelector { key: String, value: String },
Zone(String),
Region(String),
RequiresGpu,
GpuModel(String),
MinGpuMemory(u64),
RequiresTensorCores,
Architecture(String),
Expression(ConstraintExpression),
}
impl PlacementConstraint {
pub fn matches(&self, node: &NodeResources) -> bool {
match self {
PlacementConstraint::NodeSelector { key, value } => {
node.labels.get(key).map(|v| v == value).unwrap_or(false)
}
PlacementConstraint::Zone(zone) => {
node.labels.get("topology.kubernetes.io/zone")
.or_else(|| node.labels.get("zone"))
.map(|z| z == zone)
.unwrap_or(false)
}
PlacementConstraint::Region(region) => {
node.labels.get("topology.kubernetes.io/region")
.or_else(|| node.labels.get("region"))
.map(|r| r == region)
.unwrap_or(false)
}
PlacementConstraint::RequiresGpu => {
!node.gpus.is_empty() && node.gpus_available() > 0
}
PlacementConstraint::GpuModel(model) => {
node.gpus.iter().any(|g| g.model.contains(model))
}
PlacementConstraint::MinGpuMemory(min_mb) => {
node.gpus.iter().any(|g| g.memory_mb >= *min_mb)
}
PlacementConstraint::RequiresTensorCores => {
node.gpus.iter().any(|g| g.tensor_cores)
}
PlacementConstraint::Architecture(arch) => {
node.labels.get("kubernetes.io/arch")
.or_else(|| node.labels.get("arch"))
.map(|a| a == arch)
.unwrap_or(false)
}
PlacementConstraint::Expression(expr) => {
expr.evaluate(node)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintExpression {
pub key: String,
pub operator: ExpressionOperator,
pub values: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExpressionOperator {
In,
NotIn,
Exists,
DoesNotExist,
Gt,
Lt,
}
impl ConstraintExpression {
pub fn evaluate(&self, node: &NodeResources) -> bool {
let value = node.labels.get(&self.key);
match self.operator {
ExpressionOperator::In => {
value.map(|v| self.values.contains(v)).unwrap_or(false)
}
ExpressionOperator::NotIn => {
value.map(|v| !self.values.contains(v)).unwrap_or(true)
}
ExpressionOperator::Exists => value.is_some(),
ExpressionOperator::DoesNotExist => value.is_none(),
ExpressionOperator::Gt => {
if let (Some(v), Some(threshold)) = (value, self.values.first()) {
v.parse::<i64>().ok()
.zip(threshold.parse::<i64>().ok())
.map(|(v, t)| v > t)
.unwrap_or(false)
} else {
false
}
}
ExpressionOperator::Lt => {
if let (Some(v), Some(threshold)) = (value, self.values.first()) {
v.parse::<i64>().ok()
.zip(threshold.parse::<i64>().ok())
.map(|(v, t)| v < t)
.unwrap_or(false)
} else {
false
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Affinity {
pub node_affinity: Option<NodeAffinity>,
pub workload_affinity: Option<WorkloadAffinity>,
pub workload_anti_affinity: Option<WorkloadAffinity>,
}
impl Affinity {
pub fn new() -> Self {
Self {
node_affinity: None,
workload_affinity: None,
workload_anti_affinity: None,
}
}
pub fn with_node_affinity(mut self, affinity: NodeAffinity) -> Self {
self.node_affinity = Some(affinity);
self
}
pub fn with_workload_affinity(mut self, affinity: WorkloadAffinity) -> Self {
self.workload_affinity = Some(affinity);
self
}
pub fn with_workload_anti_affinity(mut self, affinity: WorkloadAffinity) -> Self {
self.workload_anti_affinity = Some(affinity);
self
}
pub fn matches(&self, node: &NodeResources) -> bool {
if let Some(node_affinity) = &self.node_affinity {
for rule in &node_affinity.required {
if !rule.matches(node) {
return false;
}
}
}
true
}
}
impl Default for Affinity {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeAffinity {
pub required: Vec<AffinityRule>,
pub preferred: Vec<WeightedAffinityRule>,
}
impl NodeAffinity {
pub fn new() -> Self {
Self {
required: Vec::new(),
preferred: Vec::new(),
}
}
pub fn require(mut self, rule: AffinityRule) -> Self {
self.required.push(rule);
self
}
pub fn prefer(mut self, weight: i32, rule: AffinityRule) -> Self {
self.preferred.push(WeightedAffinityRule { weight, rule });
self
}
}
impl Default for NodeAffinity {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkloadAffinity {
pub required: Vec<WorkloadAffinityTerm>,
pub preferred: Vec<WeightedWorkloadAffinityTerm>,
}
impl WorkloadAffinity {
pub fn new() -> Self {
Self {
required: Vec::new(),
preferred: Vec::new(),
}
}
}
impl Default for WorkloadAffinity {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkloadAffinityTerm {
pub label_selector: LabelSelector,
pub topology_key: String,
pub namespaces: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightedWorkloadAffinityTerm {
pub weight: i32,
pub term: WorkloadAffinityTerm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabelSelector {
pub match_labels: std::collections::HashMap<String, String>,
pub match_expressions: Vec<ConstraintExpression>,
}
impl LabelSelector {
pub fn new() -> Self {
Self {
match_labels: std::collections::HashMap::new(),
match_expressions: Vec::new(),
}
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.match_labels.insert(key.into(), value.into());
self
}
pub fn with_expression(mut self, expr: ConstraintExpression) -> Self {
self.match_expressions.push(expr);
self
}
}
impl Default for LabelSelector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AffinityRule {
pub match_expressions: Vec<ConstraintExpression>,
}
impl AffinityRule {
pub fn new() -> Self {
Self {
match_expressions: Vec::new(),
}
}
pub fn with_expression(mut self, expr: ConstraintExpression) -> Self {
self.match_expressions.push(expr);
self
}
pub fn matches(&self, node: &NodeResources) -> bool {
self.match_expressions.iter().all(|expr| expr.evaluate(node))
}
}
impl Default for AffinityRule {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightedAffinityRule {
pub weight: i32,
pub rule: AffinityRule,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{NodeId, GpuResources};
#[test]
fn test_node_selector() {
let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
node.labels.insert("env".to_string(), "production".to_string());
let constraint = PlacementConstraint::NodeSelector {
key: "env".to_string(),
value: "production".to_string(),
};
assert!(constraint.matches(&node));
let wrong_constraint = PlacementConstraint::NodeSelector {
key: "env".to_string(),
value: "staging".to_string(),
};
assert!(!wrong_constraint.matches(&node));
}
#[test]
fn test_gpu_constraints() {
let gpu = GpuResources::new(0, "NVIDIA A100", 40960)
.with_tensor_cores(true)
.with_compute_capability(8.0);
let node = NodeResources::new(NodeId::new(), 4000, 8192)
.with_gpu(gpu);
assert!(PlacementConstraint::RequiresGpu.matches(&node));
assert!(PlacementConstraint::RequiresTensorCores.matches(&node));
assert!(PlacementConstraint::GpuModel("A100".to_string()).matches(&node));
assert!(PlacementConstraint::MinGpuMemory(40000).matches(&node));
assert!(!PlacementConstraint::MinGpuMemory(50000).matches(&node));
}
#[test]
fn test_expression_operators() {
let mut node = NodeResources::new(NodeId::new(), 4000, 8192);
node.labels.insert("tier".to_string(), "frontend".to_string());
node.labels.insert("priority".to_string(), "10".to_string());
let in_expr = ConstraintExpression {
key: "tier".to_string(),
operator: ExpressionOperator::In,
values: vec!["frontend".to_string(), "backend".to_string()],
};
assert!(in_expr.evaluate(&node));
let gt_expr = ConstraintExpression {
key: "priority".to_string(),
operator: ExpressionOperator::Gt,
values: vec!["5".to_string()],
};
assert!(gt_expr.evaluate(&node));
let exists_expr = ConstraintExpression {
key: "tier".to_string(),
operator: ExpressionOperator::Exists,
values: vec![],
};
assert!(exists_expr.evaluate(&node));
}
}