Add MongoDB reachability validation to IPL sequence

- Add rec_services config section to beds.toml and test fixture
- Add RecNodeConfig struct; export from config module
- Add mongo::validate() and validate_all() — TCP ping per configured REC node
- Wire mongo::validate_all() into ipl() with env-aware error handling
- Add mongodb crate dependency (sync feature)
- Add unit tests for mongo validate error paths (closed port, bad address)
- Update README status table and project structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:12:13 -07:00
parent 119ec0ea45
commit 2ce87710ff
11 changed files with 1901 additions and 10 deletions

View File

@@ -26,7 +26,7 @@
//! * `2026-04-02` - mks - refactored into load() + load_from() for testability
mod structs;
pub use structs::{BedsConfig, BrokerServicesConfig};
pub use structs::{BedsConfig, BrokerServicesConfig, RecNodeConfig};
use config::{Config, File, FileFormat};

View File

@@ -1,4 +1,5 @@
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
pub struct BedsConfig {
@@ -9,6 +10,7 @@ pub struct BedsConfig {
pub audit_on: bool,
pub journal_on: bool,
pub broker_services: BrokerServicesConfig,
pub rec_services: HashMap<String, RecNodeConfig>,
}
#[derive(Debug, Deserialize)]
@@ -48,3 +50,13 @@ pub struct BrokerInstancesConfig {
pub w_broker: u32,
pub m_broker: u32,
}
#[derive(Debug, Deserialize)]
pub struct RecNodeConfig {
pub host: String,
pub port: u16,
pub user: String,
pub pass: String,
pub database: String,
pub use_ssl: bool,
}

View File

@@ -16,3 +16,4 @@
pub mod amqp;
pub mod config;
pub mod logging;
pub mod mongo;

View File

@@ -24,6 +24,7 @@
mod amqp;
mod config;
mod logging;
mod mongo;
/// Executes the BEDS Initial Program Load (IPL) sequence.
///
@@ -69,6 +70,17 @@ fn ipl() -> Result<(), String> {
}
}
// validate MongoDB reachability — fatal in production, non-fatal in all other envs
match mongo::validate_all(&cfg.rec_services) {
Ok(()) => tracing::info!("MongoDB reachable"),
Err(e) => {
if cfg.id.env_name == "production" {
return Err(e);
}
tracing::warn!("MongoDB unreachable (non-fatal in {}): {}", cfg.id.env_name, e);
}
}
Ok(())
}

109
src/mongo.rs Normal file
View File

@@ -0,0 +1,109 @@
//! # mongo.rs — MongoDB (REC) Transport Layer
//!
//! Manages all MongoDB interactions for the BEDS node. At IPL, validates that
//! each configured REC service node is reachable before the node proceeds.
//! Future phases will add connection pooling, authentication, and collection
//! access via the adapter layer.
//!
//! ## Calling Agents
//! - `ipl()` in main.rs — calls `validate_all()` during the IPL sequence
//!
//! ## Inputs
//! - `HashMap<String, RecNodeConfig>` from the loaded BEDS configuration
//!
//! ## Outputs
//! - `Ok(())` if all configured REC nodes are reachable
//! - `Err(String)` with node name, host:port, and OS error on first failure
//!
//! **Author:** mks
//! **Version:** 1.0
//!
//! ## History
//! * `2026-04-04` - mks - original coding
use std::collections::HashMap;
use std::net::TcpStream;
use std::time::Duration;
use crate::config::RecNodeConfig;
/// Validates that all configured MongoDB nodes are reachable.
///
/// Iterates every entry in the `rec_services` config block and opens a TCP
/// connection to each declared host:port. Does not authenticate or issue any
/// MongoDB wire protocol — reachability only. Fails on the first unreachable
/// node.
///
/// # Arguments
///
/// * `nodes` — map of service name → `RecNodeConfig` from `BedsConfig`
///
/// # Returns
///
/// `Ok(())` if every node responds to a TCP connect within the timeout.
/// `Err(String)` with the service name and address of the first failure.
///
/// # History
///
/// * `2026-04-04` - mks - original coding
pub fn validate_all(nodes: &HashMap<String, RecNodeConfig>) -> Result<(), String> {
for (name, node) in nodes {
validate(name, node)?;
}
Ok(())
}
/// Validates that a single MongoDB node is reachable.
///
/// # Arguments
///
/// * `name` — service name from config (e.g. "app_server") — used in error messages
/// * `node` — `RecNodeConfig` for this node
///
/// # Returns
///
/// `Ok(())` if the TCP handshake succeeds within 5 seconds.
/// `Err(String)` with a descriptive message on failure.
///
/// # History
///
/// * `2026-04-04` - mks - original coding
pub fn validate(name: &str, node: &RecNodeConfig) -> Result<(), String> {
let addr_str = format!("{}:{}", node.host, node.port);
let addr: std::net::SocketAddr = addr_str
.parse()
.map_err(|e| format!("Invalid MongoDB address for rec_services.{} ({}): {}", name, addr_str, e))?;
TcpStream::connect_timeout(&addr, Duration::from_secs(5))
.map_err(|e| format!("MongoDB unreachable at rec_services.{} ({}): {}", name, addr_str, e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::load_from;
fn test_cfg() -> crate::config::BedsConfig {
load_from("tests/fixtures/beds_test.toml", "")
.expect("test fixture beds_test.toml failed to load")
}
#[test]
fn validate_err_on_closed_port() {
let mut cfg = test_cfg();
let node = cfg.rec_services.get_mut("app_server").unwrap();
node.port = 1;
assert!(validate("app_server", node).is_err());
}
#[test]
fn validate_err_on_bad_address() {
let mut cfg = test_cfg();
let node = cfg.rec_services.get_mut("app_server").unwrap();
node.host = "not_a_valid_host!!!".to_string();
assert!(validate("app_server", node).is_err());
}
}