use kube::core::crd::CustomResourceExt;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
#[kube(
group = "sz-rust.dev",
version = "v1",
kind = "SzRustApp",
namespaced,
status = "SzRustAppStatus",
shortname = "szapp"
)]
pub struct SzRustAppSpec {
pub image: String,
#[serde(default = "default_replicas")]
pub replicas: i32,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub env: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub resources: Option<ResourceRequirements>,
#[serde(default)]
pub database: Option<DatabaseConfig>,
#[serde(default)]
pub redis: Option<RedisConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, Default)]
pub struct SzRustAppStatus {
pub ready: bool,
pub replicas: i32,
#[serde(default)]
pub conditions: Vec<SzRustAppCondition>,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, Default)]
pub struct ResourceRequirements {
#[serde(default)]
pub cpu_request: Option<String>,
#[serde(default)]
pub cpu_limit: Option<String>,
#[serde(default)]
pub memory_request: Option<String>,
#[serde(default)]
pub memory_limit: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct DatabaseConfig {
pub url: String,
#[serde(default = "default_max_connections")]
pub max_connections: u32,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct RedisConfig {
pub url: String,
#[serde(default)]
pub cluster: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
pub struct SzRustAppCondition {
pub type_: String,
pub status: String,
#[serde(default)]
pub last_transition_time: Option<String>,
#[serde(default)]
pub reason: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
fn default_replicas() -> i32 {
1
}
fn default_port() -> u16 {
8080
}
fn default_max_connections() -> u32 {
10
}
impl SzRustAppSpec {
pub fn new(image: impl Into<String>) -> Self {
Self {
image: image.into(),
replicas: default_replicas(),
port: default_port(),
env: std::collections::BTreeMap::new(),
resources: None,
database: None,
redis: None,
}
}
pub fn with_replicas(mut self, replicas: i32) -> Self {
self.replicas = replicas;
self
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
}
pub fn generate_crd_yaml() -> String {
let crd = SzRustApp::crd();
serde_yaml::to_string(&crd).unwrap_or_else(|e| format!("# CRD 序列化失败: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spec_new() {
let spec = SzRustAppSpec::new("ghcr.io/ljclz/sz-rust:latest");
assert_eq!(spec.image, "ghcr.io/ljclz/sz-rust:latest");
assert_eq!(spec.replicas, 1);
assert_eq!(spec.port, 8080);
assert!(spec.env.is_empty());
assert!(spec.resources.is_none());
assert!(spec.database.is_none());
assert!(spec.redis.is_none());
}
#[test]
fn test_spec_builder() {
let spec = SzRustAppSpec::new("my-image:v1")
.with_replicas(3)
.with_port(9090)
.with_env("DATABASE_URL", "postgres://localhost/mydb")
.with_env("REDIS_URL", "redis://localhost:6379");
assert_eq!(spec.replicas, 3);
assert_eq!(spec.port, 9090);
assert_eq!(
spec.env.get("DATABASE_URL").unwrap(),
"postgres://localhost/mydb"
);
assert_eq!(spec.env.get("REDIS_URL").unwrap(), "redis://localhost:6379");
}
#[test]
fn test_spec_serialization() {
let spec = SzRustAppSpec::new("test:latest").with_replicas(2);
let json = serde_json::to_string(&spec).unwrap();
let decoded: SzRustAppSpec = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.image, "test:latest");
assert_eq!(decoded.replicas, 2);
}
#[test]
fn test_spec_with_database() {
let spec = SzRustAppSpec::new("test:latest");
let mut spec = spec;
spec.database = Some(DatabaseConfig {
url: "postgres://localhost/db".to_string(),
max_connections: 20,
});
assert!(spec.database.is_some());
let db = spec.database.unwrap();
assert_eq!(db.url, "postgres://localhost/db");
assert_eq!(db.max_connections, 20);
}
#[test]
fn test_spec_with_redis() {
let spec = SzRustAppSpec::new("test:latest");
let mut spec = spec;
spec.redis = Some(RedisConfig {
url: "redis://localhost:6379".to_string(),
cluster: true,
});
assert!(spec.redis.is_some());
let redis = spec.redis.unwrap();
assert_eq!(redis.url, "redis://localhost:6379");
assert!(redis.cluster);
}
#[test]
fn test_status_default() {
let status = SzRustAppStatus::default();
assert!(!status.ready);
assert_eq!(status.replicas, 0);
assert!(status.conditions.is_empty());
}
#[test]
fn test_condition_serialization() {
let condition = SzRustAppCondition {
type_: "Ready".to_string(),
status: "True".to_string(),
last_transition_time: Some("2026-08-06T00:00:00Z".to_string()),
reason: Some("AllReplicasReady".to_string()),
message: None,
};
let json = serde_json::to_string(&condition).unwrap();
let decoded: SzRustAppCondition = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.type_, "Ready");
assert_eq!(decoded.status, "True");
assert_eq!(decoded.reason.unwrap(), "AllReplicasReady");
}
#[test]
fn test_resource_requirements_default() {
let req = ResourceRequirements::default();
assert!(req.cpu_request.is_none());
assert!(req.cpu_limit.is_none());
assert!(req.memory_request.is_none());
assert!(req.memory_limit.is_none());
}
#[test]
fn test_crd_yaml_generation() {
let yaml = generate_crd_yaml();
assert!(yaml.contains("sz-rust.dev"));
assert!(yaml.contains("SzRustApp"));
assert!(yaml.contains("v1"));
}
#[test]
fn test_crd_has_correct_group() {
let crd = SzRustApp::crd();
assert_eq!(crd.spec.group, "sz-rust.dev");
}
#[test]
fn test_crd_has_correct_kind() {
let crd = SzRustApp::crd();
assert_eq!(crd.spec.names.kind, "SzRustApp");
}
#[test]
fn test_crd_has_shortname() {
let crd = SzRustApp::crd();
let short_names = crd.spec.names.short_names.as_ref().unwrap();
assert!(short_names.contains(&"szapp".to_string()));
}
}