Private
Public Access
1
0

style: cargo fmt --all to fix CI format check
Some checks failed
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 45s
CI Pipeline / Rust Unit Tests (push) Successful in 1m2s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 10s
CI Pipeline / Build .deb & Release (push) Has been skipped

This commit is contained in:
2026-05-06 03:57:01 +00:00
parent 3d9b2d4917
commit 12d640e5de
12 changed files with 199 additions and 137 deletions

View File

@ -263,34 +263,32 @@ async fn run_service_check(
let detail = if data.healthy {
format!(
"Service '{}' is {}/{} (enabled: {})",
data.name,
data.active_state,
data.sub_state,
data.enabled_state
data.name, data.active_state, data.sub_state, data.enabled_state
)
} else {
format!(
"Service '{}' status: {}/{} (unhealthy, enabled: {})",
data.name, data.active_state,
data.sub_state,
data.enabled_state
data.name, data.active_state, data.sub_state, data.enabled_state
)
};
(data.healthy, detail)
},
Err(AgentClientError::Timeout) => {
(false, format!("Agent timed out querying service '{service_name}'"))
},
Err(AgentClientError::Connect(_)) => {
(false, format!("Agent connection refused for service '{service_name}'"))
},
Err(AgentClientError::Timeout) => (
false,
format!("Agent timed out querying service '{service_name}'"),
),
Err(AgentClientError::Connect(_)) => (
false,
format!("Agent connection refused for service '{service_name}'"),
),
Err(AgentClientError::ApiError { code, message }) => {
// 404, 400, 500 etc. from the agent means the service is unhealthy.
(false, format!("Agent error [{code}]: {message}"))
},
Err(e) => {
(false, format!("Agent error querying service '{service_name}': {e}"))
},
Err(e) => (
false,
format!("Agent error querying service '{service_name}': {e}"),
),
}
}
@ -300,10 +298,7 @@ async fn run_service_check(
/// Execute an HTTP check by making a GET request to the configured URL.
/// Supports optional basic auth (decrypted from DB) and substring body matching.
async fn run_http_check(
check: &HealthCheckRow,
crypto_key: &[u8; 32],
) -> (bool, String) {
async fn run_http_check(check: &HealthCheckRow, crypto_key: &[u8; 32]) -> (bool, String) {
let url = match &check.url {
Some(u) => u.clone(),
None => {
@ -325,7 +320,9 @@ async fn run_http_check(
.build()
.unwrap_or_else(|_| reqwest::Client::new())
} else {
client_builder.build().unwrap_or_else(|_| reqwest::Client::new())
client_builder
.build()
.unwrap_or_else(|_| reqwest::Client::new())
};
// Build the request.
@ -334,21 +331,22 @@ async fn run_http_check(
// Add basic auth if configured.
if let Some(user) = &check.basic_auth_user {
// Decrypt the password if present.
let password = match (&check.basic_auth_pass_encrypted, &check.basic_auth_pass_nonce) {
(Some(enc), Some(nonce)) => {
match crypto::decrypt(enc, nonce, crypto_key) {
Ok(p) => p,
Err(e) => {
return (
false,
format!("Failed to decrypt basic auth password: {e}"),
);
},
}
let password = match (
&check.basic_auth_pass_encrypted,
&check.basic_auth_pass_nonce,
) {
(Some(enc), Some(nonce)) => match crypto::decrypt(enc, nonce, crypto_key) {
Ok(p) => p,
Err(e) => {
return (false, format!("Failed to decrypt basic auth password: {e}"));
},
},
_ => {
// No encrypted password stored — treat as missing credentials.
return (false, "HTTP check has basic_auth_user but no encrypted password".to_string());
return (
false,
"HTTP check has basic_auth_user but no encrypted password".to_string(),
);
},
};
request = request.basic_auth(user.as_str(), Some(password.as_str()));
@ -382,7 +380,10 @@ async fn run_http_check(
let body = match response.text().await {
Ok(b) => b,
Err(e) => {
return (false, format!("HTTP check failed to read response body: {e}"));
return (
false,
format!("HTTP check failed to read response body: {e}"),
);
},
};
@ -391,14 +392,15 @@ async fn run_http_check(
if !body.contains(expected) {
return (
false,
format!(
"HTTP check body mismatch for {url}: expected substring not found"
),
format!("HTTP check body mismatch for {url}: expected substring not found"),
);
}
}
(true, format!("HTTP check OK for {url} (status {})", status.as_u16()))
(
true,
format!("HTTP check OK for {url} (status {})", status.as_u16()),
)
}
// ─────────────────────────────────────────────────────────────────────────────

View File

@ -870,7 +870,11 @@ async fn sync_job_status(pool: &PgPool, job_id: Uuid) {
let new_status: &str;
let set_completed: bool;
if counts.running_count > 0 || counts.pending_count > 0 || counts.queued_count > 0 || counts.waiting_health_check_count > 0 {
if counts.running_count > 0
|| counts.pending_count > 0
|| counts.queued_count > 0
|| counts.waiting_health_check_count > 0
{
// Still work in flight — keep parent running.
new_status = "running";
set_completed = false;
@ -1009,7 +1013,8 @@ pub async fn retry_pending_jobs(pool: PgPool, config: Arc<AppConfig>) {
WHERE pjh.status IN ('pending', 'waiting_health_check')
AND pjh.retry_next_at <= NOW()
AND j.status != 'cancelled'
"#,)
"#,
)
.fetch_all(&pool)
.await
{

View File

@ -19,9 +19,9 @@ use tokio::sync::Mutex;
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Message, Connector};
use uuid::Uuid;
use pm_agent_client::client::AgentClient;
use pm_agent_client::client::DEFAULT_AGENT_PORT;
use pm_core::config::AppConfig;
use pm_agent_client::client::AgentClient;
// ── Types ─────────────────────────────────────────────────────────────────────
@ -48,7 +48,7 @@ struct AgentWsEvent {
/// Payload broadcast via `pg_notify('job_update', …)`.
#[derive(Debug, Serialize)]
struct NotifyPayload {
event_type: String, // "host" or "job"
event_type: String, // "host" or "job"
job_id: String,
host_id: String,
status: String,
@ -254,7 +254,6 @@ async fn build_tls_config(config: &AppConfig) -> anyhow::Result<TlsClientConfig>
Ok(config)
}
// ── Per-job relay ─────────────────────────────────────────────────────────────
async fn relay_one_job(
@ -681,7 +680,7 @@ async fn update_parent_job_status(pool: &PgPool, job_id: Uuid) {
let payload = NotifyPayload {
event_type: "job".to_string(),
job_id: job_id.to_string(),
host_id: String::new(), // no specific host for job-level events
host_id: String::new(), // no specific host for job-level events
status: final_status.to_string(),
output: None,
error_message: None,