Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F73679
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
47 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jun 8, 9:35 AM (17 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47590
Default Alt Text
(47 KB)
Attached To
rCOLLAR collar
Event Timeline
Log In to Comment