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
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:
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user