diff --git a/backend/migrations/20230126023323_webhook.down.sql b/backend/migrations/20230126023323_webhook.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20230126023323_webhook.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20230126023323_webhook.up.sql b/backend/migrations/20230126023323_webhook.up.sql new file mode 100644 index 0000000000..ea29fe8122 --- /dev/null +++ b/backend/migrations/20230126023323_webhook.up.sql @@ -0,0 +1,5 @@ +-- Add up migration script here +ALTER TABLE + workspace_settings +ADD + COLUMN webhook text; \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index d5be47cd7b..f5ebae499c 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -883,6 +883,8 @@ paths: type: string customer_id: type: string + webhook: + type: string /w/{workspace}/workspaces/premium_info: get: @@ -962,6 +964,33 @@ paths: schema: type: string + /w/{workspace}/workspaces/edit_webhook: + post: + summary: edit webhook + operationId: editWebhook + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: WorkspaceWebhook + required: true + content: + application/json: + schema: + type: object + properties: + webhook: + type: string + + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + /w/{workspace}/users/list: get: summary: list users diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index fb9bf38b20..303550cce6 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -12,6 +12,7 @@ use crate::{ jobs::script_path_to_payload, users::{require_owner_of_path, Authed, OptAuthed}, variables::build_crypt, + webhook_util::{WebhookMessage, WebhookUtil}, }; use axum::{ extract::{Extension, Json, Path, Query}, @@ -310,6 +311,7 @@ async fn get_secret_id( async fn create_app( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path(w_id): Path, Json(app): Json, ) -> Result<(StatusCode, String)> { @@ -356,7 +358,12 @@ async fn create_app( None, ) .await?; + tx.commit().await?; + webhook.send_message(WebhookMessage::CreateApp { + workspace: w_id.clone(), + path: app.path.clone(), + }); Ok((StatusCode::CREATED, app.path)) } @@ -395,6 +402,7 @@ pub async fn get_hub_app_by_id( async fn delete_app( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { let path = path.to_path(); @@ -418,6 +426,8 @@ async fn delete_app( ) .await?; tx.commit().await?; + webhook + .send_message(WebhookMessage::DeleteApp { workspace: w_id.clone(), path: path.to_owned() }); Ok(format!("app {} deleted", path)) } @@ -425,6 +435,7 @@ async fn delete_app( async fn update_app( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, @@ -514,6 +525,11 @@ async fn update_app( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateApp { + workspace: w_id.clone(), + old_path: path.to_owned(), + new_path: npath.clone(), + }); Ok(format!("app {} updated (npath: {:?})", path, npath)) } diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index cc40111cb5..b934c397cb 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -32,6 +32,7 @@ use crate::{ db::{UserDB, DB}, schedule::clear_schedule, users::{require_owner_of_path, Authed}, + webhook_util::{WebhookMessage, WebhookUtil}, }; pub fn workspaced_service() -> Router { @@ -181,6 +182,7 @@ async fn check_path_conflict<'c>( async fn create_flow( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path(w_id): Path, Json(nf): Json, ) -> Result<(StatusCode, String)> { @@ -221,6 +223,10 @@ async fn create_flow( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::CreateFlow { + workspace: w_id.clone(), + path: nf.path.clone(), + }); let tx = user_db.begin(&authed).await?; let (dependency_job_uuid, mut tx) = push( @@ -280,6 +286,7 @@ async fn update_flow( authed: Authed, Extension(user_db): Extension, Extension(db): Extension, + webhook: WebhookUtil, Path((w_id, flow_path)): Path<(String, StripPath)>, Json(nf): Json, ) -> Result { @@ -368,6 +375,11 @@ async fn update_flow( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateFlow { + workspace: w_id.clone(), + old_path: flow_path.to_owned(), + new_path: nf.path.clone(), + }); let tx = user_db.begin(&authed).await?; let (dependency_job_uuid, mut tx) = push( @@ -450,6 +462,7 @@ async fn exists_flow_by_path( async fn archive_flow_by_path( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { let path = path.to_path(); @@ -474,6 +487,10 @@ async fn archive_flow_by_path( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::ArchiveFlow { + workspace: w_id.clone(), + path: path.to_owned(), + }); Ok(format!("Flow {path} archived")) } diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index 45927304a7..8f9f3c5251 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -9,6 +9,7 @@ use crate::{ db::{UserDB, DB}, users::Authed, + webhook_util::{WebhookMessage, WebhookUtil}, }; use axum::{ extract::{Extension, Path, Query}, @@ -139,6 +140,7 @@ async fn check_name_conflict<'c>( async fn create_folder( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path(w_id): Path, Json(ng): Json, ) -> Result { @@ -193,8 +195,12 @@ async fn create_folder( None, ) .await?; - tx.commit().await?; + webhook.send_message(WebhookMessage::CreateFolder { + workspace: w_id.clone(), + name: ng.name.clone(), + }); + Ok(format!("Created folder {}", ng.name)) } @@ -245,6 +251,7 @@ pub async fn require_is_owner( async fn update_folder( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, Json(ng): Json, ) -> Result { @@ -298,8 +305,12 @@ async fn update_folder( None, ) .await?; - tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateFolder { + workspace: w_id.clone(), + name: name.to_owned(), + }); + Ok(format!("Updated folder {}", name)) } @@ -416,6 +427,7 @@ async fn get_folder_usage( async fn delete_folder( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, ) -> Result { let mut tx = user_db.begin(&authed).await?; @@ -440,6 +452,10 @@ async fn delete_folder( ) .await?; tx.commit().await?; + + webhook + .send_message(WebhookMessage::DeleteFolder { workspace: w_id.clone(), name: name.clone() }); + Ok(format!("delete folder at name {}", name)) } @@ -447,6 +463,7 @@ async fn add_owner( authed: Authed, Extension(db): Extension, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, ) -> Result { @@ -477,6 +494,10 @@ async fn add_owner( ) .await?; tx.commit().await?; + + webhook + .send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() }); + Ok(format!("Added {} to folder {}", owner, name)) } @@ -510,6 +531,7 @@ async fn remove_owner( authed: Authed, Extension(db): Extension, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, ) -> Result { @@ -540,5 +562,9 @@ async fn remove_owner( ) .await?; tx.commit().await?; + + webhook + .send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() }); + Ok(format!("Removed {} to folder {}", owner, name)) } diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 8db6297e78..53d805fc49 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -21,6 +21,7 @@ use crate::{ oauth2::{build_oauth_clients, SlackVerifier}, tracing_init::{MyMakeSpan, MyOnResponse}, users::{Authed, OptAuthed}, + webhook_util::{WebhookShared, WebhookUtil}, }; mod apps; @@ -42,6 +43,7 @@ mod tracing_init; mod users; mod utils; mod variables; +mod webhook_util; mod worker_ping; mod workspaces; @@ -106,7 +108,8 @@ pub async fn run_server( std::env::var("COOKIE_DOMAIN").ok(), )))) .layer(Extension(http_client)) - .layer(CookieManagerLayer::new()); + .layer(CookieManagerLayer::new()) + .layer(Extension(WebhookShared::new(rx.resubscribe()))); // build our application with a route let app = Router::new() .nest( @@ -147,7 +150,8 @@ pub async fn run_server( .nest("/flows", flows::workspaced_service()) .nest("/capture", capture::workspaced_service()) .nest("/favorites", favorite::workspaced_service()) - .nest("/folders", folders::workspaced_service()), + .nest("/folders", folders::workspaced_service()) + .route_layer(from_extractor::()), ) .nest("/workspaces", workspaces::global_service()) .nest( diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index b41daad60f..4c16323927 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -9,6 +9,7 @@ use crate::{ db::{UserDB, DB}, users::{require_owner_of_path, Authed}, + webhook_util::{WebhookMessage, WebhookUtil}, }; use axum::{ extract::{Extension, Path, Query}, @@ -263,6 +264,7 @@ async fn check_path_conflict<'c>( async fn create_resource( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path(w_id): Path, Json(resource): Json, ) -> Result<(StatusCode, String)> { @@ -293,6 +295,11 @@ async fn create_resource( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::CreateResource { + workspace: w_id.clone(), + path: resource.path.clone(), + }); + Ok(( StatusCode::CREATED, format!("resource {} created", resource.path), @@ -302,6 +309,7 @@ async fn create_resource( async fn delete_resource( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { let path = path.to_path(); @@ -333,12 +341,18 @@ async fn delete_resource( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::DeleteResource { + workspace: w_id.clone(), + path: path.to_owned(), + }); + Ok(format!("resource {} deleted", path)) } async fn update_resource( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, @@ -400,6 +414,12 @@ async fn update_resource( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateResource { + workspace: w_id.clone(), + old_path: path.to_owned(), + new_path: npath.clone(), + }); + Ok(format!("resource {} updated (npath: {:?})", path, npath)) } @@ -411,6 +431,7 @@ struct UpdateResource { async fn update_resource_value( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, path)): Path<(String, StripPath)>, Json(nv): Json, ) -> Result { @@ -436,6 +457,11 @@ async fn update_resource_value( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateResource { + workspace: w_id.clone(), + old_path: path.to_owned(), + new_path: path.to_owned(), + }); Ok(format!("value of resource {} updated", path)) } @@ -513,6 +539,7 @@ async fn exists_resource_type( async fn create_resource_type( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path(w_id): Path, Json(resource_type): Json, ) -> Result<(StatusCode, String)> { @@ -543,6 +570,8 @@ async fn create_resource_type( .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::CreateResourceType { name: resource_type.name.clone() }); + Ok(( StatusCode::CREATED, format!("resource_type {} created", resource_type.name), @@ -574,6 +603,7 @@ async fn check_rt_path_conflict<'c>( async fn delete_resource_type( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, ) -> Result { require_admin(authed.is_admin, &authed.username)?; @@ -598,6 +628,7 @@ async fn delete_resource_type( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::DeleteResourceType { name: name.clone() }); Ok(format!("resource_type {} deleted", name)) } @@ -605,6 +636,7 @@ async fn delete_resource_type( async fn update_resource_type( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, name)): Path<(String, String)>, Json(ns): Json, ) -> Result { @@ -634,6 +666,7 @@ async fn update_resource_type( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::UpdateResourceType { name: name.clone() }); Ok(format!("resource_type {} updated", name)) } diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 0798825c0e..4367dc4791 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -14,6 +14,7 @@ use crate::{ db::{UserDB, DB}, schedule::clear_schedule, users::{require_owner_of_path, Authed}, + webhook_util::{WebhookMessage, WebhookUtil}, }; use axum::{ extract::{Extension, Path, Query}, @@ -184,6 +185,7 @@ fn hash_script(ns: &NewScript) -> i64 { async fn create_script( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Extension(db): Extension, Path(w_id): Path, Json(ns): Json, @@ -400,6 +402,11 @@ async fn create_script( Some([("hash", hash.to_string().as_str())].into()), ) .await?; + webhook.send_message(WebhookMessage::UpdateScript { + workspace: w_id.clone(), + path: ns.path.clone(), + hash: hash.to_string(), + }); } else { audit_log( &mut tx, @@ -417,6 +424,11 @@ async fn create_script( ), ) .await?; + webhook.send_message(WebhookMessage::CreateScript { + workspace: w_id.clone(), + path: ns.path.clone(), + hash: hash.to_string(), + }); } tx.commit().await?; @@ -600,6 +612,7 @@ async fn get_deployment_status( async fn archive_script_by_path( authed: Authed, + webhook: WebhookUtil, Extension(user_db): Extension, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -626,6 +639,10 @@ async fn archive_script_by_path( ) .await?; tx.commit().await?; + webhook.send_message(WebhookMessage::DeleteScript { + workspace: w_id.clone(), + hash: hash.to_string(), + }); Ok(()) } @@ -633,6 +650,7 @@ async fn archive_script_by_path( async fn archive_script_by_hash( authed: Authed, Extension(user_db): Extension, + webhook: WebhookUtil, Path((w_id, hash)): Path<(String, ScriptHash)>, ) -> JsonResult