use axum::{
extract::{Path, Query, State},
Json,
};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::tenant::TenantPoolManager;
#[derive(Debug, Serialize, ToSchema)]
pub struct ProvisionResponse {
pub instance_id: String,
pub status: String,
pub message: String,
}
#[derive(Debug, Default, Deserialize, ToSchema)]
pub struct ProvisionQuery {
pub parent: Option<String>,
}
#[utoipa::path(
post,
path = "/api/instances/{instance_id}/provision",
params(
("instance_id" = String, Path, description = "Instance ID (UUID) to provision"),
("parent" = Option<String>, Query, description = "Parent workspace UUID for nested instances"),
),
request_body = (),
responses(
(status = 200, description = "Instance provisioned successfully", body = ProvisionResponse),
(status = 400, description = "Invalid instance ID or missing parent"),
(status = 500, description = "Provisioning failed"),
),
tag = "instances",
)]
pub async fn provision_instance(
State(manager): State<TenantPoolManager>,
Path(instance_id): Path<String>,
Query(query): Query<ProvisionQuery>,
) -> Result<Json<ProvisionResponse>, (axum::http::StatusCode, String)> {
manager
.provision_instance(&instance_id, query.parent.as_deref())
.await
.map_err(|e| {
let status = if e.to_string().contains("invalid instance id")
|| e.to_string().contains("not provisioned")
{
axum::http::StatusCode::BAD_REQUEST
} else {
axum::http::StatusCode::INTERNAL_SERVER_ERROR
};
tracing::error!("Failed to provision instance {}: {}", instance_id, e);
(status, e.to_string())
})?;
let nested = query.parent.is_some();
Ok(Json(ProvisionResponse {
instance_id,
status: "provisioned".to_string(),
message: if nested {
"Nested instance database created and migrated".to_string()
} else {
"Instance database created and migrated".to_string()
},
}))
}
#[utoipa::path(
delete,
path = "/api/instances/{instance_id}",
params(
("instance_id" = String, Path, description = "Instance ID (UUID) to delete"),
),
responses(
(status = 200, description = "Instance deleted"),
(status = 400, description = "Invalid instance ID"),
(status = 404, description = "Instance not found"),
),
tag = "instances",
)]
pub async fn delete_instance(
State(manager): State<TenantPoolManager>,
Path(instance_id): Path<String>,
) -> Result<Json<serde_json::Value>, (axum::http::StatusCode, String)> {
manager
.delete_instance(&instance_id)
.await
.map_err(|e| {
let status = if e.to_string().contains("invalid instance id") {
axum::http::StatusCode::BAD_REQUEST
} else if e.to_string().contains("not found") {
axum::http::StatusCode::NOT_FOUND
} else {
axum::http::StatusCode::INTERNAL_SERVER_ERROR
};
tracing::error!("Failed to delete instance {}: {}", instance_id, e);
(status, e.to_string())
})?;
Ok(Json(serde_json::json!({ "deleted": true, "instance_id": instance_id })))
}
#[derive(Debug, Serialize, ToSchema)]
pub struct InstanceInfo {
pub instance_id: String,
}
#[utoipa::path(
get,
path = "/api/instances",
responses(
(status = 200, description = "List of active instances", body = [InstanceInfo]),
),
tag = "instances",
)]
pub async fn list_instances(
State(manager): State<TenantPoolManager>,
) -> Json<Vec<InstanceInfo>> {
let instances = manager
.list_instances()
.into_iter()
.map(|id| InstanceInfo { instance_id: id })
.collect();
Json(instances)
}
#[derive(Debug, Serialize, ToSchema)]
pub struct InstanceTreeNode {
pub workspace_id: String,
pub children: Vec<String>,
}
#[utoipa::path(
get,
path = "/api/instances/tree",
responses(
(status = 200, description = "Workspace tree of instances", body = [InstanceTreeNode]),
),
tag = "instances",
)]
pub async fn list_instance_tree(
State(manager): State<TenantPoolManager>,
) -> Json<Vec<InstanceTreeNode>> {
let mut nodes: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (ws, child) in manager.list_instance_tree() {
let entry = nodes.entry(ws).or_default();
if let Some(c) = child {
entry.push(c);
}
}
Json(
nodes
.into_iter()
.map(|(workspace_id, mut children)| {
children.sort();
InstanceTreeNode { workspace_id, children }
})
.collect::<Vec<_>>()
.into_iter()
.collect(),
)
}