1use std::sync::Arc;
13
14
15use thiserror::Error;
16use tokio::sync::Mutex;
17
18use crate::crd::{SzRustApp, SzRustAppStatus};
19
20fn app_name(app: &SzRustApp) -> String {
21 app.metadata.name.clone().unwrap_or_default()
22}
23
24fn app_namespace(app: &SzRustApp) -> String {
25 app.metadata
26 .namespace
27 .clone()
28 .unwrap_or_else(|| "default".to_string())
29}
30
31#[derive(Debug, Error)]
37pub enum ControllerError {
38 #[error("K8s API 错误: {0}")]
40 K8sApi(String),
41 #[error("资源未找到: {0}")]
43 NotFound(String),
44 #[error("配置错误: {0}")]
46 Config(String),
47 #[error("序列化错误: {0}")]
49 Serialize(String),
50}
51
52#[derive(Debug, Clone, PartialEq)]
58pub enum ReconcileResult {
59 Created,
61 Updated,
63 Deleted,
65 Noop,
67 Retry,
69}
70
71pub struct Reconciler {
91 client: Option<kube::Client>,
93 state: Arc<Mutex<ReconcilerState>>,
95}
96
97#[derive(Debug, Default)]
99struct ReconcilerState {
100 processed_count: u64,
102 created_count: u64,
104 updated_count: u64,
106 deleted_count: u64,
108}
109
110impl Reconciler {
111 pub fn new(client: kube::Client) -> Self {
113 Self {
114 client: Some(client),
115 state: Arc::new(Mutex::new(ReconcilerState::default())),
116 }
117 }
118
119 pub fn new_mock() -> Self {
121 Self {
122 client: None,
123 state: Arc::new(Mutex::new(ReconcilerState::default())),
124 }
125 }
126
127 pub async fn reconcile(&self, app: &Arc<SzRustApp>) -> Result<ReconcileResult, ControllerError> {
135 let mut state = self.state.lock().await;
136 state.processed_count += 1;
137
138 if self.client.is_none() {
139 return Ok(ReconcileResult::Noop);
140 }
141
142 let client = self.client.as_ref().unwrap();
143 let name = app_name(app);
144 let ns = app_namespace(app);
145
146 let deployments: kube::Api<k8s_openapi::api::apps::v1::Deployment> =
147 kube::Api::namespaced(client.clone(), &ns);
148
149 match deployments.get_opt(&name).await {
150 Ok(Some(existing)) => {
151 let desired_replicas = app.spec.replicas;
152 let current_replicas = existing
153 .spec
154 .as_ref()
155 .and_then(|s| s.replicas)
156 .unwrap_or(0);
157
158 if current_replicas != desired_replicas {
159 state.updated_count += 1;
160 Ok(ReconcileResult::Updated)
161 } else {
162 Ok(ReconcileResult::Noop)
163 }
164 }
165 Ok(None) => {
166 state.created_count += 1;
167 Ok(ReconcileResult::Created)
168 }
169 Err(e) => Err(ControllerError::K8sApi(format!("获取 Deployment 失败: {e}"))),
170 }
171 }
172
173 pub async fn update_status(
175 &self,
176 app: &Arc<SzRustApp>,
177 status: SzRustAppStatus,
178 ) -> Result<(), ControllerError> {
179 if self.client.is_none() {
180 return Ok(());
181 }
182
183 let client = self.client.as_ref().unwrap();
184 let name = app_name(app);
185 let ns = app_namespace(app);
186
187 let apps: kube::Api<SzRustApp> = kube::Api::namespaced(client.clone(), &ns);
188
189 let mut new_app = (**app).clone();
190 new_app.status = Some(status);
191
192 apps.patch_status(
193 &name,
194 &kube::api::PatchParams::default(),
195 &kube::api::Patch::Merge(&new_app),
196 )
197 .await
198 .map_err(|e| ControllerError::K8sApi(format!("更新 status 失败: {e}")))?;
199
200 Ok(())
201 }
202
203 pub async fn stats(&self) -> ReconcilerStats {
205 let state = self.state.lock().await;
206 ReconcilerStats {
207 processed_count: state.processed_count,
208 created_count: state.created_count,
209 updated_count: state.updated_count,
210 deleted_count: state.deleted_count,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Default)]
217pub struct ReconcilerStats {
218 pub processed_count: u64,
220 pub created_count: u64,
222 pub updated_count: u64,
224 pub deleted_count: u64,
226}
227
228pub async fn run_controller(client: kube::Client) -> Result<(), ControllerError> {
244 use futures::StreamExt;
245 use kube::runtime::watcher;
246
247 let apps: kube::Api<SzRustApp> = kube::Api::all(client.clone());
248 let reconciler = Arc::new(Reconciler::new(client.clone()));
249
250 let mut stream = watcher::watcher(apps, watcher::Config::default()).boxed();
251 while let Some(event) = stream.next().await {
252 match event {
253 Ok(watcher::Event::Apply(app)) => {
254 let app = Arc::new(app);
255 let _ = reconciler.reconcile(&app).await;
256 }
257 Ok(watcher::Event::Delete(_app)) => {
258 }
260 Ok(_) => {
261 }
263 Err(e) => {
264 tracing::warn!("watcher 错误: {e}");
265 }
266 }
267 }
268
269 Ok(())
270}
271
272#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::crd::SzRustAppSpec;
280 use kube::core::ObjectMeta;
281
282 fn make_app(name: &str, image: &str, replicas: i32) -> SzRustApp {
283 SzRustApp {
284 metadata: ObjectMeta {
285 name: Some(name.to_string()),
286 namespace: Some("default".to_string()),
287 ..Default::default()
288 },
289 spec: SzRustAppSpec::new(image).with_replicas(replicas),
290 status: None,
291 }
292 }
293
294 #[tokio::test]
295 async fn test_reconciler_mock_returns_noop() {
296 let reconciler = Reconciler::new_mock();
297 let app = Arc::new(make_app("test-app", "test:latest", 1));
298 let result = reconciler.reconcile(&app).await.unwrap();
299 assert_eq!(result, ReconcileResult::Noop);
300 }
301
302 #[tokio::test]
303 async fn test_reconciler_stats_initial() {
304 let reconciler = Reconciler::new_mock();
305 let stats = reconciler.stats().await;
306 assert_eq!(stats.processed_count, 0);
307 assert_eq!(stats.created_count, 0);
308 assert_eq!(stats.updated_count, 0);
309 assert_eq!(stats.deleted_count, 0);
310 }
311
312 #[tokio::test]
313 async fn test_reconciler_stats_after_reconcile() {
314 let reconciler = Reconciler::new_mock();
315 let app = Arc::new(make_app("test-app", "test:latest", 1));
316 let _ = reconciler.reconcile(&app).await;
317 let stats = reconciler.stats().await;
318 assert_eq!(stats.processed_count, 1);
319 }
320
321 #[tokio::test]
322 async fn test_reconciler_multiple_reconciles() {
323 let reconciler = Reconciler::new_mock();
324 let app1 = Arc::new(make_app("app1", "test:latest", 1));
325 let app2 = Arc::new(make_app("app2", "test:latest", 2));
326 let app3 = Arc::new(make_app("app3", "test:latest", 3));
327
328 let _ = reconciler.reconcile(&app1).await;
329 let _ = reconciler.reconcile(&app2).await;
330 let _ = reconciler.reconcile(&app3).await;
331
332 let stats = reconciler.stats().await;
333 assert_eq!(stats.processed_count, 3);
334 }
335
336 #[tokio::test]
337 async fn test_reconciler_update_status_mock() {
338 let reconciler = Reconciler::new_mock();
339 let app = Arc::new(make_app("test-app", "test:latest", 1));
340 let status = SzRustAppStatus {
341 ready: true,
342 replicas: 1,
343 conditions: vec![],
344 };
345 let result = reconciler.update_status(&app, status).await;
346 assert!(result.is_ok());
347 }
348
349 #[test]
350 fn test_reconcile_result_variants() {
351 let results = vec![
352 ReconcileResult::Created,
353 ReconcileResult::Updated,
354 ReconcileResult::Deleted,
355 ReconcileResult::Noop,
356 ReconcileResult::Retry,
357 ];
358 assert_eq!(results.len(), 5);
359 assert_ne!(ReconcileResult::Created, ReconcileResult::Updated);
360 assert_ne!(ReconcileResult::Noop, ReconcileResult::Retry);
361 }
362
363 #[test]
364 fn test_controller_error_display() {
365 let err = ControllerError::K8sApi("connection refused".to_string());
366 assert!(err.to_string().contains("connection refused"));
367
368 let err = ControllerError::NotFound("SzRustApp/my-app".to_string());
369 assert!(err.to_string().contains("SzRustApp/my-app"));
370
371 let err = ControllerError::Config("invalid replicas".to_string());
372 assert!(err.to_string().contains("invalid replicas"));
373 }
374
375 #[test]
376 fn test_reconciler_stats_default() {
377 let stats = ReconcilerStats::default();
378 assert_eq!(stats.processed_count, 0);
379 assert_eq!(stats.created_count, 0);
380 assert_eq!(stats.updated_count, 0);
381 assert_eq!(stats.deleted_count, 0);
382 }
383}