use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use greentic_setup::admin::routes::{
AdminResponse, BundleDeployRequest, BundleRemoveRequest, BundleStatus, BundleStatusResponse,
};
use greentic_setup::admin::tls::AdminTlsConfig;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Method, Request, Response, StatusCode};
use serde_json::{Value, json};
pub struct AdminState {
pub tls_config: AdminTlsConfig,
pub bundle_root: PathBuf,
pub tenant: String,
pub team: Option<String>,
pub env: String,
}
pub async fn handle_admin_request(
req: Request<Incoming>,
path: &str,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let method = req.method().clone();
let sub_path = path
.strip_prefix("/admin")
.unwrap_or("")
.trim_end_matches('/');
match (method, sub_path) {
(Method::GET, "/status") => handle_status(state).await,
(Method::POST, "/deploy") => handle_deploy(req, state).await,
(Method::POST, "/remove") => handle_remove(req, state).await,
(Method::POST, "/qa/spec") => handle_qa_spec(req, state).await,
(Method::POST, "/qa/validate") => handle_qa_validate(req, state).await,
(Method::POST, "/qa/submit") => handle_qa_submit(req, state).await,
_ => json_response(
StatusCode::NOT_FOUND,
&AdminResponse::<()>::err("not found"),
),
}
}
async fn handle_status(state: &Arc<AdminState>) -> Result<Response<Full<Bytes>>> {
let bundle_exists = state.bundle_root.exists();
let providers_dir = state.bundle_root.join("providers");
let mut provider_count = 0usize;
if providers_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&providers_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if !name.starts_with('_') && !name.starts_with('.') {
provider_count += 1;
}
}
}
}
}
}
let status = BundleStatusResponse {
status: if bundle_exists {
BundleStatus::Active
} else {
BundleStatus::Error
},
bundle_path: state.bundle_root.clone(),
pack_count: 0,
tenant_count: 1,
provider_count,
};
json_response(StatusCode::OK, &AdminResponse::ok(status))
}
async fn handle_deploy(
req: Request<Incoming>,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let body = read_body(req).await?;
let request: BundleDeployRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(e) => {
return json_response(
StatusCode::BAD_REQUEST,
&AdminResponse::<()>::err(format!("invalid request: {e}")),
);
}
};
let engine = greentic_setup::SetupEngine::new(greentic_setup::engine::SetupConfig {
tenant: state.tenant.clone(),
team: state.team.clone(),
env: state.env.clone(),
offline: false,
verbose: false,
});
let setup_request = greentic_setup::engine::SetupRequest {
bundle: request.bundle_path,
bundle_name: request.bundle_name,
pack_refs: request.pack_refs,
tenants: request.tenants,
..Default::default()
};
let mode = if state.bundle_root.exists() {
greentic_setup::SetupMode::Update
} else {
greentic_setup::SetupMode::Create
};
match engine.plan(mode, &setup_request, false) {
Ok(plan) => {
let summary = json!({
"mode": format!("{mode:?}"),
"steps": plan.steps.len(),
"bundle": state.bundle_root.display().to_string(),
});
json_response(StatusCode::OK, &AdminResponse::ok(summary))
}
Err(e) => json_response(
StatusCode::INTERNAL_SERVER_ERROR,
&AdminResponse::<()>::err(e.to_string()),
),
}
}
async fn handle_remove(
req: Request<Incoming>,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let body = read_body(req).await?;
let request: BundleRemoveRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(e) => {
return json_response(
StatusCode::BAD_REQUEST,
&AdminResponse::<()>::err(format!("invalid request: {e}")),
);
}
};
if !state.bundle_root.exists() {
return json_response(
StatusCode::NOT_FOUND,
&AdminResponse::<()>::err("bundle not found"),
);
}
let engine = greentic_setup::SetupEngine::new(greentic_setup::engine::SetupConfig {
tenant: state.tenant.clone(),
team: state.team.clone(),
env: state.env.clone(),
offline: true,
verbose: false,
});
let setup_request = greentic_setup::engine::SetupRequest {
bundle: state.bundle_root.clone(),
..Default::default()
};
match engine.plan(greentic_setup::SetupMode::Remove, &setup_request, false) {
Ok(plan) => {
let _ = &request; let summary = json!({
"mode": "remove",
"steps": plan.steps.len(),
});
json_response(StatusCode::OK, &AdminResponse::ok(summary))
}
Err(e) => json_response(
StatusCode::INTERNAL_SERVER_ERROR,
&AdminResponse::<()>::err(e.to_string()),
),
}
}
async fn handle_qa_spec(
req: Request<Incoming>,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let body = read_body(req).await?;
let request: Value = serde_json::from_slice(&body).unwrap_or_default();
let provider_id = request
.get("provider_id")
.and_then(Value::as_str)
.unwrap_or("");
if provider_id.is_empty() {
return json_response(
StatusCode::BAD_REQUEST,
&AdminResponse::<()>::err("provider_id required"),
);
}
let providers_dir = state.bundle_root.join("providers");
let pack_path = find_provider_pack(&providers_dir, provider_id);
match pack_path {
Some(path) => {
let form_spec =
greentic_setup::setup_to_formspec::pack_to_form_spec(&path, provider_id);
match form_spec {
Some(spec) => {
let json = serde_json::to_value(&spec).unwrap_or_default();
json_response(StatusCode::OK, &AdminResponse::ok(json))
}
None => json_response(
StatusCode::NOT_FOUND,
&AdminResponse::<()>::err("no setup spec found for provider"),
),
}
}
None => json_response(
StatusCode::NOT_FOUND,
&AdminResponse::<()>::err(format!("provider pack not found: {provider_id}")),
),
}
}
async fn handle_qa_validate(
req: Request<Incoming>,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let body = read_body(req).await?;
let request: Value = serde_json::from_slice(&body).unwrap_or_default();
let provider_id = request
.get("provider_id")
.and_then(Value::as_str)
.unwrap_or("");
let answers = request.get("answers").cloned().unwrap_or_default();
let providers_dir = state.bundle_root.join("providers");
let pack_path = find_provider_pack(&providers_dir, provider_id);
match pack_path {
Some(path) => {
let form_spec =
greentic_setup::setup_to_formspec::pack_to_form_spec(&path, provider_id);
match form_spec {
Some(spec) => {
match greentic_setup::qa::wizard::validate_answers_against_form_spec(
&spec, &answers,
) {
Ok(()) => json_response(
StatusCode::OK,
&AdminResponse::ok(json!({"valid": true})),
),
Err(e) => json_response(
StatusCode::OK,
&AdminResponse::ok(json!({"valid": false, "error": e.to_string()})),
),
}
}
None => json_response(
StatusCode::OK,
&AdminResponse::ok(json!({"valid": true, "note": "no spec found"})),
),
}
}
None => json_response(
StatusCode::NOT_FOUND,
&AdminResponse::<()>::err(format!("provider not found: {provider_id}")),
),
}
}
async fn handle_qa_submit(
req: Request<Incoming>,
state: &Arc<AdminState>,
) -> Result<Response<Full<Bytes>>> {
let body = read_body(req).await?;
let request: Value = serde_json::from_slice(&body).unwrap_or_default();
let provider_id = request
.get("provider_id")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let answers = request.get("answers").cloned().unwrap_or_default();
if provider_id.is_empty() {
return json_response(
StatusCode::BAD_REQUEST,
&AdminResponse::<()>::err("provider_id required"),
);
}
let persisted = crate::qa_persist::persist_all_config_as_secrets(
&state.bundle_root,
&state.env,
&state.tenant,
state.team.as_deref(),
&provider_id,
&answers,
None,
)
.await;
match persisted {
Ok(keys) => json_response(
StatusCode::OK,
&AdminResponse::ok(json!({
"persisted_keys": keys,
"provider_id": provider_id,
})),
),
Err(e) => json_response(
StatusCode::INTERNAL_SERVER_ERROR,
&AdminResponse::<()>::err(e.to_string()),
),
}
}
fn find_provider_pack(providers_dir: &std::path::Path, provider_id: &str) -> Option<PathBuf> {
for dir_name in &["messaging", "events", "oauth", "secrets", "mcp"] {
let pack = providers_dir
.join(dir_name)
.join(format!("{provider_id}.gtpack"));
if pack.exists() {
return Some(pack);
}
}
let flat = providers_dir.join(format!("{provider_id}.gtpack"));
if flat.exists() {
return Some(flat);
}
None
}
fn json_response<T: serde::Serialize>(
status: StatusCode,
body: &T,
) -> Result<Response<Full<Bytes>>> {
let json = serde_json::to_vec(body)?;
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(Full::new(Bytes::from(json)))
.unwrap())
}
async fn read_body(req: Request<Incoming>) -> Result<Vec<u8>> {
use http_body_util::BodyExt;
let body = req.into_body().collect().await?.to_bytes();
Ok(body.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_provider_pack_returns_none_for_missing() {
let tmp = tempfile::tempdir().unwrap();
assert!(find_provider_pack(tmp.path(), "nonexistent").is_none());
}
#[test]
fn find_provider_pack_finds_flat_pack() {
let tmp = tempfile::tempdir().unwrap();
let pack = tmp.path().join("messaging-telegram.gtpack");
std::fs::write(&pack, "pack").unwrap();
assert_eq!(
find_provider_pack(tmp.path(), "messaging-telegram"),
Some(pack)
);
}
}