mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
feat: critical error side channel (#3625)
* feat: critical error side channel * feat: trigger workspace error handler or critical channel on schedule error handling failure * fix: build * feat: finish critical error side channel * feat: channels instead of just emails + move to EE * fix: remove superfluous func * fix: nits + ee ref * fix: open source build * fix: nits * chore: update ee ref
This commit is contained in:
Generated
+1
-1
@@ -9683,7 +9683,6 @@ dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mail-send",
|
||||
"mime_guess",
|
||||
"object_store",
|
||||
"openidconnect",
|
||||
@@ -9777,6 +9776,7 @@ dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mail-send",
|
||||
"object_store",
|
||||
"prometheus",
|
||||
"rand 0.8.5",
|
||||
|
||||
@@ -1 +1 @@
|
||||
66d9cbb158ab9a5869a45ba253bf57f2cbb5ecb6
|
||||
f59f9c1e55e5e93f9eb6c081847cc566611029d2
|
||||
+15
-9
@@ -18,11 +18,11 @@ use tokio::fs::DirBuilder;
|
||||
use windmill_api::HTTP_CLIENT;
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CUSTOM_TAGS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
|
||||
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
},
|
||||
@@ -47,10 +47,11 @@ use windmill_worker::{
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_require_preexisting_user, load_tag_per_workspace_enabled,
|
||||
monitor_db, monitor_pool, reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
|
||||
reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting,
|
||||
reload_server_config, reload_worker_config,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_server_config,
|
||||
reload_worker_config,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -503,6 +504,11 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::error!(error = %e, "Could not reload hub base url setting");
|
||||
}
|
||||
},
|
||||
CRITICAL_ERROR_CHANNELS_SETTING => {
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload critical error emails setting");
|
||||
}
|
||||
},
|
||||
a @_ => {
|
||||
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
|
||||
}
|
||||
|
||||
+38
-7
@@ -22,15 +22,17 @@ use windmill_api::{
|
||||
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
|
||||
};
|
||||
use windmill_common::{
|
||||
ee::CriticalErrorChannel,
|
||||
error,
|
||||
flow_status::FlowStatusModule,
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING,
|
||||
},
|
||||
jobs::QueuedJob,
|
||||
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
|
||||
@@ -40,7 +42,8 @@ use windmill_common::{
|
||||
load_worker_config, reload_custom_tags_setting, DEFAULT_TAGS_PER_WORKSPACE, SERVER_CONFIG,
|
||||
WORKER_CONFIG,
|
||||
},
|
||||
BASE_URL, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_queue::cancel_job;
|
||||
use windmill_worker::{
|
||||
@@ -143,6 +146,10 @@ pub async fn initial_load(
|
||||
tracing::error!("Error reloading hub base url: {:?}", e)
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_error_channels_setting(&db).await {
|
||||
tracing::error!("Could not reload critical error emails setting: {:?}", e);
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
if !_is_agent {
|
||||
reload_s3_cache_setting(&db).await;
|
||||
@@ -1074,3 +1081,27 @@ pub async fn reload_hub_base_url_setting(db: &DB, server_mode: bool) -> error::R
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_error_channels_setting(db: &DB) -> error::Result<()> {
|
||||
let critical_error_channels =
|
||||
load_value_from_global_settings(db, CRITICAL_ERROR_CHANNELS_SETTING).await?;
|
||||
|
||||
let critical_error_channels = if let Some(q) = critical_error_channels {
|
||||
if let Ok(v) = serde_json::from_value::<Vec<CriticalErrorChannel>>(q.clone()) {
|
||||
v
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Could not parse critical_error_emails setting as an array of channels, found: {:#?}",
|
||||
&q
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut l = CRITICAL_ERROR_CHANNELS.write().await;
|
||||
*l = critical_error_channels;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ async_zip.workspace = true
|
||||
rsmq_async.workspace = true
|
||||
regex.workspace = true
|
||||
bytes.workspace = true
|
||||
mail-send.workspace = true
|
||||
samael = { workspace = true, optional = true }
|
||||
async-recursion.workspace = true
|
||||
rsa.workspace = true
|
||||
|
||||
@@ -543,7 +543,7 @@ async fn update_flow(
|
||||
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ async fn create_schedule(
|
||||
.await?;
|
||||
|
||||
if ns.enabled.unwrap_or(true) {
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -303,7 +303,7 @@ async fn edit_schedule(
|
||||
.await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -512,7 +512,7 @@ pub async fn set_enabled(
|
||||
.await?;
|
||||
|
||||
if payload.enabled {
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -560,7 +560,7 @@ pub async fn set_enabled(
|
||||
// .await?;
|
||||
|
||||
// if payload.enabled {
|
||||
// tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
// tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
// }
|
||||
// tx.commit().await?;
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ async fn create_script(
|
||||
clear_schedule(tx.transaction_mut(), &schedule.path, &w_id).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(&db, tx, schedule).await?;
|
||||
tx = push_scheduled_job(&db, tx, &schedule).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -21,15 +21,17 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use mail_send::{mail_builder::MessageBuilder, SmtpClientBuilder};
|
||||
use serde::Deserialize;
|
||||
use tokio::time::timeout;
|
||||
use windmill_common::{
|
||||
error::{self, to_anyhow, JsonResult, Result},
|
||||
error::{self, JsonResult, Result},
|
||||
global_settings::{AUTOMATE_USERNAME_CREATION_SETTING, ENV_SETTINGS, HUB_BASE_URL_SETTING},
|
||||
server::Smtp,
|
||||
utils::send_email,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
#[warn(unused_mut)]
|
||||
let r = Router::new()
|
||||
@@ -67,34 +69,17 @@ pub async fn test_email(
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let smtp = test_email.smtp;
|
||||
let to = test_email.to;
|
||||
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
|
||||
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
|
||||
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
|
||||
client = client.allow_invalid_certs();
|
||||
}
|
||||
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
|
||||
if !username.is_empty() {
|
||||
client.credentials((username, password))
|
||||
} else {
|
||||
client
|
||||
}
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from(("Windmill", smtp.from.as_str()))
|
||||
.to(to.clone())
|
||||
.subject("Test email from Windmill")
|
||||
.text_body("Test email content");
|
||||
let dur = Duration::from_secs(3);
|
||||
timeout(dur, client.connect())
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
tracing::info!("Sent test email to {to}");
|
||||
|
||||
let client_timeout = Duration::from_secs(3);
|
||||
send_email(
|
||||
"Test email from Windmill",
|
||||
"Test email content",
|
||||
vec![to],
|
||||
smtp,
|
||||
Some(client_timeout),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok("Sent test email".to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ use windmill_common::{
|
||||
variables::ExportableListableVariable,
|
||||
};
|
||||
use windmill_git_sync::handle_deployment_metadata;
|
||||
use windmill_queue::QueueTransaction;
|
||||
|
||||
use crate::oauth2_ee::InstanceEvent;
|
||||
use crate::variables::{decrypt, encrypt};
|
||||
@@ -488,25 +487,25 @@ async fn run_slack_message_test_job(
|
||||
json!(format!("$res:{WORKSPACE_SLACK_BOT_TOKEN_PATH}")),
|
||||
);
|
||||
|
||||
let tx: QueueTransaction<'_, _> = (rsmq.clone(), db.begin().await?).into();
|
||||
let (uuid, tx) = windmill_queue::handle_on_failure(
|
||||
let uuid = windmill_queue::push_error_handler(
|
||||
&db,
|
||||
tx,
|
||||
rsmq,
|
||||
Uuid::parse_str("00000000-0000-0000-0000-000000000000")?,
|
||||
"slack_message_test",
|
||||
"slack_message_test",
|
||||
None,
|
||||
Some("slack_message_test".to_string()),
|
||||
false,
|
||||
w_id.as_str(),
|
||||
&format!("script/{}", req.hub_script_path.as_str()),
|
||||
sqlx::types::Json(&fake_result),
|
||||
0,
|
||||
Utc::now(),
|
||||
None,
|
||||
Some(Utc::now()),
|
||||
Some(json!(extra_args)),
|
||||
authed.email.as_str(),
|
||||
false,
|
||||
false,
|
||||
None, // Note: we could mark it as high priority to return result quickly to the user
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(RunSlackMessageTestJobResponse {
|
||||
job_uuid: uuid.to_string(),
|
||||
|
||||
@@ -46,4 +46,5 @@ object_store = { workspace = true, optional = true }
|
||||
prometheus = { workspace = true, optional = true }
|
||||
aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-sts = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
indexmap.workspace = true
|
||||
mail-send.workspace = true
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::ee::LicensePlan::Community;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -18,3 +19,9 @@ pub async fn get_license_plan() -> LicensePlan {
|
||||
// Implementation is not open source
|
||||
return Community;
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CriticalErrorChannel {}
|
||||
|
||||
pub async fn trigger_critical_error_channels(_error_message: String) {}
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::response::Response;
|
||||
use axum::{
|
||||
response::IntoResponse,
|
||||
response::Json,
|
||||
};
|
||||
use axum::{response::IntoResponse, response::Json};
|
||||
|
||||
use hyper::StatusCode;
|
||||
use sqlx::migrate::MigrateError;
|
||||
@@ -45,6 +42,8 @@ pub enum Error {
|
||||
SqlErr(#[from] sqlx::Error),
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("Quota exceeded: {0}")]
|
||||
QuotaExceeded(String),
|
||||
#[error("Internal: {0}")]
|
||||
InternalErr(String),
|
||||
#[error("Hexadecimal decoding error: {0}")]
|
||||
@@ -81,9 +80,10 @@ impl IntoResponse for Error {
|
||||
Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
|
||||
Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
|
||||
Self::RequireAdmin(_) => axum::http::StatusCode::FORBIDDEN,
|
||||
Self::SqlErr(_) | Self::BadRequest(_) | Self::OpenAIError(_) => {
|
||||
axum::http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
Self::SqlErr(_)
|
||||
| Self::BadRequest(_)
|
||||
| Self::OpenAIError(_)
|
||||
| Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
|
||||
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
|
||||
|
||||
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
|
||||
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 50] = [
|
||||
"DISABLE_NSJAIL",
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use ee::CriticalErrorChannel;
|
||||
use error::Error;
|
||||
use scripts::ScriptLang;
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -73,6 +74,9 @@ lazy_static::lazy_static! {
|
||||
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub static ref HUB_BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new(DEFAULT_HUB_BASE_URL.to_string()));
|
||||
|
||||
|
||||
pub static ref CRITICAL_ERROR_CHANNELS: Arc<RwLock<Vec<CriticalErrorChannel>>> = Arc::new(RwLock::new(vec![]));
|
||||
}
|
||||
|
||||
pub async fn shutdown_signal(
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::ee::LICENSE_KEY_ID;
|
||||
use crate::ee::{trigger_critical_error_channels, LICENSE_KEY_ID};
|
||||
use crate::error::{to_anyhow, Error, Result};
|
||||
use crate::global_settings::UNIQUE_ID_SETTING;
|
||||
use crate::server::Smtp;
|
||||
use crate::DB;
|
||||
use git_version::git_version;
|
||||
use mail_send::mail_builder::MessageBuilder;
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -85,12 +88,20 @@ pub async fn query_elems_from_hub(
|
||||
url: &str,
|
||||
query_params: Option<Vec<(&str, String)>>,
|
||||
db: &DB,
|
||||
) -> Result<(reqwest::StatusCode, reqwest::header::HeaderMap, axum::body::Body)> {
|
||||
) -> Result<(
|
||||
reqwest::StatusCode,
|
||||
reqwest::header::HeaderMap,
|
||||
axum::body::Body,
|
||||
)> {
|
||||
let response = http_get_from_hub(http_client, url, false, query_params, db).await?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
Ok((status, response.headers().clone(), axum::body::Body::from_stream(response.bytes_stream())))
|
||||
Ok((
|
||||
status,
|
||||
response.headers().clone(),
|
||||
axum::body::Body::from_stream(response.bytes_stream()),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn http_get_from_hub(
|
||||
@@ -167,3 +178,60 @@ pub enum Mode {
|
||||
Server,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
subject: &str,
|
||||
content: &str,
|
||||
to: Vec<String>,
|
||||
smtp: Smtp,
|
||||
client_timeout: Option<tokio::time::Duration>,
|
||||
) -> Result<()> {
|
||||
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
|
||||
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
|
||||
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
|
||||
client = client.allow_invalid_certs();
|
||||
}
|
||||
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
|
||||
if !username.is_empty() {
|
||||
client.credentials((username, password))
|
||||
} else {
|
||||
client
|
||||
}
|
||||
} else {
|
||||
client
|
||||
};
|
||||
let message = MessageBuilder::new()
|
||||
.from(("Windmill", smtp.from.as_str()))
|
||||
.to(to.clone())
|
||||
.subject(subject)
|
||||
.text_body(content);
|
||||
|
||||
match client_timeout {
|
||||
Some(timeout) => {
|
||||
tokio::time::timeout(timeout, client.connect())
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
}
|
||||
None => {
|
||||
client
|
||||
.connect()
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
}
|
||||
}
|
||||
tracing::info!("Sent email to {:#?}: {subject}", to);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub async fn report_critical_error(error_message: String) -> () {
|
||||
tracing::error!("CRITICAL ERROR: {error_message}");
|
||||
trigger_critical_error_channels(error_message).await;
|
||||
}
|
||||
|
||||
+374
-353
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,9 @@ use windmill_common::{
|
||||
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
db: &DB,
|
||||
mut tx: QueueTransaction<'c, R>,
|
||||
schedule: Schedule,
|
||||
schedule: &Schedule,
|
||||
) -> Result<QueueTransaction<'c, R>> {
|
||||
let sched = cron::Schedule::from_str(&schedule.schedule)
|
||||
let sched = cron::Schedule::from_str(schedule.schedule.as_ref())
|
||||
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||
|
||||
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
|
||||
@@ -68,9 +68,9 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
|
||||
let mut args: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
|
||||
|
||||
if let Some(args_v) = schedule.args {
|
||||
if let Some(args_v) = &schedule.args {
|
||||
if let serde_json::Value::Object(args_m) = args_v {
|
||||
args = args_m
|
||||
args = args_m.clone()
|
||||
} else {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"args of scripts needs to be dict".to_string(),
|
||||
@@ -90,7 +90,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
.map(|x| (x.tag, x.dedicated_worker))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
(
|
||||
JobPayload::Flow { path: schedule.script_path, dedicated_worker },
|
||||
JobPayload::Flow { path: schedule.script_path.clone(), dedicated_worker },
|
||||
tag,
|
||||
None,
|
||||
)
|
||||
@@ -113,8 +113,8 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
.await?;
|
||||
|
||||
if schedule.retry.is_some() {
|
||||
let parsed_retry =
|
||||
serde_json::from_value::<Retry>(schedule.retry.unwrap()).map_err(|err| {
|
||||
let parsed_retry = serde_json::from_value::<Retry>(schedule.retry.clone().unwrap())
|
||||
.map_err(|err| {
|
||||
error::Error::InternalErr(format!(
|
||||
"Unable to parse retry information from schedule: {}",
|
||||
err.to_string(),
|
||||
@@ -127,7 +127,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
// if retry is set, we wrap the script into a one step flow with a retry on the module
|
||||
(
|
||||
JobPayload::SingleScriptFlow {
|
||||
path: schedule.script_path,
|
||||
path: schedule.script_path.clone(),
|
||||
hash: hash,
|
||||
retry: parsed_retry,
|
||||
args: static_args,
|
||||
@@ -144,7 +144,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
(
|
||||
JobPayload::ScriptHash {
|
||||
hash,
|
||||
path: schedule.script_path,
|
||||
path: schedule.script_path.clone(),
|
||||
concurrent_limit: concurrent_limit,
|
||||
concurrency_time_window_s: concurrency_time_window_s,
|
||||
cache_ttl: cache_ttl,
|
||||
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
priority,
|
||||
},
|
||||
if schedule.tag.as_ref().is_some_and(|x| x != "") {
|
||||
schedule.tag
|
||||
schedule.tag.clone()
|
||||
} else {
|
||||
tag
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ use windmill_common::{
|
||||
},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
};
|
||||
use windmill_queue::schedule::get_schedule_opt;
|
||||
use windmill_queue::{
|
||||
add_completed_job, add_completed_job_error, append_logs, get_queued_job,
|
||||
handle_maybe_scheduled_job, CanceledBy, PushIsolationLevel, WrappedError,
|
||||
@@ -1201,23 +1202,37 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
&& flow_job.script_path.is_some()
|
||||
&& status.step == 0
|
||||
{
|
||||
let tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
|
||||
let mut tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
|
||||
|
||||
match handle_maybe_scheduled_job(
|
||||
tx,
|
||||
db,
|
||||
flow_job.schedule_path.as_ref().unwrap(),
|
||||
flow_job.script_path.as_ref().unwrap(),
|
||||
&flow_job.workspace_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tx.commit().await?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error during handle_maybe_scheduled_job: {e}");
|
||||
}
|
||||
let schedule_path = flow_job.schedule_path.as_ref().unwrap();
|
||||
|
||||
let schedule =
|
||||
get_schedule_opt(tx.transaction_mut(), &flow_job.workspace_id, schedule_path).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
if let Err(err) = handle_maybe_scheduled_job(
|
||||
rsmq.clone(),
|
||||
db,
|
||||
flow_job,
|
||||
&schedule,
|
||||
flow_job.script_path.as_ref().unwrap(),
|
||||
&flow_job.workspace_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match err {
|
||||
Error::QuotaExceeded(_) => return Err(err.into()),
|
||||
// scheduling next job failed and could not disable schedule => make zombie job to retry
|
||||
_ => return Ok(()),
|
||||
}
|
||||
};
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Schedule {schedule_path} in {} not found. Impossible to schedule again",
|
||||
&flow_job.workspace_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { capitalize } from '$lib/utils'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import CustomOauth from './CustomOauth.svelte'
|
||||
import { AlertTriangle, Plus } from 'lucide-svelte'
|
||||
import { AlertTriangle, Plus, X } from 'lucide-svelte'
|
||||
import CustomSso from './CustomSso.svelte'
|
||||
import AuthentikSetting from '$lib/components/AuthentikSetting.svelte'
|
||||
import AutheliaSetting from '$lib/components/AutheliaSetting.svelte'
|
||||
@@ -24,6 +24,7 @@
|
||||
import ZitadelSetting from '$lib/components/ZitadelSetting.svelte'
|
||||
import Password from './Password.svelte'
|
||||
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
export let tab: string = 'Core'
|
||||
export let hideTabs: boolean = false
|
||||
@@ -498,6 +499,78 @@
|
||||
placeholder={setting.placeholder}
|
||||
bind:value={values[setting.key]}
|
||||
/>
|
||||
{:else if setting.fieldType == 'critical_error_channels'}
|
||||
<div class="w-full">
|
||||
<div class="flex max-w-md mt-1 gap-2 w-full items-center">
|
||||
<select disabled>
|
||||
<option>Tracing</option>
|
||||
</select>
|
||||
<input disabled />
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
|
||||
aria-label="Clear"
|
||||
disabled
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{#if $enterpriseLicense && Array.isArray(values[setting.key])}
|
||||
{#each values[setting.key] ?? [] as v, i}
|
||||
<div class="flex max-w-md mt-1 gap-2 w-full items-center">
|
||||
<select>
|
||||
<option value="email">Email</option>
|
||||
</select>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
on:input={(e) => {
|
||||
if (e.target?.['value']) {
|
||||
values[setting.key][i] = {
|
||||
email: e.target['value']
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={v?.email ?? ''}
|
||||
/>
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover"
|
||||
aria-label="Clear"
|
||||
on:click={() => {
|
||||
values[setting.key] = values[setting.key].filter(
|
||||
(_, index) => index !== i
|
||||
)
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex mt-2 gap-20 items-baseline">
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
if (
|
||||
values[setting.key] == undefined ||
|
||||
!Array.isArray(values[setting.key])
|
||||
) {
|
||||
values[setting.key] = []
|
||||
}
|
||||
values[setting.key] = values[setting.key].concat('')
|
||||
}}
|
||||
id="arg-input-add-item"
|
||||
startIcon={{ icon: Plus }}
|
||||
disabled={!$enterpriseLicense}
|
||||
>
|
||||
Add item
|
||||
</Button>
|
||||
</div>
|
||||
{:else if setting.fieldType == 'object_store_config'}
|
||||
<ObjectStoreConfigSettings bind:bucket_config={values[setting.key]} />
|
||||
{:else if setting.fieldType == 'number'}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface Setting {
|
||||
| 'email'
|
||||
| 'license_key'
|
||||
| 'object_store_config'
|
||||
| 'critical_error_channels'
|
||||
storage: SettingStorage
|
||||
isValid?: (value: any) => boolean
|
||||
error?: string
|
||||
@@ -102,6 +103,15 @@ export const settings: Record<string, Setting[]> = {
|
||||
storage: 'setting',
|
||||
ee_only: ''
|
||||
},
|
||||
{
|
||||
label: 'Critical Error Channels',
|
||||
description:
|
||||
'Channels to send critical errors to. SMTP must be configured for the email channel.',
|
||||
key: 'critical_error_channels',
|
||||
fieldType: 'critical_error_channels',
|
||||
storage: 'setting',
|
||||
ee_only: 'Channels other than tracing are only available in the EE version'
|
||||
},
|
||||
{
|
||||
label: 'Azure OpenAI base path',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user