diff --git a/backend/windmill-api/src/hub_publish.rs b/backend/windmill-api/src/hub_publish.rs new file mode 100644 index 0000000000..f846f83ef4 --- /dev/null +++ b/backend/windmill-api/src/hub_publish.rs @@ -0,0 +1,188 @@ +use crate::HTTP_CLIENT; +use axum::{ + extract::{Json, Path}, + http::StatusCode, + response::IntoResponse, + routing::post, + Router, +}; +use serde::{Deserialize, Serialize}; +use windmill_common::{error::Error, HUB_BASE_URL}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/publish_draft", post(publish_draft)) + .route("/scripts", post(publish_script)) + .route("/flows", post(publish_flow)) + .route("/apps", post(publish_app)) + .route("/raw_apps", post(publish_raw_app)) + .route( + "/scripts/{ask_id}/recording", + post(publish_script_recording), + ) + .route("/flows/{flow_id}/recording", post(publish_flow_recording)) +} + +#[derive(Deserialize)] +struct PublishDraftBody { + slug: String, + name: String, + summary: String, + #[serde(default)] + readme: Option, +} + +#[derive(Serialize)] +struct HubWorkspaceBody { + slug: String, + name: String, + summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + readme: Option, +} + +async fn publish_draft( + Path(_workspace): Path, + Json(body): Json, +) -> Result { + forward_to_hub( + "/workspaces", + &HubWorkspaceBody { + slug: body.slug, + name: body.name, + summary: body.summary, + readme: body.readme, + }, + ) + .await +} + +#[derive(Deserialize, Serialize)] +struct PublishScriptBody { + summary: String, + app: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + kind: Option, + content: String, + language: String, + #[serde(skip_serializing_if = "Option::is_none")] + schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + lockfile: Option, + workspace_slug: String, +} + +async fn publish_script( + Path(_workspace): Path, + Json(body): Json, +) -> Result { + forward_to_hub("/scripts/add", &body).await +} + +#[derive(Deserialize, Serialize)] +struct PublishFlowInner { + summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + value: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + schema: Option, +} + +#[derive(Deserialize, Serialize)] +struct PublishFlowBody { + flow: PublishFlowInner, + apps: Vec, + workspace_slug: String, + #[serde(skip_serializing_if = "Option::is_none")] + recording: Option, +} + +async fn publish_flow( + Path(_workspace): Path, + Json(body): Json, +) -> Result { + forward_to_hub("/flows", &body).await +} + +#[derive(Deserialize, Serialize)] +struct PublishAppBody { + app: serde_json::Value, + apps: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + summary: String, + workspace_slug: String, +} + +async fn publish_app( + Path(_workspace): Path, + Json(body): Json, +) -> Result { + forward_to_hub("/apps", &body).await +} + +#[derive(Deserialize, Serialize)] +struct PublishRawAppBody { + raw: String, + apps: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + external_embed_url: Option, + workspace_slug: String, +} + +async fn publish_raw_app( + Path(_workspace): Path, + Json(body): Json, +) -> Result { + forward_to_hub("/raw_apps", &body).await +} + +#[derive(Deserialize, Serialize)] +struct RecordingBody { + #[serde(skip_serializing_if = "Option::is_none")] + recording: Option, + workspace_slug: String, +} + +async fn publish_script_recording( + Path((_workspace, ask_id)): Path<(String, i64)>, + Json(body): Json, +) -> Result { + forward_to_hub(&format!("/scripts/{}/recording", ask_id), &body).await +} + +async fn publish_flow_recording( + Path((_workspace, flow_id)): Path<(String, i64)>, + Json(body): Json, +) -> Result { + forward_to_hub(&format!("/flows/{}/recording", flow_id), &body).await +} + +async fn forward_to_hub(path: &str, body: &T) -> Result<(StatusCode, String), Error> { + let hub_token = std::env::var("HUB_DEV_TOKEN") + .map_err(|_| Error::BadRequest("HUB_DEV_TOKEN not set".into()))?; + + let url = format!("{}{}", **HUB_BASE_URL.load(), path); + + let res = HTTP_CLIENT + .post(&url) + .bearer_auth(&hub_token) + .json(body) + .send() + .await + .map_err(|e| Error::InternalErr(format!("hub request failed: {e}")))?; + + let status = StatusCode::from_u16(res.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); + let text = res + .text() + .await + .map_err(|e| Error::InternalErr(format!("hub response read failed: {e}")))?; + + Ok((status, text)) +} diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 8b2b9d8128..3168c1c900 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -97,6 +97,7 @@ mod indexer_oss; #[cfg(feature = "private")] mod inkeep_ee; mod inkeep_oss; +mod hub_publish; mod integration; mod internal_db; mod live_migrations; @@ -627,6 +628,7 @@ pub async fn run_server( .nest("/volumes", volumes_oss::workspaced_service()) .nest("/workers", windmill_api_workers::workspaced_service()) .nest("/workspaces", workspaces::workspaced_service()) + .nest("/hub", hub_publish::workspaced_service()) .nest( "/deployment_request", windmill_api_workspaces::deployment_requests::workspaced_service(),