greentic_operator/onboard/
api.rs1use 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
16pub 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
29pub 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 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
91async 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}