mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
feat: test trigger connection (#5145)
* feat: test trigger connection * fix build * fix build * update ee ref * nit --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
@@ -7943,6 +7943,36 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/websocket_triggers/test:
|
||||
post:
|
||||
summary: test websocket connection
|
||||
operationId: testWebsocketConnection
|
||||
tags:
|
||||
- websocket_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: test websocket connection
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
required:
|
||||
- url
|
||||
responses:
|
||||
"200":
|
||||
description: successfuly connected to websocket
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/kafka_triggers/create:
|
||||
post:
|
||||
summary: create kafka trigger
|
||||
@@ -8104,6 +8134,34 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/kafka_triggers/test:
|
||||
post:
|
||||
summary: test kafka connection
|
||||
operationId: testKafkaConnection
|
||||
tags:
|
||||
- kafka_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: test kafka connection
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
connection:
|
||||
type: object
|
||||
required:
|
||||
- connection
|
||||
responses:
|
||||
"200":
|
||||
description: successfuly connected to kafka brokers
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/nats_triggers/create:
|
||||
post:
|
||||
summary: create nats trigger
|
||||
@@ -8267,6 +8325,35 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/nats_triggers/test:
|
||||
post:
|
||||
summary: test NATS connection
|
||||
operationId: testNatsConnection
|
||||
tags:
|
||||
- nats_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: test nats connection
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
connection:
|
||||
type: object
|
||||
required:
|
||||
- connection
|
||||
responses:
|
||||
"200":
|
||||
description: successfuly connected to NATS servers
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/postgres_triggers/is_valid_postgres_configuration/{path}:
|
||||
get:
|
||||
summary: check if postgres configuration is set to logical
|
||||
|
||||
@@ -35,9 +35,9 @@ use windmill_queue::{PushArgs, PushArgsOwned};
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use crate::http_triggers::{build_http_trigger_extra, HttpMethod};
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
use crate::kafka_triggers_ee::KafkaResourceSecurity;
|
||||
use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
use crate::nats_triggers_ee::NatsResourceAuth;
|
||||
use crate::nats_triggers_ee::NatsTriggerConfigConnection;
|
||||
use crate::{
|
||||
args::WebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
@@ -110,14 +110,6 @@ struct HttpTriggerConfig {
|
||||
http_method: HttpMethod,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum KafkaTriggerConfigConnection {
|
||||
Resource { kafka_resource_path: String },
|
||||
Static { brokers: Vec<String>, security: KafkaResourceSecurity },
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct KafkaTriggerConfig {
|
||||
@@ -127,14 +119,6 @@ pub struct KafkaTriggerConfig {
|
||||
pub group_id: String,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum NatsTriggerConfigConnection {
|
||||
Resource { nats_resource_path: String },
|
||||
Static { servers: Vec<String>, auth: NatsResourceAuth, require_tls: bool },
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NatsTriggerConfig {
|
||||
|
||||
@@ -521,7 +521,7 @@ async fn get_http_route_trigger(
|
||||
trigger.email.clone(),
|
||||
&trigger.workspace_id,
|
||||
&db,
|
||||
Some(username_override.unwrap_or("anonymous".to_string())),
|
||||
Some(username_override.unwrap_or(format!("http-{}", trigger.path))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -685,12 +685,6 @@ async fn route_job(
|
||||
.await,
|
||||
);
|
||||
|
||||
let label_prefix = Some(format!(
|
||||
"http-{}-{}-",
|
||||
method.as_str().to_lowercase(),
|
||||
trigger.route_path
|
||||
));
|
||||
|
||||
let run_query = RunJobQuery::default();
|
||||
|
||||
if trigger.is_flow {
|
||||
@@ -703,7 +697,7 @@ async fn route_job(
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
@@ -716,7 +710,7 @@ async fn route_job(
|
||||
user_db,
|
||||
args,
|
||||
trigger.workspace_id.clone(),
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
@@ -731,7 +725,7 @@ async fn route_job(
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
@@ -744,7 +738,7 @@ async fn route_job(
|
||||
user_db,
|
||||
trigger.workspace_id.clone(),
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
|
||||
@@ -9,9 +9,12 @@ pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub async fn start_kafka_consumers(
|
||||
pub fn start_kafka_consumers(
|
||||
_db: DB,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
// implementation is not open source
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum KafkaTriggerConfigConnection {}
|
||||
|
||||
@@ -59,8 +59,6 @@ mod auth;
|
||||
mod capture;
|
||||
mod concurrency_groups;
|
||||
mod configs;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
mod postgres_triggers;
|
||||
mod db;
|
||||
mod drafts;
|
||||
pub mod ee;
|
||||
@@ -75,6 +73,8 @@ mod http_triggers;
|
||||
mod indexer_ee;
|
||||
mod inputs;
|
||||
mod integration;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
mod postgres_triggers;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod apps_ee;
|
||||
@@ -98,12 +98,12 @@ mod scripts;
|
||||
mod service_logs;
|
||||
mod settings;
|
||||
mod slack_approvals;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod teams_ee;
|
||||
#[cfg(feature = "smtp")]
|
||||
mod smtp_server_ee;
|
||||
mod static_assets;
|
||||
mod stripe_ee;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod teams_ee;
|
||||
mod tracing_init;
|
||||
mod triggers;
|
||||
mod users;
|
||||
@@ -296,24 +296,24 @@ pub async fn run_server(
|
||||
#[cfg(feature = "websocket")]
|
||||
{
|
||||
let ws_killpill_rx = rx.resubscribe();
|
||||
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx).await;
|
||||
websocket_triggers::start_websockets(db.clone(), ws_killpill_rx);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
{
|
||||
let kafka_killpill_rx = rx.resubscribe();
|
||||
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx).await;
|
||||
kafka_triggers_ee::start_kafka_consumers(db.clone(), kafka_killpill_rx);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
{
|
||||
let nats_killpill_rx = rx.resubscribe();
|
||||
nats_triggers_ee::start_nats_consumers(db.clone(), nats_killpill_rx).await;
|
||||
nats_triggers_ee::start_nats_consumers(db.clone(), nats_killpill_rx);
|
||||
}
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
{
|
||||
let db_killpill_rx = rx.resubscribe();
|
||||
postgres_triggers::start_database(db.clone(), db_killpill_rx).await;
|
||||
postgres_triggers::start_database(db.clone(), db_killpill_rx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ pub async fn run_server(
|
||||
)
|
||||
.route("/slack", post(slack_approvals::slack_app_callback_handler))
|
||||
.nest("/teams", {
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
teams_ee::teams_service()
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub async fn start_nats_consumers(
|
||||
_db: DB,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
pub fn start_nats_consumers(_db: DB, mut _killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () {
|
||||
// implementation is not open source
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum NatsTriggerConfigConnection {}
|
||||
|
||||
@@ -110,14 +110,13 @@ async fn run_job(
|
||||
trigger: &PostgresTrigger,
|
||||
) -> anyhow::Result<()> {
|
||||
let args = PushArgsOwned { args: args.unwrap_or_default(), extra };
|
||||
let label_prefix = Some(format!("db-{}-", trigger.path));
|
||||
|
||||
let authed = fetch_api_authed(
|
||||
trigger.edited_by.clone(),
|
||||
trigger.email.clone(),
|
||||
&trigger.workspace_id,
|
||||
db,
|
||||
Some("anonymous".to_string()),
|
||||
Some(format!("postgres-{}", trigger.path)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -134,7 +133,7 @@ async fn run_job(
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
@@ -146,7 +145,7 @@ async fn run_job(
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -305,7 +305,10 @@ async fn listen_to_transactions(
|
||||
let message = match message {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
tracing::info!("Stream for postgres trigger {} is empty, leaving....", postgres_trigger.path);
|
||||
tracing::error!("Stream for postgres trigger {} closed", postgres_trigger.path);
|
||||
if let None = update_ping(&db, postgres_trigger, Some("Stream closed")).await {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -506,7 +509,7 @@ async fn listen_to_unlistened_database_events(
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn start_database(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) {
|
||||
pub fn start_database(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) {
|
||||
tokio::spawn(async move {
|
||||
listen_to_unlistened_database_events(&db, &killpill_rx).await;
|
||||
loop {
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/delete/*path", delete(delete_websocket_trigger))
|
||||
.route("/exists/*path", get(exists_websocket_trigger))
|
||||
.route("/setenabled/*path", post(set_enabled))
|
||||
.route("/test", post(test_websocket_connection))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -377,6 +378,64 @@ async fn exists_websocket_trigger(
|
||||
Ok(Json(exists))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestWebsocket {
|
||||
url: String,
|
||||
url_runnable_args: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
async fn test_websocket_connection(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(workspace_id): Path<String>,
|
||||
Json(test_websocket): Json<TestWebsocket>,
|
||||
) -> error::Result<()> {
|
||||
let url = test_websocket.url;
|
||||
|
||||
let connect_f = async {
|
||||
let connect_url: Cow<str> = if url.starts_with("$") {
|
||||
if url.starts_with("$flow:") || url.starts_with("$script:") {
|
||||
let path = url.splitn(2, ':').nth(1).unwrap();
|
||||
Cow::Owned(
|
||||
get_url_from_runnable(
|
||||
path,
|
||||
url.starts_with("$flow:"),
|
||||
&db,
|
||||
authed,
|
||||
test_websocket.url_runnable_args.as_ref(),
|
||||
&workspace_id,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
return Err(error::Error::BadConfig(format!(
|
||||
"Invalid websocket runnable path: {}",
|
||||
url
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(&url)
|
||||
};
|
||||
|
||||
connect_async(connect_url.as_ref()).await.map_err(|err| {
|
||||
error::Error::BadConfig(format!(
|
||||
"Error connecting to websocket: {}",
|
||||
err.to_string()
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error::Error::BadConfig(format!("Timeout connecting to websocket after 30 seconds"))
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen_to_unlistened_websockets(
|
||||
db: &DB,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
@@ -419,7 +478,7 @@ async fn listen_to_unlistened_websockets(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_websockets(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () {
|
||||
pub fn start_websockets(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) -> () {
|
||||
tokio::spawn(async move {
|
||||
listen_to_unlistened_websockets(&db, &killpill_rx).await;
|
||||
loop {
|
||||
@@ -509,7 +568,6 @@ async fn wait_runnable_result(
|
||||
authed: ApiAuthed,
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
trigger_path: &str,
|
||||
) -> error::Result<String> {
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
@@ -521,7 +579,6 @@ async fn wait_runnable_result(
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let label_prefix = Some(format!("ws-{}-", trigger_path));
|
||||
let (_, job_id) = if is_flow {
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
@@ -531,7 +588,7 @@ async fn wait_runnable_result(
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -543,7 +600,7 @@ async fn wait_runnable_result(
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -553,7 +610,9 @@ async fn wait_runnable_result(
|
||||
loop {
|
||||
if start_time.elapsed() > tokio::time::Duration::from_secs(300) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Timed out after 5m waiting for runnable {path} (is_flow: {is_flow}) to complete",
|
||||
"Timed out after 5m waiting for {} {} to complete",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@@ -576,7 +635,8 @@ async fn wait_runnable_result(
|
||||
Ok(Some(r)) => {
|
||||
if !r.success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Runnable {path} (is_flow: {is_flow}) failed: {:?}",
|
||||
"{} {path} failed: {:?}",
|
||||
if is_flow { "Flow" } else { "Script" },
|
||||
r.result
|
||||
)
|
||||
.into());
|
||||
@@ -590,7 +650,8 @@ async fn wait_runnable_result(
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error fetching job result for runnable {path} (is_flow: {is_flow}): {err}",
|
||||
"Error fetching job result for {} {path}: {err}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@@ -614,25 +675,24 @@ async fn get_url_from_runnable(
|
||||
authed: ApiAuthed,
|
||||
args: Option<&Box<RawValue>>,
|
||||
workspace_id: &str,
|
||||
trigger_path: &str,
|
||||
) -> error::Result<String> {
|
||||
tracing::info!("Running runnable {path} (is_flow: {is_flow}) to get websocket URL",);
|
||||
tracing::info!(
|
||||
"Running {} {} to get websocket URL",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
);
|
||||
|
||||
let result = wait_runnable_result(
|
||||
path.to_string(),
|
||||
is_flow,
|
||||
args,
|
||||
authed,
|
||||
db,
|
||||
workspace_id,
|
||||
trigger_path,
|
||||
)
|
||||
.await?;
|
||||
let result =
|
||||
wait_runnable_result(path.to_string(), is_flow, args, authed, db, workspace_id).await?;
|
||||
|
||||
if result.starts_with("\"") && result.ends_with("\"") {
|
||||
Ok(result[1..result.len() - 1].to_string())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Runnable {path} (is_flow: {is_flow}) did not return a string").into())
|
||||
Err(error::Error::BadConfig(format!(
|
||||
"{} {} did not return a string",
|
||||
if is_flow { "Flow" } else { "Script" },
|
||||
path
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,10 +781,9 @@ impl WebsocketTrigger {
|
||||
&path,
|
||||
is_flow,
|
||||
db,
|
||||
self.fetch_authed(db, Some("url".to_string())).await?,
|
||||
self.fetch_authed(db).await?,
|
||||
self.url_runnable_args.as_ref().map(|r| &r.0),
|
||||
&self.workspace_id,
|
||||
&self.path,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -763,7 +822,9 @@ impl WebsocketTrigger {
|
||||
}
|
||||
InitialMessage::RunnableResult { path, is_flow, args } => {
|
||||
tracing::info!(
|
||||
"Running runnable {path} (is_flow: {is_flow}) for initial message to websocket {}",
|
||||
"Running {} {} for initial message to websocket {}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path,
|
||||
self.url,
|
||||
);
|
||||
|
||||
@@ -771,15 +832,16 @@ impl WebsocketTrigger {
|
||||
path.clone(),
|
||||
is_flow,
|
||||
Some(&args),
|
||||
self.fetch_authed(db, Some("init".to_string())).await?,
|
||||
self.fetch_authed(db).await?,
|
||||
db,
|
||||
&self.workspace_id,
|
||||
&self.path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Sending runnable {path} (is_flow: {is_flow}) result to websocket {}",
|
||||
"Sending {} {} result to websocket {}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path,
|
||||
self.url
|
||||
);
|
||||
|
||||
@@ -794,7 +856,11 @@ impl WebsocketTrigger {
|
||||
.await
|
||||
.map_err(to_anyhow)
|
||||
.with_context(|| {
|
||||
format!("Failed to send runnable {path} (is_flow: {is_flow}) result")
|
||||
format!(
|
||||
"Failed to send {} {} result",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -818,17 +884,13 @@ impl WebsocketTrigger {
|
||||
};
|
||||
}
|
||||
|
||||
async fn fetch_authed(
|
||||
&self,
|
||||
db: &DB,
|
||||
username_override: Option<String>,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
async fn fetch_authed(&self, db: &DB) -> error::Result<ApiAuthed> {
|
||||
fetch_api_authed(
|
||||
self.edited_by.clone(),
|
||||
self.email.clone(),
|
||||
&self.workspace_id,
|
||||
db,
|
||||
username_override,
|
||||
Some(format!("ws-{}", self.path)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -932,25 +994,20 @@ impl CaptureConfigForWebsocket {
|
||||
&path,
|
||||
is_flow,
|
||||
db,
|
||||
self.fetch_authed(db, Some("url".to_string())).await?,
|
||||
self.fetch_authed(db).await?,
|
||||
url_runnable_args.as_ref(),
|
||||
&self.workspace_id,
|
||||
&self.get_trigger_path(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_authed(
|
||||
&self,
|
||||
db: &DB,
|
||||
username_override: Option<String>,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
async fn fetch_authed(&self, db: &DB) -> error::Result<ApiAuthed> {
|
||||
fetch_api_authed(
|
||||
self.owner.clone(),
|
||||
self.email.clone(),
|
||||
&self.workspace_id,
|
||||
db,
|
||||
username_override,
|
||||
Some(format!("ws-{}", self.get_trigger_path())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1017,13 +1074,9 @@ async fn listen_to_websocket(
|
||||
db: DB,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
if let None = ws.update_ping(&db, Some("Connecting")).await {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) => &ws_trigger.url,
|
||||
WebsocketEnum::Capture(capture) => &capture.trigger_config.url,
|
||||
WebsocketEnum::Trigger(ws_trigger) => ws_trigger.url.clone(),
|
||||
WebsocketEnum::Capture(capture) => capture.trigger_config.url.clone(),
|
||||
};
|
||||
|
||||
let filters: Vec<Filter> = match &ws {
|
||||
@@ -1035,101 +1088,92 @@ async fn listen_to_websocket(
|
||||
WebsocketEnum::Capture(_) => vec![],
|
||||
};
|
||||
|
||||
loop {
|
||||
let connect_url: Cow<str> = if url.starts_with("$") {
|
||||
if url.starts_with("$flow:") || url.starts_with("$script:") {
|
||||
let path = url.splitn(2, ':').nth(1).unwrap();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some(
|
||||
"Waiting on runnable to return websocket URL..."
|
||||
)) => {
|
||||
return;
|
||||
},
|
||||
let connect_url: Cow<str> = if url.starts_with("$") {
|
||||
if url.starts_with("$flow:") || url.starts_with("$script:") {
|
||||
let path = url.splitn(2, ':').nth(1).unwrap();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some(
|
||||
"Waiting on runnable to return websocket URL..."
|
||||
)) => {
|
||||
return;
|
||||
},
|
||||
|
||||
|
||||
url_result = ws.get_url_from_runnable(path, url.starts_with("$flow:"), &db) => match url_result {
|
||||
Ok(url) => Cow::Owned(url),
|
||||
Err(err) => {
|
||||
ws.disable_with_error(&db, format!(
|
||||
"Error getting websocket URL from runnable after 5 tries: {:?}",
|
||||
err
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ws.disable_with_error(&db, format!("Invalid websocket runnable path: {}", url))
|
||||
.await;
|
||||
return;
|
||||
url_result = ws.get_url_from_runnable(path, url.starts_with("$flow:"), &db) => match url_result {
|
||||
Ok(url) => Cow::Owned(url),
|
||||
Err(err) => {
|
||||
ws.disable_with_error(&db, format!(
|
||||
"Error getting websocket URL from runnable after 5 tries: {:?}",
|
||||
err
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(url)
|
||||
};
|
||||
ws.disable_with_error(&db, format!("Invalid websocket runnable path: {}", url))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(&url)
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some("Connecting...")) => {
|
||||
return;
|
||||
},
|
||||
connection = connect_async(connect_url.as_ref()) => {
|
||||
match connection {
|
||||
Ok((ws_stream, _)) => {
|
||||
tracing::info!("Listening to websocket {}", url);
|
||||
if let None = ws.update_ping(&db, None).await {
|
||||
return;
|
||||
}
|
||||
let (writer, mut reader) = ws_stream.split();
|
||||
let mut last_ping = tokio::time::Instant::now();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some("Connecting...")) => {
|
||||
return;
|
||||
},
|
||||
connection = connect_async(connect_url.as_ref()) => {
|
||||
match connection {
|
||||
Ok((ws_stream, _)) => {
|
||||
tracing::info!("Connected to websocket {}", url);
|
||||
let (writer, mut reader) = ws_stream.split();
|
||||
|
||||
|
||||
// send initial messages
|
||||
match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some("Sending initial messages...")) => {
|
||||
return;
|
||||
},
|
||||
result = ws_trigger.send_initial_messages(writer, &db) => {
|
||||
if let Err(err) = result {
|
||||
ws_trigger.disable_with_error(&db, format!("Error sending initial messages: {:?}", err)).await;
|
||||
return
|
||||
} else {
|
||||
tracing::debug!("Initial messages sent successfully to websocket {}", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
loop {
|
||||
// send initial messages
|
||||
match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some("Sending initial messages...")) => {
|
||||
return;
|
||||
},
|
||||
result = ws_trigger.send_initial_messages(writer, &db) => {
|
||||
if let Err(err) = result {
|
||||
ws_trigger.disable_with_error(&db, format!("Error sending initial messages: {:?}", err)).await;
|
||||
return
|
||||
} else {
|
||||
tracing::debug!("Initial messages sent successfully to websocket {}", url);
|
||||
}
|
||||
}
|
||||
msg = reader.next() => {
|
||||
if let Some(msg) = msg {
|
||||
if last_ping.elapsed() > tokio::time::Duration::from_secs(5) {
|
||||
if let None = ws.update_ping(&db, None).await {
|
||||
return;
|
||||
}
|
||||
last_ping = tokio::time::Instant::now();
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, None) => {
|
||||
return;
|
||||
},
|
||||
_ = async {
|
||||
loop {
|
||||
if let Some(msg) = reader.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
@@ -1170,7 +1214,7 @@ async fn listen_to_websocket(
|
||||
},
|
||||
WebsocketEnum::Capture(capture) => {
|
||||
capture.handle(&db, args).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1188,26 +1232,19 @@ async fn listen_to_websocket(
|
||||
if let None = ws.update_ping(&db, Some("Websocket closed")).await {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
|
||||
tracing::debug!("Sending ping to websocket {}", url);
|
||||
if let None = ws.update_ping(&db, None).await {
|
||||
return;
|
||||
}
|
||||
last_ping = tokio::time::Instant::now();
|
||||
},
|
||||
}
|
||||
} => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
|
||||
if let None = ws.update_ping(&db, Some(err.to_string().as_str())).await {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
|
||||
if let None = ws.update_ping(&db, Some(err.to_string().as_str())).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1216,14 +1253,12 @@ async fn listen_to_websocket(
|
||||
}
|
||||
|
||||
async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> anyhow::Result<()> {
|
||||
let label_prefix = Some(format!("ws-{}-", trigger.path));
|
||||
|
||||
let authed = fetch_api_authed(
|
||||
trigger.edited_by.clone(),
|
||||
trigger.email.clone(),
|
||||
&trigger.workspace_id,
|
||||
db,
|
||||
Some("anonymous".to_string()),
|
||||
Some(format!("ws-{}", trigger.path)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1240,7 +1275,7 @@ async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> an
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
@@ -1252,7 +1287,7 @@ async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> an
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
label_prefix,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import autosize from '$lib/autosize'
|
||||
import GfmMarkdown from './GfmMarkdown.svelte'
|
||||
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
|
||||
|
||||
export let canSave = true
|
||||
export let resource_type: string | undefined = undefined
|
||||
@@ -223,7 +224,11 @@
|
||||
{/if}
|
||||
<div class="flex w-full justify-between items-center mt-4">
|
||||
<div />
|
||||
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
|
||||
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
|
||||
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
|
||||
{:else}
|
||||
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
|
||||
{/if}
|
||||
<Toggle
|
||||
on:change={(e) => switchTab(e.detail)}
|
||||
options={{
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
CancelablePromise,
|
||||
KafkaTriggerService,
|
||||
NatsTriggerService,
|
||||
WebsocketTriggerService
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
|
||||
export let kind: 'websocket' | 'nats' | 'kafka'
|
||||
export let args: Record<string, any>
|
||||
|
||||
const kindToName: { [key: string]: string } = {
|
||||
websocket: 'Websocket',
|
||||
nats: 'NATS server(s)',
|
||||
kafka: 'Kafka broker(s)'
|
||||
}
|
||||
|
||||
let testLoading: boolean = false
|
||||
let promise: CancelablePromise<any> | null = null
|
||||
async function testTriggerConnection() {
|
||||
if (testLoading) {
|
||||
promise?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
testLoading = true
|
||||
try {
|
||||
if (kind === 'websocket') {
|
||||
promise = WebsocketTriggerService.testWebsocketConnection({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args as any
|
||||
})
|
||||
} else if (kind === 'nats') {
|
||||
promise = NatsTriggerService.testNatsConnection({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args as any
|
||||
})
|
||||
} else if (kind === 'kafka') {
|
||||
promise = KafkaTriggerService.testKafkaConnection({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args as any
|
||||
})
|
||||
}
|
||||
await promise
|
||||
sendUserToast(`Successfully connected to ${kindToName[kind]}`)
|
||||
} catch (err) {
|
||||
|
||||
if (!promise?.isCancelled) {
|
||||
sendUserToast(`Error testing ${kindToName[kind]}: ${err?.body ?? 'Unknown error'}`, true)
|
||||
}
|
||||
} finally {
|
||||
testLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-end mt-1">
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={testTriggerConnection}
|
||||
loading={testLoading}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
export let path: string
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let headless: boolean = false
|
||||
@@ -135,16 +136,19 @@
|
||||
required: ['topics', 'group_id']
|
||||
}
|
||||
|
||||
let connectionValid = false
|
||||
let isStaticConnectionValid = false
|
||||
let otherArgsValid = false
|
||||
|
||||
$: isValid =
|
||||
(selected === 'resource'
|
||||
$: isConnectionValid =
|
||||
selected === 'resource'
|
||||
? !!args.kafka_resource_path
|
||||
: connectionValid &&
|
||||
: isStaticConnectionValid &&
|
||||
args.brokers &&
|
||||
args.brokers.length > 0 &&
|
||||
args.brokers.every((b) => b.length > 0)) &&
|
||||
args.brokers.every((b) => b.length > 0)
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
otherArgsValid &&
|
||||
args.topics &&
|
||||
args.topics.length > 0 &&
|
||||
@@ -213,10 +217,18 @@
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args
|
||||
bind:isValid={connectionValid}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
{/if}
|
||||
{#if isConnectionValid}
|
||||
<TestTriggerConnection
|
||||
kind="kafka"
|
||||
args={{
|
||||
connection: args
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Subsection>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let headless: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
@@ -172,7 +173,7 @@
|
||||
required: ['subjects', 'use_jetstream', 'stream_name', 'consumer_name']
|
||||
}
|
||||
|
||||
let connectionValid = false
|
||||
let isStaticConnectionValid = false
|
||||
let otherArgsValid = false
|
||||
let globalError = ''
|
||||
|
||||
@@ -181,15 +182,18 @@
|
||||
? 'Only one subject is supported if not using JetStream.'
|
||||
: ''
|
||||
|
||||
$: isValid =
|
||||
(selected === 'resource'
|
||||
$: isConnectionValid =
|
||||
selected === 'resource'
|
||||
? !!args.nats_resource_path
|
||||
: connectionValid &&
|
||||
: isStaticConnectionValid &&
|
||||
args.servers &&
|
||||
args.servers.length > 0 &&
|
||||
args.servers.every((b) => b.length > 0) &&
|
||||
args.require_tls !== undefined &&
|
||||
args.require_tls !== null) &&
|
||||
args.require_tls !== null
|
||||
|
||||
$: isValid =
|
||||
isConnectionValid &&
|
||||
otherArgsValid &&
|
||||
args.subjects &&
|
||||
args.subjects.length > 0 &&
|
||||
@@ -264,10 +268,13 @@
|
||||
<SchemaForm
|
||||
schema={connnectionSchema}
|
||||
bind:args
|
||||
bind:isValid={connectionValid}
|
||||
bind:isValid={isStaticConnectionValid}
|
||||
lightHeader={true}
|
||||
/>
|
||||
{/if}
|
||||
{#if isConnectionValid}
|
||||
<TestTriggerConnection kind="nats" args={{ connection: args }} />
|
||||
{/if}
|
||||
</Subsection>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { FlowService, ScriptService, type Flow, type Script } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import TestTriggerConnection from '../TestTriggerConnection.svelte'
|
||||
|
||||
export let url: string | undefined
|
||||
export let url_runnable_args: Record<string, unknown> | undefined
|
||||
@@ -182,5 +183,9 @@
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isValid}
|
||||
<TestTriggerConnection kind="websocket" args={{ url, url_runnable_args }} />
|
||||
{/if}
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user