Skip to content

Commit 6b90df8

Browse files
committed
stacker agent health fix, npm detection fix, logs fix
1 parent 9c102e5 commit 6b90df8

6 files changed

Lines changed: 195 additions & 56 deletions

File tree

.claude/settings.local.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ configuration.yaml.orig
1010
docker/local/
1111
docs/*.sql
1212
config-to-validate.yaml
13-
*.bak
13+
*.bak
14+
.claude/settings.local.json

src/cli/progress.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,19 @@ pub fn update_message(pb: &ProgressBar, msg: &str) {
7676
/// Return a status icon for a deployment status string.
7777
pub fn status_icon(status: &str) -> &'static str {
7878
match status {
79+
// Deployment statuses
7980
"completed" | "confirmed" => "✓",
8081
"failed" | "error" | "cancelled" => "✗",
8182
"in_progress" => "⟳",
8283
"pending" | "wait_start" => "◷",
8384
"paused" | "wait_resume" => "⏸",
85+
// Agent statuses
86+
"online" => "●",
87+
"offline" | "disconnected" => "○",
88+
// Container states (Docker)
89+
"running" => "▶",
90+
"exited" | "stopped" | "dead" => "■",
91+
"restarting" | "created" => "⟳",
8492
_ => "?",
8593
}
8694
}
@@ -111,6 +119,13 @@ mod tests {
111119
assert_eq!(status_icon("in_progress"), "⟳");
112120
assert_eq!(status_icon("pending"), "◷");
113121
assert_eq!(status_icon("paused"), "⏸");
122+
// Agent statuses
123+
assert_eq!(status_icon("online"), "●");
124+
assert_eq!(status_icon("offline"), "○");
125+
// Container states
126+
assert_eq!(status_icon("running"), "▶");
127+
assert_eq!(status_icon("exited"), "■");
128+
assert_eq!(status_icon("restarting"), "⟳");
114129
assert_eq!(status_icon("unknown_status"), "?");
115130
}
116131
}

src/console/commands/cli/agent.rs

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,93 @@ fn print_command_result(info: &AgentCommandInfo, json: bool) {
440440
}
441441
}
442442

443+
fn print_health_result(info: &AgentCommandInfo) {
444+
if let Some(ref result) = info.result {
445+
let result_type = result.get("type").and_then(|v| v.as_str()).unwrap_or("");
446+
447+
// "all_health": list of all containers
448+
if result_type == "all_health" {
449+
let overall = result.get("status").and_then(|v| v.as_str()).unwrap_or("-");
450+
println!("Overall: {} {}", progress::status_icon(overall), overall);
451+
println!();
452+
if let Some(containers) = result.get("containers").and_then(|v| v.as_array()) {
453+
println!("{:<28} {:<10} {}", "CONTAINER", "STATE", "STATUS");
454+
for c in containers {
455+
let name = c.get("container_name").and_then(|v| v.as_str()).unwrap_or("-");
456+
let state = c.get("container_state").and_then(|v| v.as_str()).unwrap_or("-");
457+
let status = c.get("status").and_then(|v| v.as_str()).unwrap_or("-");
458+
println!(
459+
"{:<28} {} {:<8} {}",
460+
fmt::truncate(name, 26),
461+
progress::status_icon(state),
462+
state,
463+
status,
464+
);
465+
}
466+
}
467+
return;
468+
}
469+
470+
// Single-container health
471+
if result_type == "health" {
472+
let state = result.get("container_state").and_then(|v| v.as_str()).unwrap_or("-");
473+
let status = result.get("status").and_then(|v| v.as_str()).unwrap_or("-");
474+
let app = result.get("app_code").and_then(|v| v.as_str()).unwrap_or("-");
475+
println!(
476+
"{}: {} {} ({})",
477+
app,
478+
progress::status_icon(state),
479+
state,
480+
status
481+
);
482+
if let Some(metrics) = result.get("metrics") {
483+
println!("{}", fmt::pretty_json(metrics));
484+
}
485+
return;
486+
}
487+
488+
// Fallback
489+
println!("{}", fmt::pretty_json(result));
490+
}
491+
492+
if let Some(error) = agent_command_error_message(info) {
493+
eprintln!("Error: {}", error);
494+
}
495+
}
496+
497+
fn print_all_container_health(containers: &[serde_json::Value]) {
498+
if containers.is_empty() {
499+
println!("No containers found.");
500+
return;
501+
}
502+
503+
let all_running = containers.iter().all(|c| {
504+
let state = c.get("status").and_then(|v| v.as_str()).unwrap_or("-");
505+
state == "running"
506+
});
507+
let overall = if all_running { "running" } else { "degraded" };
508+
println!("Overall: {} {}", progress::status_icon(overall), overall);
509+
println!();
510+
511+
println!("{:<28} {:<12} {:<8} {:<8} {}", "CONTAINER", "STATE", "CPU%", "MEM%", "IMAGE");
512+
for c in containers {
513+
let name = c.get("name").and_then(|v| v.as_str()).unwrap_or("-");
514+
let state = c.get("status").and_then(|v| v.as_str()).unwrap_or("-");
515+
let image = c.get("image").and_then(|v| v.as_str()).unwrap_or("-");
516+
let cpu = c.get("cpu_pct").and_then(|v| v.as_f64()).unwrap_or(0.0);
517+
let mem = c.get("mem_pct").and_then(|v| v.as_f64()).unwrap_or(0.0);
518+
println!(
519+
"{:<28} {} {:<10} {:<8.1} {:<8.1} {}",
520+
fmt::truncate(name, 26),
521+
progress::status_icon(state),
522+
state,
523+
cpu,
524+
mem,
525+
fmt::truncate(image, 30),
526+
);
527+
}
528+
}
529+
443530
/// Pre-flight connection check for risky agent commands.
444531
///
445532
/// Enqueues a `check_connections` command to the agent and, if active HTTP
@@ -566,8 +653,21 @@ impl CallableTrait for AgentHealthCommand {
566653
let ctx = CliRuntime::new("agent health")?;
567654
let hash = resolve_deployment_hash(&self.deployment, &ctx)?;
568655

656+
// No specific app requested → list all containers with health metrics.
657+
// This avoids sending app_code="all" to older agents that don't handle it.
658+
if self.app_code.is_none() && !self.include_system {
659+
let containers = fetch_live_containers(&ctx, &hash)?
660+
.unwrap_or_default();
661+
if self.json {
662+
println!("{}", serde_json::to_string_pretty(&containers)?);
663+
} else {
664+
print_all_container_health(&containers);
665+
}
666+
return Ok(());
667+
}
668+
569669
let params = crate::forms::status_panel::HealthCommandRequest {
570-
app_code: self.app_code.clone().unwrap_or_else(|| "all".to_string()),
670+
app_code: self.app_code.clone().unwrap_or_default(),
571671
container: None,
572672
include_metrics: true,
573673
include_system: self.include_system,
@@ -578,7 +678,11 @@ impl CallableTrait for AgentHealthCommand {
578678
.map_err(|e| CliError::ConfigValidation(format!("Invalid parameters: {}", e)))?;
579679

580680
let info = run_agent_command(&ctx, &request, "Checking health", DEFAULT_TIMEOUT_SECS)?;
581-
print_command_result(&info, self.json);
681+
if self.json {
682+
print_command_result(&info, true);
683+
} else {
684+
print_health_result(&info);
685+
}
582686
Ok(())
583687
}
584688
}
@@ -1660,7 +1764,7 @@ fn print_containers_summary(containers: &[serde_json::Value]) {
16601764
return;
16611765
}
16621766

1663-
println!("{:<24} {:<12} {:<30}", "CONTAINER", "STATE", "IMAGE");
1767+
println!("{:<24} {:<12} {:<22} {:<30}", "CONTAINER", "STATE", "PORTS", "IMAGE");
16641768
for c in containers {
16651769
let name = c.get("name").and_then(|v| v.as_str()).unwrap_or("-");
16661770
let state = c
@@ -1669,11 +1773,23 @@ fn print_containers_summary(containers: &[serde_json::Value]) {
16691773
.and_then(|v| v.as_str())
16701774
.unwrap_or("-");
16711775
let image = c.get("image").and_then(|v| v.as_str()).unwrap_or("-");
1776+
let ports = c
1777+
.get("ports")
1778+
.and_then(|v| v.as_array())
1779+
.map(|arr| {
1780+
arr.iter()
1781+
.filter_map(|p| p.as_str())
1782+
.collect::<Vec<_>>()
1783+
.join(", ")
1784+
})
1785+
.filter(|s| !s.is_empty())
1786+
.unwrap_or_else(|| "-".to_string());
16721787
println!(
1673-
"{:<24} {} {:<10} {:<30}",
1788+
"{:<24} {} {:<10} {:<22} {:<30}",
16741789
fmt::truncate(name, 22),
16751790
progress::status_icon(state),
16761791
state,
1792+
fmt::truncate(&ports, 20),
16771793
fmt::truncate(image, 28),
16781794
);
16791795
}

src/console/commands/cli/logs.rs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -245,26 +245,32 @@ fn run_remote_logs(
245245
let app_codes: Vec<String> = if let Some(svc) = service {
246246
vec![svc.to_string()]
247247
} else {
248-
// Fetch snapshot to discover all running containers
249-
let pb = progress::spinner("Discovering containers");
250-
match ctx.block_on(ctx.client.agent_snapshot(&hash)) {
251-
Ok(snap) => {
252-
progress::finish_success(&pb, "Containers discovered");
253-
snap.get("containers")
254-
.and_then(|v| v.as_array())
255-
.map(|arr| {
256-
arr.iter()
257-
.filter_map(|c| c.get("name").and_then(|n| n.as_str()))
258-
.map(|s| s.to_string())
259-
.collect()
260-
})
261-
.unwrap_or_default()
262-
}
263-
Err(e) => {
264-
progress::finish_error(&pb, &format!("Could not discover containers: {}", e));
265-
return Err(Box::new(e));
266-
}
248+
// Discover running containers via a live list_containers command.
249+
let params = crate::forms::status_panel::ListContainersCommandRequest {
250+
include_health: false,
251+
include_logs: false,
252+
log_lines: 0,
253+
};
254+
let request = AgentEnqueueRequest::new(&hash, "list_containers")
255+
.with_parameters(&params)
256+
.map_err(|e| CliError::ConfigValidation(format!("Invalid parameters: {}", e)))?;
257+
let info = run_remote_agent_command(&ctx, &request, "Discovering containers", REMOTE_TIMEOUT_SECS)?;
258+
if info.status != "completed" {
259+
let (summary, tip) = no_containers_messages(&hash);
260+
eprintln!("{}", summary);
261+
eprintln!("{}", tip);
262+
return Ok(());
267263
}
264+
info.result
265+
.as_ref()
266+
.and_then(|r| r.get("containers").and_then(|v| v.as_array()))
267+
.map(|arr| {
268+
arr.iter()
269+
.filter_map(|c| c.get("name").and_then(|n| n.as_str()))
270+
.map(|s| s.to_string())
271+
.collect()
272+
})
273+
.unwrap_or_default()
268274
};
269275

270276
if app_codes.is_empty() {
@@ -395,20 +401,20 @@ fn print_logs_result(app_code: &str, info: &AgentCommandInfo, multi: bool) {
395401
}
396402

397403
if let Some(ref result) = info.result {
398-
// Try to extract log lines from the result JSON
399-
if let Some(logs) = result.get("logs").and_then(|v| v.as_str()) {
400-
print!("{}", logs);
401-
} else if let Some(lines) = result.get("lines").and_then(|v| v.as_array()) {
402-
for line in lines {
403-
if let Some(s) = line.as_str() {
404-
println!("{}", s);
404+
if let Some(lines) = result.get("lines").and_then(|v| v.as_array()) {
405+
if lines.is_empty() {
406+
println!("(no log output)");
407+
} else {
408+
for line in lines {
409+
let msg = line
410+
.get("message")
411+
.and_then(|v| v.as_str())
412+
.unwrap_or("");
413+
println!("{}", msg);
405414
}
406415
}
407-
} else if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
408-
print!("{}", output);
409416
} else {
410-
// Fallback: pretty-print the whole result
411-
println!("{}", fmt::pretty_json(result));
417+
println!("(no log output)");
412418
}
413419
} else {
414420
println!("(no log output)");

src/console/commands/cli/proxy.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::cli::proxy_manager::{
88
DockerCliRuntime, ProxyDetection,
99
};
1010
use crate::cli::runtime::CliRuntime;
11-
use crate::console::commands::cli::agent::AgentConfigureProxyCommand;
11+
use crate::cli::stacker_client::AgentEnqueueRequest;
12+
use crate::console::commands::cli::agent::{run_agent_command, AgentConfigureProxyCommand};
1213
use crate::console::commands::CallableTrait;
1314
use std::path::{Path, PathBuf};
1415

@@ -430,8 +431,26 @@ impl CallableTrait for ProxyDetectCommand {
430431
let ctx = CliRuntime::new("proxy detect")?;
431432
let hash = resolve_deployment_hash_for_proxy(&self.deployment, &ctx)?;
432433

433-
let snapshot = ctx.block_on(ctx.client.agent_snapshot(&hash))?;
434-
let detection = detect_proxy_from_snapshot(&snapshot);
434+
// Use a live list_containers command — the snapshot's containers
435+
// field is not populated for cloud deployments.
436+
const DETECT_TIMEOUT: u64 = 30;
437+
let params = crate::forms::status_panel::ListContainersCommandRequest {
438+
include_health: true,
439+
include_logs: false,
440+
log_lines: 0,
441+
};
442+
let request = AgentEnqueueRequest::new(&hash, "list_containers")
443+
.with_parameters(&params)
444+
.map_err(|e| CliError::ConfigValidation(format!("Invalid parameters: {}", e)))?;
445+
let info = run_agent_command(&ctx, &request, "Scanning containers", DETECT_TIMEOUT)?;
446+
let containers = info
447+
.result
448+
.as_ref()
449+
.and_then(|r| r.get("containers").and_then(|v| v.as_array()))
450+
.cloned()
451+
.unwrap_or_default();
452+
let fake_snapshot = serde_json::json!({ "containers": containers });
453+
let detection = detect_proxy_from_snapshot(&fake_snapshot);
435454
print_detection(&detection, self.json);
436455
} else {
437456
let runtime = DockerCliRuntime;

0 commit comments

Comments
 (0)