fix: migrate smtp instance settings to global settings (#4416)

* all

* all

* all

* fix

* update ee private

* all
This commit is contained in:
Ruben Fiszel
2024-09-21 01:42:15 +02:00
committed by GitHub
parent d318f10f36
commit efbcb1ffdd
21 changed files with 428 additions and 338 deletions
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "bool",
"name": "?column?",
"type_info": "Bool"
}
],
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT config FROM config WHERE name = 'server'",
"query": "SELECT value FROM global_settings WHERE name = 'smtp_settings'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "config",
"name": "value",
"type_info": "Jsonb"
}
],
@@ -13,8 +13,8 @@
"Left": []
},
"nullable": [
true
false
]
},
"hash": "8ca76fcadc659d7069b97930cdde2d5200f3cef9dedb83b76c8a6a433d2a4de3"
"hash": "83ec97f6aad154e0e06ee05a3647dab8f89b1b2d7a569c7eac8c4169e37b9f8b"
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
false,
true
true,
false
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
+29 -60
View File
@@ -286,7 +286,7 @@ dependencies = [
"arrow-data",
"arrow-schema",
"chrono",
"chrono-tz 0.9.0",
"chrono-tz",
"half",
"hashbrown 0.14.5",
"num",
@@ -1083,7 +1083,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower 0.4.13",
"tower",
"tower-layer",
"tower-service",
"tracing",
@@ -1667,18 +1667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build 0.3.0",
"phf",
]
[[package]]
name = "chrono-tz"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
dependencies = [
"chrono",
"chrono-tz-build 0.4.0",
"chrono-tz-build",
"phf",
]
@@ -1693,16 +1682,6 @@ dependencies = [
"phf_codegen",
]
[[package]]
name = "chrono-tz-build"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
dependencies = [
"parse-zoneinfo",
"phf_codegen",
]
[[package]]
name = "cipher"
version = "0.3.0"
@@ -2682,7 +2661,7 @@ dependencies = [
"tokio-rustls 0.26.0",
"tokio-socks",
"tokio-util",
"tower 0.4.13",
"tower",
"tower-http",
"tower-service",
]
@@ -2795,7 +2774,7 @@ dependencies = [
"rustls-webpki 0.102.8",
"serde",
"tokio",
"webpki-roots 0.26.6",
"webpki-roots 0.26.5",
]
[[package]]
@@ -4306,7 +4285,7 @@ dependencies = [
"pin-project-lite",
"socket2 0.5.7",
"tokio",
"tower 0.4.13",
"tower",
"tower-service",
"tracing",
]
@@ -4892,7 +4871,7 @@ dependencies = [
"smtp-proto",
"tokio",
"tokio-rustls 0.26.0",
"webpki-roots 0.26.6",
"webpki-roots 0.26.5",
]
[[package]]
@@ -8051,7 +8030,7 @@ dependencies = [
"tracing",
"url",
"uuid 1.10.0",
"webpki-roots 0.26.6",
"webpki-roots 0.26.5",
]
[[package]]
@@ -9500,16 +9479,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-cookies"
version = "0.10.0"
@@ -9924,9 +9893,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-id"
version = "0.3.5"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f"
[[package]]
name = "unicode-id-start"
@@ -9972,15 +9941,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"
version = "0.2.6"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]]
name = "unicode_categories"
@@ -10044,7 +10013,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"webpki-roots 0.26.6",
"webpki-roots 0.26.5",
]
[[package]]
@@ -10340,9 +10309,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.6"
version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
dependencies = [
"rustls-pki-types",
]
@@ -10480,7 +10449,7 @@ dependencies = [
"candle-nn",
"candle-transformers",
"chrono",
"chrono-tz 0.10.0",
"chrono-tz",
"cookie 0.17.0",
"crc",
"cron",
@@ -10492,7 +10461,7 @@ dependencies = [
"hmac",
"http 1.1.0",
"hyper 1.4.1",
"itertools 0.13.0",
"itertools 0.10.5",
"jsonwebtoken",
"lazy_static",
"magic-crypt",
@@ -10527,7 +10496,7 @@ dependencies = [
"tokio-native-tls",
"tokio-tar",
"tokio-util",
"tower 0.5.1",
"tower",
"tower-cookies",
"tower-http",
"tracing",
@@ -10597,7 +10566,7 @@ dependencies = [
"hmac",
"hyper 1.4.1",
"indexmap 2.5.0",
"itertools 0.13.0",
"itertools 0.10.5",
"lazy_static",
"magic-crypt",
"mail-send",
@@ -10684,7 +10653,7 @@ version = "1.397.4"
dependencies = [
"anyhow",
"gosyn",
"itertools 0.13.0",
"itertools 0.10.5",
"lazy_static",
"regex",
"windmill-parser",
@@ -10707,7 +10676,7 @@ name = "windmill-parser-php"
version = "1.397.4"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools 0.10.5",
"php-parser-rs",
"serde_json",
"windmill-parser",
@@ -10718,7 +10687,7 @@ name = "windmill-parser-py"
version = "1.397.4"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools 0.10.5",
"rustpython-parser",
"serde_json",
"windmill-parser",
@@ -10730,7 +10699,7 @@ version = "1.397.4"
dependencies = [
"anyhow",
"async-recursion",
"itertools 0.13.0",
"itertools 0.10.5",
"lazy_static",
"phf",
"regex",
@@ -10748,7 +10717,7 @@ version = "1.397.4"
dependencies = [
"anyhow",
"convert_case 0.6.0",
"itertools 0.13.0",
"itertools 0.10.5",
"lazy_static",
"pulldown-cmark",
"quote",
@@ -10829,12 +10798,12 @@ dependencies = [
"axum",
"bigdecimal",
"chrono",
"chrono-tz 0.10.0",
"chrono-tz",
"cron",
"futures-core",
"hex",
"hmac",
"itertools 0.13.0",
"itertools 0.10.5",
"lazy_static",
"prometheus",
"regex",
@@ -10890,7 +10859,7 @@ dependencies = [
"gcp_auth",
"git-version",
"hex",
"itertools 0.13.0",
"itertools 0.10.5",
"jsonwebtoken",
"lazy_static",
"mappable-rc",
+1 -1
View File
@@ -1 +1 @@
f8621356df7f9b9250913cf5cdbda35d0703f311
e093d51a219ce4ff0562a02db24ec402554a1f05
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,8 @@
-- Add up migration script here
INSERT INTO global_settings (name, value)
SELECT 'smtp_settings', config
FROM config
WHERE name = 'server'
ON CONFLICT (name) DO UPDATE SET value = excluded.value;
DELETE FROM config WHERE name = 'server';
+13 -6
View File
@@ -8,7 +8,10 @@
use anyhow::Context;
use git_version::git_version;
use monitor::{send_current_log_file_to_object_store, send_logs_to_object_store};
use monitor::{
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
send_logs_to_object_store,
};
use rand::Rng;
use sqlx::{postgres::PgListener, Pool, Postgres};
use std::{
@@ -35,7 +38,7 @@ use windmill_common::{
JWT_SECRET_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,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
scripts::ScriptLang,
stats_ee::schedule_stats,
@@ -74,7 +77,7 @@ use crate::monitor::{
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_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_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config,
};
@@ -563,9 +566,7 @@ Windmill Community Edition {GIT_VERSION}
"notify_config_change" => {
match n.payload() {
"server" if server_mode => {
tracing::info!("Server config change detected: {}", n.payload());
reload_server_config(&db).await;
tracing::error!("Server config change detected but server config is obsolete: {}", n.payload());
},
a@ _ if worker_mode && a == format!("worker__{}", *WORKER_GROUP) => {
tracing::info!("Worker config change detected: {}", n.payload());
@@ -609,6 +610,12 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!("Error loading default tag per workspace workspaces: {e:#}");
}
}
SMTP_SETTING => {
reload_smtp_config(&db).await;
},
TIMEOUT_WAIT_RESULT_SETTING => {
reload_timeout_wait_result_setting(&db).await
},
RETENTION_PERIOD_SECS_SETTING => {
reload_retention_period_setting(&db).await
},
+22 -11
View File
@@ -22,6 +22,7 @@ use tokio::{
#[cfg(feature = "embedding")]
use windmill_api::embeddings::update_embeddings_db;
use windmill_api::{
jobs::TIMEOUT_WAIT_RESULT,
oauth2_ee::{build_oauth_clients, OAuthClient},
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
};
@@ -40,17 +41,17 @@ use windmill_common::{
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,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
},
jobs::QueuedJob,
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
server::load_server_config,
server::load_smtp_config,
tracing_init::JSON_FMT,
users::truncate_token,
utils::{now_from_db, rd_string, report_critical_error, Mode},
worker::{
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SERVER_CONFIG, WORKER_CONFIG,
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG, WORKER_CONFIG,
WORKER_GROUP,
},
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
@@ -172,8 +173,9 @@ pub async fn initial_load(
reload_s3_cache_setting(&db).await;
}
reload_smtp_config(&db).await;
if server_mode {
reload_server_config(&db).await;
reload_retention_period_setting(&db).await;
reload_request_size(&db).await;
reload_saml_metadata_setting(&db).await;
@@ -698,6 +700,15 @@ pub async fn reload_scim_token_setting(db: &DB) {
.await;
}
pub async fn reload_timeout_wait_result_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
TIMEOUT_WAIT_RESULT_SETTING,
"TIMEOUT_WAIT_RESULT",
TIMEOUT_WAIT_RESULT.clone(),
)
.await;
}
pub async fn reload_saml_metadata_setting(db: &DB) {
reload_option_setting_with_tracing(
db,
@@ -1129,14 +1140,14 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
}
}
pub async fn reload_server_config(db: &Pool<Postgres>) {
let config = load_server_config(&db).await;
if let Err(e) = config {
tracing::error!("Error reloading server config: {:?}", e)
pub async fn reload_smtp_config(db: &Pool<Postgres>) {
let smtp_config = load_smtp_config(&db).await;
if let Err(e) = smtp_config {
tracing::error!("Error reloading smtp config: {:?}", e)
} else {
let mut wc = SERVER_CONFIG.write().await;
tracing::info!("Reloading server config...");
*wc = config.unwrap()
let mut wc = SMTP_CONFIG.write().await;
tracing::info!("Reloading smtp config...");
*wc = smtp_config.unwrap()
}
}
+29
View File
@@ -742,6 +742,35 @@ paths:
schema:
type: string
/settings/test_critical_channels:
post:
summary: test critical channels
operationId: testCriticalChannels
tags:
- setting
requestBody:
description: test critical channel payload
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
email:
type: string
slack_channel:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/settings/test_license_key:
post:
summary: test license key
+13 -2
View File
@@ -58,7 +58,7 @@ use tower_http::cors::{Any, CorsLayer};
use urlencoding::encode;
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG};
use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE};
use windmill_common::{
db::UserDB,
error::{self, to_anyhow, Error},
@@ -3165,6 +3165,17 @@ impl Drop for Guard {
}
}
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref TIMEOUT_WAIT_RESULT: Arc<RwLock<Option<u64>>> = Arc::new(RwLock::new(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse::<u64>().ok())
));
}
#[derive(Deserialize)]
pub struct WindmillCompositeResult {
windmill_status_code: Option<u16>,
@@ -3179,7 +3190,7 @@ async fn run_wait_result(
username: &str,
) -> error::Result<Response> {
let mut result = None;
let timeout = SERVER_CONFIG.read().await.timeout_wait_result.clone();
let timeout = TIMEOUT_WAIT_RESULT.read().await.clone().unwrap_or(600);
let timeout_ms = if timeout <= 0 {
2000
} else {
+20 -1
View File
@@ -26,6 +26,7 @@ use axum::extract::Query;
use serde::Deserialize;
use windmill_common::{
ee::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel},
error::{self, JsonResult, Result},
global_settings::{
AUTOMATE_USERNAME_CREATION_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
@@ -55,7 +56,8 @@ pub fn global_service() -> Router {
get(get_latest_key_renewal_attempt),
)
.route("/renew_license_key", post(renew_license_key))
.route("/customer_portal", post(create_customer_portal_session));
.route("/customer_portal", post(create_customer_portal_session))
.route("/test_critical_channels", post(test_critical_channels));
#[cfg(feature = "parquet")]
{
@@ -387,3 +389,20 @@ pub async fn create_customer_portal_session(
return Ok(url);
}
pub async fn test_critical_channels(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(test_critical_channels): Json<Vec<CriticalErrorChannel>>,
) -> Result<String> {
require_super_admin(&db, &authed.email).await?;
send_critical_alert(
"Test critical error".to_string(),
&db,
CriticalAlertKind::CriticalError,
Some(test_critical_channels),
)
.await;
Ok("Sent test critical error".to_string())
}
+7 -1
View File
@@ -36,7 +36,13 @@ pub enum CriticalAlertKind {
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(_error_message: String, _db: &DB, _kind: CriticalAlertKind) {}
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
@@ -14,6 +14,8 @@ pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url";
pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url";
pub const SCIM_TOKEN_SETTING: &str = "scim_token";
pub const SAML_METADATA_SETTING: &str = "saml_metadata";
pub const SMTP_SETTING: &str = "smtp_settings";
pub const TIMEOUT_WAIT_RESULT_SETTING: &str = "timeout_wait_result";
pub const UNIQUE_ID_SETTING: &str = "uid";
pub const DISABLE_STATS_SETTING: &str = "disable_stats";
+6 -25
View File
@@ -13,22 +13,20 @@ pub struct Smtp {
}
#[derive(Serialize, Deserialize, PartialEq)]
pub struct ServerConfigOpt {
pub struct SmtpConfigOpt {
pub smtp_host: Option<String>,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub smtp_port: Option<u16>,
pub smtp_from: Option<String>,
pub smtp_tls_implicit: Option<bool>,
pub timeout_wait_result: Option<u64>,
}
pub async fn load_server_config(db: &DB) -> error::Result<ServerConfig> {
let config: ServerConfigOpt =
sqlx::query_scalar!("SELECT config FROM config WHERE name = 'server'",)
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
let config: SmtpConfigOpt =
sqlx::query_scalar!("SELECT value FROM global_settings WHERE name = 'smtp_settings'",)
.fetch_optional(db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten()
.unwrap_or_default();
@@ -77,20 +75,10 @@ pub async fn load_server_config(db: &DB) -> error::Result<ServerConfig> {
tracing::warn!("SMTP not configured");
}
Ok(ServerConfig {
smtp,
timeout_wait_result: config
.timeout_wait_result
.ok_or(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse::<u64>().ok()),
)
.unwrap_or(600),
})
Ok(smtp)
}
impl Default for ServerConfigOpt {
impl Default for SmtpConfigOpt {
fn default() -> Self {
Self {
smtp_from: None,
@@ -99,13 +87,6 @@ impl Default for ServerConfigOpt {
smtp_port: None,
smtp_tls_implicit: None,
smtp_username: None,
timeout_wait_result: Default::default(),
}
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct ServerConfig {
pub smtp: Option<Smtp>,
pub timeout_wait_result: u64,
}
+8 -2
View File
@@ -288,7 +288,7 @@ pub async fn report_critical_error(error_message: String, _db: DB) -> () {
}
#[cfg(feature = "enterprise")]
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError).await;
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError, None).await;
}
pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
@@ -304,5 +304,11 @@ pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
tracing::error!("Failed to save critical error to database: {}", err);
}
#[cfg(feature = "enterprise")]
send_critical_alert(message, &_db, CriticalAlertKind::RecoveredCriticalError).await;
send_critical_alert(
message,
&_db,
CriticalAlertKind::RecoveredCriticalError,
None,
)
.await;
}
+9 -5
View File
@@ -14,7 +14,7 @@ use std::{
};
use tokio::sync::RwLock;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::ServerConfig, DB};
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::Smtp, DB};
lazy_static::lazy_static! {
pub static ref WORKER_GROUP: String = std::env::var("WORKER_GROUP").unwrap_or_else(|_| "default".to_string());
@@ -66,8 +66,7 @@ lazy_static::lazy_static! {
pub static ref WORKER_SUSPENDED_PULL_QUERY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref SERVER_CONFIG: Arc<RwLock<ServerConfig>> = Arc::new(RwLock::new(ServerConfig { smtp: Default::default(), timeout_wait_result: 20 }));
pub static ref SMTP_CONFIG: Arc<RwLock<Option<Smtp>>> = Arc::new(RwLock::new(None));
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
@@ -195,7 +194,11 @@ fn normalize_path(path: &Path) -> PathBuf {
}
ret
}
pub fn write_file_at_user_defined_location(job_dir: &str, user_defined_path: &str, content: &str) -> error::Result<File> {
pub fn write_file_at_user_defined_location(
job_dir: &str,
user_defined_path: &str,
content: &str,
) -> error::Result<File> {
let job_dir = Path::new(job_dir);
let user_path = PathBuf::from(user_defined_path);
@@ -210,7 +213,8 @@ pub fn write_file_at_user_defined_location(job_dir: &str, user_defined_path: &st
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Path is outside the allowed job directory.",
).into());
)
.into());
}
if let Some(parent_dir) = full_path.parent() {
@@ -1,7 +1,7 @@
<script lang="ts">
import { settings, settingsKeys, type SettingStorage } from './instanceSettings'
import { Button, Skeleton, Tab, TabContent, Tabs } from '$lib/components/common'
import { ConfigService, SettingService, SettingsService } from '$lib/gen'
import { SettingService, SettingsService } from '$lib/gen'
import Toggle from '$lib/components/Toggle.svelte'
import SecondsInput from '$lib/components/common/seconds/SecondsInput.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
@@ -54,7 +54,6 @@
attempted_at: string
} | null = null
let serverConfig = {}
let initialValues: Record<string, any> = {}
let loading = true
@@ -71,17 +70,10 @@
async function loadSettings() {
loading = true
try {
serverConfig = (await ConfigService.getConfig({ name: 'server' })) ?? {}
} catch (e) {
console.log("Sever config not found, assuming it's first setup")
}
async function getValue(key: string, storage: SettingStorage) {
if (storage == 'setting') {
return SettingService.getGlobal({ key })
} else if (storage == 'config') {
return serverConfig[key]
}
}
initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {}
@@ -110,6 +102,9 @@
if (values['base_url'] == undefined) {
values['base_url'] = 'http://localhost'
}
if (values['smtp_connect'] == undefined) {
values['smtp_connect'] = {}
}
loading = false
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
@@ -118,18 +113,6 @@
export async function saveSettings() {
if (values) {
const allSettings = Object.values(settings).flatMap((x) => Object.entries(x))
const newServerConfig = Object.fromEntries(
allSettings
.filter((x) => x[1].storage == 'config' && values?.[x[1].key] && values?.[x[1].key] != '')
.map((x) => [x[1].key, values?.[x[1].key]])
)
if (!deepEqual(newServerConfig, serverConfig)) {
await ConfigService.updateConfig({
name: 'server',
requestBody: newServerConfig
})
serverConfig = JSON.parse(JSON.stringify(newServerConfig))
}
let licenseKeySet = false
await Promise.all(
allSettings
@@ -693,79 +676,102 @@
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 class="w-full flex gap-x-16 flex-wrap">
<div class="w-full max-w-lg">
<div class="flex w-full max-w-lg mt-1 gap-2 w-full items-center">
<input
type="text"
placeholder="Logs (critical errors are always logged)"
disabled
/>
</div>
{#if $enterpriseLicense && Array.isArray(values[setting.key])}
{#each values[setting.key] ?? [] as v, i}
<div class="flex w-full max-w-lg mt-1 gap-2 w-full items-center">
<select
class="w-20"
on:change={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
[e.target['value']]: ''
}
}
}}
value={v && 'slack_channel' in v ? 'slack_channel' : 'email'}
>
<option value="email">Email</option>
<option value="slack_channel">Slack</option>
</select>
{#if v && 'slack_channel' in v}
<input
type="text"
placeholder="Slack channel"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
slack_channel: e.target['value']
}
}
}}
value={v?.slack_channel ?? ''}
/>
{:else}
<input
type="email"
placeholder="Email address"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
email: e.target['value']
}
}
}}
value={v?.email ?? ''}
/>
{/if}
<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>
{#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
on:change={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
[e.target['value']]: ''
}
}
}}
value={v && 'slack_channel' in v ? 'slack_channel' : 'email'}
>
<option value="email">Email</option>
<option value="slack_channel">Slack</option>
</select>
{#if v && 'slack_channel' in v}
<input
type="text"
placeholder="Slack channel"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
slack_channel: e.target['value']
}
}
}}
value={v?.slack_channel ?? ''}
/>
{:else}
<input
type="email"
placeholder="Email address"
on:input={(e) => {
if (e.target?.['value']) {
values[setting.key][i] = {
email: e.target['value']
}
}
}}
value={v?.email ?? ''}
/>
{/if}
<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
<div
><div class="flex mt-1">
<Button
size="xs"
variant="contained"
on:click={async () => {
try {
await SettingService.testCriticalChannels({
requestBody: values[setting.key]
})
sendUserToast(
'Test message sent successfully to critical channels',
false
)
}}
>
<X size={14} />
</button>
</div>
{/each}
{/if}
} catch (error) {
sendUserToast(
'Failed to send test message: ' + error.message,
true
)
}
}}
>
Test Critical Channels
</Button>
</div>
</div>
</div>
<div class="flex mt-2 gap-20 items-baseline">
<Button
@@ -818,6 +824,65 @@
</Button>
{/if}
</div>
{:else if setting.fieldType == 'smtp_connect'}
<div class="flex flex-col gap-4 mt-4">
{#if values[setting.key]}
<div>
<label for="smtp_host" class="block text-sm font-medium">Host</label>
<input
type="text"
id="smtp_host"
placeholder="smtp.gmail.com"
bind:value={values[setting.key].smtp_host}
/>
</div>
<div>
<label for="smtp_port" class="block text-sm font-medium">Port</label>
<input
type="number"
id="smtp_port"
placeholder="587"
bind:value={values[setting.key].smtp_port}
/>
</div>
<div>
<label for="smtp_username" class="block text-sm font-medium"
>Username</label
>
<input
type="text"
id="smtp_username"
placeholder="ruben@windmill.dev"
bind:value={values[setting.key].smtp_username}
/>
</div>
<div>
<label for="smtp_password" class="block text-sm font-medium"
>Password</label
>
<Password bind:password={values[setting.key].smtp_password} />
</div>
<div>
<label for="smtp_from" class="block text-sm font-medium"
>From Address</label
>
<input
type="email"
id="smtp_from"
placeholder="noreply@windmill.dev"
bind:value={values[setting.key].smtp_from}
/>
</div>
<div>
<Toggle
id="smtp_tls_implicit"
bind:checked={values[setting.key].smtp_tls_implicit}
options={{ right: 'Implicit TLS' }}
label="Implicit TLS"
/>
</div>
{/if}
</div>
{:else if setting.fieldType == 'object_store_config'}
<ObjectStoreConfigSettings bind:bucket_config={values[setting.key]} />
<div class="mb-6" />
@@ -863,21 +928,22 @@
</div>
</div>
{#if category == 'SMTP'}
{@const smtp = values['smtp_settings']}
<div class="flex gap-4"
><input type="email" bind:value={to} placeholder="contact@windmill.dev" />
<Button
disabled={to == ''}
disabled={to == '' || !smtp}
on:click={async () => {
await SettingService.testSmtp({
requestBody: {
to,
smtp: {
host: values['smtp_host'],
username: values['smtp_username'],
password: values['smtp_password'],
port: values['smtp_port'],
from: values['smtp_from'],
tls_implicit: values['smtp_tls_implicit']
host: smtp['smtp_host'],
username: smtp['smtp_username'],
password: smtp['smtp_password'],
port: smtp['smtp_port'],
from: smtp['smtp_from'],
tls_implicit: smtp['smtp_tls_implicit']
}
}
})
@@ -72,7 +72,7 @@
/>
</div>
{#if bucket_config}
<div class="flex gap-2">
<div class="flex gap-2 py-1">
<Button
spacingSize="sm"
size="xs"
@@ -123,81 +123,83 @@
<Tab size="sm" value="S3">S3</Tab>
<Tab size="sm" value="Azure">Azure Blob</Tab>
</Tabs>
{#if bucket_config.type === 'S3'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<div class="flex flex-col gap-2 mt-2 p-2 border rounded-md">
{#if bucket_config.type === 'S3'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Bucket</span>
<input type="text" placeholder="bucket-name" bind:value={bucket_config.bucket} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Region</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_REGION</span
>
<input type="text" bind:value={bucket_config.region} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key ID</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_ACCESS_KEY_ID, pod or ec2 profile</span
>
<input type="text" bind:value={bucket_config.access_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Secret key</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_SECRET_KEY, pod or ec2 profile</span
>
<input type="password" autocomplete="new-password" bind:value={bucket_config.secret_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Endpoint</span>
<span class="text-tertiary text-2xs"
>Only needed for non AWS S3 providers like R2 or MinIo</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
<div class="block pb-2">
<span class="text-tertiary text-2xs">Disable if using https only policy</span>
<div>
<Toggle bind:checked={bucket_config.allow_http} options={{ right: 'Allow http' }} />
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Region</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_REGION</span
>
<input type="text" bind:value={bucket_config.region} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key ID</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_ACCESS_KEY_ID, pod or ec2 profile</span
>
<input type="text" bind:value={bucket_config.access_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Secret key</span>
<span class="text-tertiary text-2xs"
>If left empty, will be derived automatically from $AWS_SECRET_KEY, pod or ec2 profile</span
>
<input type="password" autocomplete="new-password" bind:value={bucket_config.secret_key} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Endpoint</span>
<span class="text-tertiary text-2xs"
>Only needed for non AWS S3 providers like R2 or MinIo</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
<div class="block pb-2">
<span class="text-tertiary text-2xs">Disable if using https only policy</span>
<div>
<Toggle bind:checked={bucket_config.allow_http} options={{ right: 'Allow http' }} />
</div>
</div>
</div>
{:else if bucket_config.type === 'Azure'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Account name</span>
<input type="text" placeholder="account-name" bind:value={bucket_config.accountName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Container name</span>
<input type="text" placeholder="container-name" bind:value={bucket_config.containerName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key</span>
<input type="password" autocomplete="new-password" bind:value={bucket_config.accessKey} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Tenant ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.tenantId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.clientId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Endpoint <span class="text-2xs text-tertiary">(optional)</span></span
>
<span class="text-tertiary text-2xs"
>Only needed for non Azure Blob providers like Azurite</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
{:else if bucket_config.type === 'Azure'}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Account name</span>
<input type="text" placeholder="account-name" bind:value={bucket_config.accountName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Container name</span>
<input type="text" placeholder="container-name" bind:value={bucket_config.containerName} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Access key</span>
<input type="password" autocomplete="new-password" bind:value={bucket_config.accessKey} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Tenant ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.tenantId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client ID <span class="text-2xs text-tertiary">(optional)</span></span
>
<input type="text" bind:value={bucket_config.clientId} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Endpoint <span class="text-2xs text-tertiary">(optional)</span></span
>
<span class="text-tertiary text-2xs"
>Only needed for non Azure Blob providers like Azurite</span
>
<input type="text" bind:value={bucket_config.endpoint} />
</label>
{:else}
<div>Unknown bucket type {bucket_config['type']}</div>
{/if}
</div>
{/if}
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
// @ts-nocheck
import { onMount } from 'svelte'
export let password: string
export let password: string | undefined
export let placeholder = '******'
export let disabled = false
export let required = false
@@ -19,13 +19,14 @@ export interface Setting {
| 'object_store_config'
| 'critical_error_channels'
| 'slack_connect'
| 'smtp_connect'
storage: SettingStorage
isValid?: (value: any) => boolean
error?: string
defaultValue?: () => any
}
export type SettingStorage = 'setting' | 'config'
export type SettingStorage = 'setting'
export const settings: Record<string, Setting[]> = {
Core: [
@@ -76,7 +77,7 @@ export const settings: Record<string, Setting[]> = {
cloudonly: true,
fieldType: 'seconds',
placeholder: '60',
storage: 'config'
storage: 'setting'
},
{
label: 'License key',
@@ -188,45 +189,12 @@ export const settings: Record<string, Setting[]> = {
}
],
SMTP: [
{
label: 'Host',
key: 'smtp_host',
fieldType: 'text',
placeholder: 'smtp.gmail.com',
storage: 'config'
},
{
label: 'Port',
key: 'smtp_port',
fieldType: 'number',
placeholder: '587',
storage: 'config'
},
{
label: 'Username',
key: 'smtp_username',
fieldType: 'text',
placeholder: 'ruben@windmill.dev',
storage: 'config'
},
{
label: 'Password',
key: 'smtp_password',
fieldType: 'password',
storage: 'config'
},
{
label: 'From Address',
key: 'smtp_from',
placeholder: 'noreply@windmill.dev',
fieldType: 'email',
storage: 'config'
},
{
label: 'Implicit TLS',
key: 'smtp_tls_implicit',
fieldType: 'boolean',
storage: 'config'
{
label: 'SMTP',
key: 'smtp_settings',
fieldType: 'smtp_connect',
storage: 'setting',
ee_only: '',
}
],
Slack: [