Skip to main content

greentic_operator/onboard/
api.rs

1use std::sync::Arc;
2
3use http_body_util::{BodyExt, Full};
4use hyper::{
5    Method, Request, Response, StatusCode,
6    body::{Bytes, Incoming},
7    header::CONTENT_TYPE,
8};
9use serde_json::{Value, json};
10
11use crate::demo::runner_host::DemoRunnerHost;
12
13use super::providers;
14use super::wizard;
15
16/// Shared state for onboard API handlers.
17pub struct OnboardState {
18    pub runner_host: Arc<DemoRunnerHost>,
19}
20
21pub type OnboardResponse = Response<Full<Bytes>>;
22pub type OnboardError = Box<OnboardResponse>;
23pub type OnboardResult<T = OnboardResponse> = Result<T, OnboardError>;
24
25pub fn into_error(response: OnboardResponse) -> OnboardError {
26    Box::new(response)
27}
28
29/// Dispatch onboard API requests.
30///
31/// Routes:
32///   GET  /api/onboard/providers   → list available provider packs
33///   GET  /api/onboard/tenants     → list tenants
34///   GET  /api/onboard/status      → deployment status
35///   POST /api/onboard/qa/spec     → get FormSpec for a provider
36///   POST /api/onboard/qa/validate → validate partial answers
37///   POST /api/onboard/qa/submit   → submit answers → deploy
38pub async fn handle_onboard_request(
39    req: Request<Incoming>,
40    path: &str,
41    runner_host: &Arc<DemoRunnerHost>,
42) -> OnboardResult {
43    let state = OnboardState {
44        runner_host: runner_host.clone(),
45    };
46
47    let method = req.method().clone();
48    let sub_path = path
49        .strip_prefix("/api/onboard")
50        .unwrap_or("")
51        .trim_end_matches('/');
52
53    match (method, sub_path) {
54        (Method::GET, "/providers") => providers::list_providers(&state),
55        (Method::GET, "/tenants") => providers::list_tenants(&state),
56        (Method::GET, "/status") => providers::deployment_status(&state),
57        (Method::POST, "/qa/spec") => {
58            let body = read_json_body(req).await?;
59            wizard::get_form_spec(&state, &body)
60        }
61        (Method::POST, "/qa/validate") => {
62            let body = read_json_body(req).await?;
63            wizard::validate_answers(&state, &body)
64        }
65        (Method::POST, "/qa/submit") => {
66            let body = read_json_body(req).await?;
67            // Run on a dedicated thread to avoid nested Tokio runtime panics.
68            // submit_answers → invoke_provider_op → run_pack_with_options
69            // all create their own Runtime::new(), which panics on a Tokio worker.
70            std::thread::scope(|s| {
71                s.spawn(|| wizard::submit_answers(&state, &body))
72                    .join()
73                    .expect("submit thread panicked")
74            })
75        }
76        (Method::POST, "/tenants/create") => {
77            let body = read_json_body(req).await?;
78            providers::create_tenant(&state, &body)
79        }
80        (Method::POST, "/tenants/teams/create") => {
81            let body = read_json_body(req).await?;
82            providers::create_team(&state, &body)
83        }
84        _ => Err(into_error(error_response(
85            StatusCode::NOT_FOUND,
86            format!("unknown onboard endpoint: {sub_path}"),
87        ))),
88    }
89}
90
91/// Read and parse a JSON body from the request.
92async fn read_json_body(req: Request<Incoming>) -> OnboardResult<Value> {
93    let payload_bytes = req
94        .into_body()
95        .collect()
96        .await
97        .map(|collected| collected.to_bytes())
98        .map_err(|err| {
99            into_error(error_response(
100                StatusCode::BAD_REQUEST,
101                format!("read body: {err}"),
102            ))
103        })?;
104
105    if payload_bytes.is_empty() {
106        return Ok(json!({}));
107    }
108
109    serde_json::from_slice(&payload_bytes).map_err(|err| {
110        into_error(error_response(
111            StatusCode::BAD_REQUEST,
112            format!("invalid JSON: {err}"),
113        ))
114    })
115}
116
117pub fn json_ok(value: Value) -> OnboardResult {
118    Ok(json_response(StatusCode::OK, value))
119}
120
121pub fn json_response(status: StatusCode, value: Value) -> OnboardResponse {
122    let body = serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string());
123    Response::builder()
124        .status(status)
125        .header(CONTENT_TYPE, "application/json")
126        .body(Full::from(Bytes::from(body)))
127        .unwrap_or_else(|err| {
128            Response::builder()
129                .status(StatusCode::INTERNAL_SERVER_ERROR)
130                .body(Full::from(Bytes::from(format!(
131                    "failed to build response: {err}"
132                ))))
133                .unwrap()
134        })
135}
136
137pub fn error_response(status: StatusCode, message: impl Into<String>) -> OnboardResponse {
138    json_response(
139        status,
140        json!({
141            "success": false,
142            "message": message.into()
143        }),
144    )
145}