From cf917a921055797cd464808153910a09f1afa0f2 Mon Sep 17 00:00:00 2001 From: Benjamin Arntzen Date: Sun, 21 Jun 2026 21:04:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(api):=20GET=20/machines/by-mac/{mac}=20?= =?UTF-8?q?=E2=80=94=20single-row=20MAC=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Look up one machine by MAC in a single call, returning the `simple` detail projection (id, hostname, ip_address, mac_address, status, tags) or 404. Reuses the store's existing get_machine_by_mac (the same lookup admin/create upserts with). This is the primitive automation wants instead of scanning GET /machines, which paginates (per_page 25): any machine past page 1 looks absent, so Jetpack's converge re-imaged fully-Installed nodes on every run (london k8s06-11 sat on page 2). With a dedicated endpoint there is no page-2 blind spot. Consumer-side coverage (404 -> None, no list scan) lives in jetpack's dragonfly client tests (riffcc/jetpack). Co-Authored-By: Claude --- crates/dragonfly-server/src/api.rs | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/dragonfly-server/src/api.rs b/crates/dragonfly-server/src/api.rs index 936e9c09..9f7b2fef 100644 --- a/crates/dragonfly-server/src/api.rs +++ b/crates/dragonfly-server/src/api.rs @@ -249,6 +249,11 @@ pub fn api_router() -> Router { Router::new() .route("/machines", get(get_all_machines).post(register_machine)) .route("/machines/admin/create", post(admin_create_machine)) + // Look up a single machine by MAC — the right primitive for "is this MAC + // registered, and is it Installed?" in one call, instead of paginating + // the full machine list to find one row (which silently misses any + // machine past page 1). + .route("/machines/by-mac/{mac}", get(get_machine_by_mac)) .route("/machines/install-status", get(get_install_status)) .route("/machines/{id}/os", get(get_machine_os).post(assign_os)) .route("/machines/{id}/reimage", post(reimage_machine)) // Add new reimage endpoint @@ -1205,6 +1210,50 @@ async fn get_machine(State(state): State, Path(id): Path) -> Res } } +/// Look up a single machine by MAC address. +/// +/// Returns the `simple` detail projection (`id`, `hostname`, `ip_address`, +/// `mac_address`, `status`, `tags`) — the same shape `GET /machines` emits per +/// row — or `404` when no machine has that MAC. +/// +/// This is the primitive automation wants for "is this MAC registered, and is +/// it Installed?": one row, one call. The alternative callers used to reach for +/// — `GET /machines` and scanning for the MAC — paginates, so any machine past +/// page 1 looks absent and gets spuriously re-imaged on every converge run. +#[axum::debug_handler] +async fn get_machine_by_mac( + State(state): State, + _caller: AuthenticatedCaller, // authenticated read (same posture as the machine list) + Path(mac): Path, +) -> Response { + let normalized = dragonfly_common::normalize_mac(&mac); + match state.store.get_machine_by_mac(&normalized).await { + Ok(Some(v1_machine)) => { + let machine = machine_to_common(&v1_machine); + ( + StatusCode::OK, + Json(machine_to_detail_level(&machine, "simple")), + ) + .into_response() + } + Ok(None) => { + let error_response = ErrorResponse { + error: "Not Found".to_string(), + message: format!("Machine with MAC {} not found", mac), + }; + (StatusCode::NOT_FOUND, Json(error_response)).into_response() + } + Err(e) => { + error!("Failed to retrieve machine by MAC {}: {}", mac, e); + let error_response = ErrorResponse { + error: "Database Error".to_string(), + message: e.to_string(), + }; + (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)).into_response() + } + } +} + // Combined OS assignment handler #[axum::debug_handler] async fn assign_os(