1use std::collections::HashMap;
35
36use chrono::{DateTime, Utc};
37use serde::{Deserialize, Serialize};
38
39pub const API_GROUP: &str = "box.a3s.dev";
41pub const API_VERSION: &str = "v1alpha1";
43pub const KIND: &str = "BoxAutoscaler";
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct BoxAutoscalerSpec {
49 pub target_ref: TargetRef,
51 #[serde(default = "default_min_replicas")]
53 pub min_replicas: u32,
54 pub max_replicas: u32,
56 #[serde(default)]
58 pub metrics: Vec<MetricSpec>,
59 #[serde(default)]
61 pub behavior: ScalingBehavior,
62 #[serde(default = "default_cooldown")]
64 pub cooldown_secs: u64,
65}
66
67fn default_min_replicas() -> u32 {
68 1
69}
70
71fn default_cooldown() -> u64 {
72 60
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TargetRef {
78 pub kind: String,
80 pub name: String,
82 #[serde(default = "default_namespace")]
84 pub namespace: String,
85}
86
87fn default_namespace() -> String {
88 "default".to_string()
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct MetricSpec {
94 #[serde(rename = "type")]
96 pub metric_type: MetricType,
97 pub target: u32,
99 #[serde(default = "default_tolerance")]
101 pub tolerance_percent: u32,
102}
103
104fn default_tolerance() -> u32 {
105 10
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "lowercase")]
111pub enum MetricType {
112 Cpu,
114 Memory,
116 Inflight,
118 Rps,
120 Custom,
122}
123
124impl std::fmt::Display for MetricType {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 Self::Cpu => write!(f, "cpu"),
128 Self::Memory => write!(f, "memory"),
129 Self::Inflight => write!(f, "inflight"),
130 Self::Rps => write!(f, "rps"),
131 Self::Custom => write!(f, "custom"),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ScalingBehavior {
139 #[serde(default)]
141 pub scale_up: ScalingRules,
142 #[serde(default)]
144 pub scale_down: ScalingRules,
145}
146
147impl Default for ScalingBehavior {
148 fn default() -> Self {
149 Self {
150 scale_up: ScalingRules {
151 stabilization_window_secs: 60,
152 max_scale_per_minute: 3,
153 },
154 scale_down: ScalingRules {
155 stabilization_window_secs: 300,
156 max_scale_per_minute: 1,
157 },
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ScalingRules {
165 #[serde(default = "default_stabilization")]
167 pub stabilization_window_secs: u64,
168 #[serde(default = "default_max_scale")]
170 pub max_scale_per_minute: u32,
171}
172
173fn default_stabilization() -> u64 {
174 60
175}
176
177fn default_max_scale() -> u32 {
178 2
179}
180
181impl Default for ScalingRules {
182 fn default() -> Self {
183 Self {
184 stabilization_window_secs: 60,
185 max_scale_per_minute: 2,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192pub struct BoxAutoscalerStatus {
193 pub current_replicas: u32,
195 pub desired_replicas: u32,
197 #[serde(default)]
199 pub last_scale_time: Option<DateTime<Utc>>,
200 #[serde(default)]
202 pub current_metrics: Vec<MetricValue>,
203 #[serde(default)]
205 pub conditions: Vec<AutoscalerCondition>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct MetricValue {
211 #[serde(rename = "type")]
213 pub metric_type: MetricType,
214 pub current: u32,
216 pub target: u32,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct AutoscalerCondition {
223 #[serde(rename = "type")]
225 pub condition_type: String,
226 pub status: String,
228 #[serde(default)]
230 pub last_transition_time: Option<DateTime<Utc>>,
231 #[serde(default)]
233 pub reason: String,
234 #[serde(default)]
236 pub message: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct BoxAutoscaler {
242 pub api_version: String,
244 pub kind: String,
246 pub name: String,
248 #[serde(default = "default_namespace")]
250 pub namespace: String,
251 #[serde(default)]
253 pub labels: HashMap<String, String>,
254 pub spec: BoxAutoscalerSpec,
256 #[serde(default)]
258 pub status: BoxAutoscalerStatus,
259}
260
261impl BoxAutoscaler {
262 pub fn new(name: &str, spec: BoxAutoscalerSpec) -> Self {
264 Self {
265 api_version: format!("{}/{}", API_GROUP, API_VERSION),
266 kind: KIND.to_string(),
267 name: name.to_string(),
268 namespace: "default".to_string(),
269 labels: HashMap::new(),
270 spec,
271 status: BoxAutoscalerStatus::default(),
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn sample_spec() -> BoxAutoscalerSpec {
281 BoxAutoscalerSpec {
282 target_ref: TargetRef {
283 kind: "BoxService".to_string(),
284 name: "my-service".to_string(),
285 namespace: "default".to_string(),
286 },
287 min_replicas: 1,
288 max_replicas: 10,
289 metrics: vec![MetricSpec {
290 metric_type: MetricType::Cpu,
291 target: 70,
292 tolerance_percent: 10,
293 }],
294 behavior: ScalingBehavior::default(),
295 cooldown_secs: 60,
296 }
297 }
298
299 #[test]
300 fn test_box_autoscaler_new() {
301 let ba = BoxAutoscaler::new("test-scaler", sample_spec());
302 assert_eq!(ba.name, "test-scaler");
303 assert_eq!(ba.api_version, "box.a3s.dev/v1alpha1");
304 assert_eq!(ba.kind, "BoxAutoscaler");
305 assert_eq!(ba.namespace, "default");
306 assert_eq!(ba.spec.min_replicas, 1);
307 assert_eq!(ba.spec.max_replicas, 10);
308 }
309
310 #[test]
311 fn test_spec_serde_roundtrip() {
312 let spec = sample_spec();
313 let json = serde_json::to_string(&spec).unwrap();
314 let parsed: BoxAutoscalerSpec = serde_json::from_str(&json).unwrap();
315 assert_eq!(parsed.min_replicas, 1);
316 assert_eq!(parsed.max_replicas, 10);
317 assert_eq!(parsed.metrics.len(), 1);
318 assert_eq!(parsed.metrics[0].metric_type, MetricType::Cpu);
319 assert_eq!(parsed.metrics[0].target, 70);
320 }
321
322 #[test]
323 fn test_spec_deserialize_minimal() {
324 let json = r#"{
325 "target_ref": {"kind": "BoxService", "name": "svc"},
326 "max_replicas": 5
327 }"#;
328 let spec: BoxAutoscalerSpec = serde_json::from_str(json).unwrap();
329 assert_eq!(spec.min_replicas, 1); assert_eq!(spec.max_replicas, 5);
331 assert_eq!(spec.cooldown_secs, 60); assert!(spec.metrics.is_empty());
333 assert_eq!(spec.target_ref.namespace, "default"); }
335
336 #[test]
337 fn test_metric_type_display() {
338 assert_eq!(MetricType::Cpu.to_string(), "cpu");
339 assert_eq!(MetricType::Memory.to_string(), "memory");
340 assert_eq!(MetricType::Inflight.to_string(), "inflight");
341 assert_eq!(MetricType::Rps.to_string(), "rps");
342 assert_eq!(MetricType::Custom.to_string(), "custom");
343 }
344
345 #[test]
346 fn test_metric_type_serde() {
347 let json = r#""cpu""#;
348 let mt: MetricType = serde_json::from_str(json).unwrap();
349 assert_eq!(mt, MetricType::Cpu);
350
351 let json = serde_json::to_string(&MetricType::Inflight).unwrap();
352 assert_eq!(json, r#""inflight""#);
353 }
354
355 #[test]
356 fn test_scaling_behavior_default() {
357 let behavior = ScalingBehavior::default();
358 assert_eq!(behavior.scale_up.stabilization_window_secs, 60);
359 assert_eq!(behavior.scale_up.max_scale_per_minute, 3);
360 assert_eq!(behavior.scale_down.stabilization_window_secs, 300);
361 assert_eq!(behavior.scale_down.max_scale_per_minute, 1);
362 }
363
364 #[test]
365 fn test_status_default() {
366 let status = BoxAutoscalerStatus::default();
367 assert_eq!(status.current_replicas, 0);
368 assert_eq!(status.desired_replicas, 0);
369 assert!(status.last_scale_time.is_none());
370 assert!(status.current_metrics.is_empty());
371 assert!(status.conditions.is_empty());
372 }
373
374 #[test]
375 fn test_status_serde_roundtrip() {
376 let status = BoxAutoscalerStatus {
377 current_replicas: 3,
378 desired_replicas: 5,
379 last_scale_time: Some(Utc::now()),
380 current_metrics: vec![MetricValue {
381 metric_type: MetricType::Cpu,
382 current: 85,
383 target: 70,
384 }],
385 conditions: vec![AutoscalerCondition {
386 condition_type: "ScalingActive".to_string(),
387 status: "True".to_string(),
388 last_transition_time: Some(Utc::now()),
389 reason: "HighCPU".to_string(),
390 message: "CPU above target".to_string(),
391 }],
392 };
393 let json = serde_json::to_string(&status).unwrap();
394 let parsed: BoxAutoscalerStatus = serde_json::from_str(&json).unwrap();
395 assert_eq!(parsed.current_replicas, 3);
396 assert_eq!(parsed.desired_replicas, 5);
397 assert_eq!(parsed.current_metrics.len(), 1);
398 assert_eq!(parsed.conditions.len(), 1);
399 assert_eq!(parsed.conditions[0].reason, "HighCPU");
400 }
401
402 #[test]
403 fn test_full_resource_serde() {
404 let ba = BoxAutoscaler::new("my-scaler", sample_spec());
405 let json = serde_json::to_string_pretty(&ba).unwrap();
406 let parsed: BoxAutoscaler = serde_json::from_str(&json).unwrap();
407 assert_eq!(parsed.name, "my-scaler");
408 assert_eq!(parsed.spec.max_replicas, 10);
409 assert_eq!(parsed.status.current_replicas, 0);
410 }
411
412 #[test]
413 fn test_target_ref_serde() {
414 let tr = TargetRef {
415 kind: "BoxService".to_string(),
416 name: "web".to_string(),
417 namespace: "production".to_string(),
418 };
419 let json = serde_json::to_string(&tr).unwrap();
420 let parsed: TargetRef = serde_json::from_str(&json).unwrap();
421 assert_eq!(parsed.kind, "BoxService");
422 assert_eq!(parsed.name, "web");
423 assert_eq!(parsed.namespace, "production");
424 }
425
426 #[test]
427 fn test_metric_spec_with_tolerance() {
428 let ms = MetricSpec {
429 metric_type: MetricType::Rps,
430 target: 1000,
431 tolerance_percent: 15,
432 };
433 let json = serde_json::to_string(&ms).unwrap();
434 let parsed: MetricSpec = serde_json::from_str(&json).unwrap();
435 assert_eq!(parsed.metric_type, MetricType::Rps);
436 assert_eq!(parsed.target, 1000);
437 assert_eq!(parsed.tolerance_percent, 15);
438 }
439
440 #[test]
441 fn test_metric_spec_default_tolerance() {
442 let json = r#"{"type": "cpu", "target": 80}"#;
443 let ms: MetricSpec = serde_json::from_str(json).unwrap();
444 assert_eq!(ms.tolerance_percent, 10); }
446
447 #[test]
448 fn test_constants() {
449 assert_eq!(API_GROUP, "box.a3s.dev");
450 assert_eq!(API_VERSION, "v1alpha1");
451 assert_eq!(KIND, "BoxAutoscaler");
452 }
453}