Private
Public Access
1
0

fix: expand empty packages to all available patches + refresh_listener UPSERT
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 4s
CI Pipeline / Clippy Lints (push) Successful in 46s
CI Pipeline / Rust Unit Tests (push) Successful in 1m1s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 12s
CI Pipeline / Build .deb & Release (push) Has been skipped

BUG-15: Empty patch_selection sent to agent as-is, causing apply nothing
instead of apply all available patches per SPEC. When packages is empty,
now query host_patch_data and expand to full package list.

BUG-16: refresh_listener used INSERT instead of UPSERT for
host_patch_data, causing duplicate key constraint errors.
This commit is contained in:
2026-04-30 02:13:20 +00:00
parent 2a9c6e8ed3
commit 1c03522835
2 changed files with 47 additions and 1 deletions

View File

@ -388,9 +388,49 @@ async fn execute_host_job(
}, },
}; };
let packages: Vec<String> = let mut packages: Vec<String> =
serde_json::from_value(patch_sel.patch_selection).unwrap_or_default(); serde_json::from_value(patch_sel.patch_selection).unwrap_or_default();
// ── 2b. Expand empty packages to all available patches ─────────────────
// Per SPEC: "empty = all available patches". The agent treats an empty
// list as "apply nothing", so we must expand it here.
if packages.is_empty() {
match sqlx::query_scalar::<_, serde_json::Value>(
r#"
SELECT available_patches
FROM host_patch_data
WHERE host_id = $1
ORDER BY polled_at DESC
LIMIT 1
"#,
)
.bind(host_id)
.fetch_optional(&pool)
.await
{
Ok(Some(val)) => {
if let Ok(patches) = serde_json::from_value::<Vec<serde_json::Value>>(val) {
for p in &patches {
if let Some(name) = p.get("name").and_then(|n| n.as_str()) {
packages.push(name.to_string());
}
}
tracing::info!(
%pjh_id,
count = packages.len(),
"execute_host_job: expanded empty packages to all available patches"
);
}
},
Ok(None) => {
tracing::warn!(%pjh_id, "execute_host_job: no patch data for host, sending empty packages");
},
Err(e) => {
tracing::error!(%pjh_id, error = %e, "execute_host_job: failed to fetch patch data for expansion");
},
}
}
// ── 3. Load mTLS certs ─────────────────────────────────────────────────── // ── 3. Load mTLS certs ───────────────────────────────────────────────────
let certs = match load_agent_certs(&config.security) { let certs = match load_agent_certs(&config.security) {
Ok(c) => c, Ok(c) => c,

View File

@ -181,6 +181,12 @@ async fn refresh_host(
INSERT INTO host_patch_data INSERT INTO host_patch_data
(host_id, available_patches, installed_packages, patch_count, cve_count) (host_id, available_patches, installed_packages, patch_count, cve_count)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (host_id) DO UPDATE SET
available_patches = EXCLUDED.available_patches,
installed_packages = EXCLUDED.installed_packages,
patch_count = EXCLUDED.patch_count,
cve_count = EXCLUDED.cve_count,
polled_at = NOW()
"#, "#,
) )
.bind(host.id) .bind(host.id)