use serde::Serialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum TolerationOperator {
Equal,
Exists,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum TolerationEffect {
NoSchedule,
PreferNoSchedule,
NoExecute,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct K8sToleration {
pub key: String,
pub operator: TolerationOperator,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
pub effect: TolerationEffect,
#[serde(skip_serializing_if = "Option::is_none")]
pub toleration_seconds: Option<i64>,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, to_value};
use super::{K8sToleration, TolerationEffect, TolerationOperator};
use crate::providers::claude::k8s::common::{
DEFAULT_INPUT_INIT_IMAGE, ImagePullPolicy, K8sResources, PodConfig, build_pod_spec,
};
fn config_with<'a>(
tolerations: &'a [K8sToleration],
resources: &'a K8sResources,
pull_policy: &'a ImagePullPolicy,
node_selector: &'a BTreeMap<String, String>,
extra_labels: &'a BTreeMap<String, String>,
) -> PodConfig<'a> {
PodConfig {
name: "test-pod",
image: "img:v1",
command: vec!["sh".to_string()],
namespace: "default",
resources,
service_account: None,
restart_policy: "Never",
image_pull_policy: pull_policy,
env_vars: &[],
image_pull_secrets: &[],
extra_labels,
node_selector,
tolerations,
volumes: &[],
pvc_volumes: &[],
inputs: &[],
input_init_image: DEFAULT_INPUT_INIT_IMAGE,
prompt_configmap: None,
prompt_mount_path: "",
}
}
#[test]
fn operator_and_effect_serialize_to_pascal_case() {
assert_eq!(to_value(TolerationOperator::Equal).unwrap(), json!("Equal"));
assert_eq!(
to_value(TolerationOperator::Exists).unwrap(),
json!("Exists")
);
assert_eq!(
to_value(TolerationEffect::NoSchedule).unwrap(),
json!("NoSchedule")
);
assert_eq!(
to_value(TolerationEffect::PreferNoSchedule).unwrap(),
json!("PreferNoSchedule")
);
assert_eq!(
to_value(TolerationEffect::NoExecute).unwrap(),
json!("NoExecute")
);
}
#[test]
fn serializes_camel_case_and_omits_none_fields() {
let value = to_value(K8sToleration {
key: "dedicated".to_string(),
operator: TolerationOperator::Equal,
value: Some("worker".to_string()),
effect: TolerationEffect::NoSchedule,
toleration_seconds: None,
})
.unwrap();
assert_eq!(
value,
json!({
"key": "dedicated",
"operator": "Equal",
"value": "worker",
"effect": "NoSchedule"
})
);
assert!(value.get("tolerationSeconds").is_none());
}
#[test]
fn serializes_exists_operator_omits_value() {
let value = to_value(K8sToleration {
key: "gpu".to_string(),
operator: TolerationOperator::Exists,
value: None,
effect: TolerationEffect::NoExecute,
toleration_seconds: Some(30),
})
.unwrap();
assert!(value.get("value").is_none());
assert_eq!(value["tolerationSeconds"], 30);
}
#[test]
fn build_pod_spec_with_tolerations() {
let tolerations = vec![K8sToleration {
key: "dedicated".to_string(),
operator: TolerationOperator::Equal,
value: Some("worker".to_string()),
effect: TolerationEffect::NoSchedule,
toleration_seconds: None,
}];
let resources = K8sResources::default();
let pull_policy = ImagePullPolicy::default();
let empty = BTreeMap::new();
let pod = build_pod_spec(&config_with(
&tolerations,
&resources,
&pull_policy,
&empty,
&empty,
))
.unwrap();
let tolerations = pod.spec.unwrap().tolerations.expect("tolerations present");
assert_eq!(tolerations.len(), 1);
let t = &tolerations[0];
assert_eq!(t.key.as_deref(), Some("dedicated"));
assert_eq!(t.operator.as_deref(), Some("Equal"));
assert_eq!(t.value.as_deref(), Some("worker"));
assert_eq!(t.effect.as_deref(), Some("NoSchedule"));
}
#[test]
fn build_pod_spec_without_tolerations_omits_key() {
let resources = K8sResources::default();
let pull_policy = ImagePullPolicy::default();
let empty = BTreeMap::new();
let pod =
build_pod_spec(&config_with(&[], &resources, &pull_policy, &empty, &empty)).unwrap();
assert!(
pod.spec.unwrap().tolerations.is_none(),
"no tolerations key when the slice is empty"
);
}
}