M5: Patch Deployment & Job Management
Backend: - migrations/003_jobs_scheduling.sql: retry_next_at/last_error columns, pg_notify trigger for immediate job dispatch, retry index - pm-agent-client: ApplyPatchesRequest/Response, AgentJobStatus, RollbackResponse types; apply_patches/job_status/rollback_job client methods + generic POST helper - pm-core/models: JobStatus, JobKind, PatchJob, PatchJobHost, CreateJobRequest, PatchJobSummary - pm-web/routes/jobs.rs: POST/GET /api/v1/jobs, GET /jobs/:id, POST /jobs/:id/cancel, POST /jobs/:id/rollback - pm-worker/job_executor.rs: NOTIFY listener, periodic scanner, execute_host_job, poll_running_jobs, handle_host_failure (3-retry exponential backoff 1m/5m/30m), sync_job_status, retry_pending_jobs - pm-worker/main.rs: spawn job_executor Frontend: - types/index.ts: PatchInfo, PatchJobHost, PatchJob, PatchJobSummary, CreateJobRequest interfaces - api/client.ts: jobsApi (list/get/create/cancel/rollback), patchesApi (getHostPatches) - pages/PatchDeploymentPage.tsx: 3-step MUI Stepper (host select → configure → result) - pages/JobsPage.tsx: job list table, expandable per-host detail, cancel/rollback actions with confirm dialog, load-more pagination - App.tsx: /jobs and /deployment routes wired to real pages cargo check: 0 errors | vite build: 0 errors
This commit is contained in:
@ -7,6 +7,7 @@
|
||||
//! GET /api/v1/hosts/{id}/groups — list groups for host
|
||||
//! POST /api/v1/hosts/{id}/groups — assign host to group
|
||||
//! DELETE /api/v1/hosts/{id}/groups/{group_id} — remove host from group
|
||||
//! POST /api/v1/hosts/{id}/refresh — queue on-demand refresh (operator+)
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
@ -34,6 +35,7 @@ pub fn router() -> Router<AppState> {
|
||||
.route("/:id", get(get_host).delete(remove_host))
|
||||
.route("/:id/groups", get(list_host_groups).post(add_host_to_group))
|
||||
.route("/:id/groups/:group_id", delete(remove_host_from_group))
|
||||
.route("/:id/refresh", post(refresh_host))
|
||||
}
|
||||
|
||||
// ── Query params ─────────────────────────────────────────────────────────────
|
||||
@ -470,3 +472,56 @@ async fn resolve_fqdn(fqdn: &str) -> Result<String, String> {
|
||||
_ => Err(format!("Failed to resolve FQDN: {fqdn}")),
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST /api/v1/hosts/:id/refresh ───────────────────────────────────────────
|
||||
|
||||
/// Queue an on-demand health + patch refresh for a single host.
|
||||
///
|
||||
/// Sends a PostgreSQL NOTIFY on the `refresh_requested` channel; the
|
||||
/// pm-worker refresh listener picks this up and polls the host immediately.
|
||||
/// Requires Operator or Admin role (any authenticated user).
|
||||
async fn refresh_host(
|
||||
State(state): State<AppState>,
|
||||
_auth: AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<(StatusCode, Json<Value>), (StatusCode, Json<Value>)> {
|
||||
// Verify the host exists.
|
||||
let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hosts WHERE id = $1)")
|
||||
.bind(id)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, %id, "refresh_host: db error checking host existence");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({ "error": { "code": "internal_error", "message": "Database error" } })),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !exists {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({ "error": { "code": "not_found", "message": "Host not found" } })),
|
||||
));
|
||||
}
|
||||
|
||||
// NOTIFY the worker's refresh listener.
|
||||
sqlx::query("SELECT pg_notify('refresh_requested', $1)")
|
||||
.bind(id.to_string())
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, %id, "refresh_host: pg_notify failed");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({ "error": { "code": "internal_error", "message": "Failed to queue refresh" } })),
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::info!(%id, "On-demand refresh queued");
|
||||
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
Json(json!({ "message": "Refresh queued" })),
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user