Page MenuHomePhabricator

No OneTemporary

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<Vec<NetworkInterfaceRequest>>,
+ pub metadata: Option<HashMap<String, String>>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct NetworkInterfaceRequest {
+ pub network_name: String,
+ pub interface_name: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct UpdateInstanceRequest {
+ pub provider_config: Option<serde_json::Value>,
+ pub network_interfaces: Option<Vec<NetworkInterfaceRequest>>,
+ pub metadata: Option<HashMap<String, String>>,
+}
+
+pub async fn get_instances(State(state): State<Arc<AppState>>) -> 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<Arc<AppState>>, Json(req): Json<CreateInstanceRequest>) -> 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<Arc<AppState>>, Path(name): Path<String>) -> 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<Arc<AppState>>, Path(name): Path<String>) -> 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<Instance> = 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<String>;
/// Deserialize provider configuration from JSON
fn from_json(json: &str) -> Result<Self> 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<bool>;
/// Get instance status information
fn status(&self, name: &str) -> Result<ProviderStatus>;
}
/// Status information from a provider
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderStatus {
pub running: bool,
pub pid: Option<u32>,
pub uptime: Option<u64>,
pub memory_usage: Option<u64>,
pub cpu_usage: Option<f64>,
}
/// 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<String>, // 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<NetworkInterface>,
pub metadata: HashMap<String, String>,
}
/// 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<Self> {
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<Instance> {
// 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<Instance, sled::transaction::ConflictableTransactionError<Error>> {
// 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<Instance> {
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<Instance> {
// 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<bool> {
Ok(self.instances.contains_key(name)?)
}
/// List all instances
pub fn list(&self) -> Result<Vec<Instance>> {
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<Vec<Instance>> {
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<bool> {
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<String>,
}
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<String> {
serde_json::to_string(self)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))
}
fn from_json(json: &str) -> Result<Self> {
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<bool> {
// TODO: Implement jail status check
Ok(false)
}
fn status(&self, _name: &str) -> Result<ProviderStatus> {
// 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<String>,
}
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<String> {
serde_json::to_string(self)
.map_err(|e| Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))))
}
fn from_json(json: &str) -> Result<Self> {
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<bool> {
// TODO: Implement container status check
Ok(false)
}
fn status(&self, _name: &str) -> Result<ProviderStatus> {
// 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<Self> {
let db = sled::open(path)?;
Ok(Db { prefix, db })
}
pub fn open_tree(&self, name: &str) -> Result<sled::Tree> {
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<Store> {
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<Store> {
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
+ }
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jun 8, 9:35 AM (12 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47590
Default Alt Text
(47 KB)

Event Timeline