diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 1c4610d306..b321c4abb3 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -4685,6 +4685,7 @@ dependencies = [ "lazy_static", "magic-crypt", "mime_guess", + "prometheus", "rand 0.8.5", "reqwest", "retainer", 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/sqlx-data.json b/backend/sqlx-data.json index 048563d040..bf616a5933 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -558,6 +558,11 @@ "name": "plan", "ordinal": 8, "type_info": "Varchar" + }, + { + "name": "webhook", + "ordinal": 9, + "type_info": "Text" } ], "nullable": [ @@ -569,6 +574,7 @@ true, true, true, + true, true ], "parameters": { @@ -1231,6 +1237,18 @@ }, "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)" }, + "33d69b3915ddfde40323ace65c14e39fa4bbc8b5dd50a34e165765eaea1f4966": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1" + }, "355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": { "describe": { "columns": [ @@ -1850,6 +1868,11 @@ "name": "plan", "ordinal": 8, "type_info": "Varchar" + }, + { + "name": "webhook", + "ordinal": 9, + "type_info": "Text" } ], "nullable": [ @@ -1861,6 +1884,7 @@ true, true, true, + true, true ], "parameters": { @@ -2710,6 +2734,19 @@ }, "query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash" }, + "8292b7b2cce5319575bc09ad18f29b63270872b6e5c6df1f0a326370058f13b0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2" + }, "82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": { "describe": { "columns": [], @@ -3765,6 +3802,26 @@ }, "query": "SELECT email FROM usr where username = $1 AND workspace_id = $2" }, + "a34b79872766941cae2d62c99d80e28b7214dd2fcbb68020a63325bbcb34f417": { + "describe": { + "columns": [ + { + "name": "webhook", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT webhook FROM workspace_settings WHERE workspace_id = $1" + }, "a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": { "describe": { "columns": [ diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index be164c109c..936284f177 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -70,4 +70,5 @@ cookie.workspace = true sha2.workspace = true urlencoding.workspace = true async-stripe.workspace = true -lazy_static.workspace = true \ No newline at end of file +lazy_static.workspace = true +prometheus.workspace = true \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index fbe31c1995..91f64f7f0f 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..ca85300719 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, WebhookShared}, }; 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, + Extension(webhook): Extension, 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( + w_id.clone(), + WebhookMessage::CreateApp { workspace: w_id, 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, + Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { let path = path.to_path(); @@ -418,6 +426,10 @@ async fn delete_app( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone().clone(), + WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() }, + ); Ok(format!("app {} deleted", path)) } @@ -425,6 +437,7 @@ async fn delete_app( async fn update_app( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, @@ -514,6 +527,14 @@ async fn update_app( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateApp { + workspace: w_id, + 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..03ecf8bae7 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, WebhookShared}, }; 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, + Extension(webhook): Extension, 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( + w_id.clone(), + 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, + Extension(webhook): Extension, Path((w_id, flow_path)): Path<(String, StripPath)>, Json(nf): Json, ) -> Result { @@ -368,6 +375,14 @@ async fn update_flow( .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + 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 +465,7 @@ async fn exists_flow_by_path( async fn archive_flow_by_path( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { let path = path.to_path(); @@ -474,6 +490,10 @@ async fn archive_flow_by_path( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::ArchiveFlow { workspace: w_id, 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..cb10f45b8d 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, WebhookShared}, }; 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, + Extension(webhook): Extension, Path(w_id): Path, Json(ng): Json, ) -> Result { @@ -193,8 +195,12 @@ async fn create_folder( None, ) .await?; - tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::CreateFolder { workspace: w_id, 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, + Extension(webhook): Extension, 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( + w_id.clone().clone(), + WebhookMessage::UpdateFolder { workspace: w_id, 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, + Extension(webhook): Extension, Path((w_id, name)): Path<(String, String)>, ) -> Result { let mut tx = user_db.begin(&authed).await?; @@ -440,6 +452,12 @@ async fn delete_folder( ) .await?; tx.commit().await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() }, + ); + Ok(format!("delete folder at name {}", name)) } @@ -447,6 +465,7 @@ async fn add_owner( authed: Authed, Extension(db): Extension, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, ) -> Result { @@ -477,6 +496,12 @@ async fn add_owner( ) .await?; tx.commit().await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() }, + ); + Ok(format!("Added {} to folder {}", owner, name)) } @@ -510,6 +535,7 @@ async fn remove_owner( authed: Authed, Extension(db): Extension, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, ) -> Result { @@ -540,5 +566,11 @@ async fn remove_owner( ) .await?; tx.commit().await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateFolder { workspace: w_id, 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..f3a2bbcde0 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, }; 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(), db.clone()))); // build our application with a route let app = Router::new() .nest( diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index b41daad60f..d31078115c 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, WebhookShared}, }; 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, + Extension(webhook): Extension, 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( + w_id.clone(), + WebhookMessage::CreateResource { workspace: w_id, 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, + Extension(webhook): Extension, 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( + w_id.clone(), + WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() }, + ); + Ok(format!("resource {} deleted", path)) } async fn update_resource( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, @@ -400,6 +414,15 @@ async fn update_resource( .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateResource { + workspace: w_id, + old_path: path.to_owned(), + new_path: npath.clone(), + }, + ); + Ok(format!("resource {} updated (npath: {:?})", path, npath)) } @@ -411,6 +434,7 @@ struct UpdateResource { async fn update_resource_value( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(nv): Json, ) -> Result { @@ -436,6 +460,14 @@ async fn update_resource_value( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateResource { + workspace: w_id, + old_path: path.to_owned(), + new_path: path.to_owned(), + }, + ); Ok(format!("value of resource {} updated", path)) } @@ -513,6 +545,7 @@ async fn exists_resource_type( async fn create_resource_type( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path(w_id): Path, Json(resource_type): Json, ) -> Result<(StatusCode, String)> { @@ -543,6 +576,11 @@ async fn create_resource_type( .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::CreateResourceType { name: resource_type.name.clone() }, + ); + Ok(( StatusCode::CREATED, format!("resource_type {} created", resource_type.name), @@ -574,6 +612,7 @@ async fn check_rt_path_conflict<'c>( async fn delete_resource_type( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, name)): Path<(String, String)>, ) -> Result { require_admin(authed.is_admin, &authed.username)?; @@ -598,6 +637,10 @@ async fn delete_resource_type( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::DeleteResourceType { name: name.clone() }, + ); Ok(format!("resource_type {} deleted", name)) } @@ -605,6 +648,7 @@ async fn delete_resource_type( async fn update_resource_type( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, name)): Path<(String, String)>, Json(ns): Json, ) -> Result { @@ -634,6 +678,10 @@ async fn update_resource_type( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + 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..444f9c2f3a 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, WebhookShared}, }; 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, + Extension(webhook): Extension, Extension(db): Extension, Path(w_id): Path, Json(ns): Json, @@ -400,6 +402,14 @@ async fn create_script( Some([("hash", hash.to_string().as_str())].into()), ) .await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateScript { + workspace: w_id, + path: ns.path.clone(), + hash: hash.to_string(), + }, + ); } else { audit_log( &mut tx, @@ -417,6 +427,14 @@ async fn create_script( ), ) .await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::CreateScript { + workspace: w_id, + path: ns.path.clone(), + hash: hash.to_string(), + }, + ); } tx.commit().await?; @@ -600,6 +618,7 @@ async fn get_deployment_status( async fn archive_script_by_path( authed: Authed, + Extension(webhook): Extension, Extension(user_db): Extension, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -626,6 +645,10 @@ async fn archive_script_by_path( ) .await?; tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() }, + ); Ok(()) } @@ -633,6 +656,7 @@ async fn archive_script_by_path( async fn archive_script_by_hash( authed: Authed, Extension(user_db): Extension, + Extension(webhook): Extension, Path((w_id, hash)): Path<(String, ScriptHash)>, ) -> JsonResult