mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat(backend): add instance events webhook
This commit is contained in:
@@ -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
@@ -112,7 +112,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
"INCLUDE_HEADERS",
|
||||
"WHITELIST_WORKSPACES",
|
||||
"BLACKLIST_WORKSPACES",
|
||||
"NEW_USER_WEBHOOK",
|
||||
"INSTANCE_EVENTS_WEBHOOK",
|
||||
"CLOUD_HOSTED",
|
||||
]);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user