Page MenuHomePhabricator

instance.rs
No OneTemporary

instance.rs

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");
}
}

File Metadata

Mime Type
text/plain
Expires
Mon, Jun 9, 11:52 AM (17 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47645
Default Alt Text
instance.rs (8 KB)

Event Timeline