feat(backend): add instance events webhook

This commit is contained in:
Ruben Fiszel
2023-04-11 01:45:56 +02:00
parent e71653998b
commit db075f2ca1
7 changed files with 77 additions and 39 deletions
+1 -1
View File
@@ -332,7 +332,7 @@ it being synced automatically everyday.
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
| NEW_USER_WEBHOOK | None | Webhook to notify of a new user added, signup/invite. Can hook back to windmill to send emails | Server |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails | Server |
## Run a local dev setup
+1 -1
View File
@@ -112,7 +112,7 @@ Windmill Community Edition {GIT_VERSION}
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"NEW_USER_WEBHOOK",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
]);
+4 -9
View File
@@ -32,7 +32,8 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db};
use crate::users::{truncate_token, Authed, NEW_USER_WEBHOOK};
use crate::users::{truncate_token, Authed};
use crate::webhook_util::{InstanceEvent, WebhookShared};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{
db::{UserDB, DB},
@@ -815,6 +816,7 @@ async fn login_callback(
Path(client_name): Path<String>,
cookies: Cookies,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client_w_config = &OAUTH_CLIENTS
@@ -940,14 +942,7 @@ async fn login_callback(
}
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &email, "event": "oauth_signup"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
webhook.send_instance_event(InstanceEvent::UserSignupOAuth { email: email.clone() });
Ok("Successfully logged in".to_string())
} else {
+41 -4
View File
@@ -13,6 +13,26 @@ lazy_static::lazy_static! {
"Histogram of webhook requests made"
)
.unwrap();
pub static ref INSTANCE_EVENTS_WEBHOOK: Option<String> = std::env::var("INSTANCE_EVENTS_WEBHOOK").ok();
}
pub enum WebhookPayload {
WorkspaceEvent(String, WebhookMessage),
InstanceEvent(InstanceEvent),
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum InstanceEvent {
UserSignupOAuth { email: String },
UserAdded { email: String },
// UserDeleted { email: String },
// UserDeletedWorkspace { workspace: String, email: String },
UserAddedWorkspace { workspace: String, email: String },
UserInvitedWorkspace { workspace: String, email: String },
UserJoinedWorkspace { workspace: String, email: String, username: String },
}
#[derive(Serialize)]
@@ -46,12 +66,12 @@ pub enum WebhookMessage {
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
pub channel: mpsc::UnboundedSender<WebhookPayload>,
}
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 (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
@@ -66,7 +86,7 @@ impl WebhookShared {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((workspace_id, message)) => {
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
Some(guard) => {
guard
@@ -96,6 +116,13 @@ impl WebhookShared {
drop(url_guard);
}
},
Some(WebhookPayload::InstanceEvent(event)) => {
if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let r = client.post(INSTANCE_EVENTS_WEBHOOK.as_ref().unwrap()).json(&event).send().await;
if let Err(e) = r {
tracing::error!("Error sending instance event: {}", e);
}
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
@@ -110,6 +137,16 @@ impl WebhookShared {
}
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
let _ = self.channel.send((workspace_id.clone(), message));
let _ = self.channel.send(WebhookPayload::WorkspaceEvent(
workspace_id.clone(),
message,
));
}
pub fn send_instance_event(&self, event: InstanceEvent) {
if INSTANCE_EVENTS_WEBHOOK.is_none() {
return;
}
let _ = self.channel.send(WebhookPayload::InstanceEvent(event));
}
}
+12 -18
View File
@@ -16,10 +16,10 @@ use crate::{
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK, VALID_USERNAME},
users::{Authed, WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
variables::build_crypt,
HTTP_CLIENT,
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
@@ -943,6 +943,7 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
async fn invite_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceInvite>,
) -> Result<(StatusCode, String)> {
@@ -966,14 +967,10 @@ async fn invite_user(
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = &HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_invite"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
email: nu.email.clone(),
workspace: w_id,
});
Ok((
StatusCode::CREATED,
@@ -984,6 +981,7 @@ async fn invite_user(
async fn add_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
@@ -1022,14 +1020,10 @@ async fn add_user(
tx.commit().await?;
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
let _ = HTTP_CLIENT
.post(&new_user_webhook)
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_add"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
webhook.send_instance_event(InstanceEvent::UserAddedWorkspace {
workspace: w_id.clone(),
email: nu.email.clone(),
});
Ok((
StatusCode::CREATED,
+15 -3
View File
@@ -317,16 +317,28 @@
></span
>
<div class="flex flex-row gap-1 w-full">
<div class="flex flex-row items-center gap-1 w-full">
<select class="grow w-full" {disabled} bind:value={meta.owner}>
{#each folders as { name, write }}
<option disabled={!write}>{name}{write ? '' : ' (read-only)'}</option>
{/each}
</select>
<Button variant="border" size="xs" on:click={viewFolder.openDrawer}>
<Button
title="View folder"
btnClasses="!p-1.5"
variant="border"
size="xs"
on:click={viewFolder.openDrawer}
>
<Icon scale={0.8} data={faEye} /></Button
>
<Button variant="border" size="xs" on:click={newFolder.openDrawer}>
<Button
title="New folder"
btnClasses="!p-1.5"
variant="border"
size="xs"
on:click={newFolder.openDrawer}
>
<Icon scale={0.8} data={faPlus} /></Button
></div
>
@@ -8,9 +8,9 @@
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
class={$$props.class}
class:animate-[spin-counter-clockwise_5s_linear_infinite]={spin === 'fast'}
class:animate-[spin-counter-clockwise_15s_linear_infinite]={spin === 'medium'}
class:animate-[spin-counter-clockwise_50s_linear_infinite]={spin === 'slow'}
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
version="1.1"
id="Calque_1"
xmlns="http://www.w3.org/2000/svg"