diff --git a/crates/api/src/internal/mod.rs b/crates/api/src/internal/mod.rs new file mode 100644 index 0000000..38c33d5 --- /dev/null +++ b/crates/api/src/internal/mod.rs @@ -0,0 +1,15 @@ +//! Debug and development tools that will be removed in the future +//! These modules provide endpoints for internal testing and debugging + +pub mod namespace; +pub mod ng; +pub mod range; + +#[cfg(test)] +mod tests { + #[test] + fn internal_modules_available() { + // This test just ensures the module structure compiles correctly + assert!(true); + } +} \ No newline at end of file diff --git a/crates/api/src/internal/namespace.rs b/crates/api/src/internal/namespace.rs new file mode 100644 index 0000000..0f4b74a --- /dev/null +++ b/crates/api/src/internal/namespace.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, +}; + +use crate::AppState; + +/// Debug endpoint to create a new namespace +/// This is for development purposes and will be removed in the future +pub async fn post_ns(State(state): State>, Path(name): Path) -> Result { + let ns = state.collar.store.namespaces(); + + // First, ensure the namespace exists + if let Err(e) = ns.define(&name) { + return Err(format!("Failed to define namespace '{}': {:?}", name, e)); + } + + Ok("ok".to_string()) +} + +/// Debug endpoint to set a key-value pair in a namespace +/// This is for development purposes and will be removed in the future +pub async fn post_ns_key(State(state): State>, Path((name, key, value)): Path<(String, String, String)>) -> Result { + let ns = state.collar.store.namespaces(); + + // Try to reserve the key-value pair + match ns.reserve(&name, &key, &value) { + Ok(_) => Ok(format!("Reserved key '{}' with value '{}' in namespace '{}'", key, value, name)), + Err(e) => Err(format!("Failed to reserve key '{}' in namespace '{}': {:?}", key, name, e)), + } +} + +/// Debug endpoint to get a key from a namespace +/// This is for development purposes and will be removed in the future +pub async fn get_ns_key(State(state): State>, Path((name, key)): Path<(String, String)>) -> Result { + let ns = state.collar.store.namespaces(); + + match ns.get(&name, &key) { + Ok(Some(value)) => Ok(value), + Ok(None) => Err(format!("Key '{}' not found in namespace '{}'", key, name)), + Err(e) => Err(format!("Failed to get key '{}' from namespace '{}': {:?}", key, name, e)), + } +} + diff --git a/crates/api/src/internal/ng.rs b/crates/api/src/internal/ng.rs new file mode 100644 index 0000000..148834c --- /dev/null +++ b/crates/api/src/internal/ng.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, +}; + +use crate::AppState; + +/// Debug endpoint for creating network interfaces +/// This is for development purposes and will be removed in the future +pub async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { + let fname = name.clone(); + let result = state.collar.leash().gated(move || { + Ok(libcollar::ng::new_eiface(&name)) + }).await; + Ok(format!("sup {} => {:?}", &fname, result)) +} \ No newline at end of file diff --git a/crates/api/src/internal/range.rs b/crates/api/src/internal/range.rs new file mode 100644 index 0000000..08b950c --- /dev/null +++ b/crates/api/src/internal/range.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, +}; + +use crate::AppState; + +/// Debug endpoint to define a new range +/// This is for development purposes and will be removed in the future +pub async fn post_range_define(State(state): State>, Path((name, size)): Path<(String, u64)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.define(&name, size) { + Ok(_) => Ok(format!("Defined range '{}' with size {}", name, size)), + Err(e) => Err(format!("Failed to define range '{}': {:?}", name, e)), + } +} + +/// Debug endpoint to assign a value to a range +/// This is for development purposes and will be removed in the future +pub async fn post_range_assign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.assign(&name, &value) { + Ok(position) => Ok(format!("Assigned '{}' to position {} in range '{}'", value, position, name)), + Err(e) => Err(format!("Failed to assign '{}' to range '{}': {:?}", value, name, e)), + } +} + +/// Debug endpoint to unassign a value from a range +/// This is for development purposes and will be removed in the future +pub async fn post_range_unassign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.unassign(&name, &value) { + Ok(true) => Ok(format!("Unassigned '{}' from range '{}'", value, name)), + Ok(false) => Err(format!("Value '{}' not found in range '{}'", value, name)), + Err(e) => Err(format!("Failed to unassign '{}' from range '{}': {:?}", value, name, e)), + } +} + +/// Debug endpoint to get a value at a specific position in a range +/// This is for development purposes and will be removed in the future +pub async fn get_range_position(State(state): State>, Path((name, position)): Path<(String, u64)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.get(&name, position) { + Ok(Some(value)) => Ok(value), + Ok(None) => Err(format!("No value at position {} in range '{}'", position, name)), + Err(e) => Err(format!("Failed to get position {} from range '{}': {:?}", position, name, e)), + } +} + +/// Debug endpoint to list all assignments in a range +/// This is for development purposes and will be removed in the future +pub async fn get_range_list(State(state): State>, Path(name): Path) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.list_range(&name) { + Ok(assignments) => { + let mut result = format!("Assignments in range '{}':\n", name); + for (position, value) in assignments { + result.push_str(&format!(" {}: {}\n", position, value)); + } + Ok(result) + }, + Err(e) => Err(format!("Failed to list range '{}': {:?}", name, e)), + } +} + +/// Debug endpoint to list all ranges +/// This is for development purposes and will be removed in the future +pub async fn get_ranges_list(State(state): State>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.list_ranges() { + Ok(range_list) => { + let mut result = "Available ranges:\n".to_string(); + for (name, size) in range_list { + result.push_str(&format!(" {} (size: {})\n", name, size)); + } + Ok(result) + }, + Err(e) => Err(format!("Failed to list ranges: {:?}", e)), + } +} + diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 69e8a59..7025faa 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,318 +1,93 @@ //! # Collar API //! //! HTTP API for network management functionality. //! //! ## Network Endpoints //! //! - `GET /networks` - List all networks //! - `POST /networks` - Create a new network //! - `GET /networks/{name}` - Get network details //! - `DELETE /networks/{name}` - Delete a network //! //! ## Network Creation //! //! Networks can be created with different provider and assigner configurations: //! //! ### Basic Network //! ```json //! { //! "name": "test-network", //! "cidr": "192.168.1.0/24" //! } //! ``` //! //! ### Network with Basic Assigner //! ```json //! { //! "name": "test-network", //! "cidr": "192.168.1.0/24", //! "assigner_type": "basic", //! "assigner_config": { //! "strategy": "sequential" //! } //! } //! ``` //! //! ### Network with Range Assigner //! ```json //! { //! "name": "test-network", //! "cidr": "192.168.1.0/24", //! "assigner_type": "range", //! "assigner_config": { //! "range_name": "my-range" //! } //! } //! ``` +//! +//! ## Internal Debug Endpoints +//! +//! The API also provides several internal debug endpoints that will be removed in the future. +//! These are organized in the `internal` module. use std::sync::Arc; -use axum::{ - routing::get, - extract::{State, Path}, - routing::post, - response::{Response, IntoResponse}, - http::StatusCode, - Json, -}; -use serde::{Deserialize, Serialize}; -use store::network::{NetRange, BasicProvider, BasicAssigner, RangeAssigner}; +use axum::routing::get; pub use axum::serve; -struct AppState { +mod network; +mod internal; + +pub struct AppState { collar: libcollar::State, } pub fn app(collar: libcollar::State) -> axum::Router { let state = Arc::new(AppState { collar }); - // build our application with a single route + // build our application with routes let app = axum::Router::new() .route("/", get(|| async { "collared." })) - .route("/networks", get(get_networks)) - .route("/networks", post(post_networks)) - .route("/networks/{name}", get(get_network)) - .route("/networks/{name}", axum::routing::delete(delete_network)) - .route("/ng/eiface/{name}", post(post_ng_eiface)) - .route("/store/namespace/{name}", post(post_ns)) - .route("/store/namespace/{name}/{key}/{value}", post(post_ns_key)) - .route("/store/namespace/{name}/{key}", get(get_ns_key)) - .route("/store/range/new/{name}/{size}", post(post_range_define)) - .route("/store/range/{name}/assign/{value}", post(post_range_assign)) - .route("/store/range/{name}/unassign/{value}", post(post_range_unassign)) - .route("/store/range/{name}/{position}", get(get_range_position)) - .route("/store/range/{name}", get(get_range_list)) - .route("/store/ranges", get(get_ranges_list)) + // Network management routes + .route("/networks", get(network::get_networks)) + .route("/networks", axum::routing::post(network::post_networks)) + .route("/networks/{name}", get(network::get_network)) + .route("/networks/{name}", axum::routing::delete(network::delete_network)) + // Internal debug routes + .route("/internal/ng/eiface/{name}", axum::routing::post(internal::ng::post_ng_eiface)) + .route("/internal/store/namespace/{name}", axum::routing::post(internal::namespace::post_ns)) + .route("/internal/store/namespace/{name}/{key}/{value}", axum::routing::post(internal::namespace::post_ns_key)) + .route("/internal/store/namespace/{name}/{key}", get(internal::namespace::get_ns_key)) + .route("/internal/store/range/new/{name}/{size}", axum::routing::post(internal::range::post_range_define)) + .route("/internal/store/range/{name}/assign/{value}", axum::routing::post(internal::range::post_range_assign)) + .route("/internal/store/range/{name}/unassign/{value}", axum::routing::post(internal::range::post_range_unassign)) + .route("/internal/store/range/{name}/{position}", get(internal::range::get_range_position)) + .route("/internal/store/range/{name}", get(internal::range::get_range_list)) + .route("/internal/store/ranges", get(internal::range::get_ranges_list)) .with_state(state) .layer(tower_http::trace::TraceLayer::new_for_http()); app } -async fn post_ns(State(state): State>, Path(name): Path) -> Result { - let ns = state.collar.store.namespaces(); - - // First, ensure the namespace exists - if let Err(e) = ns.define(&name) { - return Err(format!("Failed to define namespace '{}': {:?}", name, e)); - } - - Ok("ok".to_string()) -} - -async fn post_ns_key(State(state): State>, Path((name, key, value)): Path<(String, String, String)>) -> Result { - let ns = state.collar.store.namespaces(); - - // Try to reserve the key-value pair - match ns.reserve(&name, &key, &value) { - Ok(_) => Ok(format!("Reserved key '{}' with value '{}' in namespace '{}'", key, value, name)), - Err(e) => Err(format!("Failed to reserve key '{}' in namespace '{}': {:?}", key, name, e)), - } -} - -async fn get_ns_key(State(state): State>, Path((name, key)): Path<(String, String)>) -> Result { - let ns = state.collar.store.namespaces(); - - match ns.get(&name, &key) { - Ok(Some(value)) => Ok(value), - Ok(None) => Err(format!("Key '{}' not found in namespace '{}'", key, name)), - Err(e) => Err(format!("Failed to get key '{}' from namespace '{}': {:?}", key, name, e)), - } -} - -async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { - let fname = name.clone(); - let result = state.collar.leash().gated(move || { - Ok(libcollar::ng::new_eiface(&name)) - }).await; - Ok(format!("sup {} => {:?}", &fname, result)) -} - -async fn post_range_define(State(state): State>, Path((name, size)): Path<(String, u64)>) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.define(&name, size) { - Ok(_) => Ok(format!("Defined range '{}' with size {}", name, size)), - Err(e) => Err(format!("Failed to define range '{}': {:?}", name, e)), - } -} - -async fn post_range_assign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.assign(&name, &value) { - Ok(position) => Ok(format!("Assigned '{}' to position {} in range '{}'", value, position, name)), - Err(e) => Err(format!("Failed to assign '{}' to range '{}': {:?}", value, name, e)), - } -} - -async fn get_range_position(State(state): State>, Path((name, position)): Path<(String, u64)>) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.get(&name, position) { - Ok(Some(value)) => Ok(value), - Ok(None) => Err(format!("No value at position {} in range '{}'", position, name)), - Err(e) => Err(format!("Failed to get position {} from range '{}': {:?}", position, name, e)), - } -} - -async fn get_range_list(State(state): State>, Path(name): Path) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.list_range(&name) { - Ok(assignments) => { - let mut result = format!("Assignments in range '{}':\n", name); - for (position, value) in assignments { - result.push_str(&format!(" {}: {}\n", position, value)); - } - Ok(result) - }, - Err(e) => Err(format!("Failed to list range '{}': {:?}", name, e)), - } -} - -async fn get_ranges_list(State(state): State>) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.list_ranges() { - Ok(range_list) => { - let mut result = "Available ranges:\n".to_string(); - for (name, size) in range_list { - result.push_str(&format!(" {} (size: {})\n", name, size)); - } - Ok(result) - }, - Err(e) => Err(format!("Failed to list ranges: {:?}", e)), - } -} - -#[derive(Serialize, Deserialize)] -struct CreateNetworkRequest { - name: String, - cidr: String, - provider_type: Option, - provider_config: Option, - assigner_type: Option, - assigner_config: Option, -} - -async fn get_networks(State(state): State>) -> impl IntoResponse { - let networks = state.collar.store.networks(); - - match networks.list() { - Ok(network_list) => (StatusCode::OK, Json(network_list)).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list networks: {:?}", e)).into_response(), - } -} - -async fn post_networks(State(state): State>, Json(req): Json) -> impl IntoResponse { - let networks = state.collar.store.networks(); - - // Parse the CIDR - let netrange = match NetRange::from_cidr(&req.cidr) { - Ok(range) => range, - Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid CIDR '{}': {:?}", req.cidr, e)).into_response(), - }; - - // Create basic provider with optional config - let provider = if let Some(config) = req.provider_config { - BasicProvider::new().with_config("config", &config.to_string()) - } else { - BasicProvider::new() - }; - - // Handle different assigner types with separate create calls - let result = if let Some(assigner_type) = req.assigner_type { - match assigner_type.as_str() { - "basic" => { - let mut basic_assigner = BasicAssigner::new("sequential"); - if let Some(config) = req.assigner_config { - basic_assigner = basic_assigner.with_config("config", &config.to_string()); - } - networks.create(&req.name, netrange, provider, Some(basic_assigner)) - }, - "range" => { - if let Some(config) = req.assigner_config { - if let Some(range_name) = config.get("range_name").and_then(|v| v.as_str()) { - let mut range_assigner = RangeAssigner::with_range_name(range_name); - if let Some(extra_config) = config.get("config") { - range_assigner = range_assigner.with_config("config", &extra_config.to_string()); - } - networks.create(&req.name, netrange, provider, Some(range_assigner)) - } else { - return (StatusCode::BAD_REQUEST, "Range assigner requires 'range_name' in config".to_string()).into_response(); - } - } else { - return (StatusCode::BAD_REQUEST, "Range assigner requires config with 'range_name'".to_string()).into_response(); - } - }, - _ => return (StatusCode::BAD_REQUEST, format!("Unknown assigner type: {}", assigner_type)).into_response(), - } - } else { - networks.create(&req.name, netrange, provider, None::) - }; - - // Handle the result - match result { - Ok(_) => (StatusCode::OK, format!("Created network '{}'", req.name)).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create network '{}': {:?}", req.name, e)).into_response(), - } -} - -async fn get_network(State(state): State>, Path(name): Path) -> impl IntoResponse { - let networks = state.collar.store.networks(); - - match networks.get(&name) { - Ok(Some(network)) => { - let network_json = serde_json::json!({ - "name": network.name, - "netrange": match network.netrange { - NetRange::V4 { addr, prefix } => { - serde_json::json!({ - "type": "v4", - "addr": addr.to_string(), - "prefix": prefix - }) - }, - NetRange::V6 { addr, prefix } => { - serde_json::json!({ - "type": "v6", - "addr": addr.to_string(), - "prefix": prefix - }) - } - }, - "provider_type": network.provider_type, - "provider_config": serde_json::from_str::(&network.provider_config).unwrap_or(serde_json::Value::Null), - "assigner_type": network.assigner_type, - "assigner_config": network.assigner_config.as_ref() - .and_then(|config| serde_json::from_str::(config).ok()) - .unwrap_or(serde_json::Value::Null) - }); - (StatusCode::OK, Json(network_json)).into_response() - }, - Ok(None) => (StatusCode::NOT_FOUND, format!("Network '{}' not found", name)).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get network '{}': {:?}", name, e)).into_response(), - } -} - -async fn delete_network(State(state): State>, Path(name): Path) -> impl IntoResponse { - let networks = state.collar.store.networks(); - - match networks.delete(&name) { - Ok(true) => (StatusCode::OK, format!("Deleted network '{}'", name)).into_response(), - Ok(false) => (StatusCode::NOT_FOUND, format!("Network '{}' not found", name)).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete network '{}': {:?}", name, e)).into_response(), - } -} - -async fn post_range_unassign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { - let ranges = state.collar.store.ranges(); - - match ranges.unassign(&name, &value) { - Ok(true) => Ok(format!("Unassigned '{}' from range '{}'", value, name)), - Ok(false) => Err(format!("Value '{}' not found in range '{}'", value, name)), - Err(e) => Err(format!("Failed to unassign '{}' from range '{}': {:?}", value, name, e)), - } -} diff --git a/crates/api/src/network.rs b/crates/api/src/network.rs new file mode 100644 index 0000000..0f568a9 --- /dev/null +++ b/crates/api/src/network.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde::{Deserialize, Serialize}; +use store::network::{BasicAssigner, BasicProvider, NetRange, RangeAssigner}; + +use crate::AppState; + +#[derive(Serialize, Deserialize)] +pub struct CreateNetworkRequest { + pub name: String, + pub cidr: String, + pub provider_type: Option, + pub provider_config: Option, + pub assigner_type: Option, + pub assigner_config: Option, +} + +pub async fn get_networks(State(state): State>) -> impl IntoResponse { + let networks = state.collar.store.networks(); + + match networks.list() { + Ok(network_list) => (StatusCode::OK, Json(network_list)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list networks: {:?}", e)).into_response(), + } +} + +pub async fn post_networks(State(state): State>, Json(req): Json) -> impl IntoResponse { + let networks = state.collar.store.networks(); + + // Parse the CIDR + let netrange = match NetRange::from_cidr(&req.cidr) { + Ok(range) => range, + Err(e) => return (StatusCode::BAD_REQUEST, format!("Invalid CIDR '{}': {:?}", req.cidr, e)).into_response(), + }; + + // Create basic provider with optional config + let provider = if let Some(config) = req.provider_config { + BasicProvider::new().with_config("config", &config.to_string()) + } else { + BasicProvider::new() + }; + + // Handle different assigner types with separate create calls + let result = if let Some(assigner_type) = req.assigner_type { + match assigner_type.as_str() { + "basic" => { + let mut basic_assigner = BasicAssigner::new("sequential"); + if let Some(config) = req.assigner_config { + basic_assigner = basic_assigner.with_config("config", &config.to_string()); + } + networks.create(&req.name, netrange, provider, Some(basic_assigner)) + }, + "range" => { + if let Some(config) = req.assigner_config { + if let Some(range_name) = config.get("range_name").and_then(|v| v.as_str()) { + let mut range_assigner = RangeAssigner::with_range_name(range_name); + if let Some(extra_config) = config.get("config") { + range_assigner = range_assigner.with_config("config", &extra_config.to_string()); + } + networks.create(&req.name, netrange, provider, Some(range_assigner)) + } else { + return (StatusCode::BAD_REQUEST, "Range assigner requires 'range_name' in config".to_string()).into_response(); + } + } else { + return (StatusCode::BAD_REQUEST, "Range assigner requires config with 'range_name'".to_string()).into_response(); + } + }, + _ => return (StatusCode::BAD_REQUEST, format!("Unknown assigner type: {}", assigner_type)).into_response(), + } + } else { + networks.create(&req.name, netrange, provider, None::) + }; + + // Handle the result + match result { + Ok(_) => (StatusCode::OK, format!("Created network '{}'", req.name)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create network '{}': {:?}", req.name, e)).into_response(), + } +} + +pub async fn get_network(State(state): State>, Path(name): Path) -> impl IntoResponse { + let networks = state.collar.store.networks(); + + match networks.get(&name) { + Ok(Some(network)) => { + let network_json = serde_json::json!({ + "name": network.name, + "netrange": match network.netrange { + NetRange::V4 { addr, prefix } => { + serde_json::json!({ + "type": "v4", + "addr": addr.to_string(), + "prefix": prefix + }) + }, + NetRange::V6 { addr, prefix } => { + serde_json::json!({ + "type": "v6", + "addr": addr.to_string(), + "prefix": prefix + }) + } + }, + "provider_type": network.provider_type, + "provider_config": serde_json::from_str::(&network.provider_config).unwrap_or(serde_json::Value::Null), + "assigner_type": network.assigner_type, + "assigner_config": network.assigner_config.as_ref() + .and_then(|config| serde_json::from_str::(config).ok()) + .unwrap_or(serde_json::Value::Null) + }); + (StatusCode::OK, Json(network_json)).into_response() + }, + Ok(None) => (StatusCode::NOT_FOUND, format!("Network '{}' not found", name)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get network '{}': {:?}", name, e)).into_response(), + } +} + +pub async fn delete_network(State(state): State>, Path(name): Path) -> impl IntoResponse { + let networks = state.collar.store.networks(); + + match networks.delete(&name) { + Ok(true) => (StatusCode::OK, format!("Deleted network '{}'", name)).into_response(), + Ok(false) => (StatusCode::NOT_FOUND, format!("Network '{}' not found", name)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete network '{}': {:?}", name, e)).into_response(), + } +} + diff --git a/crates/api/tests/network_api_tests.rs b/crates/api/tests/network_api_tests.rs index 9204e6f..deb35ac 100644 --- a/crates/api/tests/network_api_tests.rs +++ b/crates/api/tests/network_api_tests.rs @@ -1,249 +1,249 @@ use axum::http::StatusCode; use axum_test::TestServer; use serde_json::json; use tempfile::TempDir; async fn create_test_app() -> (TestServer, TempDir) { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); let db_path = temp_dir.path().join("test_db"); let collar_state = libcollar::new_test(&db_path.to_string_lossy()).expect("Failed to create collar state"); let app = api::app(collar_state); let server = TestServer::new(app).expect("Failed to create test server"); (server, temp_dir) } #[tokio::test] async fn test_get_networks_empty() { let (server, _temp_dir) = create_test_app().await; let response = server.get("/networks").await; response.assert_status(StatusCode::OK); let networks: Vec = response.json(); assert!(networks.is_empty()); } #[tokio::test] async fn test_create_network_basic() { let (server, _temp_dir) = create_test_app().await; let create_request = json!({ "name": "test-network", "cidr": "192.168.1.0/24" }); let response = server.post("/networks").json(&create_request).await; response.assert_status(StatusCode::OK); response.assert_text("Created network 'test-network'"); } #[tokio::test] async fn test_create_network_with_assigner() { let (server, _temp_dir) = create_test_app().await; let create_request = json!({ "name": "test-network-assigner", "cidr": "10.0.0.0/16", "assigner_type": "basic", "assigner_config": { "strategy": "sequential" } }); let response = server.post("/networks").json(&create_request).await; response.assert_status(StatusCode::OK); response.assert_text("Created network 'test-network-assigner'"); } #[tokio::test] async fn test_create_network_with_range_assigner() { let (server, _temp_dir) = create_test_app().await; // First create a range - let range_response = server.post("/store/range/new/test-range/256").await; + let range_response = server.post("/internal/store/range/new/test-range/256").await; range_response.assert_status(StatusCode::OK); let create_request = json!({ "name": "test-range-network", "cidr": "172.16.0.0/24", "assigner_type": "range", "assigner_config": { "range_name": "test-range" } }); let response = server.post("/networks").json(&create_request).await; response.assert_status(StatusCode::OK); response.assert_text("Created network 'test-range-network'"); } #[tokio::test] async fn test_create_network_invalid_cidr() { let (server, _temp_dir) = create_test_app().await; let create_request = json!({ "name": "invalid-network", "cidr": "invalid-cidr" }); let response = server.post("/networks").json(&create_request).await; response.assert_status(StatusCode::BAD_REQUEST); let error_text = response.text(); assert!(error_text.contains("Invalid CIDR")); } #[tokio::test] async fn test_get_network_after_creation() { let (server, _temp_dir) = create_test_app().await; // Create network first let create_request = json!({ "name": "get-test-network", "cidr": "192.168.100.0/24", "provider_config": { "region": "us-west-2" } }); let create_response = server.post("/networks").json(&create_request).await; create_response.assert_status(StatusCode::OK); // Get the network let response = server.get("/networks/get-test-network").await; response.assert_status(StatusCode::OK); let network: serde_json::Value = response.json(); assert_eq!(network["name"], "get-test-network"); assert_eq!(network["netrange"]["type"], "v4"); assert_eq!(network["netrange"]["addr"], "192.168.100.0"); assert_eq!(network["netrange"]["prefix"], 24); assert_eq!(network["provider_type"], "basic"); } #[tokio::test] async fn test_get_nonexistent_network() { let (server, _temp_dir) = create_test_app().await; let response = server.get("/networks/nonexistent").await; response.assert_status(StatusCode::NOT_FOUND); let error_text = response.text(); assert!(error_text.contains("not found")); } #[tokio::test] async fn test_list_networks_after_creation() { let (server, _temp_dir) = create_test_app().await; // Create multiple networks let networks = vec![ ("network-1", "192.168.1.0/24"), ("network-2", "192.168.2.0/24"), ("network-v6", "2001:db8::/64"), ]; for (name, cidr) in &networks { let create_request = json!({ "name": name, "cidr": cidr }); let response = server.post("/networks").json(&create_request).await; response.assert_status(StatusCode::OK); } // List networks let response = server.get("/networks").await; response.assert_status(StatusCode::OK); let network_list: Vec = response.json(); assert_eq!(network_list.len(), 3); for (name, _) in &networks { assert!(network_list.contains(&name.to_string())); } } #[tokio::test] async fn test_delete_network() { let (server, _temp_dir) = create_test_app().await; // Create network first let create_request = json!({ "name": "delete-test-network", "cidr": "10.10.0.0/16" }); let create_response = server.post("/networks").json(&create_request).await; create_response.assert_status(StatusCode::OK); // Verify it exists let get_response = server.get("/networks/delete-test-network").await; get_response.assert_status(StatusCode::OK); // Delete the network let delete_response = server.delete("/networks/delete-test-network").await; delete_response.assert_status(StatusCode::OK); delete_response.assert_text("Deleted network 'delete-test-network'"); // Verify it's gone let get_after_delete = server.get("/networks/delete-test-network").await; get_after_delete.assert_status(StatusCode::NOT_FOUND); } #[tokio::test] async fn test_delete_nonexistent_network() { let (server, _temp_dir) = create_test_app().await; let response = server.delete("/networks/nonexistent").await; response.assert_status(StatusCode::NOT_FOUND); let error_text = response.text(); assert!(error_text.contains("not found")); } #[tokio::test] async fn test_ipv6_network_creation_and_retrieval() { let (server, _temp_dir) = create_test_app().await; let create_request = json!({ "name": "ipv6-network", "cidr": "2001:db8::/32" }); let create_response = server.post("/networks").json(&create_request).await; create_response.assert_status(StatusCode::OK); // Get the IPv6 network let response = server.get("/networks/ipv6-network").await; response.assert_status(StatusCode::OK); let network: serde_json::Value = response.json(); assert_eq!(network["name"], "ipv6-network"); assert_eq!(network["netrange"]["type"], "v6"); assert_eq!(network["netrange"]["addr"], "2001:db8::"); assert_eq!(network["netrange"]["prefix"], 32); } #[tokio::test] async fn test_create_duplicate_network() { let (server, _temp_dir) = create_test_app().await; let create_request = json!({ "name": "duplicate-network", "cidr": "192.168.50.0/24" }); // Create first network let first_response = server.post("/networks").json(&create_request).await; first_response.assert_status(StatusCode::OK); // Try to create duplicate let duplicate_response = server.post("/networks").json(&create_request).await; duplicate_response.assert_status(StatusCode::INTERNAL_SERVER_ERROR); let error_text = duplicate_response.text(); assert!(error_text.contains("Failed to create network")); } \ No newline at end of file diff --git a/crates/store/src/combined.rs b/crates/store/src/combined.rs index 0905601..8f3ca7e 100644 --- a/crates/store/src/combined.rs +++ b/crates/store/src/combined.rs @@ -1,563 +1,562 @@ //! Generic transaction module for atomic operations across multiple stores. //! //! # Example //! //! This module provides a generic transaction mechanism for atomic operations across //! different types of stores. Each store implementation can plug into this system //! by implementing the `TransactionProvider` trait. //! //! ```no_run //! use store::{Transaction, TransactionContext}; //! //! // Assuming you have stores and trees //! # fn example_usage() -> store::Result<()> { //! # let temp_dir = tempfile::tempdir().unwrap(); //! # let db = sled::open(temp_dir.path())?; //! # let range_store = store::RangeStore::open(&db)?; //! # let namespace_store = store::NamespaceStore::open(&db)?; //! # let metadata_tree = db.open_tree("metadata")?; //! //! // Create a transaction with the stores you want to include //! let transaction = Transaction::new() //! .with_store("ranges", &range_store) //! .with_store("namespaces", &namespace_store) //! .with_tree(&metadata_tree); //! //! // Execute the transaction //! let result = transaction.execute(|ctx| { //! // Access stores by name //! let range_trees = ctx.store_trees("ranges")?; //! let namespace_trees = ctx.store_trees("namespaces")?; //! //! // Access additional trees by index //! let metadata = ctx.tree(0)?; //! -//! metadata.insert("operation", "test") -//! .map_err(|e| store::Error::StoreError(e))?; +//! metadata.insert("operation", "test")?; //! //! Ok(()) //! })?; //! # Ok(()) //! # } //! ``` use crate::{Result, Error}; use sled::Transactional; use std::collections::HashMap; /// Helper function to convert transaction errors fn convert_transaction_error(e: sled::transaction::ConflictableTransactionError, default_error: Error) -> Error { match e { sled::transaction::ConflictableTransactionError::Storage(storage_err) => Error::StoreError(storage_err), sled::transaction::ConflictableTransactionError::Abort(_) => default_error, _ => Error::StoreError(sled::Error::Unsupported("Unknown transaction error".to_string())), } } /// Trait for types that can provide trees to a transaction pub trait TransactionProvider { /// Return the trees that should be included in a transaction fn transaction_trees(&self) -> Vec<&sled::Tree>; } /// Implement TransactionProvider for individual trees impl TransactionProvider for sled::Tree { fn transaction_trees(&self) -> Vec<&sled::Tree> { vec![self] } } /// Implement TransactionProvider for RangeStore impl TransactionProvider for crate::RangeStore { fn transaction_trees(&self) -> Vec<&sled::Tree> { vec![&self.names, &self.map, &self.assign] } } /// Implement TransactionProvider for NamespaceStore impl TransactionProvider for crate::NamespaceStore { fn transaction_trees(&self) -> Vec<&sled::Tree> { vec![&self.names, &self.spaces] } } /// Implement TransactionProvider for NetworkStore impl TransactionProvider for crate::NetworkStore { fn transaction_trees(&self) -> Vec<&sled::Tree> { let mut trees = self.namespaces.transaction_trees(); trees.push(&self.networks); trees } } /// Generic transaction context provided to transaction operations pub struct TransactionContext<'ctx> { store_map: HashMap, // name -> (start_idx, end_idx) trees: Vec<&'ctx sled::transaction::TransactionalTree>, transactional_trees: &'ctx [sled::transaction::TransactionalTree], additional_trees_start: usize, } impl<'ctx> TransactionContext<'ctx> { /// Create a new transaction context fn new( store_map: HashMap, trees: Vec<&'ctx sled::transaction::TransactionalTree>, transactional_trees: &'ctx [sled::transaction::TransactionalTree], additional_trees_start: usize, ) -> Self { Self { store_map, trees, transactional_trees, additional_trees_start, } } /// Get trees for a store by name pub fn store_trees(&self, store_name: &str) -> Result<&[&sled::transaction::TransactionalTree]> { let (start_idx, end_idx) = self.store_map .get(store_name) .ok_or_else(|| Error::StoreError(sled::Error::Unsupported(format!("Store '{}' not found in transaction", store_name))))?; Ok(&self.trees[*start_idx..*end_idx]) } /// Access additional trees by index pub fn tree(&self, index: usize) -> Result<&sled::transaction::TransactionalTree> { self.trees.get(self.additional_trees_start + index) .copied() .ok_or_else(|| Error::StoreError(sled::Error::Unsupported(format!("Tree at index {} not found", index)))) } /// Access a raw transactional tree by its absolute index pub fn raw_tree(&self, index: usize) -> Result<&sled::transaction::TransactionalTree> { self.trees.get(index) .copied() .ok_or_else(|| Error::StoreError(sled::Error::Unsupported(format!("Raw tree at index {} not found", index)))) } /// Access the entire slice of transactional trees pub fn all_trees(&self) -> &[sled::transaction::TransactionalTree] { self.transactional_trees } /// Get store map for debugging or extension purposes pub fn store_map(&self) -> &HashMap { &self.store_map } } /// Generic transaction struct for atomic operations across multiple stores pub struct Transaction<'a> { stores: HashMap, additional_trees: Vec<&'a sled::Tree>, } impl<'a> Transaction<'a> { /// Create a new empty transaction pub fn new() -> Self { Self { stores: HashMap::new(), additional_trees: Vec::new(), } } /// Add a store with a name identifier pub fn with_store(mut self, name: &str, store: &'a T) -> Self { self.stores.insert(name.to_string(), store); self } /// Add a single tree to the transaction pub fn with_tree(mut self, tree: &'a sled::Tree) -> Self { self.additional_trees.push(tree); self } /// Execute a transaction with the configured stores pub fn execute(&self, operations: F) -> Result where F: Fn(&TransactionContext) -> Result, { // Collect all trees for the transaction let mut all_trees = Vec::new(); let mut store_map = HashMap::new(); // Add trees from stores for (name, store) in &self.stores { let start_idx = all_trees.len(); all_trees.extend(store.transaction_trees()); let end_idx = all_trees.len(); store_map.insert(name.clone(), (start_idx, end_idx)); } // Add additional trees let additional_trees_start = all_trees.len(); all_trees.extend(&self.additional_trees); // Execute the transaction let result = all_trees.transaction(|trees| { let context = TransactionContext::new( store_map.clone(), trees.into_iter().collect(), trees, additional_trees_start, ); operations(&context).map_err(|e| match e { Error::StoreError(store_err) => sled::transaction::ConflictableTransactionError::Storage(store_err), _ => sled::transaction::ConflictableTransactionError::Abort(()), }) }).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => Error::StoreError(sled::Error::Unsupported("Transaction aborted".to_string())), sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; Ok(result) } } impl<'a> Default for Transaction<'a> { fn default() -> Self { Self::new() } } /// Legacy alias for backward compatibility pub type CombinedTransaction<'a> = Transaction<'a>; /// Legacy alias for backward compatibility pub type CombinedTransactionContext<'ctx> = TransactionContext<'ctx>; #[cfg(test)] mod tests { use super::*; use crate::{RangeStore, NamespaceStore}; use tempfile::tempdir; fn create_test_stores() -> Result<(RangeStore, NamespaceStore, sled::Db)> { let temp_dir = tempdir().unwrap(); let db = sled::open(temp_dir.path())?; let range_store = RangeStore::open(&db)?; let namespace_store = NamespaceStore::open(&db)?; Ok((range_store, namespace_store, db)) } #[test] fn test_generic_transaction_basic() -> Result<()> { let (range_store, namespace_store, db) = create_test_stores()?; // Setup: define range and namespace range_store.define("test_range", 100)?; namespace_store.define("test_namespace")?; // Create additional tree for testing let extra_tree = db.open_tree("extra")?; let transaction = Transaction::new() .with_store("ranges", &range_store) .with_store("namespaces", &namespace_store) .with_tree(&extra_tree); // Execute transaction using generic interface transaction.execute(|ctx| { // Access range store trees let range_trees = ctx.store_trees("ranges")?; assert_eq!(range_trees.len(), 3); // names, map, assign // Access namespace store trees let namespace_trees = ctx.store_trees("namespaces")?; assert_eq!(namespace_trees.len(), 2); // names, spaces // Use additional tree let tree = ctx.tree(0)?; tree.insert("test_key", "test_value") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; Ok(()) })?; // Verify the additional tree was modified let value = extra_tree.get("test_key")?; assert_eq!(value, Some("test_value".as_bytes().into())); Ok(()) } #[test] fn test_transaction_with_single_store() -> Result<()> { let (range_store, _, _) = create_test_stores()?; // Setup range_store.define("test_range", 50)?; let transaction = Transaction::new() .with_store("ranges", &range_store); // Execute transaction with just one store transaction.execute(|ctx| { let range_trees = ctx.store_trees("ranges")?; assert_eq!(range_trees.len(), 3); // Verify we can access the trees let _names_tree = range_trees[0]; let _map_tree = range_trees[1]; let _assign_tree = range_trees[2]; Ok(()) })?; Ok(()) } #[test] fn test_transaction_with_only_trees() -> Result<()> { let (_, _, db) = create_test_stores()?; let tree1 = db.open_tree("tree1")?; let tree2 = db.open_tree("tree2")?; let transaction = Transaction::new() .with_tree(&tree1) .with_tree(&tree2); transaction.execute(|ctx| { let t1 = ctx.tree(0)?; let t2 = ctx.tree(1)?; t1.insert("key1", "value1") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; t2.insert("key2", "value2") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; Ok(()) })?; // Verify the trees were modified let value1 = tree1.get("key1")?; assert_eq!(value1, Some("value1".as_bytes().into())); let value2 = tree2.get("key2")?; assert_eq!(value2, Some("value2".as_bytes().into())); Ok(()) } #[test] fn test_multiple_stores_same_type() -> Result<()> { let (range_store1, _, db) = create_test_stores()?; let range_store2 = RangeStore::open(&db)?; // Setup both stores range_store1.define("range1", 50)?; range_store2.define("range2", 100)?; let transaction = Transaction::new() .with_store("ranges1", &range_store1) .with_store("ranges2", &range_store2); transaction.execute(|ctx| { let trees1 = ctx.store_trees("ranges1")?; let trees2 = ctx.store_trees("ranges2")?; assert_eq!(trees1.len(), 3); assert_eq!(trees2.len(), 3); // Verify different stores have different trees assert_ne!(trees1[0] as *const _, trees2[0] as *const _); Ok(()) })?; Ok(()) } #[test] fn test_store_not_found_error() -> Result<()> { let (range_store, _, _) = create_test_stores()?; let transaction = Transaction::new() .with_store("ranges", &range_store); let result: Result<()> = transaction.execute(|ctx| { // Try to access a store that doesn't exist let _trees = ctx.store_trees("nonexistent")?; Ok(()) }); assert!(result.is_err()); Ok(()) } #[test] fn test_tree_index_out_of_bounds() -> Result<()> { let (_, _, db) = create_test_stores()?; let tree = db.open_tree("single_tree")?; let transaction = Transaction::new() .with_tree(&tree); let result: Result<()> = transaction.execute(|ctx| { // Try to access tree at index that doesn't exist let _tree = ctx.tree(5)?; Ok(()) }); assert!(result.is_err()); Ok(()) } #[test] fn test_transaction_rollback() -> Result<()> { let (_, _, db) = create_test_stores()?; let tree = db.open_tree("rollback_test")?; // First, insert some initial data tree.insert("initial", "data")?; let transaction = Transaction::new() .with_tree(&tree); // Execute a transaction that should fail let result: Result<()> = transaction.execute(|ctx| { let t = ctx.tree(0)?; // Insert some data t.insert("temp", "value") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; // Force an error to trigger rollback Err(Error::StoreError(sled::Error::Unsupported("Forced error".to_string()))) }); assert!(result.is_err()); // Verify rollback - temp key should not exist let temp_value = tree.get("temp")?; assert_eq!(temp_value, None); // But initial data should still be there let initial_value = tree.get("initial")?; assert_eq!(initial_value, Some("data".as_bytes().into())); Ok(()) } #[test] fn test_complex_multi_store_transaction() -> Result<()> { let (range_store, namespace_store, db) = create_test_stores()?; // Setup range_store.define("ip_pool", 100)?; namespace_store.define("users")?; let metadata_tree = db.open_tree("metadata")?; let logs_tree = db.open_tree("logs")?; let transaction = Transaction::new() .with_store("ranges", &range_store) .with_store("namespaces", &namespace_store) .with_tree(&metadata_tree) .with_tree(&logs_tree); // Complex transaction transaction.execute(|ctx| { let range_trees = ctx.store_trees("ranges")?; let namespace_trees = ctx.store_trees("namespaces")?; let metadata = ctx.tree(0)?; let logs = ctx.tree(1)?; // Verify we have the right number of trees assert_eq!(range_trees.len(), 3); assert_eq!(namespace_trees.len(), 2); // Use metadata tree metadata.insert("operation", "complex_transaction") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; // Use logs tree logs.insert("log_entry_1", "Started complex operation") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; Ok(()) })?; // Verify all operations succeeded let op_value = metadata_tree.get("operation")?; assert_eq!(op_value, Some("complex_transaction".as_bytes().into())); let log_value = logs_tree.get("log_entry_1")?; assert_eq!(log_value, Some("Started complex operation".as_bytes().into())); Ok(()) } #[test] fn test_raw_tree_access() -> Result<()> { let (range_store, namespace_store, db) = create_test_stores()?; let extra_tree = db.open_tree("extra")?; let transaction = Transaction::new() .with_store("ranges", &range_store) .with_store("namespaces", &namespace_store) .with_tree(&extra_tree); transaction.execute(|ctx| { // Test raw tree access by absolute index let tree0 = ctx.raw_tree(0)?; // First range store tree let tree3 = ctx.raw_tree(3)?; // First namespace store tree let tree5 = ctx.raw_tree(5)?; // Extra tree // All should be valid trees tree0.insert("raw0", "value0") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; tree3.insert("raw3", "value3") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; tree5.insert("raw5", "value5") .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("Insert failed: {}", e))))?; // Test accessing out of bounds let invalid_result = ctx.raw_tree(10); assert!(invalid_result.is_err()); Ok(()) })?; Ok(()) } #[test] fn test_store_map_access() -> Result<()> { let (range_store, namespace_store, _) = create_test_stores()?; let transaction = Transaction::new() .with_store("my_ranges", &range_store) .with_store("my_namespaces", &namespace_store); transaction.execute(|ctx| { let store_map = ctx.store_map(); // Verify store map contains our stores assert!(store_map.contains_key("my_ranges")); assert!(store_map.contains_key("my_namespaces")); // Verify ranges let (start, end) = store_map.get("my_ranges").unwrap(); assert_eq!(*start, 0); assert_eq!(*end, 3); // RangeStore has 3 trees // Verify namespaces let (start, end) = store_map.get("my_namespaces").unwrap(); assert_eq!(*start, 3); assert_eq!(*end, 5); // NamespaceStore has 2 trees Ok(()) })?; Ok(()) } } \ No newline at end of file