feat: Add workspace webhook (#1158)
* Add workspace webhook * Update SQLX * Add webhook request histogram * Move workspace webhook retrieval OOP * Add removal to frontend, cache removed webhooks * Remove WebhookUtil --------- Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@@ -4685,6 +4685,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mime_guess",
|
||||
"prometheus",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"retainer",
|
||||
|
||||
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE
|
||||
workspace_settings
|
||||
ADD
|
||||
COLUMN webhook text;
|
||||
@@ -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": [
|
||||
|
||||
@@ -70,4 +70,5 @@ cookie.workspace = true
|
||||
sha2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
async-stripe.workspace = true
|
||||
lazy_static.workspace = true
|
||||
lazy_static.workspace = true
|
||||
prometheus.workspace = true
|
||||
@@ -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
|
||||
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(app): Json<CreateApp>,
|
||||
) -> 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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditApp>,
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> 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<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||
Json(nf): Json<NewFlow>,
|
||||
) -> Result<String> {
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ng): Json<NewFolder>,
|
||||
) -> Result<String> {
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(ng): Json<UpdateFolder>,
|
||||
) -> Result<String> {
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
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<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(Owner { owner }): Json<Owner>,
|
||||
) -> Result<String> {
|
||||
@@ -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<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(Owner { owner }): Json<Owner>,
|
||||
) -> Result<String> {
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(resource): Json<CreateResource>,
|
||||
) -> 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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditResource>,
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(nv): Json<UpdateResource>,
|
||||
) -> Result<String> {
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(resource_type): Json<CreateResourceType>,
|
||||
) -> 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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<String> {
|
||||
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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Json(ns): Json<EditResourceType>,
|
||||
) -> Result<String> {
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
@@ -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<WebhookShared>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
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<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
@@ -657,12 +681,18 @@ async fn archive_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
|
||||
);
|
||||
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn delete_script_by_hash(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||
) -> JsonResult<Script> {
|
||||
@@ -691,6 +721,11 @@ async fn delete_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
|
||||
);
|
||||
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
oauth2::{AllClients, _refresh_token},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
BaseUrl,
|
||||
};
|
||||
/*
|
||||
@@ -224,6 +225,7 @@ async fn check_path_conflict<'c>(
|
||||
async fn create_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(variable): Json<CreateVariable>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
@@ -265,6 +267,11 @@ async fn create_variable(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateVariable { workspace: w_id, path: variable.path.clone() },
|
||||
);
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("variable {} created", variable.path),
|
||||
@@ -274,6 +281,7 @@ async fn create_variable(
|
||||
async fn delete_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
@@ -306,6 +314,11 @@ async fn delete_variable(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
|
||||
);
|
||||
|
||||
Ok(format!("variable {} deleted", path))
|
||||
}
|
||||
|
||||
@@ -320,6 +333,7 @@ struct EditVariable {
|
||||
async fn update_variable(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(ns): Json<EditVariable>,
|
||||
@@ -405,6 +419,15 @@ async fn update_variable(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::UpdateVariable {
|
||||
workspace: w_id,
|
||||
old_path: path.to_owned(),
|
||||
new_path: npath.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
|
||||
112
backend/windmill-api/src/webhook_util.rs
Normal file
112
backend/windmill-api/src/webhook_util.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Serialize;
|
||||
use tokio::{select, sync::mpsc, time::interval};
|
||||
|
||||
use crate::db::DB;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
|
||||
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
|
||||
"webhook_request",
|
||||
"Histogram of webhook requests made"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WebhookMessage {
|
||||
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
|
||||
CreateApp { workspace: String, path: String },
|
||||
DeleteApp { workspace: String, path: String },
|
||||
UpdateApp { workspace: String, old_path: String, new_path: String },
|
||||
CreateFlow { workspace: String, path: String },
|
||||
UpdateFlow { workspace: String, old_path: String, new_path: String },
|
||||
ArchiveFlow { workspace: String, path: String },
|
||||
CreateFolder { workspace: String, name: String },
|
||||
UpdateFolder { workspace: String, name: String },
|
||||
DeleteFolder { workspace: String, name: String },
|
||||
DeleteResource { workspace: String, path: String },
|
||||
CreateResource { workspace: String, path: String },
|
||||
UpdateResource { workspace: String, old_path: String, new_path: String },
|
||||
CreateResourceType { name: String },
|
||||
DeleteResourceType { name: String },
|
||||
UpdateResourceType { name: String },
|
||||
CreateScript { workspace: String, path: String, hash: String },
|
||||
UpdateScript { workspace: String, path: String, hash: String },
|
||||
DeleteScript { workspace: String, hash: String },
|
||||
CreateVariable { workspace: String, path: String },
|
||||
UpdateVariable { workspace: String, old_path: String, new_path: String },
|
||||
DeleteVariable { workspace: String, path: String },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebhookShared {
|
||||
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
|
||||
}
|
||||
|
||||
impl WebhookShared {
|
||||
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
|
||||
let _process = tokio::spawn(async move {
|
||||
let client = reqwest::Client::builder()
|
||||
// TODO: investigate pool timeouts and such if TCP load is high
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let cache = retainer::Cache::new();
|
||||
let mut cache_purge_interval = interval(Duration::from_secs(30));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = shutdown_rx.recv() => break,
|
||||
r = rx.recv() => match r {
|
||||
Some((workspace_id, message)) => {
|
||||
let url_guard = match cache.get(&workspace_id).await {
|
||||
Some(guard) => {
|
||||
guard
|
||||
},
|
||||
None => {
|
||||
let Ok(webook_opt) =
|
||||
sqlx::query_scalar!(
|
||||
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(
|
||||
&db,
|
||||
)
|
||||
.await else {
|
||||
tracing::error!("Webhook Message to send - but cannot get workspace settings! Workspace: {workspace_id}");
|
||||
continue;
|
||||
};
|
||||
cache.insert(workspace_id.clone(), webook_opt, Duration::from_secs(30)).await;
|
||||
cache.get(&workspace_id).await.unwrap()
|
||||
}
|
||||
};
|
||||
let webook_opt = url_guard.value();
|
||||
if let Some(url) = webook_opt {
|
||||
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
|
||||
let _ = client.post(url).json(&message).send().await;
|
||||
timer.stop_and_record();
|
||||
drop(url_guard);
|
||||
}
|
||||
},
|
||||
None => break,
|
||||
},
|
||||
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
|
||||
tracing::trace!("Purging Webhook Cache");
|
||||
cache.purge(10, 0.50).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self { channel: tx }
|
||||
}
|
||||
|
||||
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
|
||||
let _ = self.channel.send((workspace_id.clone(), message));
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/delete_invite", post(delete_invite))
|
||||
.route("/get_settings", get(get_settings))
|
||||
.route("/edit_slack_command", post(edit_slack_command))
|
||||
.route("/edit_webhook", post(edit_webhook))
|
||||
.route("/edit_auto_invite", post(edit_auto_invite))
|
||||
.route("/tarball", get(tarball_workspace))
|
||||
.route("/premium_info", get(premium_info))
|
||||
@@ -90,6 +91,7 @@ pub struct WorkspaceSettings {
|
||||
pub auto_invite_operator: Option<bool>,
|
||||
pub customer_id: Option<String>,
|
||||
pub plan: Option<String>,
|
||||
pub webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -117,6 +119,11 @@ struct EditAutoInvite {
|
||||
operator: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditWebhook {
|
||||
webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateWorkspace {
|
||||
id: String,
|
||||
@@ -519,6 +526,48 @@ async fn edit_auto_invite(
|
||||
))
|
||||
}
|
||||
|
||||
async fn edit_webhook(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Authed { is_admin, username, .. }: Authed,
|
||||
Json(ew): Json<EditWebhook>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if let Some(webhook) = &ew.webhook {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
|
||||
webhook,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"workspaces.edit_webhook",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Edit webhook for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
async fn list_workspaces_as_super_admin(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -47,9 +47,14 @@
|
||||
let nbDisplayed = 30
|
||||
let plan: string | undefined = undefined
|
||||
let customer_id: string | undefined = undefined
|
||||
let webhook: string | undefined = undefined
|
||||
let tab =
|
||||
($page.url.searchParams.get('tab') as 'users' | 'slack' | 'premium' | 'export_delete') ??
|
||||
'users'
|
||||
($page.url.searchParams.get('tab') as
|
||||
| 'users'
|
||||
| 'slack'
|
||||
| 'premium'
|
||||
| 'export_delete'
|
||||
| 'webhook') ?? 'users'
|
||||
|
||||
// function getDropDownItems(username: string): DropdownItem[] {
|
||||
// return [
|
||||
@@ -84,6 +89,23 @@
|
||||
sendUserToast(`slack command script set to ${scriptPath}`)
|
||||
}
|
||||
|
||||
async function editWebhook(): Promise<void> {
|
||||
// in JS, an empty string is also falsy
|
||||
if (webhook) {
|
||||
await WorkspaceService.editWebhook({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { webhook }
|
||||
})
|
||||
sendUserToast(`webhook set to ${webhook}`)
|
||||
} else {
|
||||
await WorkspaceService.editWebhook({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { webhook: undefined }
|
||||
})
|
||||
sendUserToast(`webhook removed`)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
@@ -93,6 +115,7 @@
|
||||
plan = settings.plan
|
||||
customer_id = settings.customer_id
|
||||
initialPath = scriptPath
|
||||
webhook = settings.webhook
|
||||
}
|
||||
|
||||
async function listUsers(): Promise<void> {
|
||||
@@ -189,7 +212,7 @@
|
||||
<Tab size="md" value="users">
|
||||
<div class="flex gap-2 items-center my-1"> Users & Invites </div>
|
||||
</Tab>
|
||||
<Tab size="md" value="slack">
|
||||
<Tab size="md" value="slack">
|
||||
<div class="flex gap-2 items-center my-1"> Slack Command </div>
|
||||
</Tab>
|
||||
{#if isCloudHosted()}
|
||||
@@ -197,11 +220,14 @@
|
||||
<div class="flex gap-2 items-center my-1"> Premium Plans </div>
|
||||
</Tab>
|
||||
{/if}
|
||||
<Tab size="md" value="export_delete">
|
||||
<div class="flex gap-2 items-center my-1"> Export & Delete Workspace </div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Tab size="md" value="export_delete">
|
||||
<div class="flex gap-2 items-center my-1"> Export & Delete Workspace </div>
|
||||
</Tab>
|
||||
<Tab size="md" value="webhook">
|
||||
<div class="flex gap-2 items-center my-1">Webhook for CLI Sync</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
{#if tab == 'users'}
|
||||
<PageHeader title="Members ({users?.length ?? ''})" primary={false} />
|
||||
|
||||
@@ -685,6 +711,29 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tab == 'webhook'}
|
||||
<PageHeader title="Webhook for CLI Sync" primary={false} />
|
||||
|
||||
<div class="mt-2"
|
||||
><Alert type="info" title="Send events to an external service"
|
||||
>Connect your windmill workspace to an external service to sync or get notified about any
|
||||
changes</Alert
|
||||
></div
|
||||
>
|
||||
|
||||
<h3 class="mt-5 text-gray-700"
|
||||
>URL to send requests to<Tooltip>
|
||||
This URL will be POSTed to with a JSON body depending on the type of event. The type is
|
||||
indicated by the <pre>type</pre> field. The other fields are dependent on the type.
|
||||
</Tooltip>
|
||||
</h3>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<input class="justify-start" type="text" bind:value={webhook} />
|
||||
<Button color="blue" btnClasses="justify-end" size="md" on:click={editWebhook}
|
||||
>Set Webhook</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">
|
||||
|
||||
Reference in New Issue
Block a user