diff --git a/crates/api/src/instance.rs b/crates/api/src/instance.rs new file mode 100644 index 0000000..ec35ec8 --- /dev/null +++ b/crates/api/src/instance.rs @@ -0,0 +1,235 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde::{Deserialize, Serialize}; +use store::instance::{Instance, InstanceConfig, NetworkInterface}; +use std::collections::HashMap; + +use crate::AppState; + +#[derive(Serialize, Deserialize)] +pub struct CreateInstanceRequest { + pub name: String, + pub provider_type: String, + pub provider_config: serde_json::Value, + pub network_interfaces: Option>, + pub metadata: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct NetworkInterfaceRequest { + pub network_name: String, + pub interface_name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct UpdateInstanceRequest { + pub provider_config: Option, + pub network_interfaces: Option>, + pub metadata: Option>, +} + +pub async fn get_instances(State(state): State>) -> impl IntoResponse { + let instances = state.collar.store.instances(); + + match instances.list() { + Ok(instance_list) => (StatusCode::OK, Json(instance_list)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to list instances: {:?}", e)).into_response(), + } +} + +pub async fn post_instances(State(state): State>, Json(req): Json) -> impl IntoResponse { + let instances = state.collar.store.instances(); + + // Validate provider type + if !matches!(req.provider_type.as_str(), "jail" | "container") { + return (StatusCode::BAD_REQUEST, format!("Invalid provider type '{}'. Must be 'jail' or 'container'", req.provider_type)).into_response(); + } + + // Convert network interfaces + let network_interfaces = req.network_interfaces.unwrap_or_default() + .into_iter() + .map(|ni| NetworkInterface { + network_name: ni.network_name, + interface_name: ni.interface_name, + assignment: None, // Will be assigned during creation + }) + .collect(); + + // Create instance config + let config = InstanceConfig { + provider_config: req.provider_config.to_string(), + network_interfaces, + metadata: req.metadata.unwrap_or_default(), + }; + + // Create instance + let instance = Instance::new(req.name.clone(), req.provider_type, config); + + match instances.create(instance) { + Ok(created_instance) => (StatusCode::CREATED, Json(created_instance)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create instance '{}': {:?}", req.name, e)).into_response(), + } +} + +pub async fn get_instance(State(state): State>, Path(name): Path) -> impl IntoResponse { + let instances = state.collar.store.instances(); + + match instances.get(&name) { + Ok(instance) => (StatusCode::OK, Json(instance)).into_response(), + Err(e) => { + if e.to_string().contains("not found") { + (StatusCode::NOT_FOUND, format!("Instance '{}' not found", name)).into_response() + } else { + (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get instance '{}': {:?}", name, e)).into_response() + } + } + } +} + +pub async fn delete_instance(State(state): State>, Path(name): Path) -> impl IntoResponse { + let instances = state.collar.store.instances(); + + // Check if instance exists first + match instances.exists(&name) { + Ok(false) => return (StatusCode::NOT_FOUND, format!("Instance '{}' not found", name)).into_response(), + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to check instance '{}': {:?}", name, e)).into_response(), + Ok(true) => {} + } + + match instances.delete(&name) { + Ok(()) => (StatusCode::OK, format!("Deleted instance '{}'", name)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete instance '{}': {:?}", name, e)).into_response(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::StatusCode; + use axum_test::TestServer; + use serde_json::json; + + + async fn setup_test_server() -> TestServer { + let collar = libcollar::new_test(&format!("test_api_instance_{}", std::process::id())).unwrap(); + let app = crate::app(collar); + TestServer::new(app).unwrap() + } + + #[tokio::test] + async fn test_create_and_get_instance() { + let server = setup_test_server().await; + + let create_req = json!({ + "name": "test-instance", + "provider_type": "jail", + "provider_config": { + "jail_conf": "/etc/jail.conf", + "dataset": "zroot/jails" + } + }); + + let response = server.post("/instances").json(&create_req).await; + assert_eq!(response.status_code(), StatusCode::CREATED); + + let response = server.get("/instances/test-instance").await; + assert_eq!(response.status_code(), StatusCode::OK); + + let instance: Instance = response.json(); + assert_eq!(instance.name, "test-instance"); + assert_eq!(instance.provider_type, "jail"); + } + + #[tokio::test] + async fn test_list_instances() { + let server = setup_test_server().await; + + let create_req = json!({ + "name": "list-test-instance", + "provider_type": "container", + "provider_config": { + "runtime": "docker", + "image": "alpine:latest" + } + }); + + server.post("/instances").json(&create_req).await; + + let response = server.get("/instances").await; + assert_eq!(response.status_code(), StatusCode::OK); + + let instances: Vec = response.json(); + assert!(!instances.is_empty()); + assert!(instances.iter().any(|i| i.name == "list-test-instance")); + } + + #[tokio::test] + async fn test_delete_instance() { + let server = setup_test_server().await; + + let create_req = json!({ + "name": "delete-test-instance", + "provider_type": "jail", + "provider_config": {} + }); + + server.post("/instances").json(&create_req).await; + + let response = server.delete("/instances/delete-test-instance").await; + assert_eq!(response.status_code(), StatusCode::OK); + + let response = server.get("/instances/delete-test-instance").await; + assert_eq!(response.status_code(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn test_invalid_provider_type() { + let server = setup_test_server().await; + + let create_req = json!({ + "name": "invalid-provider-instance", + "provider_type": "invalid", + "provider_config": {} + }); + + let response = server.post("/instances").json(&create_req).await; + assert_eq!(response.status_code(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn test_instance_with_network_interfaces() { + let server = setup_test_server().await; + + // First create a network + let network_req = json!({ + "name": "test-network", + "cidr": "192.168.1.0/24" + }); + server.post("/networks").json(&network_req).await; + + let create_req = json!({ + "name": "networked-instance", + "provider_type": "jail", + "provider_config": {}, + "network_interfaces": [ + { + "network_name": "test-network", + "interface_name": "eth0" + } + ] + }); + + let response = server.post("/instances").json(&create_req).await; + assert_eq!(response.status_code(), StatusCode::CREATED); + + let instance: Instance = response.json(); + assert_eq!(instance.config.network_interfaces.len(), 1); + assert_eq!(instance.config.network_interfaces[0].network_name, "test-network"); + } +} \ No newline at end of file diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 7025faa..e8d60ee 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,93 +1,150 @@ //! # Collar API -//! -//! HTTP API for network management functionality. -//! +//! +//! HTTP API for network management and instance 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", +//! "name": "test-network", //! "cidr": "192.168.1.0/24", //! "assigner_type": "range", //! "assigner_config": { //! "range_name": "my-range" //! } //! } //! ``` -//! +//! +//! ## Instance Endpoints +//! +//! - `GET /instances` - List all instances +//! - `POST /instances` - Create a new instance +//! - `GET /instances/{name}` - Get instance details +//! - `DELETE /instances/{name}` - Delete an instance +//! +//! ## Instance Creation +//! +//! Instances can be created with different provider types: +//! +//! ### Jail Instance +//! ```json +//! { +//! "name": "my-jail", +//! "provider_type": "jail", +//! "provider_config": { +//! "jail_conf": "/etc/jail.conf", +//! "dataset": "zroot/jails" +//! } +//! } +//! ``` +//! +//! ### Container Instance +//! ```json +//! { +//! "name": "my-container", +//! "provider_type": "container", +//! "provider_config": { +//! "runtime": "docker", +//! "image": "alpine:latest" +//! } +//! } +//! ``` +//! +//! ### Instance with Network Interfaces +//! ```json +//! { +//! "name": "networked-instance", +//! "provider_type": "jail", +//! "provider_config": {}, +//! "network_interfaces": [ +//! { +//! "network_name": "test-network", +//! "interface_name": "eth0" +//! } +//! ] +//! } +//! ``` +//! //! ## 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; +use axum::routing::post; +use axum::routing::delete; pub use axum::serve; mod network; +mod instance; 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 routes let app = axum::Router::new() .route("/", get(|| async { "collared." })) + // Instances routes + .route("/instances", get(instance::get_instances)) + .route("/instances", post(instance::post_instances)) + .route("/instances/{name}", get(instance::get_instance)) + .route("/instances/{name}", delete(instance::delete_instance)) // Network management routes .route("/networks", get(network::get_networks)) - .route("/networks", axum::routing::post(network::post_networks)) + .route("/networks", post(network::post_networks)) .route("/networks/{name}", get(network::get_network)) - .route("/networks/{name}", axum::routing::delete(network::delete_network)) + .route("/networks/{name}", 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 } - diff --git a/crates/store/src/instance.rs b/crates/store/src/instance.rs index d58d4dd..3cf1d85 100644 --- a/crates/store/src/instance.rs +++ b/crates/store/src/instance.rs @@ -1,945 +1,946 @@ use crate::{Result, Error, NamespaceStore, NetworkStore}; use serde::{Serialize, Deserialize}; use std::collections::HashMap; /// Generic provider trait for different virtualization backends pub trait Provider: Send + Sync { /// Get the provider type identifier fn provider_type(&self) -> &'static str; /// Serialize provider configuration to JSON fn to_json(&self) -> Result; /// Deserialize provider configuration from JSON fn from_json(json: &str) -> Result where Self: Sized; /// Start the instance with the given configuration fn start(&self, name: &str, config: &InstanceConfig) -> Result<()>; /// Stop the instance fn stop(&self, name: &str) -> Result<()>; /// Check if instance is running fn is_running(&self, name: &str) -> Result; /// Get instance status information fn status(&self, name: &str) -> Result; } /// Status information from a provider #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderStatus { pub running: bool, pub pid: Option, pub uptime: Option, pub memory_usage: Option, pub cpu_usage: Option, } /// Network interface assignment for an instance #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkInterface { pub network_name: String, pub interface_name: String, pub assignment: Option, // IP assignment from network } /// Instance configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstanceConfig { pub provider_config: String, // JSON config specific to provider pub network_interfaces: Vec, pub metadata: HashMap, } /// Instance representation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Instance { pub name: String, pub provider_type: String, pub config: InstanceConfig, pub created_at: u64, pub updated_at: u64, } impl Instance { /// Create a new instance pub fn new(name: String, provider_type: String, config: InstanceConfig) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); Self { name, provider_type, config, created_at: now, updated_at: now, } } /// Update instance configuration pub fn update_config(&mut self, config: InstanceConfig) { self.config = config; self.updated_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); } } /// Store for managing instances +#[derive(Debug, Clone)] pub struct InstanceStore { pub(crate) instances: sled::Tree, pub(crate) namespaces: NamespaceStore, pub(crate) networks: NetworkStore, } impl InstanceStore { const INSTANCE_NAMESPACE: &'static str = "instances"; /// Open instance store with existing namespace and network stores pub fn open( db: &sled::Db, namespaces: NamespaceStore, networks: NetworkStore, ) -> Result { let instances = db.open_tree("instances")?; let store = Self { instances, namespaces, networks, }; // Ensure instance namespace exists store.namespaces.define(Self::INSTANCE_NAMESPACE)?; Ok(store) } /// Create a new instance with network assignments pub fn create(&self, mut instance: Instance) -> Result { // Reserve name in namespace self.namespaces.reserve(Self::INSTANCE_NAMESPACE, &instance.name, &instance.name)?; // Get network assignments for interfaces for interface in &mut instance.config.network_interfaces { if let Ok(Some(network)) = self.networks.get(&interface.network_name) { match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { Ok(Some(assignment)) => { interface.assignment = Some(assignment.to_string()); } Ok(None) => { // No assignment available } Err(e) => { // Clean up reserved name on failure let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); return Err(e); } } } } // Store instance let instance_json = serde_json::to_string(&instance) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?; match self.instances.insert(&instance.name, instance_json.as_bytes()) { Ok(_) => Ok(instance), Err(e) => { // Clean up on failure let _ = self.namespaces.remove(Self::INSTANCE_NAMESPACE, &instance.name); Err(Error::StoreError(e)) } } } /// Create instance within a transaction pub fn create_in_transaction( &self, names_tree: &sled::transaction::TransactionalTree, spaces_tree: &sled::transaction::TransactionalTree, instances_tree: &sled::transaction::TransactionalTree, mut instance: Instance, ) -> std::result::Result> { // Reserve name in namespace within transaction match self.namespaces.reserve_in_transaction(names_tree, spaces_tree, &instance.name, Self::INSTANCE_NAMESPACE, &instance.name) { Ok(_) => {}, Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("Namespace reservation error: {:?}", e))) )), } // Get network assignments for interfaces for interface in &mut instance.config.network_interfaces { if let Ok(Some(network)) = self.networks.get(&interface.network_name) { match network.get_assignment(&instance.name, self.networks.ranges.as_ref()) { Ok(Some(assignment)) => { interface.assignment = Some(assignment.to_string()); } Ok(None) => { // No assignment available } Err(e) => return Err(sled::transaction::ConflictableTransactionError::Abort(e)), } } } // Store instance in transaction let instance_json = serde_json::to_string(&instance) .map_err(|e| sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) ))?; instances_tree.insert(instance.name.as_bytes(), instance_json.as_bytes()) .map_err(|e| sled::transaction::ConflictableTransactionError::Abort( Error::StoreError(sled::Error::Unsupported(format!("Transaction insert error: {}", e))) ))?; Ok(instance) } /// Get an instance by name pub fn get(&self, name: &str) -> Result { match self.instances.get(name)? { Some(data) => { let json = String::from_utf8(data.to_vec())?; serde_json::from_str(&json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } None => Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", name)))), } } /// Update an existing instance pub fn update(&self, instance: Instance) -> Result { // Check if instance exists if !self.exists(&instance.name)? { return Err(Error::StoreError(sled::Error::Unsupported(format!("Instance '{}' not found", instance.name)))); } let instance_json = serde_json::to_string(&instance) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))?; self.instances.insert(&instance.name, instance_json.as_bytes())?; Ok(instance) } /// Delete an instance pub fn delete(&self, name: &str) -> Result<()> { // Get instance to clean up network assignments if let Ok(instance) = self.get(name) { // TODO: Clean up network assignments when unassign functionality is available for _interface in &instance.config.network_interfaces { // network.unassign_ip(&name)?; } } // Remove from instance store self.instances.remove(name)?; // Remove from namespace self.namespaces.remove(Self::INSTANCE_NAMESPACE, name)?; Ok(()) } /// Check if instance exists pub fn exists(&self, name: &str) -> Result { Ok(self.instances.contains_key(name)?) } /// List all instances pub fn list(&self) -> Result> { let mut instances = Vec::new(); for item in self.instances.iter() { let (_, value_bytes) = item?; let json = String::from_utf8(value_bytes.to_vec())?; let instance: Instance = serde_json::from_str(&json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))))?; instances.push(instance); } instances.sort_by(|a, b| a.name.cmp(&b.name)); Ok(instances) } /// List instances by provider type pub fn list_by_provider(&self, provider_type: &str) -> Result> { Ok(self.list()? .into_iter() .filter(|instance| instance.provider_type == provider_type) .collect()) } /// Check if an instance name is reserved in the namespace pub fn is_name_reserved(&self, name: &str) -> Result { self.namespaces.key_exists(Self::INSTANCE_NAMESPACE, name) } } // Example provider implementations /// Jail provider for FreeBSD jails #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JailProvider { pub jail_conf: String, pub dataset: Option, } impl JailProvider { pub fn new() -> Self { Self { jail_conf: String::new(), dataset: None, } } pub fn with_dataset(mut self, dataset: String) -> Self { self.dataset = Some(dataset); self } } impl Provider for JailProvider { fn provider_type(&self) -> &'static str { "jail" } fn to_json(&self) -> Result { serde_json::to_string(self) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))) } fn from_json(json: &str) -> Result { serde_json::from_str(json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> { // TODO: Implement jail start logic println!("Starting jail: {}", name); Ok(()) } fn stop(&self, name: &str) -> Result<()> { // TODO: Implement jail stop logic println!("Stopping jail: {}", name); Ok(()) } fn is_running(&self, _name: &str) -> Result { // TODO: Implement jail status check Ok(false) } fn status(&self, _name: &str) -> Result { // TODO: Implement jail status collection Ok(ProviderStatus { running: false, pid: None, uptime: None, memory_usage: None, cpu_usage: None, }) } } /// Container provider for Docker/Podman containers #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContainerProvider { pub runtime: String, // "docker" or "podman" pub image: String, pub registry: Option, } impl ContainerProvider { pub fn docker(image: String) -> Self { Self { runtime: "docker".to_string(), image, registry: None, } } pub fn podman(image: String) -> Self { Self { runtime: "podman".to_string(), image, registry: None, } } } impl Provider for ContainerProvider { fn provider_type(&self) -> &'static str { "container" } fn to_json(&self) -> Result { serde_json::to_string(self) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))) } fn from_json(json: &str) -> Result { serde_json::from_str(json) .map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e)))) } fn start(&self, name: &str, _config: &InstanceConfig) -> Result<()> { // TODO: Implement container start logic println!("Starting {} container: {}", self.runtime, name); Ok(()) } fn stop(&self, name: &str) -> Result<()> { // TODO: Implement container stop logic println!("Stopping {} container: {}", self.runtime, name); Ok(()) } fn is_running(&self, _name: &str) -> Result { // TODO: Implement container status check Ok(false) } fn status(&self, _name: &str) -> Result { // TODO: Implement container status collection Ok(ProviderStatus { running: false, pid: None, uptime: None, memory_usage: None, cpu_usage: None, }) } } #[cfg(test)] mod tests { use super::*; use crate::{NamespaceStore, NetworkStore, RangeStore}; use tempfile::TempDir; fn create_test_stores() -> Result<(TempDir, InstanceStore)> { let temp_dir = TempDir::new().unwrap(); let db = sled::open(temp_dir.path())?; let namespaces = NamespaceStore::open(&db)?; let ranges = RangeStore::open(&db)?; let networks = NetworkStore::open_with_ranges(&db, ranges)?; let instances = InstanceStore::open(&db, namespaces, networks)?; Ok((temp_dir, instances)) } #[test] fn test_create_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); let created = store.create(instance.clone())?; assert_eq!(created.name, instance.name); assert_eq!(created.provider_type, instance.provider_type); Ok(()) } #[test] fn test_get_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); store.create(instance.clone())?; let retrieved = store.get(&instance.name)?; assert_eq!(retrieved.name, instance.name); assert_eq!(retrieved.provider_type, instance.provider_type); Ok(()) } #[test] fn test_duplicate_instance_name() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance1 = Instance::new( "test-instance".to_string(), "jail".to_string(), config.clone(), ); let instance2 = Instance::new( "test-instance".to_string(), "container".to_string(), config, ); store.create(instance1)?; // Should fail due to duplicate name assert!(store.create(instance2).is_err()); Ok(()) } #[test] fn test_list_instances() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance1 = Instance::new( "instance1".to_string(), "jail".to_string(), config.clone(), ); let instance2 = Instance::new( "instance2".to_string(), "container".to_string(), config, ); store.create(instance1)?; store.create(instance2)?; let instances = store.list()?; assert_eq!(instances.len(), 2); let jail_instances = store.list_by_provider("jail")?; assert_eq!(jail_instances.len(), 1); assert_eq!(jail_instances[0].name, "instance1"); Ok(()) } #[test] fn test_delete_instance() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance = Instance::new( "test-instance".to_string(), "jail".to_string(), config, ); store.create(instance.clone())?; assert!(store.exists(&instance.name)?); store.delete(&instance.name)?; assert!(!store.exists(&instance.name)?); Ok(()) } #[test] fn test_jail_provider() -> Result<()> { let provider = JailProvider::new().with_dataset("tank/jails".to_string()); let json = provider.to_json()?; let deserialized = JailProvider::from_json(&json)?; assert_eq!(provider.dataset, deserialized.dataset); assert_eq!(provider.provider_type(), "jail"); Ok(()) } #[test] fn test_container_provider() -> Result<()> { let provider = ContainerProvider::docker("nginx:latest".to_string()); let json = provider.to_json()?; let deserialized = ContainerProvider::from_json(&json)?; assert_eq!(provider.runtime, deserialized.runtime); assert_eq!(provider.image, deserialized.image); assert_eq!(provider.provider_type(), "container"); Ok(()) } #[test] fn test_instance_with_range_assigner_network() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network with RangeAssigner use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("test_range"); store.networks.create( "test_network", NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create instance with network interface let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "test_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "test-with-network".to_string(), "jail".to_string(), config, ); let created = store.create(instance)?; // Verify instance was created with network assignment assert_eq!(created.name, "test-with-network"); assert_eq!(created.config.network_interfaces.len(), 1); let interface = &created.config.network_interfaces[0]; assert_eq!(interface.network_name, "test_network"); assert_eq!(interface.interface_name, "eth0"); // Note: IP assignment would be available if range was properly initialized // For this test, we verify the structure is correct Ok(()) } #[test] fn test_multiple_instances_different_ip_assignments() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network with RangeAssigner use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("multi_test_range"); store.networks.create( "multi_network", NetRange::ipv4("10.0.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create first instance let config1 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "multi_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance1 = Instance::new( "instance1".to_string(), "jail".to_string(), config1, ); // Create second instance let config2 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "multi_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance2 = Instance::new( "instance2".to_string(), "container".to_string(), config2, ); let created1 = store.create(instance1)?; let created2 = store.create(instance2)?; // Verify both instances have network interfaces assert_eq!(created1.config.network_interfaces.len(), 1); assert_eq!(created2.config.network_interfaces.len(), 1); // Verify they're connected to the same network assert_eq!(created1.config.network_interfaces[0].network_name, "multi_network"); assert_eq!(created2.config.network_interfaces[0].network_name, "multi_network"); // Both instances should exist in the store assert!(store.exists("instance1")?); assert!(store.exists("instance2")?); Ok(()) } #[test] fn test_instance_with_multiple_networks() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up multiple networks use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; // Management network let mgmt_provider = BasicProvider::new(); let mgmt_assigner = RangeAssigner::with_range_name("mgmt_range"); store.networks.create( "management", NetRange::ipv4("10.0.1.0".parse().unwrap(), 24)?, mgmt_provider, Some(mgmt_assigner), )?; // Public network let public_provider = BasicProvider::new(); let public_assigner = RangeAssigner::with_range_name("public_range"); store.networks.create( "public", NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?, public_provider, Some(public_assigner), )?; // Create instance with multiple network interfaces let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "management".to_string(), interface_name: "eth0".to_string(), assignment: None, }, NetworkInterface { network_name: "public".to_string(), interface_name: "eth1".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "multi-network-instance".to_string(), "container".to_string(), config, ); let created = store.create(instance)?; // Verify instance has both network interfaces assert_eq!(created.config.network_interfaces.len(), 2); let interfaces: Vec<&str> = created.config.network_interfaces .iter() .map(|i| i.network_name.as_str()) .collect(); assert!(interfaces.contains(&"management")); assert!(interfaces.contains(&"public")); Ok(()) } #[test] fn test_instance_network_assignment_cleanup_on_delete() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("cleanup_range"); store.networks.create( "cleanup_network", NetRange::ipv4("172.16.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create instance let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "cleanup_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "cleanup-test".to_string(), "jail".to_string(), config, ); let created = store.create(instance)?; assert!(store.exists("cleanup-test")?); // Delete the instance store.delete("cleanup-test")?; assert!(!store.exists("cleanup-test")?); // Verify instance is removed from namespace assert!(!store.namespaces.key_exists("instances", "cleanup-test")?); Ok(()) } #[test] fn test_instance_creation_fails_with_nonexistent_network() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Try to create instance with non-existent network let config = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "nonexistent_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance = Instance::new( "test-nonexistent-network".to_string(), "jail".to_string(), config, ); // This should succeed because network assignment is not enforced during creation // The instance will be created but without IP assignments let created = store.create(instance)?; assert_eq!(created.config.network_interfaces[0].assignment, None); Ok(()) } #[test] fn test_instance_transaction_rollback_preserves_state() -> Result<()> { let (_temp_dir, store) = create_test_stores()?; // Set up a network use crate::network::{Network, NetRange, BasicProvider, RangeAssigner}; let provider = BasicProvider::new(); let assigner = RangeAssigner::with_range_name("tx_test_range"); store.networks.create( "tx_network", NetRange::ipv4("172.20.0.0".parse().unwrap(), 24)?, provider, Some(assigner), )?; // Create first instance successfully let config1 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![ NetworkInterface { network_name: "tx_network".to_string(), interface_name: "eth0".to_string(), assignment: None, }, ], metadata: HashMap::new(), }; let instance1 = Instance::new( "tx-test-1".to_string(), "jail".to_string(), config1, ); store.create(instance1)?; assert!(store.exists("tx-test-1")?); // Verify namespace contains the instance assert!(store.namespaces.key_exists("instances", "tx-test-1")?); // Try to create duplicate instance (should fail) let config2 = InstanceConfig { provider_config: "{}".to_string(), network_interfaces: vec![], metadata: HashMap::new(), }; let instance2 = Instance::new( "tx-test-1".to_string(), // Same name - should fail "container".to_string(), config2, ); // This should fail due to duplicate name assert!(store.create(instance2).is_err()); // Original instance should still exist assert!(store.exists("tx-test-1")?); assert!(store.namespaces.key_exists("instances", "tx-test-1")?); Ok(()) } } \ No newline at end of file diff --git a/crates/store/src/store.rs b/crates/store/src/store.rs index 64cd918..746ea1d 100644 --- a/crates/store/src/store.rs +++ b/crates/store/src/store.rs @@ -1,70 +1,83 @@ use crate::Result; #[derive(Debug, Clone)] pub struct Db { prefix: String, db: sled::Db } #[derive(Debug, Clone)] pub struct Store { db: Db, namespaces: crate::namespace::NamespaceStore, ranges: crate::range::RangeStore, networks: crate::network::NetworkStore, + instances: crate::instance::InstanceStore, } impl Db { pub fn open(path: String, prefix: String) -> Result { let db = sled::open(path)?; Ok(Db { prefix, db }) } pub fn open_tree(&self, name: &str) -> Result { Ok(self.db.open_tree(self.tree_path(name))?) } pub fn tree_path(&self, name: &str) -> String { format!("t1/{}/{}", self.prefix, name) } } pub fn open() -> Result { let db = Db::open("libcollar_store".to_string(), "bonefire".to_string())?; let ranges = crate::range::RangeStore::open(&db.db)?; + let namespaces = crate::namespace::NamespaceStore::open(&db.db)?; + let networks = crate::network::NetworkStore::open_with_ranges(&db.db, ranges.clone())?; + let instances = crate::instance::InstanceStore::open(&db.db, namespaces.clone(), networks.clone())?; Ok(Store { - namespaces: crate::namespace::NamespaceStore::open(&db.db)?, - ranges: ranges.clone(), - networks: crate::network::NetworkStore::open_with_ranges(&db.db, ranges)?, + instances, + namespaces, + ranges, + networks, db: db, }) } pub fn open_test(path: &str) -> Result { let db = Db::open(path.to_string(), "test".to_string())?; let ranges = crate::range::RangeStore::open(&db.db)?; + let namespaces = crate::namespace::NamespaceStore::open(&db.db)?; + let networks = crate::network::NetworkStore::open_with_ranges(&db.db, ranges.clone())?; + let instances = crate::instance::InstanceStore::open(&db.db, namespaces.clone(), networks.clone())?; Ok(Store { - namespaces: crate::namespace::NamespaceStore::open(&db.db)?, - ranges: ranges.clone(), - networks: crate::network::NetworkStore::open_with_ranges(&db.db, ranges)?, + instances, + namespaces, + ranges, + networks, db: db, }) } impl Store { fn tree_path(&self, name: &str) -> String { self.db.tree_path(name) } pub fn namespaces(&self) -> &crate::namespace::NamespaceStore { &self.namespaces } pub fn ranges(&self) -> &crate::range::RangeStore { &self.ranges } pub fn networks(&self) -> &crate::network::NetworkStore { &self.networks } + + pub fn instances(&self) -> &crate::instance::InstanceStore { + &self.instances + } }