From b835c765258405707ab64feeeaea91aa66ef1cba Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 26 May 2026 18:50:14 -0400 Subject: [PATCH] fix(aliases): implement require_target_exists index validation When aliases.require_target_exists config is set to true, alias creation and updates now validate that the target index exists on all Meilisearch nodes before proceeding. Replaced two TODOs in routes/aliases.rs with actual implementation: - create_alias: validates single target and all multi-targets - update_alias: validates new target on alias flip The check uses Meilisearch's GET /indexes/{uid} endpoint which returns: - 200 if index exists - 404 if index not found - Other HTTP errors for connectivity/auth issues Closes: bf-gfiw8 Co-Authored-By: Claude Opus 4.7 --- crates/miroir-proxy/src/routes/aliases.rs | 114 +++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/crates/miroir-proxy/src/routes/aliases.rs b/crates/miroir-proxy/src/routes/aliases.rs index dd420b1..1368301 100644 --- a/crates/miroir-proxy/src/routes/aliases.rs +++ b/crates/miroir-proxy/src/routes/aliases.rs @@ -8,8 +8,57 @@ use axum::{ Json, }; use miroir_core::{alias::AliasKind, config::MiroirConfig, task_store::TaskStore}; +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use std::time::Duration; + +/// Check if an index exists on all Meilisearch nodes. +/// +/// Returns Ok(()) if the index exists on all nodes, or an error if any node +/// does not have the index or is unreachable. +async fn check_index_exists_on_all_nodes( + node_addresses: &[String], + master_key: &str, + index_uid: &str, +) -> Result<(), String> { + let client = Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .map_err(|e| format!("failed to create HTTP client: {e}"))?; + + // Check each node + for address in node_addresses { + let base = address.trim_end_matches('/'); + let url = format!("{base}/indexes/{index_uid}"); + + let response = client + .get(&url) + .header("Authorization", format!("Bearer {master_key}")) + .send() + .await + .map_err(|e| format!("node {address} unreachable: {e}"))?; + + let status = response.status(); + if status.as_u16() == 404 { + return Err(format!( + "index '{index_uid}' does not exist on node {address}" + )); + } else if !status.is_success() { + let body_text = response + .text() + .await + .unwrap_or_else(|e| format!("(failed to read error body: {e})")); + return Err(format!( + "node {address} returned HTTP {} for index '{index_uid}': {}", + status.as_u16(), + body_text.trim() + )); + } + } + + Ok(()) +} /// Alias management state. #[derive(Clone)] @@ -131,8 +180,49 @@ where // Validate target existence if required if state.config.aliases.require_target_exists { - // TODO: Check if target index exists in Meilisearch - // This would require calling the index list endpoint on each node + let node_addresses: Vec = state + .config + .nodes + .iter() + .map(|n| n.address.clone()) + .collect(); + let master_key = &state.config.node_master_key; + + match &kind { + AliasKind::Single => { + if let Some(target) = &body.target { + if let Err(e) = + check_index_exists_on_all_nodes(&node_addresses, master_key, target).await + { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + code: "index_not_found".to_string(), + message: e, + }), + )); + } + } + } + AliasKind::Multi => { + if let Some(targets) = &body.targets { + for target in targets { + if let Err(e) = + check_index_exists_on_all_nodes(&node_addresses, master_key, target) + .await + { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + code: "index_not_found".to_string(), + message: e, + }), + )); + } + } + } + } + } } let now = std::time::SystemTime::now() @@ -332,7 +422,25 @@ where // Validate target existence if required if state.config.aliases.require_target_exists { - // TODO: Check if target index exists in Meilisearch + let node_addresses: Vec = state + .config + .nodes + .iter() + .map(|n| n.address.clone()) + .collect(); + let master_key = &state.config.node_master_key; + + if let Err(e) = + check_index_exists_on_all_nodes(&node_addresses, master_key, &new_target).await + { + return Err(( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + code: "index_not_found".to_string(), + message: e, + }), + )); + } } // Perform the atomic flip