1use kube::core::crd::CustomResourceExt;
23use kube::CustomResource;
24use schemars::JsonSchema;
25use serde::{Deserialize, Serialize};
26
27#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema)]
35#[kube(
36 group = "sz-rust.dev",
37 version = "v1",
38 kind = "SzRustApp",
39 namespaced,
40 status = "SzRustAppStatus",
41 shortname = "szapp"
42)]
43pub struct SzRustAppSpec {
44 pub image: String,
46
47 #[serde(default = "default_replicas")]
49 pub replicas: i32,
50
51 #[serde(default = "default_port")]
53 pub port: u16,
54
55 #[serde(default)]
57 pub env: std::collections::BTreeMap<String, String>,
58
59 #[serde(default)]
61 pub resources: Option<ResourceRequirements>,
62
63 #[serde(default)]
65 pub database: Option<DatabaseConfig>,
66
67 #[serde(default)]
69 pub redis: Option<RedisConfig>,
70}
71
72#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, Default)]
74pub struct SzRustAppStatus {
75 pub ready: bool,
77
78 pub replicas: i32,
80
81 #[serde(default)]
83 pub conditions: Vec<SzRustAppCondition>,
84}
85
86#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, Default)]
88pub struct ResourceRequirements {
89 #[serde(default)]
91 pub cpu_request: Option<String>,
92 #[serde(default)]
94 pub cpu_limit: Option<String>,
95 #[serde(default)]
97 pub memory_request: Option<String>,
98 #[serde(default)]
100 pub memory_limit: Option<String>,
101}
102
103#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
105pub struct DatabaseConfig {
106 pub url: String,
108 #[serde(default = "default_max_connections")]
110 pub max_connections: u32,
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
115pub struct RedisConfig {
116 pub url: String,
118 #[serde(default)]
120 pub cluster: bool,
121}
122
123#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
125pub struct SzRustAppCondition {
126 pub type_: String,
128 pub status: String,
130 #[serde(default)]
132 pub last_transition_time: Option<String>,
133 #[serde(default)]
135 pub reason: Option<String>,
136 #[serde(default)]
138 pub message: Option<String>,
139}
140
141fn default_replicas() -> i32 {
142 1
143}
144
145fn default_port() -> u16 {
146 8080
147}
148
149fn default_max_connections() -> u32 {
150 10
151}
152
153impl SzRustAppSpec {
154 pub fn new(image: impl Into<String>) -> Self {
156 Self {
157 image: image.into(),
158 replicas: default_replicas(),
159 port: default_port(),
160 env: std::collections::BTreeMap::new(),
161 resources: None,
162 database: None,
163 redis: None,
164 }
165 }
166
167 pub fn with_replicas(mut self, replicas: i32) -> Self {
169 self.replicas = replicas;
170 self
171 }
172
173 pub fn with_port(mut self, port: u16) -> Self {
175 self.port = port;
176 self
177 }
178
179 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
181 self.env.insert(key.into(), value.into());
182 self
183 }
184}
185
186pub fn generate_crd_yaml() -> String {
194 let crd = SzRustApp::crd();
195 serde_yaml::to_string(&crd).unwrap_or_else(|e| format!("# CRD 序列化失败: {e}"))
196}
197
198#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_spec_new() {
208 let spec = SzRustAppSpec::new("ghcr.io/ljclz/sz-rust:latest");
209 assert_eq!(spec.image, "ghcr.io/ljclz/sz-rust:latest");
210 assert_eq!(spec.replicas, 1);
211 assert_eq!(spec.port, 8080);
212 assert!(spec.env.is_empty());
213 assert!(spec.resources.is_none());
214 assert!(spec.database.is_none());
215 assert!(spec.redis.is_none());
216 }
217
218 #[test]
219 fn test_spec_builder() {
220 let spec = SzRustAppSpec::new("my-image:v1")
221 .with_replicas(3)
222 .with_port(9090)
223 .with_env("DATABASE_URL", "postgres://localhost/mydb")
224 .with_env("REDIS_URL", "redis://localhost:6379");
225
226 assert_eq!(spec.replicas, 3);
227 assert_eq!(spec.port, 9090);
228 assert_eq!(
229 spec.env.get("DATABASE_URL").unwrap(),
230 "postgres://localhost/mydb"
231 );
232 assert_eq!(spec.env.get("REDIS_URL").unwrap(), "redis://localhost:6379");
233 }
234
235 #[test]
236 fn test_spec_serialization() {
237 let spec = SzRustAppSpec::new("test:latest").with_replicas(2);
238 let json = serde_json::to_string(&spec).unwrap();
239 let decoded: SzRustAppSpec = serde_json::from_str(&json).unwrap();
240 assert_eq!(decoded.image, "test:latest");
241 assert_eq!(decoded.replicas, 2);
242 }
243
244 #[test]
245 fn test_spec_with_database() {
246 let spec = SzRustAppSpec::new("test:latest");
247 let mut spec = spec;
248 spec.database = Some(DatabaseConfig {
249 url: "postgres://localhost/db".to_string(),
250 max_connections: 20,
251 });
252 assert!(spec.database.is_some());
253 let db = spec.database.unwrap();
254 assert_eq!(db.url, "postgres://localhost/db");
255 assert_eq!(db.max_connections, 20);
256 }
257
258 #[test]
259 fn test_spec_with_redis() {
260 let spec = SzRustAppSpec::new("test:latest");
261 let mut spec = spec;
262 spec.redis = Some(RedisConfig {
263 url: "redis://localhost:6379".to_string(),
264 cluster: true,
265 });
266 assert!(spec.redis.is_some());
267 let redis = spec.redis.unwrap();
268 assert_eq!(redis.url, "redis://localhost:6379");
269 assert!(redis.cluster);
270 }
271
272 #[test]
273 fn test_status_default() {
274 let status = SzRustAppStatus::default();
275 assert!(!status.ready);
276 assert_eq!(status.replicas, 0);
277 assert!(status.conditions.is_empty());
278 }
279
280 #[test]
281 fn test_condition_serialization() {
282 let condition = SzRustAppCondition {
283 type_: "Ready".to_string(),
284 status: "True".to_string(),
285 last_transition_time: Some("2026-08-06T00:00:00Z".to_string()),
286 reason: Some("AllReplicasReady".to_string()),
287 message: None,
288 };
289 let json = serde_json::to_string(&condition).unwrap();
290 let decoded: SzRustAppCondition = serde_json::from_str(&json).unwrap();
291 assert_eq!(decoded.type_, "Ready");
292 assert_eq!(decoded.status, "True");
293 assert_eq!(decoded.reason.unwrap(), "AllReplicasReady");
294 }
295
296 #[test]
297 fn test_resource_requirements_default() {
298 let req = ResourceRequirements::default();
299 assert!(req.cpu_request.is_none());
300 assert!(req.cpu_limit.is_none());
301 assert!(req.memory_request.is_none());
302 assert!(req.memory_limit.is_none());
303 }
304
305 #[test]
306 fn test_crd_yaml_generation() {
307 let yaml = generate_crd_yaml();
308 assert!(yaml.contains("sz-rust.dev"));
309 assert!(yaml.contains("SzRustApp"));
310 assert!(yaml.contains("v1"));
311 }
312
313 #[test]
314 fn test_crd_has_correct_group() {
315 let crd = SzRustApp::crd();
316 assert_eq!(crd.spec.group, "sz-rust.dev");
317 }
318
319 #[test]
320 fn test_crd_has_correct_kind() {
321 let crd = SzRustApp::crd();
322 assert_eq!(crd.spec.names.kind, "SzRustApp");
323 }
324
325 #[test]
326 fn test_crd_has_shortname() {
327 let crd = SzRustApp::crd();
328 let short_names = crd.spec.names.short_names.as_ref().unwrap();
329 assert!(short_names.contains(&"szapp".to_string()));
330 }
331}