1use std::collections::HashMap;
10use std::io::{self, Write};
11use std::time::{Duration, Instant};
12
13use console::{Term, style};
14
15pub struct ProgressReporter {
17 term: Term,
19 resources: HashMap<String, ResourceState>,
21 current_wave: Option<i32>,
23 start_time: Instant,
25 #[allow(dead_code)]
27 colors_enabled: bool,
28 verbose: bool,
30}
31
32#[derive(Debug, Clone)]
34pub struct ResourceState {
35 pub kind: String,
36 pub name: String,
37 pub status: ResourceStatus,
38 pub ready: Option<i32>,
39 pub desired: Option<i32>,
40 pub message: Option<String>,
41 pub last_update: Instant,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ResourceStatus {
47 Pending,
48 Applying,
49 Applied,
50 WaitingForReady,
51 Ready,
52 Failed,
53 Skipped,
54}
55
56impl ResourceStatus {
57 fn symbol(&self) -> &'static str {
58 match self {
59 ResourceStatus::Pending => "○",
60 ResourceStatus::Applying => "◐",
61 ResourceStatus::Applied => "◑",
62 ResourceStatus::WaitingForReady => "◕",
63 ResourceStatus::Ready => "●",
64 ResourceStatus::Failed => "✗",
65 ResourceStatus::Skipped => "⊘",
66 }
67 }
68
69 fn styled_symbol(&self) -> console::StyledObject<&'static str> {
70 match self {
71 ResourceStatus::Pending => style(self.symbol()).dim(),
72 ResourceStatus::Applying => style(self.symbol()).cyan(),
73 ResourceStatus::Applied => style(self.symbol()).blue(),
74 ResourceStatus::WaitingForReady => style(self.symbol()).yellow(),
75 ResourceStatus::Ready => style(self.symbol()).green(),
76 ResourceStatus::Failed => style(self.symbol()).red(),
77 ResourceStatus::Skipped => style(self.symbol()).dim(),
78 }
79 }
80}
81
82impl ProgressReporter {
83 pub fn new() -> Self {
85 Self {
86 term: Term::stderr(),
87 resources: HashMap::new(),
88 current_wave: None,
89 start_time: Instant::now(),
90 colors_enabled: console::colors_enabled(),
91 verbose: false,
92 }
93 }
94
95 pub fn verbose(mut self) -> Self {
97 self.verbose = true;
98 self
99 }
100
101 pub fn add_resource(&mut self, kind: &str, name: &str) {
103 let key = format!("{}/{}", kind, name);
104 self.resources.insert(
105 key,
106 ResourceState {
107 kind: kind.to_string(),
108 name: name.to_string(),
109 status: ResourceStatus::Pending,
110 ready: None,
111 desired: None,
112 message: None,
113 last_update: Instant::now(),
114 },
115 );
116 }
117
118 pub fn set_wave(&mut self, wave: i32) {
120 self.current_wave = Some(wave);
121 self.print_wave_header(wave);
122 }
123
124 pub fn update_status(&mut self, key: &str, status: ResourceStatus) {
126 if let Some(resource) = self.resources.get_mut(key) {
127 resource.status = status;
128 resource.last_update = Instant::now();
129 self.print_resource_update(key);
130 }
131 }
132
133 pub fn update_readiness(&mut self, key: &str, ready: i32, desired: i32, message: Option<&str>) {
135 if let Some(resource) = self.resources.get_mut(key) {
136 resource.ready = Some(ready);
137 resource.desired = Some(desired);
138 resource.message = message.map(String::from);
139 resource.last_update = Instant::now();
140
141 if ready == desired {
142 resource.status = ResourceStatus::Ready;
143 }
144
145 self.print_resource_update(key);
146 }
147 }
148
149 pub fn fail(&mut self, key: &str, error: &str) {
151 if let Some(resource) = self.resources.get_mut(key) {
152 resource.status = ResourceStatus::Failed;
153 resource.message = Some(error.to_string());
154 resource.last_update = Instant::now();
155 self.print_resource_update(key);
156 }
157 }
158
159 fn print_wave_header(&self, wave: i32) {
161 let wave_resources: Vec<_> = self
162 .resources
163 .values()
164 .filter(|_| true) .collect();
166
167 let _ = writeln!(
168 io::stderr(),
169 "\n{} Wave {} ({} resources)",
170 style("▶").cyan().bold(),
171 wave,
172 wave_resources.len()
173 );
174 }
175
176 fn print_resource_update(&self, key: &str) {
178 if let Some(resource) = self.resources.get(key) {
179 let styled_symbol = resource.status.styled_symbol();
180
181 let readiness = match (resource.ready, resource.desired) {
182 (Some(r), Some(d)) => format!(" ({}/{})", r, d),
183 _ => String::new(),
184 };
185
186 let message = resource
187 .message
188 .as_ref()
189 .map(|m| format!(" - {}", style(m).dim()))
190 .unwrap_or_default();
191
192 let line = format!(
193 " {} {}/{}{}{}",
194 styled_symbol, resource.kind, resource.name, readiness, message
195 );
196
197 let _ = writeln!(io::stderr(), "{}", line);
198 }
199 }
200
201 pub fn hook_start(&self, phase: &str, name: &str) {
203 let _ = writeln!(
204 io::stderr(),
205 " {} Hook [{}] {}",
206 style("⟳").cyan(),
207 phase,
208 name
209 );
210 }
211
212 pub fn hook_result(&self, name: &str, success: bool, duration: Duration, error: Option<&str>) {
214 let symbol = if success {
215 style("✓").green()
216 } else {
217 style("✗").red()
218 };
219
220 let duration_str = format!("{:.1}s", duration.as_secs_f64());
221
222 let error_msg = error
223 .map(|e| format!(" - {}", style(e).red()))
224 .unwrap_or_default();
225
226 let _ = writeln!(
227 io::stderr(),
228 " {} Hook {} ({}){}",
229 symbol,
230 name,
231 duration_str,
232 error_msg
233 );
234 }
235
236 pub fn print_summary(&self) {
238 let total = self.resources.len();
239 let ready = self
240 .resources
241 .values()
242 .filter(|r| r.status == ResourceStatus::Ready)
243 .count();
244 let failed = self
245 .resources
246 .values()
247 .filter(|r| r.status == ResourceStatus::Failed)
248 .count();
249
250 let elapsed = self.start_time.elapsed();
251
252 let _ = writeln!(io::stderr());
253
254 if failed > 0 {
255 let _ = writeln!(
256 io::stderr(),
257 "{} {}/{} resources ready, {} failed ({:.1}s)",
258 style("✗").red().bold(),
259 ready,
260 total,
261 failed,
262 elapsed.as_secs_f64()
263 );
264 } else if ready == total {
265 let _ = writeln!(
266 io::stderr(),
267 "{} All {} resources ready ({:.1}s)",
268 style("✓").green().bold(),
269 total,
270 elapsed.as_secs_f64()
271 );
272 } else {
273 let _ = writeln!(
274 io::stderr(),
275 "{} {}/{} resources ready ({:.1}s)",
276 style("○").yellow(),
277 ready,
278 total,
279 elapsed.as_secs_f64()
280 );
281 }
282 }
283
284 pub fn message(&self, msg: &str) {
286 let _ = writeln!(io::stderr(), " {}", msg);
287 }
288
289 pub fn info(&self, msg: &str) {
291 let _ = writeln!(io::stderr(), " {} {}", style("ℹ").blue(), msg);
292 }
293
294 pub fn warn(&self, msg: &str) {
296 let _ = writeln!(io::stderr(), " {} {}", style("⚠").yellow(), msg);
297 }
298
299 pub fn error(&self, msg: &str) {
301 let _ = writeln!(io::stderr(), " {} {}", style("✗").red(), msg);
302 }
303
304 pub fn success(&self, msg: &str) {
306 let _ = writeln!(io::stderr(), " {} {}", style("✓").green(), msg);
307 }
308
309 pub fn elapsed(&self) -> Duration {
311 self.start_time.elapsed()
312 }
313
314 pub fn clear(&self) {
316 let _ = self.term.clear_screen();
317 }
318
319 pub fn all_ready(&self) -> bool {
321 self.resources
322 .values()
323 .all(|r| r.status == ResourceStatus::Ready || r.status == ResourceStatus::Skipped)
324 }
325
326 pub fn any_failed(&self) -> bool {
328 self.resources
329 .values()
330 .any(|r| r.status == ResourceStatus::Failed)
331 }
332
333 pub fn failed_resources(&self) -> Vec<&ResourceState> {
335 self.resources
336 .values()
337 .filter(|r| r.status == ResourceStatus::Failed)
338 .collect()
339 }
340}
341
342impl Default for ProgressReporter {
343 fn default() -> Self {
344 Self::new()
345 }
346}
347
348pub struct QuietProgressReporter;
350
351impl QuietProgressReporter {
352 pub fn new() -> Self {
353 Self
354 }
355
356 pub fn error(&self, msg: &str) {
357 eprintln!("Error: {}", msg);
358 }
359
360 pub fn warn(&self, msg: &str) {
361 eprintln!("Warning: {}", msg);
362 }
363}
364
365impl Default for QuietProgressReporter {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371pub struct JsonProgressReporter {
373 resources: HashMap<String, ResourceState>,
374}
375
376impl JsonProgressReporter {
377 pub fn new() -> Self {
378 Self {
379 resources: HashMap::new(),
380 }
381 }
382
383 pub fn add_resource(&mut self, kind: &str, name: &str) {
384 let key = format!("{}/{}", kind, name);
385 self.resources.insert(
386 key,
387 ResourceState {
388 kind: kind.to_string(),
389 name: name.to_string(),
390 status: ResourceStatus::Pending,
391 ready: None,
392 desired: None,
393 message: None,
394 last_update: Instant::now(),
395 },
396 );
397 }
398
399 pub fn update_status(&mut self, key: &str, status: ResourceStatus) {
400 if let Some(resource) = self.resources.get_mut(key) {
401 resource.status = status;
402 self.emit_event(key, "status_changed");
403 }
404 }
405
406 pub fn update_readiness(&mut self, key: &str, ready: i32, desired: i32) {
407 if let Some(resource) = self.resources.get_mut(key) {
408 resource.ready = Some(ready);
409 resource.desired = Some(desired);
410 if ready == desired {
411 resource.status = ResourceStatus::Ready;
412 }
413 self.emit_event(key, "readiness_changed");
414 }
415 }
416
417 fn emit_event(&self, key: &str, event_type: &str) {
418 if let Some(resource) = self.resources.get(key) {
419 let event = serde_json::json!({
420 "type": event_type,
421 "resource": {
422 "kind": resource.kind,
423 "name": resource.name,
424 "status": format!("{:?}", resource.status),
425 "ready": resource.ready,
426 "desired": resource.desired,
427 "message": resource.message,
428 }
429 });
430 println!("{}", event);
431 }
432 }
433
434 pub fn print_summary(&self) {
435 let summary: Vec<_> = self
436 .resources
437 .values()
438 .map(|r| {
439 serde_json::json!({
440 "kind": r.kind,
441 "name": r.name,
442 "status": format!("{:?}", r.status),
443 "ready": r.ready,
444 "desired": r.desired,
445 })
446 })
447 .collect();
448
449 let output = serde_json::json!({
450 "type": "summary",
451 "resources": summary,
452 });
453 println!("{}", output);
454 }
455}
456
457impl Default for JsonProgressReporter {
458 fn default() -> Self {
459 Self::new()
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_resource_status_symbols() {
469 assert_eq!(ResourceStatus::Pending.symbol(), "○");
470 assert_eq!(ResourceStatus::Ready.symbol(), "●");
471 assert_eq!(ResourceStatus::Failed.symbol(), "✗");
472 }
473
474 #[test]
475 fn test_progress_reporter_add_resource() {
476 let mut reporter = ProgressReporter::new();
477 reporter.add_resource("Deployment", "my-app");
478
479 assert!(reporter.resources.contains_key("Deployment/my-app"));
480 assert_eq!(
481 reporter.resources["Deployment/my-app"].status,
482 ResourceStatus::Pending
483 );
484 }
485
486 #[test]
487 fn test_progress_reporter_update_status() {
488 let mut reporter = ProgressReporter::new();
489 reporter.add_resource("Deployment", "my-app");
490 reporter.update_status("Deployment/my-app", ResourceStatus::Applied);
491
492 assert_eq!(
493 reporter.resources["Deployment/my-app"].status,
494 ResourceStatus::Applied
495 );
496 }
497
498 #[test]
499 fn test_progress_reporter_update_readiness() {
500 let mut reporter = ProgressReporter::new();
501 reporter.add_resource("Deployment", "my-app");
502 reporter.update_readiness("Deployment/my-app", 2, 3, Some("Waiting for pods"));
503
504 let resource = &reporter.resources["Deployment/my-app"];
505 assert_eq!(resource.ready, Some(2));
506 assert_eq!(resource.desired, Some(3));
507 assert_eq!(resource.message, Some("Waiting for pods".to_string()));
508 }
509
510 #[test]
511 fn test_progress_reporter_readiness_triggers_ready() {
512 let mut reporter = ProgressReporter::new();
513 reporter.add_resource("Deployment", "my-app");
514 reporter.update_readiness("Deployment/my-app", 3, 3, None);
515
516 assert_eq!(
517 reporter.resources["Deployment/my-app"].status,
518 ResourceStatus::Ready
519 );
520 }
521
522 #[test]
523 fn test_progress_reporter_all_ready() {
524 let mut reporter = ProgressReporter::new();
525 reporter.add_resource("Deployment", "app1");
526 reporter.add_resource("Deployment", "app2");
527
528 assert!(!reporter.all_ready());
529
530 reporter.update_status("Deployment/app1", ResourceStatus::Ready);
531 assert!(!reporter.all_ready());
532
533 reporter.update_status("Deployment/app2", ResourceStatus::Ready);
534 assert!(reporter.all_ready());
535 }
536
537 #[test]
538 fn test_progress_reporter_any_failed() {
539 let mut reporter = ProgressReporter::new();
540 reporter.add_resource("Deployment", "app1");
541 reporter.add_resource("Deployment", "app2");
542
543 assert!(!reporter.any_failed());
544
545 reporter.fail("Deployment/app1", "ImagePullBackOff");
546 assert!(reporter.any_failed());
547 }
548}