Compare commits

...

4 Commits

Author SHA1 Message Date
Ruben Fiszel
ece4573076 all 2023-02-01 19:46:48 +01:00
Kai Jellinghaus
833d0df965 Add webhook request histogram 2023-01-26 07:03:49 +01:00
Kai Jellinghaus
1e7b7cc8d6 Update SQLX 2023-01-26 06:59:29 +01:00
Kai Jellinghaus
1de8eefe96 Add workspace webhook 2023-01-26 06:42:10 +01:00
18 changed files with 507 additions and 6 deletions

2
backend/Cargo.lock generated
View File

@@ -4570,8 +4570,10 @@ dependencies = [
"hmac",
"hyper",
"itertools",
"lazy_static",
"magic-crypt",
"mime_guess",
"prometheus",
"rand 0.8.5",
"reqwest",
"retainer",

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

@@ -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": [

View File

@@ -70,3 +70,5 @@ cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe.workspace = true
lazy_static.workspace = true
prometheus.workspace = true

View File

@@ -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

View File

@@ -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<UserDB>,
webhook: WebhookUtil,
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(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<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
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<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
@@ -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))
}

View File

@@ -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<UserDB>,
webhook: WebhookUtil,
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(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>,
webhook: WebhookUtil,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
@@ -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<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
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"))
}

View File

@@ -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<UserDB>,
webhook: WebhookUtil,
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(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<UserDB>,
webhook: WebhookUtil,
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(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<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
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<DB>,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -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<DB>,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -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))
}

View File

@@ -103,7 +103,9 @@ async fn get_result_by_id(
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
tracing::error!("get_result_by_id_bef: {:?} {:?}", flow_id, node_id);
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
tracing::error!("get_result_by_id: {:?}", res);
Ok(Json(res))
}

View File

@@ -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::<WebhookUtil>()),
)
.nest("/workspaces", workspaces::global_service())
.nest(

View File

@@ -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<UserDB>,
webhook: WebhookUtil,
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(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<UserDB>,
webhook: WebhookUtil,
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(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<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
@@ -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<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
@@ -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<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> 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<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
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<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -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))
}

View File

@@ -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<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
@@ -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<UserDB>,
Extension(db): Extension<DB>,
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<UserDB>,
webhook: WebhookUtil,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
@@ -657,12 +675,18 @@ async fn archive_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteScript {
workspace: w_id.clone(),
hash: hash.to_string(),
});
Ok(Json(script))
}
async fn delete_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
@@ -691,6 +715,11 @@ async fn delete_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteScript {
workspace: w_id.clone(),
hash: hash.to_string(),
});
Ok(Json(script))
}

View File

@@ -12,6 +12,7 @@ use crate::{
db::{UserDB, DB},
oauth2::{AllClients, _refresh_token},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookUtil},
BaseUrl,
};
/*
@@ -224,6 +225,7 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
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(WebhookMessage::CreateVariable {
workspace: w_id.clone(),
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>,
webhook: WebhookUtil,
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(WebhookMessage::DeleteVariable {
workspace: w_id.clone(),
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>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditVariable>,
@@ -405,6 +419,12 @@ async fn update_variable(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateVariable {
workspace: w_id.clone(),
old_path: path.to_owned(),
new_path: npath.clone(),
});
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}

View File

@@ -0,0 +1,156 @@
use std::time::Duration;
use axum::{
async_trait,
extract::{FromRequestParts, OriginalUri},
http::request::Parts,
Extension,
};
use hyper::StatusCode;
use serde::Serialize;
use tokio::{select, sync::mpsc};
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<()>) -> 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();
loop {
select! {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((url, message)) => {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
},
None => break,
}
}
}
});
Self { channel: tx }
}
}
#[derive(Clone)]
pub struct WebhookUtil {
webhook: Option<String>,
shared: Extension<WebhookShared>,
}
impl WebhookUtil {
pub fn send_message(&self, message: WebhookMessage) {
let Some(webhook) = &self.webhook else {
return;
};
let _ = self.shared.channel.send((webhook.clone(), message));
}
}
#[async_trait]
impl<S> FromRequestParts<S> for WebhookUtil
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
None
};
let webhook = sqlx::query_scalar!(
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
workspace_id
)
.fetch_one(
&Extension::<DB>::from_request_parts(parts, state)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not aquire DB while retrieving webhook".to_owned(),
)
})?
.0,
)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Could not execute DB query {:?}", e),
)
})?;
let shared = Extension::<WebhookShared>::from_request_parts(parts, state)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not aquire shared process while retrieving webhook".to_owned(),
)
})?;
Ok(Self { webhook, shared })
}
}

View File

@@ -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,
@@ -509,6 +516,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>,

View File

@@ -307,6 +307,19 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
let node_id = &args[4];
let client = windmill_api_client::create_client(base_url, token.clone());
let err = client
.result_by_id(workspace, flow_job_id, node_id, Some(true))
.await
.err()
.unwrap();
let res = match err {
windmill_api_client::Error::UnexpectedResponse(e) => {
tracing::error!("{:?}", e.text().await);
anyhow::anyhow!("bar")
}
_ => anyhow::anyhow!("foo"),
};
tracing::error!("{:?}", res);
let result = client
.result_by_id(workspace, flow_job_id, node_id, Some(true))
.await

View File

@@ -41,9 +41,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 [
@@ -78,6 +83,14 @@
sendUserToast(`slack command script set to ${scriptPath}`)
}
async function editWebhook(): Promise<void> {
await WorkspaceService.editWebhook({
workspace: $workspaceStore!,
requestBody: { webhook }
})
sendUserToast(`webhook set to ${webhook}`)
}
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
@@ -87,6 +100,7 @@
plan = settings.plan
customer_id = settings.customer_id
initialPath = scriptPath
webhook = settings.webhook
}
async function listUsers(): Promise<void> {
@@ -193,6 +207,9 @@
<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>
{#if tab == 'users'}
<PageHeader title="Members ({users?.length ?? ''})" primary={false} />
@@ -652,6 +669,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">