diff --git a/.gitignore b/.gitignore index 471fe31b59..b81d183fff 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ backend/target frontend/node_modules typescript-client/node_modules frontend/.svelte-kit +backend/chrome_profiler.json diff --git a/backend/.sqlx/query-80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36.json b/backend/.sqlx/query-80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36.json deleted file mode 100644 index 5be45ac0fb..0000000000 --- a/backend/.sqlx/query-80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36" -} diff --git a/backend/.sqlx/query-e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940.json b/backend/.sqlx/query-e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940.json new file mode 100644 index 0000000000..41534f4f71 --- /dev/null +++ b/backend/.sqlx/query-e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940" +} diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 53f13b16d8..dcc993b6c5 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -37,11 +37,15 @@ Windmill uses a workspace-based architecture with multiple crates: - Update database schema with migration if necessary - Use `sqlx` for database operations with prepared statements - Use transactions for multi-step operations +- To apply pending migrations: `sqlx migrate run` (never manually run .sql files) +- **Never use `SQLX_OFFLINE=true`** — a live database is always available for compilation +- After all code changes are done, run `./update-sqlx` to regenerate the offline query cache ## Enterprise Features - Enterprise files use the `*_ee.rs` suffix -- Enterprise source is in `windmill-ee-private` folder (sibling directory), symlinked into each crate's `src/` +- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/` +- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there) - Use feature flags: `#[cfg(feature = "enterprise")]` - Isolate enterprise code in separate modules diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ebeb36ed66..3bc383bf77 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15768,7 +15768,9 @@ dependencies = [ "url", "uuid", "windmill-api", + "windmill-api-agent-workers", "windmill-api-client", + "windmill-api-settings", "windmill-autoscaling", "windmill-common", "windmill-dep-map", @@ -15781,6 +15783,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "windmill-alerting" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "serde", + "serde_json", + "sqlx", + "tracing", + "windmill-common", +] + [[package]] name = "windmill-api" version = "1.630.2" @@ -15867,13 +15882,25 @@ dependencies = [ "url", "urlencoding", "uuid", + "windmill-alerting", + "windmill-api-agent-workers", + "windmill-api-assets", "windmill-api-auth", + "windmill-api-configs", + "windmill-api-debug", "windmill-api-embeddings", + "windmill-api-flow-conversations", "windmill-api-groups", + "windmill-api-inputs", "windmill-api-jobs", + "windmill-api-npm-proxy", + "windmill-api-openapi", + "windmill-api-schedule", "windmill-api-scripts", + "windmill-api-settings", "windmill-api-sse", "windmill-api-users", + "windmill-api-workers", "windmill-api-workspaces", "windmill-audit", "windmill-autoscaling", @@ -15905,6 +15932,42 @@ dependencies = [ "windmill-worker", ] +[[package]] +name = "windmill-api-agent-workers" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "http 1.4.0", + "hyper 1.8.1", + "lazy_static", + "quick_cache", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "uuid", + "windmill-api-auth", + "windmill-common", + "windmill-parser-py-imports", + "windmill-queue", + "windmill-worker", +] + +[[package]] +name = "windmill-api-assets" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "serde", + "serde_json", + "sqlx", + "windmill-api-auth", + "windmill-common", +] + [[package]] name = "windmill-api-auth" version = "1.630.2" @@ -15941,6 +16004,46 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "windmill-api-configs" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "itertools 0.14.0", + "serde", + "serde_json", + "sqlx", + "windmill-api-auth", + "windmill-audit", + "windmill-autoscaling", + "windmill-common", + "windmill-worker", +] + +[[package]] +name = "windmill-api-debug" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "base64 0.22.1", + "chrono", + "ed25519-dalek", + "hex", + "lazy_static", + "rand 0.9.0", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "tokio", + "tracing", + "uuid", + "windmill-api-auth", + "windmill-audit", + "windmill-common", +] + [[package]] name = "windmill-api-embeddings" version = "1.630.2" @@ -15964,6 +16067,22 @@ dependencies = [ "windmill-store", ] +[[package]] +name = "windmill-api-flow-conversations" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "serde", + "sql-builder", + "sqlx", + "tokio", + "tracing", + "uuid", + "windmill-api-auth", + "windmill-common", +] + [[package]] name = "windmill-api-groups" version = "1.630.2" @@ -15984,6 +16103,20 @@ dependencies = [ "windmill-git-sync", ] +[[package]] +name = "windmill-api-inputs" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "serde", + "serde_json", + "sql-builder", + "sqlx", + "windmill-api-auth", + "windmill-common", +] + [[package]] name = "windmill-api-jobs" version = "1.630.2" @@ -16009,6 +16142,63 @@ dependencies = [ "windmill-queue", ] +[[package]] +name = "windmill-api-npm-proxy" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "flate2", + "serde", + "serde_json", + "sqlx", + "tar", + "tower-http", + "tracing", + "windmill-api-auth", + "windmill-common", +] + +[[package]] +name = "windmill-api-openapi" +version = "1.630.2" +dependencies = [ + "anyhow", + "axum 0.7.9", + "http 1.4.0", + "indexmap 2.11.1", + "itertools 0.14.0", + "lazy_static", + "serde", + "serde_json", + "serde_yml", + "sqlx", + "url", + "windmill-api-auth", + "windmill-common", + "windmill-store", + "windmill-trigger-http", +] + +[[package]] +name = "windmill-api-schedule" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "chrono-tz", + "serde", + "serde_json", + "sql-builder", + "sqlx", + "tracing", + "windmill-api-auth", + "windmill-api-settings", + "windmill-audit", + "windmill-common", + "windmill-git-sync", + "windmill-queue", +] + [[package]] name = "windmill-api-scripts" version = "1.630.2" @@ -16038,6 +16228,32 @@ dependencies = [ "windmill-queue", ] +[[package]] +name = "windmill-api-settings" +version = "1.630.2" +dependencies = [ + "anyhow", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "chrono", + "futures", + "lazy_static", + "object_store", + "regex", + "rsa", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "tokio", + "tracing", + "uuid", + "windmill-alerting", + "windmill-api-auth", + "windmill-common", +] + [[package]] name = "windmill-api-sse" version = "1.630.2" @@ -16073,6 +16289,20 @@ dependencies = [ "windmill-git-sync", ] +[[package]] +name = "windmill-api-workers" +version = "1.630.2" +dependencies = [ + "axum 0.7.9", + "chrono", + "serde", + "serde_json", + "sqlx", + "uuid", + "windmill-api-auth", + "windmill-common", +] + [[package]] name = "windmill-api-workspaces" version = "1.630.2" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index a245b55c31..37b367619d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,7 +26,19 @@ members = [ "./windmill-trigger-gcp", "./windmill-trigger-http", "./windmill-native-triggers", + "./windmill-alerting", + "./windmill-api-agent-workers", + "./windmill-api-assets", + "./windmill-api-configs", + "./windmill-api-debug", "./windmill-api-embeddings", + "./windmill-api-flow-conversations", + "./windmill-api-inputs", + "./windmill-api-npm-proxy", + "./windmill-api-openapi", + "./windmill-api-schedule", + "./windmill-api-settings", + "./windmill-api-workers", "./windmill-store", "./windmill-queue", "./windmill-worker", @@ -80,9 +92,9 @@ lto = "thin" [features] default = [] -private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"] -agent_worker_server = ["windmill-api/agent_worker_server"] -enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"] +private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"] +agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers"] +enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"] local_reports = ["windmill-common/local_reports"] enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"] stripe = ["windmill-api/stripe"] @@ -114,7 +126,7 @@ native_trigger = ["windmill-api/native_trigger"] sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"] gcp_trigger = ["windmill-api/gcp_trigger"] smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"] -license = ["windmill-api/license"] +license = ["windmill-api/license", "windmill-api-settings/license"] oauth2 = ["windmill-api/oauth2"] zip = ["windmill-api/zip"] static_frontend = ["windmill-api/static_frontend"] @@ -182,6 +194,8 @@ windmill-queue.workspace = true windmill-common = { workspace = true, default-features = false } windmill-git-sync.workspace = true windmill-api = { workspace = true, default-features = false } +windmill-api-agent-workers = { workspace = true, optional = true } +windmill-api-settings.workspace = true windmill-worker.workspace = true windmill-indexer = { workspace = true, optional = true } windmill-autoscaling = { workspace = true, optional = true } @@ -269,7 +283,19 @@ windmill-trigger-sqs = { path = "./windmill-trigger-sqs" } windmill-trigger-gcp = { path = "./windmill-trigger-gcp" } windmill-trigger-http = { path = "./windmill-trigger-http" } windmill-native-triggers = { path = "./windmill-native-triggers" } +windmill-alerting = { path = "./windmill-alerting" } +windmill-api-agent-workers = { path = "./windmill-api-agent-workers" } +windmill-api-assets = { path = "./windmill-api-assets" } +windmill-api-configs = { path = "./windmill-api-configs" } +windmill-api-debug = { path = "./windmill-api-debug" } windmill-api-embeddings = { path = "./windmill-api-embeddings" } +windmill-api-flow-conversations = { path = "./windmill-api-flow-conversations" } +windmill-api-inputs = { path = "./windmill-api-inputs" } +windmill-api-npm-proxy = { path = "./windmill-api-npm-proxy" } +windmill-api-openapi = { path = "./windmill-api-openapi" } +windmill-api-schedule = { path = "./windmill-api-schedule" } +windmill-api-settings = { path = "./windmill-api-settings" } +windmill-api-workers = { path = "./windmill-api-workers" } windmill-store = { path = "./windmill-store" } windmill-parser = { path = "./parsers/windmill-parser" } windmill-parser-ts = { path = "./parsers/windmill-parser-ts" } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index ac93a62720..c3e5ea69bb 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -0eaf364f5421db5710318eb36c062114ab6ca081 \ No newline at end of file +b2bd490cd14e7794bebbd8737c2833ea671370b3 \ No newline at end of file diff --git a/backend/tests/agent_workers.rs b/backend/tests/agent_workers.rs new file mode 100644 index 0000000000..26c4003212 --- /dev/null +++ b/backend/tests/agent_workers.rs @@ -0,0 +1,269 @@ +#![cfg(all(feature = "private", feature = "agent_worker_server"))] + +mod common; + +use common::*; +use serde_json::json; +use sqlx::{Pool, Postgres}; +use windmill_common::{ + jobs::{JobPayload, RawCode}, + scripts::ScriptLang, +}; + +fn bun_code(code: &str) -> RawCode { + RawCode { + hash: None, + content: code.to_string(), + path: None, + language: ScriptLang::Bun, + lock: None, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: + windmill_common::runnable_settings::ConcurrencySettings::default().into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + } +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_simple_script(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + let result = RunJob::from(JobPayload::Code(bun_code( + "export function main() { return 42; }", + ))) + .run_until_complete(&db, false, port) + .await; + + assert!(result.success, "job should succeed"); + assert_eq!(result.json_result(), Some(json!(42))); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_script_with_args(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + let result = RunJob::from(JobPayload::Code(bun_code( + "export function main(x: number, y: number) { return x + y; }", + ))) + .arg("x", json!(10)) + .arg("y", json!(32)) + .run_until_complete(&db, false, port) + .await; + + assert!(result.success, "job should succeed"); + assert_eq!(result.json_result(), Some(json!(42))); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_script_with_logs(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + let result = RunJob::from(JobPayload::Code(bun_code( + r#"export function main() { + console.log("hello from agent worker"); + console.log("processing step 1"); + console.log("processing step 2"); + return "done"; +}"#, + ))) + .run_until_complete(&db, false, port) + .await; + + assert!(result.success, "job should succeed"); + assert_eq!(result.json_result(), Some(json!("done"))); + + let logs = sqlx::query_scalar::<_, String>( + "SELECT logs FROM job_logs WHERE job_id = $1 AND workspace_id = 'test-workspace'", + ) + .bind(result.id) + .fetch_optional(&db) + .await?; + + let logs = logs.expect("logs should exist"); + assert!( + logs.contains("hello from agent worker"), + "logs should contain the printed output, got: {logs}" + ); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_script_failure(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + let result = RunJob::from(JobPayload::Code(bun_code( + "export function main() { throw new Error('test error'); }", + ))) + .run_until_complete(&db, false, port) + .await; + + assert!(!result.success, "job should fail"); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_complex_result(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + let result = RunJob::from(JobPayload::Code(bun_code( + r#"export function main() { + return { + items: [1, 2, 3], + metadata: { key: "value" }, + count: 3, + }; +}"#, + ))) + .run_until_complete(&db, false, port) + .await; + + assert!(result.success, "job should succeed"); + let json = result.json_result().unwrap(); + assert_eq!(json["items"], json!([1, 2, 3])); + assert_eq!(json["metadata"]["key"], json!("value")); + assert_eq!(json["count"], json!(3)); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_token_creation(db: Pool) -> anyhow::Result<()> { + let (client, _port, _server) = init_client_agent_mode(db.clone()).await; + + // client.baseurl() already includes /api + let resp = client + .client() + .post(format!( + "{}/agent_workers/create_agent_token", + client.baseurl() + )) + .json(&json!({ + "worker_group": "lifecycle-test", + "tags": ["bun", "flow", "dependency"], + "exp": usize::MAX + })) + .send() + .await?; + + assert!( + resp.status().is_success(), + "create_agent_token should succeed, got: {}", + resp.status() + ); + + let token = resp.text().await?; + let token = token.trim_matches('"'); + assert!( + token.starts_with("jwt_agent_"), + "token should start with jwt_agent_ prefix, got: {token}" + ); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_token_and_ping(db: Pool) -> anyhow::Result<()> { + let (client, port, _server) = init_client_agent_mode(db.clone()).await; + + let resp = client + .client() + .post(format!( + "{}/agent_workers/create_agent_token", + client.baseurl() + )) + .json(&json!({ + "worker_group": "lifecycle-test", + "tags": ["bun", "flow", "dependency"], + "exp": usize::MAX + })) + .send() + .await?; + + assert!(resp.status().is_success()); + let token = resp.text().await?; + let token = token.trim_matches('"'); + + let suffix = windmill_common::utils::create_default_worker_suffix("lifecycle-test"); + let base_url = format!("http://localhost:{port}"); + let http_client = + windmill_common::agent_workers::build_agent_http_client(&suffix, &token, &base_url); + + // Initial ping inserts the worker record into the database + let resp = http_client + .client + .post(format!("{}/api/agent_workers/update_ping", base_url)) + .json(&json!({ + "worker_instance": "test-instance", + "ip": "127.0.0.1", + "tags": ["bun"], + "version": "test", + "vcpus": 4, + "memory": 8192, + "ping_type": "Initial" + })) + .send() + .await?; + + assert!( + resp.status().is_success(), + "initial ping should succeed, got: {}", + resp.status() + ); + + // Verify the ping was recorded in the database + let worker_count = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM worker_ping WHERE worker_instance = 'test-instance'", + ) + .fetch_one(&db) + .await?; + + assert!(worker_count > 0, "worker ping should be recorded in database"); + + // MainLoop ping updates the existing record + let resp = http_client + .client + .post(format!("{}/api/agent_workers/update_ping", base_url)) + .json(&json!({ + "tags": ["bun"], + "vcpus": 4, + "memory": 8192, + "jobs_executed": 0, + "ping_type": "MainLoop" + })) + .send() + .await?; + + assert!( + resp.status().is_success(), + "main loop ping should succeed, got: {}", + resp.status() + ); + + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_agent_worker_multiple_jobs_sequential(db: Pool) -> anyhow::Result<()> { + let (_client, port, _server) = init_client_agent_mode(db.clone()).await; + + for i in 0..3 { + let result = RunJob::from(JobPayload::Code(bun_code(&format!( + "export function main() {{ return {i}; }}" + )))) + .run_until_complete(&db, false, port) + .await; + + assert!(result.success, "job {i} should succeed"); + assert_eq!(result.json_result(), Some(json!(i))); + } + + Ok(()) +} diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 17e190fcb2..e377df5164 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -838,7 +838,7 @@ pub async fn testing_http_connection(port: u16) -> Connection { "{}{}", windmill_common::agent_workers::AGENT_JWT_PREFIX, windmill_common::jwt::encode_with_internal_secret( - windmill_api::agent_workers_ee::AgentAuth { + windmill_api_agent_workers::AgentAuth { worker_group: "testing-agent".to_owned(), suffix: Some(suffix.clone()), tags: vec!["flow".into(), "python3".into(), "dependency".into()], diff --git a/backend/windmill-alerting/Cargo.toml b/backend/windmill-alerting/Cargo.toml new file mode 100644 index 0000000000..6f7352426e --- /dev/null +++ b/backend/windmill-alerting/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "windmill-alerting" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_alerting" +path = "src/lib.rs" + +[dependencies] +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +chrono.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +tracing.workspace = true diff --git a/backend/windmill-alerting/src/lib.rs b/backend/windmill-alerting/src/lib.rs new file mode 100644 index 0000000000..f653a99c26 --- /dev/null +++ b/backend/windmill-alerting/src/lib.rs @@ -0,0 +1,208 @@ +use axum::Json; +use serde::Deserialize; + +use windmill_common::error::{self, JsonResult}; +use windmill_common::worker::CLOUD_HOSTED; +use windmill_common::DB; + +#[derive(serde::Serialize)] +pub struct CriticalAlert { + id: i32, + alert_type: String, + message: String, + created_at: chrono::DateTime, + acknowledged: Option, + workspace_id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct AlertQueryParams { + pub page: Option, + pub page_size: Option, + pub acknowledged: Option, +} + +pub async fn get_critical_alerts( + db: DB, + params: AlertQueryParams, + workspace_id: Option, +) -> JsonResult { + let page = params.page.unwrap_or(1).max(1); + let page_size = params.page_size.unwrap_or(10).min(100) as i64; + let offset = ((page - 1) * page_size as i32) as i64; + + let total_rows = if let Some(workspace_id) = &workspace_id { + if params.acknowledged.is_none() { + sqlx::query_scalar!( + "SELECT COUNT(*) + FROM alerts + WHERE workspace_id = $1", + workspace_id + ) + .fetch_one(&db) + .await? + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) + FROM alerts + WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2", + workspace_id, + params.acknowledged + ) + .fetch_one(&db) + .await? + } + } else { + if params.acknowledged.is_none() { + sqlx::query_scalar!( + "SELECT COUNT(*) + FROM alerts" + ) + .fetch_one(&db) + .await? + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) + FROM alerts + WHERE COALESCE(acknowledged, false) = $1", + params.acknowledged + ) + .fetch_one(&db) + .await? + } + }; + + let alerts = if let Some(workspace_id) = workspace_id { + if params.acknowledged.is_none() { + sqlx::query_as!( + CriticalAlert, + "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id + FROM alerts + WHERE workspace_id = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3", + workspace_id, + page_size, + offset + ) + .fetch_all(&db) + .await? + } else { + sqlx::query_as!( + CriticalAlert, + "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id + FROM alerts + WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2 + ORDER BY created_at DESC + LIMIT $3 OFFSET $4", + workspace_id, + params.acknowledged, + page_size, + offset + ) + .fetch_all(&db) + .await? + } + } else { + if params.acknowledged.is_none() { + sqlx::query_as!( + CriticalAlert, + "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id + FROM alerts + ORDER BY created_at DESC + LIMIT $1 OFFSET $2", + page_size, + offset + ) + .fetch_all(&db) + .await? + } else { + sqlx::query_as!( + CriticalAlert, + "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id + FROM alerts + WHERE COALESCE(acknowledged, false) = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3", + params.acknowledged, + page_size, + offset + ) + .fetch_all(&db) + .await? + } + }; + + let total_rows = total_rows.unwrap_or(0); + let total_pages = ((total_rows as f64) / (page_size as f64)).ceil() as i64; + + Ok(Json(serde_json::json!({ + "alerts": alerts, + "total_rows": total_rows, + "total_pages": total_pages + }))) +} + +pub async fn acknowledge_critical_alert( + db: DB, + workspace_id: Option, + id: i32, +) -> error::Result { + sqlx::query!( + "UPDATE alerts + SET + acknowledged = true, + acknowledged_workspace = CASE + WHEN $3 THEN + CASE + WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true + ELSE acknowledged_workspace + END + ELSE true + END + WHERE id = $1", + id, + workspace_id, + *CLOUD_HOSTED + ) + .execute(&db) + .await?; + + tracing::info!( + "Acknowledged critical alert with id: {}{}", + id, + workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w)) + ); + Ok("Critical alert acknowledged".to_string()) +} + +pub async fn acknowledge_all_critical_alerts( + db: DB, + workspace_id: Option, +) -> error::Result { + sqlx::query!( + "UPDATE alerts + SET + acknowledged = true, + acknowledged_workspace = CASE + WHEN $2 THEN + CASE + WHEN $1::text IS NOT NULL THEN true + ELSE acknowledged_workspace + END + ELSE true + END + WHERE ($1::text IS NOT NULL AND workspace_id = $1) + OR ($1::text IS NULL)", + workspace_id, + *CLOUD_HOSTED + ) + .execute(&db) + .await?; + + tracing::info!( + "Acknowledged all unacknowledged critical alerts{}", + workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w)) + ); + Ok("All unacknowledged critical alerts acknowledged".to_string()) +} diff --git a/backend/windmill-api-agent-workers/Cargo.toml b/backend/windmill-api-agent-workers/Cargo.toml new file mode 100644 index 0000000000..667c592e47 --- /dev/null +++ b/backend/windmill-api-agent-workers/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "windmill-api-agent-workers" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_agent_workers" +path = "src/lib.rs" + +[features] +default = [] +enterprise = ["windmill-common/enterprise", "windmill-queue/enterprise"] +private = ["windmill-common/private", "windmill-queue/private"] +python = ["dep:windmill-parser-py-imports"] +benchmark = [] + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-queue.workspace = true +windmill-worker.workspace = true +axum.workspace = true +lazy_static.workspace = true +chrono.workspace = true +http.workspace = true +hyper.workspace = true +quick_cache.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true +windmill-parser-py-imports = { workspace = true, optional = true } diff --git a/backend/windmill-api/src/agent_workers_oss.rs b/backend/windmill-api-agent-workers/src/lib.rs similarity index 94% rename from backend/windmill-api/src/agent_workers_oss.rs rename to backend/windmill-api-agent-workers/src/lib.rs index 1459efef1d..d2913a217c 100644 --- a/backend/windmill-api/src/agent_workers_oss.rs +++ b/backend/windmill-api-agent-workers/src/lib.rs @@ -1,6 +1,8 @@ #[cfg(feature = "private")] +mod ee; +#[cfg(feature = "private")] #[allow(unused)] -pub use crate::agent_workers_ee::*; +pub use ee::*; /* * Author: Ruben Fiszel @@ -11,7 +13,7 @@ pub use crate::agent_workers_ee::*; */ #[cfg(not(feature = "private"))] -use crate::db::DB; +use windmill_common::DB; #[cfg(not(feature = "private"))] use axum::Router; diff --git a/backend/windmill-api-assets/Cargo.toml b/backend/windmill-api-assets/Cargo.toml new file mode 100644 index 0000000000..1ea16c3f0d --- /dev/null +++ b/backend/windmill-api-assets/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "windmill-api-assets" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_assets" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +chrono.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true diff --git a/backend/windmill-api/src/assets.rs b/backend/windmill-api-assets/src/lib.rs similarity index 99% rename from backend/windmill-api/src/assets.rs rename to backend/windmill-api-assets/src/lib.rs index 8e8afc7036..368bf98a26 100644 --- a/backend/windmill-api/src/assets.rs +++ b/backend/windmill-api-assets/src/lib.rs @@ -12,7 +12,7 @@ use windmill_common::{ error::JsonResult, }; -use crate::db::ApiAuthed; +use windmill_api_auth::ApiAuthed; pub fn workspaced_service() -> Router { Router::new() diff --git a/backend/windmill-api-configs/Cargo.toml b/backend/windmill-api-configs/Cargo.toml new file mode 100644 index 0000000000..bfb702c73b --- /dev/null +++ b/backend/windmill-api-configs/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "windmill-api-configs" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_configs" +path = "src/lib.rs" + +[features] +default = [] +enterprise = ["dep:windmill-autoscaling"] +private = [] +python = [] +inline_preview = ["dep:windmill-worker", "dep:itertools"] + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-audit.workspace = true +windmill-autoscaling = { workspace = true, optional = true } +windmill-worker = { workspace = true, optional = true } +axum.workspace = true +chrono.workspace = true +itertools = { workspace = true, optional = true } +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true diff --git a/backend/windmill-api/src/configs.rs b/backend/windmill-api-configs/src/lib.rs similarity index 99% rename from backend/windmill-api/src/configs.rs rename to backend/windmill-api-configs/src/lib.rs index 1d149a08b5..35ace8d6a3 100644 --- a/backend/windmill-api/src/configs.rs +++ b/backend/windmill-api-configs/src/lib.rs @@ -23,7 +23,7 @@ use windmill_common::{ DB, }; -use crate::{db::ApiAuthed, utils::require_devops_role}; +use windmill_api_auth::{ApiAuthed, require_devops_role}; pub fn global_service() -> Router { Router::new() diff --git a/backend/windmill-api-debug/Cargo.toml b/backend/windmill-api-debug/Cargo.toml new file mode 100644 index 0000000000..de94a3aba3 --- /dev/null +++ b/backend/windmill-api-debug/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "windmill-api-debug" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_debug" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-audit.workspace = true +axum.workspace = true +base64.workspace = true +chrono.workspace = true +ed25519-dalek.workspace = true +hex.workspace = true +lazy_static.workspace = true +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +sqlx.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true diff --git a/backend/windmill-api/src/debug.rs b/backend/windmill-api-debug/src/lib.rs similarity index 99% rename from backend/windmill-api/src/debug.rs rename to backend/windmill-api-debug/src/lib.rs index bdf769833d..4f793e7da7 100644 --- a/backend/windmill-api/src/debug.rs +++ b/backend/windmill-api-debug/src/lib.rs @@ -41,7 +41,7 @@ use windmill_common::{ users::username_to_permissioned_as, }; -use crate::db::ApiAuthed; +use windmill_api_auth::ApiAuthed; /// TTL for debug tokens in seconds (60 seconds) pub const DEBUG_TOKEN_TTL_SECS: i64 = 60; diff --git a/backend/windmill-api-embeddings/src/lib.rs b/backend/windmill-api-embeddings/src/lib.rs index 1225f789c3..1d16facfff 100644 --- a/backend/windmill-api-embeddings/src/lib.rs +++ b/backend/windmill-api-embeddings/src/lib.rs @@ -6,6 +6,8 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc}; use windmill_common::DEFAULT_HUB_BASE_URL; #[cfg(feature = "embedding")] use windmill_common::HUB_BASE_URL; +#[cfg(feature = "embedding")] +use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; use axum::Router; @@ -54,13 +56,6 @@ lazy_static::lazy_static! { pub static ref EMBEDDINGS_DB: Arc>> = Arc::new(RwLock::new(None)); pub static ref MODEL_INSTANCE: Arc>>> = Arc::new(RwLock::new(None)); pub static ref HUB_EMBEDDINGS_PULLING_INTERVAL_SECS: u64 = std::env::var("HUB_EMBEDDINGS_PULLING_INTERVAL_SECS").ok().map(|x| x.parse::().ok()).flatten().unwrap_or(3600 * 24); - // Match windmill-api's HTTP_CLIENT config: 30s timeout, ACCEPT_INVALID_CERTS support - static ref HTTP_CLIENT: reqwest::Client = windmill_common::utils::configure_client(reqwest::ClientBuilder::new() - .user_agent("windmill/beta") - .connect_timeout(std::time::Duration::from_secs(10)) - .timeout(std::time::Duration::from_secs(30)) - .danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())) - .build().unwrap(); } #[cfg(feature = "embedding")] diff --git a/backend/windmill-api-flow-conversations/Cargo.toml b/backend/windmill-api-flow-conversations/Cargo.toml new file mode 100644 index 0000000000..dfb927e92e --- /dev/null +++ b/backend/windmill-api-flow-conversations/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "windmill-api-flow-conversations" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_flow_conversations" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +chrono.workspace = true +serde.workspace = true +sql-builder.workspace = true +sqlx.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true diff --git a/backend/windmill-api/src/flow_conversations.rs b/backend/windmill-api-flow-conversations/src/lib.rs similarity index 99% rename from backend/windmill-api/src/flow_conversations.rs rename to backend/windmill-api-flow-conversations/src/lib.rs index 6f3b879ff7..bc37c9863d 100644 --- a/backend/windmill-api/src/flow_conversations.rs +++ b/backend/windmill-api-flow-conversations/src/lib.rs @@ -9,7 +9,7 @@ use sql_builder::prelude::*; use sqlx::{FromRow, Postgres}; use uuid::Uuid; -use crate::db::ApiAuthed; +use windmill_api_auth::ApiAuthed; pub use windmill_common::flow_conversations::FlowConversation; use windmill_common::{ db::{UserDB, DB}, diff --git a/backend/windmill-api-inputs/Cargo.toml b/backend/windmill-api-inputs/Cargo.toml new file mode 100644 index 0000000000..30d3f0d9f5 --- /dev/null +++ b/backend/windmill-api-inputs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "windmill-api-inputs" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_inputs" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +chrono.workspace = true +serde.workspace = true +serde_json.workspace = true +sql-builder.workspace = true +sqlx.workspace = true diff --git a/backend/windmill-api/src/inputs.rs b/backend/windmill-api-inputs/src/lib.rs similarity index 99% rename from backend/windmill-api/src/inputs.rs rename to backend/windmill-api-inputs/src/lib.rs index 9894275737..501d28d1db 100644 --- a/backend/windmill-api/src/inputs.rs +++ b/backend/windmill-api-inputs/src/lib.rs @@ -6,7 +6,7 @@ * LICENSE-AGPL for a copy of the license. */ -use crate::db::ApiAuthed; +use windmill_api_auth::ApiAuthed; use axum::{ extract::{Path, Query}, routing::{get, post}, diff --git a/backend/windmill-api-npm-proxy/Cargo.toml b/backend/windmill-api-npm-proxy/Cargo.toml new file mode 100644 index 0000000000..c5852090ce --- /dev/null +++ b/backend/windmill-api-npm-proxy/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "windmill-api-npm-proxy" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_npm_proxy" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +flate2.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +tar.workspace = true +tower-http.workspace = true +tracing.workspace = true diff --git a/backend/windmill-api/src/npm_proxy.rs b/backend/windmill-api-npm-proxy/src/lib.rs similarity index 99% rename from backend/windmill-api/src/npm_proxy.rs rename to backend/windmill-api-npm-proxy/src/lib.rs index 02f8a3dbb8..6bcffe0d0a 100644 --- a/backend/windmill-api/src/npm_proxy.rs +++ b/backend/windmill-api-npm-proxy/src/lib.rs @@ -18,7 +18,8 @@ use windmill_common::{ utils::StripPath, }; -use crate::{db::ApiAuthed, HTTP_CLIENT}; +use windmill_api_auth::ApiAuthed; +use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; #[derive(Deserialize)] struct ProxyQuery { diff --git a/backend/windmill-api-openapi/Cargo.toml b/backend/windmill-api-openapi/Cargo.toml new file mode 100644 index 0000000000..efba2aad44 --- /dev/null +++ b/backend/windmill-api-openapi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "windmill-api-openapi" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_openapi" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-store.workspace = true +windmill-trigger-http.workspace = true +anyhow.workspace = true +axum.workspace = true +http.workspace = true +indexmap.workspace = true +itertools.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yml.workspace = true +sqlx.workspace = true +url.workspace = true diff --git a/backend/windmill-api/src/openapi.rs b/backend/windmill-api-openapi/src/lib.rs similarity index 99% rename from backend/windmill-api/src/openapi.rs rename to backend/windmill-api-openapi/src/lib.rs index c564836b81..b4ba40ca79 100644 --- a/backend/windmill-api/src/openapi.rs +++ b/backend/windmill-api-openapi/src/lib.rs @@ -22,11 +22,9 @@ use windmill_common::{ }; use windmill_store::resources::try_get_resource_from_db_as; -use crate::{ - db::ApiAuthed, - triggers::http::{ - http_trigger_auth::ApiKeyAuthentication, AuthenticationMethod, HttpMethod, RequestType, - }, +use windmill_api_auth::ApiAuthed; +use windmill_trigger_http::{ + http_trigger_auth::ApiKeyAuthentication, AuthenticationMethod, HttpMethod, RequestType, }; lazy_static::lazy_static! { diff --git a/backend/windmill-api-schedule/Cargo.toml b/backend/windmill-api-schedule/Cargo.toml new file mode 100644 index 0000000000..5017985820 --- /dev/null +++ b/backend/windmill-api-schedule/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "windmill-api-schedule" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_schedule" +path = "src/lib.rs" + +[features] +default = [] +enterprise = [] + +[dependencies] +windmill-api-auth.workspace = true +windmill-api-settings.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-audit.workspace = true +windmill-git-sync.workspace = true +windmill-queue.workspace = true +axum.workspace = true +chrono.workspace = true +chrono-tz.workspace = true +serde.workspace = true +serde_json.workspace = true +sql-builder.workspace = true +sqlx.workspace = true +tracing.workspace = true diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api-schedule/src/lib.rs similarity index 99% rename from backend/windmill-api/src/schedule.rs rename to backend/windmill-api-schedule/src/lib.rs index 0ea5f9e054..bc01a7525c 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api-schedule/src/lib.rs @@ -6,12 +6,8 @@ * LICENSE-AGPL for a copy of the license. */ -use crate::{ - db::{ApiAuthed, DB}, - settings::{delete_global_setting, set_global_setting_internal}, - users::maybe_refresh_folders, - utils::{check_scopes, require_super_admin}, -}; +use windmill_api_auth::{check_scopes, maybe_refresh_folders, require_super_admin, ApiAuthed}; +use windmill_common::DB; use axum::{ extract::{Extension, Path, Query}, routing::{delete, get, post}, @@ -922,9 +918,9 @@ async fn set_default_error_handler( }; if let Some(value_content) = value { - set_global_setting_internal(&db, key, value_content).await?; + windmill_api_settings::set_global_setting_internal(&db, key, value_content).await?; } else { - delete_global_setting(&db, key.as_str()).await?; + windmill_api_settings::delete_global_setting(&db, key.as_str()).await?; } if payload.override_existing { diff --git a/backend/windmill-api-settings/Cargo.toml b/backend/windmill-api-settings/Cargo.toml new file mode 100644 index 0000000000..0a79246175 --- /dev/null +++ b/backend/windmill-api-settings/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "windmill-api-settings" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_settings" +path = "src/lib.rs" + +[features] +default = [] +enterprise = [] +private = ["windmill-common/private"] +parquet = ["dep:object_store", "windmill-common/parquet"] +license = ["dep:rsa"] + +[dependencies] +windmill-alerting.workspace = true +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +anyhow.workspace = true +bytes.workspace = true +chrono.workspace = true +futures.workspace = true +lazy_static.workspace = true +regex.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true +base64.workspace = true +sha2.workspace = true +rsa = { workspace = true, optional = true } +object_store = { workspace = true, optional = true } diff --git a/backend/windmill-api-settings/src/ee_oss.rs b/backend/windmill-api-settings/src/ee_oss.rs new file mode 100644 index 0000000000..d0e0f69b62 --- /dev/null +++ b/backend/windmill-api-settings/src/ee_oss.rs @@ -0,0 +1,14 @@ +#[cfg(feature = "private")] +#[allow(unused)] +pub use crate::ee::*; + +#[cfg(not(feature = "private"))] +use anyhow::anyhow; +#[cfg(not(feature = "private"))] +pub async fn validate_license_key( + _license_key: String, + _db: Option<&windmill_common::DB>, +) -> anyhow::Result<(String, bool)> { + // Implementation is not open source + Err(anyhow!("License can't be validated in Windmill CE")) +} diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api-settings/src/lib.rs similarity index 98% rename from backend/windmill-api/src/settings.rs rename to backend/windmill-api-settings/src/lib.rs index d68797f7ee..de234e33c2 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -8,12 +8,18 @@ use std::{collections::HashMap, time::Duration}; -use crate::{ - db::{ApiAuthed, DB}, - ee_oss::validate_license_key, - utils::{generate_instance_username_for_all_users, require_super_admin}, - HTTP_CLIENT, -}; +#[cfg(feature = "private")] +mod ee; +pub mod ee_oss; + +use windmill_api_auth::{require_super_admin, ApiAuthed}; +#[cfg(feature = "enterprise")] +use windmill_api_auth::require_devops_role; +use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; +use windmill_common::DB; + +use ee_oss::validate_license_key; +use windmill_common::usernames::generate_instance_username_for_all_users; use axum::{ extract::{Extension, Path}, @@ -25,9 +31,6 @@ use axum::{ use axum::extract::Query; use serde_json::json; -#[cfg(feature = "enterprise")] -use crate::utils::require_devops_role; - use serde::{Deserialize, Serialize}; #[cfg(feature = "enterprise")] use windmill_common::ee_oss::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel}; @@ -575,11 +578,11 @@ pub async fn test_critical_channels() -> Result { pub async fn get_critical_alerts( Extension(db): Extension, authed: ApiAuthed, - Query(params): Query, + Query(params): Query, ) -> JsonResult { require_devops_role(&db, &authed.email).await?; - crate::utils::get_critical_alerts(db, params, None).await + windmill_alerting::get_critical_alerts(db, params, None).await } #[cfg(not(feature = "enterprise"))] @@ -594,7 +597,7 @@ pub async fn acknowledge_critical_alert( Path(id): Path, ) -> error::Result { require_devops_role(&db, &authed.email).await?; - crate::utils::acknowledge_critical_alert(db, None, id).await + windmill_alerting::acknowledge_critical_alert(db, None, id).await } #[cfg(not(feature = "enterprise"))] @@ -609,7 +612,7 @@ pub async fn acknowledge_all_critical_alerts( ) -> error::Result { require_super_admin(&db, &authed.email).await?; - crate::utils::acknowledge_all_critical_alerts(db, None).await + windmill_alerting::acknowledge_all_critical_alerts(db, None).await } #[cfg(not(feature = "enterprise"))] diff --git a/backend/windmill-api-workers/Cargo.toml b/backend/windmill-api-workers/Cargo.toml new file mode 100644 index 0000000000..8e54d84e77 --- /dev/null +++ b/backend/windmill-api-workers/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "windmill-api-workers" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_workers" +path = "src/lib.rs" + +[dependencies] +windmill-api-auth.workspace = true +windmill-common = { workspace = true, default-features = false } +axum.workspace = true +chrono.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +uuid.workspace = true diff --git a/backend/windmill-api/src/workers.rs b/backend/windmill-api-workers/src/lib.rs similarity index 99% rename from backend/windmill-api/src/workers.rs rename to backend/windmill-api-workers/src/lib.rs index dc2480ce15..93c9deae84 100644 --- a/backend/windmill-api/src/workers.rs +++ b/backend/windmill-api-workers/src/lib.rs @@ -24,7 +24,7 @@ use windmill_common::{ DB, }; -use crate::{db::ApiAuthed, utils::require_super_admin}; +use windmill_api_auth::{ApiAuthed, require_super_admin}; pub fn global_service() -> Router { Router::new() diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index bbf1492d84..bb08985003 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -10,15 +10,15 @@ path = "src/lib.rs" [features] default = [] -private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-email?/private"] -enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"] +private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-email?/private"] +enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"] stripe = [] -inline_preview = ["dep:windmill-worker"] -agent_worker_server = ["dep:windmill-worker"] +inline_preview = ["dep:windmill-worker", "windmill-api-configs/inline_preview"] +agent_worker_server = ["dep:windmill-worker", "dep:windmill-api-agent-workers"] enterprise_saml = ["dep:samael", "dep:libxml"] benchmark = [] embedding = ["windmill-api-embeddings/embedding"] -parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker?/parquet", "windmill-api-users/parquet", "dep:aws-sigv4", "dep:aws-sdk-config"] +parquet = ["dep:datafusion", "dep:object_store", "windmill-common/parquet", "windmill-worker?/parquet", "windmill-api-users/parquet", "windmill-api-settings/parquet", "dep:aws-sigv4", "dep:aws-sdk-config"] prometheus = ["windmill-common/prometheus", "windmill-queue/prometheus", "dep:prometheus", "windmill-worker?/prometheus"] openidconnect = ["dep:openidconnect", "windmill-common/openidconnect", "windmill-store/openidconnect"] tantivy = ["dep:windmill-indexer"] @@ -27,10 +27,10 @@ kafka-gssapi = ["kafka", "windmill-trigger-kafka/kafka-gssapi"] nats = ["dep:windmill-trigger-nats", "windmill-store/nats"] websocket = ["dep:windmill-trigger-websocket"] smtp = ["dep:mail-parser", "dep:openssl", "windmill-common/smtp", "dep:windmill-trigger-email"] -license = ["dep:rsa"] +license = ["dep:rsa", "windmill-api-settings/license"] zip = ["dep:async_zip"] oauth2 = ["dep:windmill-oauth", "windmill-store/oauth2"] -http_trigger = ["dep:matchit", "dep:windmill-trigger-http", "windmill-store/http_trigger"] +http_trigger = ["dep:matchit", "dep:windmill-trigger-http", "windmill-store/http_trigger", "dep:windmill-api-openapi"] static_frontend = ["dep:rust-embed"] postgres_trigger = ["dep:windmill-trigger-postgres", "windmill-store/postgres_trigger"] mqtt_trigger = ["dep:windmill-trigger-mqtt", "windmill-store/mqtt_trigger"] @@ -40,7 +40,7 @@ gcp_trigger = ["dep:windmill-trigger-gcp", "windmill-store/gcp_trigger"] cloud = ["windmill-common/cloud", "windmill-api-auth/cloud", "windmill-store/cloud", "windmill-api-workspaces/cloud"] mcp = ["dep:windmill-mcp", "windmill-mcp/server", "windmill-mcp/auth", "windmill-api-auth/mcp", "windmill-store/mcp"] bedrock = ["dep:aws-sdk-bedrock", "dep:aws-sdk-bedrockruntime", "windmill-common/bedrock", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-types"] -python = ["windmill-dep-map/python", "dep:windmill-parser-py", "dep:windmill-parser-py-imports", "windmill-api-scripts/python", "windmill-trigger/python", "windmill-common/python"] +python = ["windmill-dep-map/python", "dep:windmill-parser-py", "dep:windmill-parser-py-imports", "windmill-api-scripts/python", "windmill-api-configs/python", "windmill-api-agent-workers?/python", "windmill-trigger/python", "windmill-common/python"] no_auth = ["windmill-api-auth/no_auth", "windmill-store/no_auth", "windmill-api-users/no_auth"] quickjs = ["windmill-jseval/quickjs"] @@ -142,7 +142,19 @@ windmill-trigger-sqs = { workspace = true, optional = true } windmill-trigger-gcp = { workspace = true, optional = true } windmill-trigger-http = { workspace = true, optional = true } windmill-native-triggers = { workspace = true, optional = true } +windmill-alerting.workspace = true +windmill-api-agent-workers = { workspace = true, optional = true } +windmill-api-assets.workspace = true +windmill-api-configs = { workspace = true } +windmill-api-debug.workspace = true windmill-api-embeddings.workspace = true +windmill-api-flow-conversations.workspace = true +windmill-api-inputs.workspace = true +windmill-api-npm-proxy.workspace = true +windmill-api-openapi = { workspace = true, optional = true } +windmill-api-schedule.workspace = true +windmill-api-settings = { workspace = true } +windmill-api-workers.workspace = true const_format.workspace = true pin-project.workspace = true http.workspace = true diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index af392b54b3..3d544bec11 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -29,7 +29,7 @@ use crate::{ webhook_util::WebhookShared, }; #[cfg(feature = "agent_worker_server")] -use agent_workers_oss::AgentCache; +use windmill_api_agent_workers::AgentCache; use anyhow::Context; use argon2::Argon2; @@ -39,7 +39,6 @@ use axum::http::HeaderValue; use axum::response::Response; use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Json, Router}; use db::DB; -use reqwest::Client; use tokio::task::JoinHandle; use windmill_common::global_settings::load_value_from_global_settings; use windmill_common::global_settings::EMAIL_DOMAIN_SETTING; @@ -60,21 +59,16 @@ use windmill_common::worker::CLOUD_HOSTED; #[allow(unused_imports)] pub(crate) use windmill_common::BASE_URL; use windmill_common::{ - utils::{configure_client, GIT_VERSION}, + utils::GIT_VERSION, INSTANCE_NAME, }; use crate::scim_oss::has_scim_token; use windmill_common::error::AppError; -#[cfg(all(feature = "agent_worker_server", feature = "private"))] -pub mod agent_workers_ee; -#[cfg(feature = "agent_worker_server")] -mod agent_workers_oss; mod ai; mod apps; pub mod args; -mod assets; mod audit; pub mod auth; #[cfg(all(feature = "private", feature = "parquet"))] @@ -84,16 +78,14 @@ mod azure_proxy_oss; mod bedrock; mod capture; mod concurrency_groups; -mod configs; mod db; -pub mod debug; + mod drafts; #[cfg(feature = "private")] pub mod ee; pub mod ee_oss; pub mod embeddings; mod favorite; -mod flow_conversations; pub mod flows; mod folder_history; mod folders; @@ -106,12 +98,8 @@ mod indexer_oss; #[cfg(feature = "private")] mod inkeep_ee; mod inkeep_oss; -mod inputs; mod integration; mod live_migrations; -mod npm_proxy; -#[cfg(feature = "http_trigger")] -mod openapi; #[cfg(all(feature = "private", feature = "parquet"))] pub mod s3_proxy_ee; mod s3_proxy_oss; @@ -145,14 +133,12 @@ mod resources; #[cfg(feature = "private")] pub mod saml_ee; mod saml_oss; -mod schedule; #[cfg(feature = "private")] pub mod scim_ee; mod scim_oss; mod scripts; mod secret_backend_ext; mod service_logs; -mod settings; mod slack_approvals; #[cfg(all(feature = "smtp", feature = "private"))] pub mod smtp_server_ee; @@ -187,7 +173,6 @@ mod users_oss; mod utils; mod variables; pub mod webhook_util; -mod workers; mod workspaces; #[cfg(feature = "private")] pub mod workspaces_ee; @@ -215,17 +200,10 @@ lazy_static::lazy_static! { // COOKIE_DOMAIN and IS_SECURE are now in windmill_common::utils - - pub static ref HTTP_CLIENT: Client = configure_client(reqwest::ClientBuilder::new() - .user_agent("windmill/beta") - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) - .danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())) - .build().unwrap(); - - } +pub use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; + pub use windmill_common::utils::{COOKIE_DOMAIN, IS_SECURE}; #[cfg(feature = "oauth2")] @@ -304,7 +282,7 @@ pub async fn run_server( let argon2 = Arc::new(Argon2::default()); // Initialize debug signing key for debugger authentication - debug::init_debug_signing_key().await; + windmill_api_debug::init_debug_signing_key().await; let disable_response_logs = std::env::var("DISABLE_RESPONSE_LOGS") .ok() @@ -444,7 +422,7 @@ pub async fn run_server( #[cfg(feature = "agent_worker_server")] let (agent_workers_router, agent_workers_bg_processor, agent_workers_job_completed_tx) = if server_mode { - agent_workers_oss::workspaced_service(db.clone(), _base_internal_url.clone()) + windmill_api_agent_workers::workspaced_service(db.clone(), _base_internal_url.clone()) } else { (Router::new(), vec![], None) }; @@ -463,7 +441,7 @@ pub async fn run_server( // Reordered alphabetically .nest("/acls", granular_acls::workspaced_service()) .nest("/apps", apps::workspaced_service()) - .nest("/assets", assets::workspaced_service()) + .nest("/assets", windmill_api_assets::workspaced_service()) .nest("/audit", audit::workspaced_service()) .nest("/capture", capture::workspaced_service()) .nest( @@ -480,17 +458,17 @@ pub async fn run_server( ) .nest( "/flow_conversations", - flow_conversations::workspaced_service(), + windmill_api_flow_conversations::workspaced_service(), ) .nest("/folders", folders::workspaced_service()) .nest("/folders_history", folder_history::workspaced_service()) .nest("/groups", groups::workspaced_service()) .nest("/groups_history", group_history::workspaced_service()) - .nest("/inputs", inputs::workspaced_service()) + .nest("/inputs", windmill_api_inputs::workspaced_service()) .nest("/job_metrics", job_metrics::workspaced_service()) .nest("/job_helpers", job_helpers_service) .nest("/jobs", jobs::workspaced_service()) - .nest("/debug", debug::workspaced_service()) + .nest("/debug", windmill_api_debug::workspaced_service()) .nest("/native_triggers", { #[cfg(feature = "native_trigger")] { @@ -523,23 +501,23 @@ pub async fn run_server( Router::new() }) .nest("/ai", ai::workspaced_service()) - .nest("/npm_proxy", npm_proxy::workspaced_service()) + .nest("/npm_proxy", windmill_api_npm_proxy::workspaced_service()) .nest("/raw_apps", raw_apps::workspaced_service()) .nest("/resources", resources::workspaced_service()) - .nest("/schedules", schedule::workspaced_service()) + .nest("/schedules", windmill_api_schedule::workspaced_service()) .nest("/scripts", scripts::workspaced_service()) .nest( "/users", users::workspaced_service().layer(Extension(argon2.clone())), ) .nest("/variables", variables::workspaced_service()) - .nest("/workers", workers::workspaced_service()) + .nest("/workers", windmill_api_workers::workspaced_service()) .nest("/workspaces", workspaces::workspaced_service()) .nest("/oidc", oidc_oss::workspaced_service()) .nest("/openapi", { #[cfg(feature = "http_trigger")] { - openapi::openapi_service() + windmill_api_openapi::openapi_service() } #[cfg(not(feature = "http_trigger"))] @@ -554,16 +532,16 @@ pub async fn run_server( "/users", users::global_service().layer(Extension(argon2.clone())), ) - .nest("/settings", settings::global_service()) - .nest("/workers", workers::global_service()) + .nest("/settings", windmill_api_settings::global_service()) + .nest("/workers", windmill_api_workers::global_service()) .nest("/service_logs", service_logs::global_service()) - .nest("/configs", configs::global_service()) + .nest("/configs", windmill_api_configs::global_service()) .nest("/scripts", scripts::global_service()) .nest("/integrations", integration::global_service()) .nest("/groups", groups::global_service()) .nest("/flows", flows::global_service()) .nest("/apps", apps::global_service().layer(cors.clone())) - .nest("/schedules", schedule::global_service()) + .nest("/schedules", windmill_api_schedule::global_service()) .nest("/embeddings", embeddings::global_service()) .nest("/ai", ai::global_service()) .nest("/inkeep", inkeep_oss::global_service()) @@ -588,7 +566,7 @@ pub async fn run_server( ) .nest("/srch/index", indexer_oss::global_service()) .nest("/oidc", oidc_oss::global_service()) - .nest("/debug", debug::global_service()) + .nest("/debug", windmill_api_debug::global_service()) .nest( "/saml", saml_oss::global_service().layer(Extension(Arc::clone(&sp_extension))), @@ -628,7 +606,7 @@ pub async fn run_server( if let Some(agent_workers_job_completed_tx) = agent_workers_job_completed_tx.clone() { - agent_workers_oss::global_service(agent_workers_job_completed_tx) + windmill_api_agent_workers::global_service(agent_workers_job_completed_tx) .layer(Extension(agent_cache.clone())) } else { Router::new() @@ -800,7 +778,7 @@ pub async fn run_server( }, ) // JWKS endpoint for HashiCorp Vault JWT authentication (must be outside /api prefix) - .route("/.well-known/jwks.json", get(settings::get_jwks)) + .route("/.well-known/jwks.json", get(windmill_api_settings::get_jwks)) .fallback(static_assets::static_handler) .layer(middleware_stack); diff --git a/backend/windmill-api/src/utils.rs b/backend/windmill-api/src/utils.rs index 3470fe21d7..d63720d9a9 100644 --- a/backend/windmill-api/src/utils.rs +++ b/backend/windmill-api/src/utils.rs @@ -8,61 +8,18 @@ use axum::{body::Body, response::Response}; use serde::{Deserialize, Deserializer}; -#[cfg(feature = "enterprise")] -use windmill_common::worker::CLOUD_HOSTED; -use windmill_common::{ - error::{self}, - DB, -}; pub use windmill_api_auth::{check_scopes, require_devops_role, require_super_admin}; -#[cfg(feature = "enterprise")] -use windmill_common::error::JsonResult; - -#[cfg(feature = "enterprise")] -use axum::Json; - +#[cfg(feature = "private")] pub use windmill_common::usernames::generate_instance_wide_unique_username; pub use windmill_common::utils::WithStarredInfoQuery; -pub async fn generate_instance_username_for_all_users(db: &DB) -> error::Result<()> { - let mut tx = db.begin().await?; - // get users that have a no instance username and either 1 or 0 workspace usernames - let users = sqlx::query!(r#"SELECT p.email as "email!", u.username as "username?" FROM password p LEFT JOIN usr u ON p.email = u.email WHERE p.username IS NULL AND (SELECT COUNT(DISTINCT username) FROM usr WHERE email = p.email) <= 1"#) - .fetch_all(&mut *tx) - .await?; - - for user in users { - let username = if let Some(username) = user.username { - // if has workspace username, check that username is unique - let username_conflict = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 and email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)", - &username, - &user.email - ).fetch_one(&mut *tx).await?.unwrap_or(false); - - if !username_conflict { - username - } else { - generate_instance_wide_unique_username(&mut tx, &user.email).await? - } - } else { - generate_instance_wide_unique_username(&mut tx, &user.email).await? - }; - - sqlx::query!( - "UPDATE password SET username = $1 WHERE email = $2", - &username, - &user.email - ) - .execute(&mut *tx) - .await?; - } - - tx.commit().await?; - Ok(()) -} +#[cfg(feature = "enterprise")] +pub use windmill_alerting::{ + acknowledge_all_critical_alerts, acknowledge_critical_alert, get_critical_alerts, + AlertQueryParams, +}; pub fn content_plain(body: Body) -> Response { use axum::http::header; @@ -81,222 +38,6 @@ where Ok(o.filter(|s| !s.trim().is_empty())) } -#[cfg(feature = "enterprise")] -#[derive(serde::Serialize)] -pub struct CriticalAlert { - id: i32, - alert_type: String, - message: String, - created_at: chrono::DateTime, - acknowledged: Option, - workspace_id: Option, -} - -#[cfg(feature = "enterprise")] -#[derive(Deserialize, Debug)] -pub struct AlertQueryParams { - pub page: Option, - pub page_size: Option, - pub acknowledged: Option, -} - -#[cfg(feature = "enterprise")] -pub async fn get_critical_alerts( - db: DB, - params: AlertQueryParams, - workspace_id: Option, -) -> JsonResult { - // Returning total rows and total pages - let page = params.page.unwrap_or(1).max(1); - let page_size = params.page_size.unwrap_or(10).min(100) as i64; - let offset = ((page - 1) * page_size as i32) as i64; - - // Count total rows - let total_rows = if let Some(workspace_id) = &workspace_id { - if params.acknowledged.is_none() { - sqlx::query_scalar!( - "SELECT COUNT(*) - FROM alerts - WHERE workspace_id = $1", - workspace_id - ) - .fetch_one(&db) - .await? - } else { - sqlx::query_scalar!( - "SELECT COUNT(*) - FROM alerts - WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2", - workspace_id, - params.acknowledged - ) - .fetch_one(&db) - .await? - } - } else { - if params.acknowledged.is_none() { - sqlx::query_scalar!( - "SELECT COUNT(*) - FROM alerts" - ) - .fetch_one(&db) - .await? - } else { - sqlx::query_scalar!( - "SELECT COUNT(*) - FROM alerts - WHERE COALESCE(acknowledged, false) = $1", - params.acknowledged - ) - .fetch_one(&db) - .await? - } - }; - - // Fetch paginated rows - let alerts = if let Some(workspace_id) = workspace_id { - // `workspace_id` is provided => workspace admin - if params.acknowledged.is_none() { - // Case: return all rows where `workspace_id` matches - sqlx::query_as!( - CriticalAlert, - "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id - FROM alerts - WHERE workspace_id = $1 - ORDER BY created_at DESC - LIMIT $2 OFFSET $3", - workspace_id, - page_size, - offset - ) - .fetch_all(&db) - .await? - } else { - // Case: return rows where `acknowledged_workspace` matches `params.acknowledged` - sqlx::query_as!( - CriticalAlert, - "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id - FROM alerts - WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2 - ORDER BY created_at DESC - LIMIT $3 OFFSET $4", - workspace_id, - params.acknowledged, - page_size, - offset - ) - .fetch_all(&db) - .await? - } - } else { - // `workspace_id` is not provided => superadmin - if params.acknowledged.is_none() { - // Case: Return all rows unfiltered with global acknowledged as acknowledged - sqlx::query_as!( - CriticalAlert, - "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id - FROM alerts - ORDER BY created_at DESC - LIMIT $1 OFFSET $2", - page_size, - offset - ) - .fetch_all(&db) - .await? - } else { - // Case: Return rows where global acknowledged matches params.acknowledged - sqlx::query_as!( - CriticalAlert, - "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id - FROM alerts - WHERE COALESCE(acknowledged, false) = $1 - ORDER BY created_at DESC - LIMIT $2 OFFSET $3", - params.acknowledged, - page_size, - offset - ) - .fetch_all(&db) - .await? - } - }; - - let total_rows = total_rows.unwrap_or(0); - let total_pages = ((total_rows as f64) / (page_size as f64)).ceil() as i64; - - Ok(Json(serde_json::json!({ - "alerts": alerts, - "total_rows": total_rows, - "total_pages": total_pages - }))) -} - -#[cfg(feature = "enterprise")] -pub async fn acknowledge_critical_alert( - db: DB, - workspace_id: Option, - id: i32, -) -> error::Result { - sqlx::query!( - "UPDATE alerts - SET - acknowledged = true, - acknowledged_workspace = CASE - WHEN $3 THEN - CASE - WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true - ELSE acknowledged_workspace - END - ELSE true - END - WHERE id = $1", - id, - workspace_id, - *CLOUD_HOSTED - ) - .execute(&db) - .await?; - - tracing::info!( - "Acknowledged critical alert with id: {}{}", - id, - workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w)) - ); - Ok("Critical alert acknowledged".to_string()) -} - -#[cfg(feature = "enterprise")] -pub async fn acknowledge_all_critical_alerts( - db: DB, - workspace_id: Option, -) -> error::Result { - sqlx::query!( - "UPDATE alerts - SET - acknowledged = true, - acknowledged_workspace = CASE - WHEN $2 THEN - CASE - WHEN $1::text IS NOT NULL THEN true - ELSE acknowledged_workspace - END - ELSE true - END - WHERE ($1::text IS NOT NULL AND workspace_id = $1) - OR ($1::text IS NULL)", - workspace_id, - *CLOUD_HOSTED - ) - .execute(&db) - .await?; - - tracing::info!( - "Acknowledged all unacknowledged critical alerts{}", - workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w)) - ); - Ok("All unacknowledged critical alerts acknowledged".to_string()) -} - #[cfg(feature = "http_trigger")] pub use windmill_common::utils::ExpiringCacheEntry; diff --git a/backend/windmill-common/src/usernames.rs b/backend/windmill-common/src/usernames.rs index 645ee90e51..2501841cac 100644 --- a/backend/windmill-common/src/usernames.rs +++ b/backend/windmill-common/src/usernames.rs @@ -10,6 +10,7 @@ use regex::Regex; use sqlx::{Postgres, Transaction}; use crate::error::{self, Error}; +use crate::DB; lazy_static::lazy_static! { pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap(); @@ -57,6 +58,42 @@ pub async fn generate_instance_wide_unique_username<'c>( Ok(username) } +pub async fn generate_instance_username_for_all_users(db: &DB) -> error::Result<()> { + let mut tx = db.begin().await?; + let users = sqlx::query!(r#"SELECT p.email as "email!", u.username as "username?" FROM password p LEFT JOIN usr u ON p.email = u.email WHERE p.username IS NULL AND (SELECT COUNT(DISTINCT username) FROM usr WHERE email = p.email) <= 1"#) + .fetch_all(&mut *tx) + .await?; + + for user in users { + let username = if let Some(username) = user.username { + let username_conflict = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 and email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)", + &username, + &user.email + ).fetch_one(&mut *tx).await?.unwrap_or(false); + + if !username_conflict { + username + } else { + generate_instance_wide_unique_username(&mut tx, &user.email).await? + } + } else { + generate_instance_wide_unique_username(&mut tx, &user.email).await? + }; + + sqlx::query!( + "UPDATE password SET username = $1 WHERE email = $2", + &username, + &user.email + ) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(()) +} + pub async fn get_instance_username_or_create_pending<'c>( tx: &mut Transaction<'c, Postgres>, email: &str, diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 9f6bbf7893..0c5f4b4a23 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -71,6 +71,12 @@ lazy_static::lazy_static! { builder.build().unwrap() }; + pub static ref HTTP_CLIENT_PERMISSIVE: Client = configure_client(reqwest::ClientBuilder::new() + .user_agent("windmill/beta") + .connect_timeout(std::time::Duration::from_secs(10)) + .timeout(std::time::Duration::from_secs(30)) + .danger_accept_invalid_certs(std::env::var("ACCEPT_INVALID_CERTS").is_ok())) + .build().unwrap(); pub static ref GIT_SEM_VERSION: Version = Version::parse( if GIT_VERSION.starts_with('v') { &GIT_VERSION[1..]