chore(release): stage v1.7.55-alpha
This commit is contained in:
@@ -2,6 +2,10 @@ use super::package::validate_app_id;
|
||||
use super::transitional::Op;
|
||||
use super::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use std::time::Duration;
|
||||
|
||||
const PODMAN_INSPECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const PODMAN_PS_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
impl RpcHandler {
|
||||
pub(super) async fn handle_container_install(
|
||||
@@ -379,6 +383,10 @@ impl RpcHandler {
|
||||
// If app_id is provided, get health for that app.
|
||||
if let Some(params) = params {
|
||||
if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) {
|
||||
if let Some(health) = self.stack_health(app_id).await? {
|
||||
return Ok(serde_json::json!({ app_id: health }));
|
||||
}
|
||||
|
||||
let mut last_err: Option<anyhow::Error> = None;
|
||||
for candidate in status_app_id_candidates(app_id) {
|
||||
match orchestrator.health(&candidate).await {
|
||||
@@ -434,6 +442,78 @@ impl RpcHandler {
|
||||
|
||||
Ok(serde_json::Value::Object(health_map))
|
||||
}
|
||||
|
||||
async fn stack_health(&self, app_id: &str) -> Result<Option<String>> {
|
||||
let Some(members) = stack_health_members(app_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let mut saw_starting = false;
|
||||
let mut saw_unknown = false;
|
||||
for member in members {
|
||||
match member_health(orchestrator.as_ref(), member)
|
||||
.await
|
||||
.as_deref()
|
||||
{
|
||||
Ok(health) if health == "healthy" => {}
|
||||
Ok(health) if health == "starting" => saw_starting = true,
|
||||
Ok(health) if health == "unknown" => saw_unknown = true,
|
||||
Ok(_) => return Ok(Some("unhealthy".to_string())),
|
||||
Err(_) => saw_unknown = true,
|
||||
}
|
||||
}
|
||||
|
||||
if saw_unknown {
|
||||
Ok(Some("unknown".to_string()))
|
||||
} else if saw_starting {
|
||||
Ok(Some("starting".to_string()))
|
||||
} else {
|
||||
Ok(Some("healthy".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn member_health(
|
||||
orchestrator: &dyn crate::container::traits::ContainerOrchestrator,
|
||||
app_id: &str,
|
||||
) -> Result<String> {
|
||||
if let Ok(health) = orchestrator.health(app_id).await {
|
||||
return Ok(health);
|
||||
}
|
||||
for name in status_container_name_candidates(app_id) {
|
||||
if let Some(health) = inspect_container_health_value(&name).await {
|
||||
return Ok(health);
|
||||
}
|
||||
}
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
|
||||
fn stack_health_members(app_id: &str) -> Option<&'static [&'static str]> {
|
||||
match app_id {
|
||||
"mempool" | "mempool-web" => {
|
||||
Some(&["archy-mempool-db", "mempool-api", "archy-mempool-web"])
|
||||
}
|
||||
"btcpay-server" | "btcpayserver" | "btcpay" => {
|
||||
Some(&["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"])
|
||||
}
|
||||
"immich" => Some(&["immich_postgres", "immich_redis", "immich_server"]),
|
||||
"indeedhub" => Some(&[
|
||||
"indeedhub-postgres",
|
||||
"indeedhub-redis",
|
||||
"indeedhub-minio",
|
||||
"indeedhub-relay",
|
||||
"indeedhub-api",
|
||||
"indeedhub-ffmpeg",
|
||||
"indeedhub",
|
||||
]),
|
||||
"fedimint" => Some(&["fedimint"]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn status_app_id_candidates(app_id: &str) -> Vec<String> {
|
||||
@@ -504,15 +584,21 @@ fn status_container_name_candidates(app_id: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
async fn inspect_container_state_value(name: &str) -> Option<serde_json::Value> {
|
||||
let out = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"inspect",
|
||||
name,
|
||||
"--format",
|
||||
"{{.State.Status}} {{.State.Running}}",
|
||||
])
|
||||
.output()
|
||||
if let Some(v) = ps_container_state_value(name).await {
|
||||
return Some(v);
|
||||
}
|
||||
|
||||
let mut cmd = tokio::process::Command::new("podman");
|
||||
cmd.args([
|
||||
"inspect",
|
||||
name,
|
||||
"--format",
|
||||
"{{.State.Status}} {{.State.Running}} {{if .State.Healthcheck}}{{.State.Healthcheck.Status}}{{else}}none{{end}}",
|
||||
]);
|
||||
cmd.kill_on_drop(true);
|
||||
let out = tokio::time::timeout(PODMAN_INSPECT_TIMEOUT, cmd.output())
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
@@ -525,20 +611,90 @@ async fn inspect_container_state_value(name: &str) -> Option<serde_json::Value>
|
||||
let mut parts = line.split_whitespace();
|
||||
let status = parts.next().unwrap_or("unknown");
|
||||
let running = parts.next().unwrap_or("false") == "true";
|
||||
let health = parts.next().unwrap_or("none");
|
||||
Some(serde_json::json!({
|
||||
"name": name,
|
||||
"status": status,
|
||||
"state": status,
|
||||
"running": running,
|
||||
"health": health,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn ps_container_state_value(name: &str) -> Option<serde_json::Value> {
|
||||
let mut cmd = tokio::process::Command::new("podman");
|
||||
cmd.args([
|
||||
"ps",
|
||||
"-a",
|
||||
"--filter",
|
||||
&format!("name={name}"),
|
||||
"--format",
|
||||
"{{.Names}}|{{.Status}}",
|
||||
]);
|
||||
cmd.kill_on_drop(true);
|
||||
let out = tokio::time::timeout(PODMAN_PS_TIMEOUT, cmd.output())
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
let mut parts = line.splitn(2, '|');
|
||||
let container_name = parts.next().unwrap_or_default();
|
||||
if container_name != name {
|
||||
continue;
|
||||
}
|
||||
let status = parts.next().unwrap_or_default();
|
||||
let state = state_from_podman_status(status);
|
||||
let health = parse_health_from_status(status).unwrap_or("none");
|
||||
return Some(serde_json::json!({
|
||||
"name": name,
|
||||
"status": state,
|
||||
"state": state,
|
||||
"running": state.eq_ignore_ascii_case("running"),
|
||||
"health": health,
|
||||
}));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn state_from_podman_status(status: &str) -> &str {
|
||||
if status.starts_with("Up ") {
|
||||
"running"
|
||||
} else if status.starts_with("Exited ") {
|
||||
"exited"
|
||||
} else if status.starts_with("Created") {
|
||||
"created"
|
||||
} else if status.starts_with("Stopping") {
|
||||
"stopping"
|
||||
} else if status.starts_with("Removing") {
|
||||
"removing"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_health_from_status(status: &str) -> Option<&str> {
|
||||
let start = status.rfind('(')?;
|
||||
let end = status.rfind(')')?;
|
||||
(start < end).then(|| &status[start + 1..end])
|
||||
}
|
||||
|
||||
async fn inspect_container_health_value(name: &str) -> Option<String> {
|
||||
let v = inspect_container_state_value(name).await?;
|
||||
if let Some(health) = v.get("health").and_then(|s| s.as_str()) {
|
||||
if health != "none" {
|
||||
return Some(health.to_string());
|
||||
}
|
||||
}
|
||||
match v.get("state").and_then(|s| s.as_str()).unwrap_or("unknown") {
|
||||
"running" => Some("healthy".to_string()),
|
||||
"created" => Some("starting".to_string()),
|
||||
"paused" => Some("paused".to_string()),
|
||||
"stopping" => Some("unhealthy".to_string()),
|
||||
"exited" | "stopped" => Some("unhealthy".to_string()),
|
||||
other => Some(format!("unknown:{other}")),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user